b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
235 lines
11 KiB
YAML
235 lines
11 KiB
YAML
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"
|