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"