Query Counts — Apply #4763
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: Query Counts — Apply | |
| # Runs after the "Query Counts" workflow completes on a PR. This job has | |
| # elevated permissions to push back to the PR branch and comment, but it | |
| # never executes code from the PR — it only downloads the inert JSON + | |
| # diff artifact produced by the measure job. | |
| on: | |
| workflow_run: | |
| workflows: ["Query Counts"] | |
| types: [completed] | |
| permissions: | |
| # actions:read is needed for listWorkflowRunArtifacts / | |
| # downloadArtifact. pull-requests:read is needed for pulls.get in the | |
| # Resolve PR step. contents:read is a safety default; the commit-back | |
| # step uses the app token, not GITHUB_TOKEN. | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| apply: | |
| name: Apply | |
| if: github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Download snapshot artifact | |
| id: download | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const { data } = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.payload.workflow_run.id, | |
| }); | |
| const artifact = data.artifacts.find((a) => a.name === 'query-counts-snapshots'); | |
| if (!artifact) { | |
| core.setOutput('found', 'false'); | |
| core.info('No snapshot artifact — nothing to apply.'); | |
| return; | |
| } | |
| const download = await github.rest.actions.downloadArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: artifact.id, | |
| archive_format: 'zip', | |
| }); | |
| // Stage the artifact under RUNNER_TEMP, not GITHUB_WORKSPACE. | |
| // GITHUB_WORKSPACE is wiped by the later actions/checkout step | |
| // (clean: true is the default), which would delete our payload | |
| // before the Apply step can copy it into scripts/. RUNNER_TEMP | |
| // sits outside the workspace and survives checkout. | |
| const outDir = path.join(process.env.RUNNER_TEMP, 'qc-artifact'); | |
| fs.mkdirSync(outDir, { recursive: true }); | |
| fs.writeFileSync(path.join(outDir, 'artifact.zip'), Buffer.from(download.data)); | |
| core.setOutput('found', 'true'); | |
| core.setOutput('dir', outDir); | |
| - name: Unpack artifact | |
| if: steps.download.outputs.found == 'true' | |
| run: | | |
| cd "$RUNNER_TEMP/qc-artifact" | |
| unzip -o artifact.zip | |
| ls -la | |
| - name: Resolve PR | |
| if: steps.download.outputs.found == 'true' | |
| id: pr | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const prNumber = Number( | |
| fs.readFileSync( | |
| path.join(process.env.RUNNER_TEMP, 'qc-artifact', 'pr-number'), | |
| 'utf8', | |
| ).trim(), | |
| ); | |
| if (!Number.isInteger(prNumber) || prNumber <= 0) { | |
| core.setFailed(`Invalid PR number in artifact: ${prNumber}`); | |
| return; | |
| } | |
| // Cross-check: fetch the PR directly and verify its head SHA | |
| // matches the workflow_run's head SHA. This is more robust | |
| // than listPullRequestsAssociatedWithCommit, which doesn't | |
| // reliably surface fork PRs from the base repo's API view | |
| // and was rejecting valid fork artifacts as "not associated". | |
| const headSha = context.payload.workflow_run.head_sha; | |
| let pr; | |
| try { | |
| const { data } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| pr = data; | |
| } catch (err) { | |
| core.setFailed(`Failed to fetch PR #${prNumber}: ${err.message}`); | |
| return; | |
| } | |
| if (pr.head.sha !== headSha) { | |
| core.setFailed( | |
| `PR #${prNumber} head SHA (${pr.head.sha}) does not match workflow_run head SHA (${headSha}). The branch has likely moved on; the next PR event will trigger a fresh measure+apply.`, | |
| ); | |
| return; | |
| } | |
| core.setOutput('number', String(pr.number)); | |
| core.setOutput('ref', pr.head.ref); | |
| // Use the workflow_run's head_sha — the commit that was | |
| // actually measured — rather than the PR's current head. | |
| // If the branch has moved on since, we want to push onto | |
| // the measured commit (and let the push fail non-fast- | |
| // forward) rather than apply stale snapshots to a newer | |
| // tree. | |
| core.setOutput('sha', headSha); | |
| core.setOutput('full_name', pr.head.repo.full_name); | |
| const isFork = pr.head.repo.fork | |
| || pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`; | |
| core.setOutput('is_fork', isFork.toString()); | |
| - name: Generate app token | |
| if: steps.download.outputs.found == 'true' | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| # Push the updated snapshots back to the PR branch. Nothing else. | |
| permission-contents: write | |
| # --- Same-repo PRs: checkout the pinned SHA and push directly --- | |
| - name: Checkout (same-repo) | |
| if: steps.download.outputs.found == 'true' && steps.pr.outputs.is_fork == 'false' | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ steps.pr.outputs.sha }} | |
| token: ${{ steps.app-token.outputs.token }} | |
| # Intentional: the same-repo push step below pushes the | |
| # updated snapshots back to the PR branch using this credential. | |
| persist-credentials: true | |
| # --- Fork PRs: checkout the fork at the pinned SHA so we can push back --- | |
| - name: Checkout (fork) | |
| if: steps.download.outputs.found == 'true' && steps.pr.outputs.is_fork == 'true' | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: ${{ steps.pr.outputs.full_name }} | |
| ref: ${{ steps.pr.outputs.sha }} | |
| persist-credentials: false | |
| # Reviewed opt-in to checkout v7's fork guard: no code from the | |
| # fork tree is ever executed here. The steps below only copy the | |
| # inert snapshot JSONs from the trusted measure artifact and run | |
| # git add/commit/push — no scripts, hooks, or tooling from the | |
| # checked-out tree. Credentials are not persisted; the push uses | |
| # a scoped app token via GIT_ASKPASS. | |
| allow-unsafe-pr-checkout: true | |
| - name: Apply snapshots | |
| if: steps.download.outputs.found == 'true' | |
| id: apply | |
| run: | | |
| cp "$RUNNER_TEMP/qc-artifact/query-counts.snapshot.sqlite.json" scripts/ | |
| cp "$RUNNER_TEMP/qc-artifact/query-counts.snapshot.d1.json" scripts/ | |
| cp "$RUNNER_TEMP/qc-artifact/query-counts.queries.sqlite.json" scripts/ | |
| cp "$RUNNER_TEMP/qc-artifact/query-counts.queries.d1.json" scripts/ | |
| git add \ | |
| scripts/query-counts.snapshot.sqlite.json \ | |
| scripts/query-counts.snapshot.d1.json \ | |
| scripts/query-counts.queries.sqlite.json \ | |
| scripts/query-counts.queries.d1.json | |
| if git diff --staged --quiet; then | |
| echo "no_diff=true" >> "$GITHUB_OUTPUT" | |
| echo "Artifact matches the PR head — nothing to push (probably a race with a newer commit)." | |
| else | |
| echo "no_diff=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Commit and push (same-repo) | |
| if: | | |
| steps.download.outputs.found == 'true' | |
| && steps.pr.outputs.is_fork == 'false' | |
| && steps.apply.outputs.no_diff == 'false' | |
| env: | |
| HEAD_REF: ${{ steps.pr.outputs.ref }} | |
| HEAD_SHA: ${{ steps.pr.outputs.sha }} | |
| run: | | |
| set -e | |
| git config user.name "emdashbot[bot]" | |
| git config user.email "emdashbot[bot]@users.noreply.github.com" | |
| git commit -m "ci: update query-count snapshots" | |
| # Detached-HEAD push onto the PR branch. If the branch has | |
| # advanced past the measured SHA, this fails with a non-fast- | |
| # forward — safe outcome, since applying stale counts to a | |
| # newer tree would hide a regression. The next PR event will | |
| # kick off a fresh measure+apply against the new head. | |
| if ! git push origin "HEAD:$HEAD_REF"; then | |
| echo "::error::Non-fast-forward push — the PR branch moved past the measured SHA $HEAD_SHA. Rerun the harness against the new head (the next PR event will do this automatically)." >&2 | |
| exit 1 | |
| fi | |
| - name: Commit and push (fork) | |
| if: | | |
| steps.download.outputs.found == 'true' | |
| && steps.pr.outputs.is_fork == 'true' | |
| && steps.apply.outputs.no_diff == 'false' | |
| env: | |
| APP_TOKEN: ${{ steps.app-token.outputs.token }} | |
| HEAD_REF: ${{ steps.pr.outputs.ref }} | |
| FULL_NAME: ${{ steps.pr.outputs.full_name }} | |
| run: | | |
| set -e | |
| git config user.name "emdashbot[bot]" | |
| git config user.email "emdashbot[bot]@users.noreply.github.com" | |
| git commit -m "ci: update query-count snapshots" | |
| export GIT_ASKPASS="$RUNNER_TEMP/git-askpass.sh" | |
| printf '#!/bin/sh\necho "%s"\n' "$APP_TOKEN" > "$GIT_ASKPASS" | |
| chmod +x "$GIT_ASKPASS" | |
| git remote add fork "https://x-access-token@github.com/$FULL_NAME.git" | |
| # Fail the workflow if we can't push — most likely cause is | |
| # "Allow edits by maintainers" being disabled on the fork PR. | |
| # The reviewer needs to know the snapshots couldn't be applied. | |
| if ! git push fork "HEAD:$HEAD_REF"; then | |
| rm -f "$GIT_ASKPASS" | |
| echo "::error::Failed to push snapshots to fork. The contributor may have 'Allow edits by maintainers' disabled. They need to run 'pnpm query-counts --target sqlite --update && pnpm query-counts --target d1 --update' locally and commit, or re-enable maintainer edits." >&2 | |
| exit 1 | |
| fi | |
| rm -f "$GIT_ASKPASS" |