Repository navigation
202 lines (192 loc) · 8.67 KB
/
Copy pathfinal-check.yml
File metadata and controls
202 lines (192 loc) · 8.67 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
name: final-check
# A maintainer adds the `final-check` label to an important PR. The check job
# runs what CI and Pages would run on the PR merged into its base (the ES
# validator, the tests, and the static index build) and lists the records the
# PR changes. TechEngineBot leaves ONE comment with the verdict; adding the
# label again edits that comment.
#
# The check job runs PR code, so it gets no secrets and a read-only token.
# Only the comment job sees TECHENGINEBOT_TOKEN, which must be a fine-grained
# token of TechEngineBot with Pull requests: read and write on this repository
# and nothing more; the job refuses any other kind of token or account. Fork
# PRs never get a comment.
on:
pull_request:
types: [labeled]
branches: [develop, main]
permissions: {}
jobs:
check:
if: github.event.label.name == 'final-check'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
concurrency:
group: final-check-${{ github.event.pull_request.number }}
cancel-in-progress: true
outputs:
body: ${{ steps.report.outputs.body }}
env:
PYTHONIOENCODING: utf-8
steps:
# The merge commit, as develop/main would look after merging. Depth 2
# keeps HEAD^1 (the base) for the list of changed records.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.12"
- name: Validate ES catalog
id: validate
continue-on-error: true
run: |
status=0
python -m app.validate > "$RUNNER_TEMP/validate.log" 2>&1 || status=$?
cat "$RUNNER_TEMP/validate.log"
exit "$status"
- name: Tests
id: tests
continue-on-error: true
run: |
status=0
{ python -m pip install --disable-pip-version-check pytest && python -m pytest -q; } \
> "$RUNNER_TEMP/tests.log" 2>&1 || status=$?
tail -n 40 "$RUNNER_TEMP/tests.log"
exit "$status"
# site/build.py replays history from data commits; with a shallow clone
# history.json is partial, which is fine for a build check.
- name: Build static index
id: site
continue-on-error: true
run: |
status=0
python site/build.py > "$RUNNER_TEMP/site.log" 2>&1 || status=$?
cat "$RUNNER_TEMP/site.log"
exit "$status"
# The comment quotes tool output and record names, which come from the
# PR: logs stay inside code fences with backticks and @ removed, so they
# can neither break out into markdown nor mention anyone.
- name: Report
id: report
if: always()
env:
VALIDATE: ${{ steps.validate.outcome }}
TESTS: ${{ steps.tests.outcome }}
SITE: ${{ steps.site.outcome }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
clean() { tr -d '`@' | cut -c1-300; }
fence() { # fence <log> <lines>
echo '```text'
tail -n "$2" "$1" 2>/dev/null | clean
echo '```'
}
verdict='✅ Final check passed'
[ "$VALIDATE/$TESTS/$SITE" = success/success/success ] || verdict='❌ Final check failed'
git diff --name-status --no-renames HEAD^1 HEAD -- data/cpu > "$RUNNER_TEMP/changed.txt" 2>/dev/null || true
added=$(grep -c '^A' "$RUNNER_TEMP/changed.txt" || true)
modified=$(grep -c '^M' "$RUNNER_TEMP/changed.txt" || true)
removed=$(grep -c '^D' "$RUNNER_TEMP/changed.txt" || true)
{
echo '<!-- cpu-es-final-check -->'
echo "### $verdict"
echo
echo "Head \`${HEAD_SHA::7}\`, checked as merged into the base · [run]($RUN_URL)"
echo
echo '| Step | Result |'
echo '| --- | --- |'
echo "| ES validator (\`python -m app.validate\`) | ${VALIDATE:-skipped} |"
echo "| Tests (\`pytest\`) | ${TESTS:-skipped} |"
echo "| Static index build (\`site/build.py\`) | ${SITE:-skipped} |"
echo
echo "Records: +${added:-0} added · ~${modified:-0} modified · −${removed:-0} removed"
if [ -s "$RUNNER_TEMP/changed.txt" ]; then
echo
echo '<details><summary>Changed records</summary>'
echo
echo '```text'
head -n 200 "$RUNNER_TEMP/changed.txt" | clean
echo '```'
echo '</details>'
fi
[ "$VALIDATE" = failure ] && { echo; echo '<details><summary>Validator log</summary>'; echo; fence "$RUNNER_TEMP/validate.log" 30; echo '</details>'; }
[ "$TESTS" = failure ] && { echo; echo '<details><summary>Test log</summary>'; echo; fence "$RUNNER_TEMP/tests.log" 30; echo '</details>'; }
[ "$SITE" = failure ] && { echo; echo '<details><summary>Build log</summary>'; echo; fence "$RUNNER_TEMP/site.log" 20; echo '</details>'; }
true
} > "$RUNNER_TEMP/body.md"
cat "$RUNNER_TEMP/body.md" >> "$GITHUB_STEP_SUMMARY"
delim="BODY_$(openssl rand -hex 16)"
{ echo "body<<$delim"; cat "$RUNNER_TEMP/body.md"; echo "$delim"; } >> "$GITHUB_OUTPUT"
[ "$verdict" = '✅ Final check passed' ]
comment:
needs: check
if: >-
always() && github.event.label.name == 'final-check' &&
(needs.check.result == 'success' || needs.check.result == 'failure') &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check the token
id: token
env:
TOKEN: ${{ secrets.TECHENGINEBOT_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "::warning::TECHENGINEBOT_TOKEN is not set; the verdict is in the check job's summary only."
echo "ok=false" >> "$GITHUB_OUTPUT"
exit 0
fi
case "$TOKEN" in
github_pat_*) echo "ok=true" >> "$GITHUB_OUTPUT" ;;
*) echo "::error::TECHENGINEBOT_TOKEN must be a fine-grained token (github_pat_...) of TechEngineBot with Pull requests: read and write."
exit 1 ;;
esac
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
if: steps.token.outputs.ok == 'true'
env:
BODY: ${{ needs.check.outputs.body }}
PR: ${{ github.event.pull_request.number }}
with:
github-token: ${{ secrets.TECHENGINEBOT_TOKEN }}
script: |
const marker = '<!-- cpu-es-final-check -->';
// The check job ran the PR's code, so its report is untrusted here:
// drop every @ and break issue references and issue/PR links, so the
// comment can neither mention anyone nor cross-reference anything.
const zw = String.fromCharCode(0x200b); // zero-width space
const body = (process.env.BODY || '')
.replace(/@/g, '')
.replace(/(?<!&)#(?=\d+\b)/g, `#${zw}`)
.replace(/\bgh-(?=\d+\b)/gi, `$&${zw}`)
.replace(/\/(issues|pull|discussions)\/(?=\d)/g, `/$1/${zw}`)
.slice(0, 65000);
if (!body.startsWith(marker)) {
core.setFailed('The check job produced no report; nothing posted.');
return;
}
const { data: me } = await github.rest.users.getAuthenticated();
if (me.login !== 'TechEngineBot') {
core.setFailed(`TECHENGINEBOT_TOKEN belongs to ${me.login}, not TechEngineBot; nothing posted.`);
return;
}
const { owner, repo } = context.repo;
const issue_number = Number(process.env.PR);
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number, per_page: 100,
});
const mine = comments.find((c) => c.user?.login === me.login && c.body?.startsWith(marker));
if (!mine) {
const { data } = await github.rest.issues.createComment({ owner, repo, issue_number, body });
core.info(`Posted ${data.html_url}`);
} else if (mine.body !== body) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
core.info(`Updated ${mine.html_url}`);
} else {
core.info(`Unchanged: ${mine.html_url}`);
}