-
Notifications
You must be signed in to change notification settings - Fork 1.2k
234 lines (221 loc) · 10.6 KB
/
Copy pathquery-counts-apply.yml
File metadata and controls
234 lines (221 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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"