Skip to content

Commit 79ac4be

Browse files
committed
feat(template): add the label-triggered final check to new satellites
Both satellites run a final-check workflow and the template had none. The new one is game-catalog's with the repository and category filled in. The checklist now registers the repo in repos.json with its allowed secret and names the label and the token to set up, and the tests allow TECHENGINEBOT_TOKEN in final-check.yml only.
1 parent 94aa1f1 commit 79ac4be

3 files changed

Lines changed: 211 additions & 3 deletions

File tree

‎machine/new_satellite.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,15 @@ def checklist(v: dict[str, str]) -> str:
122122
3. gh api -X POST repos/{repo}/pages -f build_type=workflow
123123
4. gh api -X POST repos/{repo}/environments/github-pages/deployment-branch-policies -f name=main
124124
(skip this and every deploy fails with no log — game-catalog, 2026-09-17)
125-
5. add {{"name": "{repo}", "branches": ["develop", "main"]}} to TechMachine machine/repos.json
125+
5. add {{"name": "{repo}", "branches": ["develop", "main"],
126+
"allowed_secrets": ["TECHENGINEBOT_TOKEN"]}} to TechMachine machine/repos.json
126127
and the summary.json URL to its endpoints
127128
6. add {{ key: "{v['plural']}", label: "{v['plural']}", base: "https://gettechapi.github.io/{v['repo']}/" }}
128129
to SATELLITES in TechAPI site/src/scripts/techapi.js
130+
7. for the final check: create the `final-check` label and set the
131+
TECHENGINEBOT_TOKEN secret to a fine-grained token of TechEngineBot with
132+
Pull requests: read and write on {repo} only. Without the secret the
133+
verdict stays in the run summary and nothing is posted.
129134
"""
130135

131136

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
name: final-check
2+
3+
# A maintainer adds the `final-check` label to an important PR. The check job
4+
# runs, on the PR merged into its base, everything a push to develop/main
5+
# would: the full-catalog validator, the tests and the site build. TechEngineBot
6+
# leaves ONE comment with the verdict; adding the label again edits that
7+
# comment. This is the only place the TechEngineBot account is used here.
8+
#
9+
# The check job runs PR code, so it gets no secrets and a read-only token.
10+
# Only the comment job sees TECHENGINEBOT_TOKEN, which must be a fine-grained
11+
# token of TechEngineBot with Pull requests: read and write; the job refuses
12+
# any other kind of token or account. Fork PRs never get a comment.
13+
on:
14+
pull_request:
15+
types: [labeled]
16+
branches: [develop, main]
17+
18+
permissions: {}
19+
20+
jobs:
21+
check:
22+
if: github.event.label.name == 'final-check'
23+
runs-on: ubuntu-24.04
24+
timeout-minutes: 30
25+
permissions:
26+
contents: read
27+
concurrency:
28+
group: final-check-${{ github.event.pull_request.number }}
29+
cancel-in-progress: true
30+
outputs:
31+
body: ${{ steps.report.outputs.body }}
32+
steps:
33+
# The merge commit, as a push would build it after merging. Full history:
34+
# site/build.py replays every data commit for history.json.
35+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36+
with:
37+
fetch-depth: 0
38+
persist-credentials: false
39+
40+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
41+
with:
42+
python-version: "3.12"
43+
44+
- name: Validate the whole catalog
45+
id: validate
46+
continue-on-error: true
47+
run: |
48+
status=0
49+
python -m app.validate > "$RUNNER_TEMP/validate.log" 2>&1 || status=$?
50+
tail -n 40 "$RUNNER_TEMP/validate.log"
51+
exit "$status"
52+
53+
- name: Tests
54+
id: tests
55+
continue-on-error: true
56+
run: |
57+
status=0
58+
{ python -m pip install --disable-pip-version-check pytest && python -m pytest -q; } \
59+
> "$RUNNER_TEMP/tests.log" 2>&1 || status=$?
60+
tail -n 40 "$RUNNER_TEMP/tests.log"
61+
exit "$status"
62+
63+
- name: Build the site
64+
id: site
65+
continue-on-error: true
66+
run: |
67+
status=0
68+
python site/build.py > "$RUNNER_TEMP/site.log" 2>&1 || status=$?
69+
cat "$RUNNER_TEMP/site.log"
70+
exit "$status"
71+
72+
# The comment quotes tool output, which can carry text from the PR's
73+
# records: logs stay inside code fences with backticks and @ removed, so
74+
# they can neither break out into markdown nor mention anyone.
75+
- name: Report
76+
id: report
77+
if: always()
78+
env:
79+
VALIDATE: ${{ steps.validate.outcome }}
80+
TESTS: ${{ steps.tests.outcome }}
81+
SITE: ${{ steps.site.outcome }}
82+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
83+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
84+
run: |
85+
clean() { tr -d '`@' | cut -c1-300; }
86+
fence() { # fence <log> <lines>
87+
echo '```text'
88+
tail -n "$2" "$1" 2>/dev/null | clean
89+
echo '```'
90+
}
91+
verdict='✅ Final check passed'
92+
[ "$VALIDATE/$TESTS/$SITE" = success/success/success ] || verdict='❌ Final check failed'
93+
changed=$(git diff --name-only HEAD^1 HEAD -- data/{{category}} | grep -c '\.json$' || true)
94+
{
95+
echo '<!-- {{repo}}-final-check -->'
96+
echo "### $verdict"
97+
echo
98+
echo "Head \`${HEAD_SHA::7}\`, checked as merged into the base · ${changed:-0} changed record(s) · [run]($RUN_URL)"
99+
echo
100+
echo '| Step | Result |'
101+
echo '| --- | --- |'
102+
echo "| Validator, whole catalog | ${VALIDATE:-skipped} |"
103+
echo "| Tests | ${TESTS:-skipped} |"
104+
echo "| Site build (summary.json, history.json) | ${SITE:-skipped} |"
105+
[ "$VALIDATE" = failure ] && { echo; echo '<details><summary>Validator log</summary>'; echo; fence "$RUNNER_TEMP/validate.log" 30; echo '</details>'; }
106+
[ "$TESTS" = failure ] && { echo; echo '<details><summary>Test log</summary>'; echo; fence "$RUNNER_TEMP/tests.log" 30; echo '</details>'; }
107+
[ "$SITE" = failure ] && { echo; echo '<details><summary>Site build log</summary>'; echo; fence "$RUNNER_TEMP/site.log" 20; echo '</details>'; }
108+
true
109+
} > "$RUNNER_TEMP/body.md"
110+
cat "$RUNNER_TEMP/body.md" >> "$GITHUB_STEP_SUMMARY"
111+
delim="BODY_$(openssl rand -hex 16)"
112+
{ echo "body<<$delim"; cat "$RUNNER_TEMP/body.md"; echo "$delim"; } >> "$GITHUB_OUTPUT"
113+
[ "$verdict" = '✅ Final check passed' ]
114+
115+
comment:
116+
needs: check
117+
if: >-
118+
always() && github.event.label.name == 'final-check' &&
119+
(needs.check.result == 'success' || needs.check.result == 'failure') &&
120+
github.event.pull_request.head.repo.full_name == github.repository
121+
runs-on: ubuntu-24.04
122+
timeout-minutes: 5
123+
permissions: {}
124+
steps:
125+
- name: Check the token
126+
id: token
127+
env:
128+
TOKEN: ${{ secrets.TECHENGINEBOT_TOKEN }}
129+
run: |
130+
if [ -z "$TOKEN" ]; then
131+
echo "::warning::TECHENGINEBOT_TOKEN is not set; the verdict is in the check job's summary only."
132+
echo "ok=false" >> "$GITHUB_OUTPUT"
133+
exit 0
134+
fi
135+
case "$TOKEN" in
136+
github_pat_*) echo "ok=true" >> "$GITHUB_OUTPUT" ;;
137+
*) echo "::error::TECHENGINEBOT_TOKEN must be a fine-grained token (github_pat_...) of TechEngineBot with Pull requests: read and write."
138+
exit 1 ;;
139+
esac
140+
141+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
142+
if: steps.token.outputs.ok == 'true'
143+
env:
144+
BODY: ${{ needs.check.outputs.body }}
145+
PR: ${{ github.event.pull_request.number }}
146+
with:
147+
github-token: ${{ secrets.TECHENGINEBOT_TOKEN }}
148+
script: |
149+
const marker = '<!-- {{repo}}-final-check -->';
150+
// The check job ran the PR's code, so its report is untrusted here:
151+
// drop every @ and break issue references and issue/PR links, so the
152+
// comment can neither mention anyone nor cross-reference anything.
153+
const zw = String.fromCharCode(0x200b); // zero-width space
154+
const body = (process.env.BODY || '')
155+
.replace(/@/g, '')
156+
.replace(/(?<!&)#(?=\d+\b)/g, `#${zw}`)
157+
.replace(/\bgh-(?=\d+\b)/gi, `$&${zw}`)
158+
.replace(/\/(issues|pull|discussions)\/(?=\d)/g, `/$1/${zw}`)
159+
.slice(0, 65000);
160+
if (!body.startsWith(marker)) {
161+
core.setFailed('The check job produced no report; nothing posted.');
162+
return;
163+
}
164+
const { data: me } = await github.rest.users.getAuthenticated();
165+
if (me.login !== 'TechEngineBot') {
166+
core.setFailed(`TECHENGINEBOT_TOKEN belongs to ${me.login}, not TechEngineBot; nothing posted.`);
167+
return;
168+
}
169+
const { owner, repo } = context.repo;
170+
const issue_number = Number(process.env.PR);
171+
const comments = await github.paginate(github.rest.issues.listComments, {
172+
owner, repo, issue_number, per_page: 100,
173+
});
174+
const mine = comments.find((c) => c.user?.login === me.login && c.body?.startsWith(marker));
175+
if (!mine) {
176+
const { data } = await github.rest.issues.createComment({ owner, repo, issue_number, body });
177+
core.info(`Posted ${data.html_url}`);
178+
} else if (mine.body !== body) {
179+
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
180+
core.info(`Updated ${mine.html_url}`);
181+
} else {
182+
core.info(`Unchanged: ${mine.html_url}`);
183+
}

‎tests/test_new_satellite.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,29 @@ def test_unknown_placeholder_is_an_error():
6060
render("{{nope}}", {})
6161

6262

63-
def test_generated_workflows_use_only_the_builtin_token(generated: Path):
63+
# TechEngineBot's token is allowed in one place: the label-triggered final check.
64+
BOT_SECRET = {"final-check.yml": {"TECHENGINEBOT_TOKEN"}}
65+
66+
67+
def test_generated_workflows_use_the_bot_token_only_in_final_check(generated: Path):
6468
from machine.health import foreign_secrets
6569
workflows = sorted((generated / ".github" / "workflows").glob("*.yml"))
6670
assert workflows
6771
for path in workflows:
68-
assert foreign_secrets(path.read_text(encoding="utf-8")) == [], path
72+
text = path.read_text(encoding="utf-8")
73+
assert foreign_secrets(text, BOT_SECRET.get(path.name, set())) == [], path
74+
final_check = (generated / ".github" / "workflows" / "final-check.yml").read_text(encoding="utf-8")
75+
assert foreign_secrets(final_check) == ["TECHENGINEBOT_TOKEN"]
76+
77+
78+
def test_generated_final_check_is_named_after_the_repo(generated: Path):
79+
text = (generated / ".github" / "workflows" / "final-check.yml").read_text(encoding="utf-8")
80+
assert text.count("<!-- demo-catalog-final-check -->") == 2
81+
assert "data/demo " in text
82+
83+
84+
def test_checklist_registers_the_repo_with_the_bot_secret():
85+
from machine.new_satellite import checklist
86+
text = checklist({"repo": "demo-catalog", "plural": "demos"})
87+
assert '"allowed_secrets": ["TECHENGINEBOT_TOKEN"]' in text
88+
assert "final-check" in text

0 commit comments

Comments
 (0)