Publish generated output diff #197
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish generated output diff | |
| on: | |
| workflow_run: | |
| workflows: | |
| - Generated output diff | |
| types: | |
| - completed | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| publish: | |
| name: Publish report and update PR comment | |
| if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Download comparison data | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: generated-output-diff | |
| path: comparison | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ github.token }} | |
| - name: Check out trusted report renderer | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| path: trusted-source | |
| - name: Validate metadata and result | |
| id: metadata | |
| env: | |
| EVENT_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| EVENT_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }} | |
| EVENT_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| EVENT_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| PR_NUMBER=$(node -e 'const m = require("./comparison/metadata.json"); if (!Number.isSafeInteger(m.prNumber) || m.prNumber <= 0) process.exit(1); process.stdout.write(String(m.prNumber))') | |
| gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" > /tmp/pull-request.json | |
| if [[ -z "$EVENT_PR_NUMBER" ]]; then | |
| HEAD_OWNER=${EVENT_HEAD_REPOSITORY%%/*} | |
| gh api --method GET "repos/$REPOSITORY/pulls" -f state=open -f "head=$HEAD_OWNER:$EVENT_HEAD_BRANCH" > /tmp/source-pulls.json | |
| else | |
| printf '[]\n' > /tmp/source-pulls.json | |
| fi | |
| node - <<'NODE' | |
| const fs = require("node:fs"); | |
| const metadata = JSON.parse(fs.readFileSync("comparison/metadata.json", "utf8")); | |
| const result = JSON.parse(fs.readFileSync("comparison/result.json", "utf8")); | |
| const pullRequest = JSON.parse(fs.readFileSync("/tmp/pull-request.json", "utf8")); | |
| const sourcePulls = JSON.parse(fs.readFileSync("/tmp/source-pulls.json", "utf8")); | |
| const sha = /^[0-9a-f]{40,64}$/; | |
| if (!Number.isSafeInteger(metadata.prNumber) || metadata.prNumber <= 0) throw new Error("Invalid PR number"); | |
| if (process.env.EVENT_PR_NUMBER !== "" && String(metadata.prNumber) !== process.env.EVENT_PR_NUMBER) throw new Error("Artifact PR does not match workflow event"); | |
| if (process.env.EVENT_PR_NUMBER === "" && (sourcePulls.length !== 1 || sourcePulls[0].number !== metadata.prNumber)) throw new Error("Could not uniquely associate fork workflow with artifact PR"); | |
| if (metadata.headSha !== process.env.EVENT_HEAD_SHA) throw new Error("Artifact head does not match workflow event"); | |
| if (pullRequest.number !== metadata.prNumber || pullRequest.base.repo.full_name.toLowerCase() !== process.env.REPOSITORY.toLowerCase() || pullRequest.head.repo.full_name.toLowerCase() !== process.env.EVENT_HEAD_REPOSITORY.toLowerCase() || pullRequest.head.ref !== process.env.EVENT_HEAD_BRANCH) throw new Error("Artifact PR does not match workflow source"); | |
| for (const key of ["baseSha", "prSha", "headSha"]) { | |
| if (!sha.test(metadata[key])) throw new Error(`Invalid ${key}`); | |
| } | |
| if (typeof metadata.supported !== "boolean" || typeof result.hasDifferences !== "boolean") throw new Error("Invalid comparison state"); | |
| const summary = result.summary; | |
| for (const key of ["files", "modified", "added", "deleted", "changedLines", "insertions", "deletions"]) { | |
| if (!Number.isSafeInteger(summary[key]) || summary[key] < 0) throw new Error(`Invalid summary field ${key}`); | |
| } | |
| const prUrl = `https://github.com/${process.env.REPOSITORY}/pull/${metadata.prNumber}`; | |
| const values = { | |
| base_sha: metadata.baseSha, | |
| changed_lines: summary.changedLines, | |
| deleted: summary.deleted, | |
| deletions: summary.deletions, | |
| files: summary.files, | |
| has_differences: result.hasDifferences, | |
| head_sha: metadata.headSha, | |
| insertions: summary.insertions, | |
| modified: summary.modified, | |
| new_files: summary.added, | |
| pr_number: metadata.prNumber, | |
| pr_sha: metadata.prSha, | |
| pr_url: prUrl, | |
| supported: metadata.supported, | |
| }; | |
| const output = Object.entries(values).map(([key, value]) => `${key}=${value}`).join("\n") + "\n"; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, output); | |
| NODE | |
| - name: Set up Node | |
| if: steps.metadata.outputs.supported == 'true' && steps.metadata.outputs.has_differences == 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: trusted-source/.nvmrc | |
| cache: npm | |
| cache-dependency-path: trusted-source/package-lock.json | |
| - name: Install trusted report dependencies | |
| if: steps.metadata.outputs.supported == 'true' && steps.metadata.outputs.has_differences == 'true' | |
| working-directory: trusted-source | |
| run: npm ci --ignore-scripts | |
| - name: Render report | |
| if: steps.metadata.outputs.supported == 'true' && steps.metadata.outputs.has_differences == 'true' | |
| run: | | |
| mkdir -p report | |
| trusted-source/node_modules/.bin/tsx trusted-source/script/output-diff.ts render \ | |
| comparison/result.json comparison/output.diff report/index.html \ | |
| '${{ steps.metadata.outputs.pr_url }}' \ | |
| '${{ steps.metadata.outputs.base_sha }}' \ | |
| '${{ steps.metadata.outputs.pr_sha }}' \ | |
| '${{ steps.metadata.outputs.head_sha }}' | |
| cp comparison/output.diff report/output.diff | |
| - name: Publish immutable report to hostr | |
| if: steps.metadata.outputs.supported == 'true' && steps.metadata.outputs.has_differences == 'true' | |
| env: | |
| HOSTR_TOKEN: ${{ secrets.HOSTR_TOKEN }} | |
| REPORT_PATH: quicktype/output-diffs/pr-${{ steps.metadata.outputs.pr_number }}/${{ steps.metadata.outputs.pr_sha }}/${{ steps.metadata.outputs.head_sha }} | |
| run: | | |
| test -n "$HOSTR_TOKEN" | |
| upload_immutable() { | |
| local file=$1 content_type=$2 url=$3 status | |
| status=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "$url") | |
| case "$status" in | |
| 200) echo "Already published: $url" ;; | |
| 404) | |
| curl --fail-with-body --retry 3 -X POST \ | |
| -H "Authorization: Bearer $HOSTR_TOKEN" \ | |
| -H "Content-Type: $content_type" \ | |
| --data-binary "@$file" "$url" | |
| ;; | |
| *) echo "Unexpected hostr response $status for $url" >&2; return 1 ;; | |
| esac | |
| } | |
| upload_immutable report/index.html 'text/html; charset=utf-8' \ | |
| "https://hostr.flingit.run/s/$REPORT_PATH/index.html" | |
| upload_immutable report/output.diff 'text/x-diff; charset=utf-8' \ | |
| "https://hostr.flingit.run/s/$REPORT_PATH/output.diff" | |
| - name: Check whether this is still the current PR head | |
| if: steps.metadata.outputs.supported == 'true' | |
| id: current | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} | |
| run: | | |
| CURRENT_HEAD=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" --jq .head.sha) | |
| if [[ "$CURRENT_HEAD" == "${{ steps.metadata.outputs.head_sha }}" ]]; then | |
| echo "is_current=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_current=false" >> "$GITHUB_OUTPUT" | |
| echo "The PR advanced to $CURRENT_HEAD; not posting a comment for this stale comparison." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Create output-diff comment | |
| if: steps.metadata.outputs.supported == 'true' && steps.current.outputs.is_current == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| HAS_DIFFERENCES: ${{ steps.metadata.outputs.has_differences }} | |
| PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} | |
| REPORT_URL: https://hostr.flingit.run/s/quicktype/output-diffs/pr-${{ steps.metadata.outputs.pr_number }}/${{ steps.metadata.outputs.pr_sha }}/${{ steps.metadata.outputs.head_sha }}/index.html | |
| run: | | |
| MARKER='<!-- quicktype-generated-output-diff -->' | |
| if [[ "$HAS_DIFFERENCES" == true ]]; then | |
| BODY=$(cat <<EOF | |
| $MARKER | |
| ### Generated-output differences | |
| **${{ steps.metadata.outputs.files }} files differ** — ${{ steps.metadata.outputs.modified }} modified, ${{ steps.metadata.outputs.new_files }} new, ${{ steps.metadata.outputs.deleted }} deleted | |
| **${{ steps.metadata.outputs.changed_lines }} changed lines** — +${{ steps.metadata.outputs.insertions }} / −${{ steps.metadata.outputs.deletions }} | |
| [Open the generated-output report →]($REPORT_URL) | |
| EOF | |
| ) | |
| else | |
| BODY=$(cat <<EOF | |
| $MARKER | |
| ### No generated-output differences | |
| ✅ This PR does not change generated outputs. | |
| EOF | |
| ) | |
| fi | |
| gh api --method POST "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" -f body="$BODY" >/dev/null |