-
Notifications
You must be signed in to change notification settings - Fork 1.2k
234 lines (220 loc) · 10.1 KB
/
Copy pathauto-format-apply.yml
File metadata and controls
234 lines (220 loc) · 10.1 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: Auto Format — Apply
# Stage 2 of 2. Runs after "Auto Format" completes on a PR. This job has
# elevated permissions to push the formatted patch back to the PR branch,
# but it never executes code from the PR — it only downloads the inert
# patch artifact, checks out the measured head SHA, and applies the diff.
on:
workflow_run:
workflows: ["Auto Format"]
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 push-back steps use the app 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 patch 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 === 'auto-format-patch');
if (!artifact) {
core.setOutput('found', 'false');
core.info('No patch 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 the payload
// before the Apply step can reach it. RUNNER_TEMP sits outside
// the workspace and survives checkout.
const outDir = path.join(process.env.RUNNER_TEMP, 'af-artifact');
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'artifact.zip'), Buffer.from(download.data));
core.setOutput('found', 'true');
- name: Unpack artifact
if: steps.download.outputs.found == 'true'
run: |
cd "$RUNNER_TEMP/af-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, 'af-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 and verify its head SHA matches the
// workflow_run's head SHA, so a forged artifact can't redirect
// the push at an unrelated branch.
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 moved on; the next PR event will re-run format+apply.`,
);
return;
}
core.setOutput('number', String(pr.number));
core.setOutput('ref', pr.head.ref);
// Push onto the exact commit that was formatted. If the branch
// has advanced since, the push fails non-fast-forward rather than
// applying a stale patch 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 formatted commit (contents) and comment on push failure
# (pull-requests). Nothing else.
permission-contents: write
permission-pull-requests: write
# --- Same-repo PRs: checkout the measured 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 }}
# The same-repo push step below pushes the formatted commit back to
# the PR branch using this credential.
persist-credentials: true
# --- Fork PRs: checkout the fork at the measured 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 `git apply`
# the patch from the trusted format 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 patch
if: steps.download.outputs.found == 'true'
id: apply
run: |
git apply "$RUNNER_TEMP/af-artifact/format.patch"
git add -A
if git diff --staged --quiet; then
echo "no_diff=true" >> "$GITHUB_OUTPUT"
echo "Patch is a no-op against the checked-out head — nothing to push."
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 "style: format"
# Detached-HEAD push onto the PR branch. If the branch has advanced
# past the measured SHA, this fails non-fast-forward — the next PR
# event re-runs format+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. The next PR event will re-run format+apply." >&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'
id: push-fork
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_REF: ${{ steps.pr.outputs.ref }}
FULL_NAME: ${{ steps.pr.outputs.full_name }}
run: |
git config user.name "emdashbot[bot]"
git config user.email "emdashbot[bot]@users.noreply.github.com"
git commit -m "style: format"
# Push the formatted commit to the fork via the app token, supplied
# through GIT_ASKPASS so it never lands in process args or git config.
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"
if git push fork "HEAD:$HEAD_REF"; then
echo "push_failed=false" >> "$GITHUB_OUTPUT"
else
echo "push_failed=true" >> "$GITHUB_OUTPUT"
fi
rm -f "$GIT_ASKPASS"
- name: Comment on push failure
if: steps.push-fork.outputs.push_failed == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.PR_NUMBER),
body: `Could not push formatting changes to this fork. The contributor may have "Allow edits by maintainers" disabled.\n\nPlease run the formatter locally:\n\n\`\`\`\npnpm format\n\`\`\``,
});