-
Notifications
You must be signed in to change notification settings - Fork 7.2k
Expand file tree
/
Copy pathcastiron-custom-code-comment.yml
More file actions
263 lines (249 loc) · 13.9 KB
/
Copy pathcastiron-custom-code-comment.yml
File metadata and controls
263 lines (249 loc) · 13.9 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
name: Castiron custom code comment
on:
workflow_run:
workflows: [Castiron custom code]
types: [completed]
permissions: {}
concurrency:
group: castiron-custom-code-comment-${{ github.event.workflow_run.head_repository.id }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
queue: max # Preserve newer pending evaluations if older runs arrive out of order.
jobs:
compute:
name: Compute trusted custom-code report
if: contains(fromJSON('["pull_request", "merge_group"]'), github.event.workflow_run.event) && github.event.workflow_run.path == '.github/workflows/castiron-custom-code.yml'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
pull-requests: read
outputs:
number: ${{ steps.report.outputs.number }}
artifact-id: ${{ steps.artifact.outputs.artifact-id }}
artifact-run-attempt: ${{ github.run_attempt }}
base-sha: ${{ steps.budget.outputs.base_sha }}
head-sha: ${{ steps.budget.outputs.head_sha }}
isolation: ${{ steps.budget.outputs.isolation }}
budget: ${{ steps.budget.outputs.budget }}
steps:
# Selecting main here pins both the executable checker and PR policy base.
# Subsequent steps use this checkout's SHA even if main advances.
- name: Check out the trusted reporter
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
persist-credentials: false
- name: Compute from the selected main and pull request Git objects
id: report
if: github.event.workflow_run.event == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
GIT_CONFIG_COUNT: '2'
GIT_CONFIG_KEY_0: credential.helper
GIT_CONFIG_VALUE_0: ''
GIT_CONFIG_KEY_1: credential.https://github.com.helper
GIT_CONFIG_VALUE_1: '!gh auth git-credential'
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
run: |
python3 -I scripts/castiron/custom_code_report.py trusted-report \
--repo "$RUNNER_TEMP/castiron-objects.git" \
--base "$(git rev-parse HEAD)" \
--repository "$REPOSITORY" --run-id "$RUN_ID" --run-attempt "$RUN_ATTEMPT" \
--out "$RUNNER_TEMP/castiron-custom-code"
if test -f "$RUNNER_TEMP/castiron-custom-code/context.json"; then
number=$(jq -er '.pr' "$RUNNER_TEMP/castiron-custom-code/context.json")
printf 'number=%s\n' "$number" >> "$GITHUB_OUTPUT"
cat "$RUNNER_TEMP/castiron-custom-code/summary.md" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload the trusted report and patch
id: artifact
if: steps.report.outputs.number != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: castiron-custom-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/castiron-custom-code/
if-no-files-found: error
retention-days: 7
# The prior step's report was computed here from
# Git objects by main's reporter, not downloaded from the candidate run.
- name: Evaluate main's custom-code budget
id: budget
if: always()
continue-on-error: true # A budget failure must not suppress the existing report comment.
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
SOURCE_EVENT: ${{ github.event.workflow_run.event }}
GIT_CONFIG_COUNT: '2'
GIT_CONFIG_KEY_0: credential.helper
GIT_CONFIG_VALUE_0: ''
GIT_CONFIG_KEY_1: credential.https://github.com.helper
GIT_CONFIG_VALUE_1: '!gh auth git-credential'
run: |
trusted_sha=$(git rev-parse HEAD)
reuse=()
if [[ "$SOURCE_EVENT" == pull_request ]]; then
reuse=(--trusted-report-dir "$RUNNER_TEMP/castiron-custom-code")
fi
python3 -I scripts/castiron/custom_code_budget.py github \
--repository "$REPOSITORY" --event-path "$GITHUB_EVENT_PATH" \
--trusted-sha "$trusted_sha" --repo "$RUNNER_TEMP/castiron-objects.git" \
"${reuse[@]}" --out "$RUNNER_TEMP/custom-code-budget"
- name: Add the budget to the run summary
if: always()
run: |
if test -f "$RUNNER_TEMP/custom-code-budget/summary.md"; then
cat "$RUNNER_TEMP/custom-code-budget/summary.md" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload trusted budget measurements
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: custom-code-budget-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/custom-code-budget/
if-no-files-found: error
retention-days: 7
budget-status:
name: Publish custom-code budget checks
needs: compute
if: always() && !cancelled() && needs.compute.result != 'skipped'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
actions: read
pull-requests: read
statuses: write
steps:
# This workflow definition is from main. No candidate checkout/artifacts.
- name: Publish exact-head statuses after checking freshness
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BASE_SHA: ${{ needs.compute.outputs.base-sha }}
HEAD_SHA: ${{ needs.compute.outputs.head-sha }}
ISOLATION_RESULT: ${{ needs.compute.outputs.isolation }}
BUDGET_RESULT: ${{ needs.compute.outputs.budget }}
PUBLISH_ATTEMPT: ${{ github.run_attempt }}
with:
script: |
const event = context.payload.workflow_run;
const {data: run} = await github.rest.actions.getWorkflowRun({...context.repo, run_id: event.id});
if (run.event !== event.event || run.head_sha !== event.head_sha || run.run_attempt !== event.run_attempt ||
run.status !== 'completed' || run.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml' ||
run.repository.full_name !== `${context.repo.owner}/${context.repo.repo}`) return;
const head = run.head_sha;
if (!/^[0-9a-f]{40}$/.test(head)) throw new Error('Invalid candidate SHA');
const base = process.env.BASE_SHA;
let fresh = /^[0-9a-f]{40}$/.test(base) && head === process.env.HEAD_SHA;
if (run.event === 'pull_request') {
let pulls = run.pull_requests.length ? run.pull_requests : await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: head});
if (!pulls.length) pulls = await github.paginate(github.rest.pulls.list, {
...context.repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`,
});
const current = [];
for (const pull of pulls) {
const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: pull.number});
if (pr.state === 'open' && pr.head.sha === head &&
pr.base.ref === 'main' && pr.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) current.push(pr);
}
if (current.length !== 1) return;
} else if (run.event !== 'merge_group' || !run.head_branch.startsWith('gh-readonly-queue/main/')) {
return;
} else {
const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'});
fresh = fresh && base === main.object.sha;
}
const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
// Concurrency serializes writes, but does not order runs or retries.
// Keep an older evaluation from overwriting a newer result for this head.
const order = [run.id, run.run_attempt, Number(context.runId), Number(process.env.PUBLISH_ATTEMPT)];
const marker = `[evaluation ${order.join(':')}]`;
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {...context.repo, ref: head});
const names = ['Castiron / budget-only change', 'Castiron / custom-code budget'];
for (const status of statuses) {
if (!names.includes(status.context) || status.creator?.login !== 'github-actions[bot]') continue;
const prior = status.description?.match(/\[evaluation (\d+):(\d+):(\d+):(\d+)\]$/);
if (!prior || status.target_url !== `${url.slice(0, url.lastIndexOf('/') + 1)}${prior[3]}`) continue;
const previous = prior.slice(1).map(Number);
const different = previous.findIndex((value, index) => value !== order[index]);
if (different !== -1 && previous[different] > order[different]) return;
}
for (const [name, result] of [
['Castiron / budget-only change', process.env.ISOLATION_RESULT],
['Castiron / custom-code budget', process.env.BUDGET_RESULT],
]) {
const state = fresh && result === 'success' ? 'success' : 'failure';
const description = !fresh ? 'Evaluation unavailable or queue base changed; inspect the trusted run and rerun.'
: `${state === 'success' ? 'Passed' : 'Failed'} against main ${base.slice(0, 12)}. See the trusted run summary.`;
await github.rest.repos.createCommitStatus({...context.repo, sha: head, context: name,
state, description: `${description} ${marker}`, target_url: url});
}
comment:
name: Update custom-code comment
needs: compute
if: always() && !cancelled() && github.event.workflow_run.event == 'pull_request' && (needs.compute.result == 'failure' || needs.compute.outputs.number != '')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
actions: read
pull-requests: write
steps:
- name: Check out the trusted publisher
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.workflow_sha }}
persist-credentials: false
- name: Download this workflow's trusted report
if: needs.compute.result == 'success'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.compute.outputs.artifact-id }}
merge-multiple: true
path: ${{ runner.temp }}/castiron-custom-code
- name: Create or update the single report comment
id: publish
if: needs.compute.result == 'success'
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ needs.compute.outputs.number }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
ARTIFACT_RUN_ID: ${{ github.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ needs.compute.outputs.artifact-run-attempt }}
run: |
python3 -I scripts/castiron/custom_code_report.py comment \
--report "$RUNNER_TEMP/castiron-custom-code/report.json" \
--repository "$REPOSITORY" --pr "$PR_NUMBER" --run-id "$RUN_ID" \
--run-attempt "$RUN_ATTEMPT" \
--artifact-run-id "$ARTIFACT_RUN_ID" --artifact-run-attempt "$ARTIFACT_RUN_ATTEMPT"
- name: Publish a trusted failure status
if: always() && !cancelled() && steps.publish.outcome != 'success'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const marker = '<!-- castiron:custom-code-report:v1 -->';
const run = context.payload.workflow_run;
if (run.event !== 'pull_request' || run.path !== '.github/workflows/castiron-custom-code.yml') return;
let pulls = run.pull_requests?.length ? run.pull_requests : await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: run.head_sha});
if (!pulls.length) pulls = await github.paginate(github.rest.pulls.list, {
...context.repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`,
});
for (const pull of pulls) {
const {data: current} = await github.rest.pulls.get({...context.repo, pull_number: pull.number});
if (current.state !== 'open' || current.head.sha !== run.head_sha) continue;
const comments = await github.paginate(github.rest.issues.listComments, {...context.repo, issue_number: pull.number});
const previous = comments.find(c => c.user?.type === 'Bot' && c.user?.login === 'github-actions[bot]' && c.body?.startsWith(marker));
const prior = previous?.body?.match(/<!-- castiron:run:v1:(\d+):(\d+) -->/);
if (prior && (Number(prior[1]) > run.id || (Number(prior[1]) === run.id && Number(prior[2]) > run.run_attempt))) continue;
const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${run.id}`;
const body = `${marker}\n\n## Castiron custom code\n\n⚠️ Report unavailable for \`${run.head_sha.slice(0, 12)}\`.\n\nThe report setup or validation failed. [Inspect the workflow run](${url}).\n\n<!-- castiron:run:v1:${run.id}:${run.run_attempt} -->`;
if (previous) await github.rest.issues.updateComment({...context.repo, comment_id: previous.id, body});
else await github.rest.issues.createComment({...context.repo, issue_number: pull.number, body});
}