name: Lintcheck summary

# The workflow_run event runs in the context of the Clippy repo giving it write
# access, needed here to create a PR comment when the PR originates from a fork
#
# The summary artifact is a JSON file that we verify in this action to prevent
# the creation of arbitrary comments
#
# This action must not checkout/run code from the originating workflow_run
# or directly interpolate ${{}} untrusted fields into code
#
# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run
# https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections

on:
  workflow_run:
    workflows: [Lintcheck]
    types: [completed]

# Restrict the default permission scope https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#defining-access-for-the-github_token-scopes
permissions:
  pull-requests: write

jobs:
  download:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v8
        with:
          name: summary
          path: untrusted
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ github.token }}

      - name: Format comment
        uses: actions/github-script@v9
        with:
          script: |
            const fs = require("fs");
            const assert = require("assert/strict");

            function validateName(s) {
              assert.match(s, /^[a-z0-9_:]+$/);
              return s;
            }

            function validateInt(i) {
              assert.ok(Number.isInteger(i));
              return i;
            }

            function tryReadSummary() {
              try {
                return JSON.parse(fs.readFileSync("untrusted/summary.json"));
              } catch {
                return null;
              }
            }

            const prNumber = parseInt(fs.readFileSync("untrusted/pr.txt"), 10);
            core.exportVariable("PR", prNumber.toString());

            const untrustedSummary = tryReadSummary();
            if (!Array.isArray(untrustedSummary)) {
              return;
            }

            let summary = `Lintcheck changes for ${context.payload.workflow_run.head_sha}
            
            | Lint | Added | Removed | Changed |
            | ---- | ----: | ------: | ------: |
            `;

            for (const untrustedRow of untrustedSummary) {
              const name = validateName(untrustedRow.name);

              const added = validateInt(untrustedRow.added);
              const removed = validateInt(untrustedRow.removed);
              const changed = validateInt(untrustedRow.changed);

              const id = name.replace("clippy::", "user-content-").replaceAll("_", "-");
              const url = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${context.payload.workflow_run.id}#${id}`;

              summary += `| [\`${name}\`](${url}) | ${added} | ${removed} | ${changed} |\n`;
            }

            summary += "\nThis comment will be updated if you push new changes";

            fs.writeFileSync("summary.md", summary);

      - name: Create/update comment
        run: |
          if [[ -f summary.md ]]; then
             gh pr comment "$PR" --body-file summary.md --edit-last --create-if-none
          else
            # There were no changes detected by Lintcheck
            # - If a comment exists from a previous run that did detect changes, edit it (--edit-last)
            # - If no comment exists do not create one, the `gh` command exits with an error which
            #   `|| true` ignores
            gh pr comment "$PR" --body "No changes for ${{ github.event.workflow_run.head_sha }}" --edit-last || true
          fi
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
