Skip to content

Commit 55ba700

Browse files
committed
ci(final-check): add TechEngineBot's final check on labeled PRs
A maintainer adds the `final-check` label to an important PR into main. The check job runs test.yml's checks (ruff, mypy, the seed-data validator, pytest with coverage) with a read-only token and no secrets, and writes a report to the job summary. The comment job, same-repo PRs only, posts or edits ONE TechEngineBot comment with that report. It refuses a token that is not a fine-grained github_pat_ token or not TechEngineBot's. This replaces Copilot as the reviewer; it is the same shape as TechAPI's final-check.
1 parent 3c50cb8 commit 55ba700

4 files changed

Lines changed: 224 additions & 4 deletions

File tree

‎.github/workflows/final-check.yml‎

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
name: final-check
2+
3+
# A maintainer adds the `final-check` label to an important PR. The check job
4+
# runs what test.yml runs (ruff, mypy, the seed-data validator, pytest with
5+
# coverage) on the PR, and TechEngineBot leaves ONE comment with the verdict;
6+
# adding the label again edits that comment. This is the only place the
7+
# TechEngineBot account is used in this repo, and it replaces Copilot reviews.
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 any
12+
# other kind of token or account. Fork PRs never get a comment.
13+
on:
14+
pull_request:
15+
types: [labeled]
16+
branches: [main]
17+
18+
permissions: {}
19+
20+
jobs:
21+
check:
22+
if: github.event.label.name == 'final-check'
23+
runs-on: ubuntu-24.04
24+
# test.yml takes about 10-15 minutes.
25+
timeout-minutes: 45
26+
permissions:
27+
contents: read
28+
concurrency:
29+
group: final-check-${{ github.event.pull_request.number }}
30+
cancel-in-progress: true
31+
outputs:
32+
body: ${{ steps.report.outputs.body }}
33+
env:
34+
PYTHONIOENCODING: utf-8
35+
TECHAPI_DATA_DIR: ${{ github.workspace }}/TechAPI/data
36+
steps:
37+
# The merge commit, as main would look after merging.
38+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39+
with:
40+
persist-credentials: false
41+
42+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43+
with:
44+
repository: GetTechAPI/TechAPI
45+
path: TechAPI
46+
persist-credentials: false
47+
48+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
49+
with:
50+
python-version: "3.12"
51+
52+
- name: Install dependencies
53+
id: install
54+
continue-on-error: true
55+
run: |
56+
status=0
57+
python -m pip install --disable-pip-version-check -e ".[dev]" > "$RUNNER_TEMP/install.log" 2>&1 || status=$?
58+
tail -n 40 "$RUNNER_TEMP/install.log"
59+
exit "$status"
60+
61+
- name: Lint (ruff)
62+
id: ruff
63+
if: steps.install.outcome == 'success'
64+
continue-on-error: true
65+
run: |
66+
status=0
67+
ruff check app tests > "$RUNNER_TEMP/ruff.log" 2>&1 || status=$?
68+
cat "$RUNNER_TEMP/ruff.log"
69+
exit "$status"
70+
71+
- name: Type check (mypy)
72+
id: mypy
73+
if: steps.install.outcome == 'success'
74+
continue-on-error: true
75+
run: |
76+
status=0
77+
mypy app > "$RUNNER_TEMP/mypy.log" 2>&1 || status=$?
78+
cat "$RUNNER_TEMP/mypy.log"
79+
exit "$status"
80+
81+
- name: Validate seed data
82+
id: validate
83+
if: steps.install.outcome == 'success'
84+
continue-on-error: true
85+
run: |
86+
status=0
87+
python -m app.validate > "$RUNNER_TEMP/validate.log" 2>&1 || status=$?
88+
tail -n 40 "$RUNNER_TEMP/validate.log"
89+
exit "$status"
90+
91+
- name: Tests + coverage (fail under 60%)
92+
id: pytest
93+
if: steps.install.outcome == 'success'
94+
continue-on-error: true
95+
run: |
96+
status=0
97+
pytest --cov=app --cov-report=term --cov-fail-under=60 > "$RUNNER_TEMP/pytest.log" 2>&1 || status=$?
98+
tail -n 60 "$RUNNER_TEMP/pytest.log"
99+
exit "$status"
100+
101+
# The comment quotes tool output, which can carry text from the PR: logs
102+
# stay inside code fences with backticks and @ removed, so they can
103+
# neither break out into markdown nor mention anyone.
104+
- name: Report
105+
id: report
106+
if: always()
107+
env:
108+
INSTALL: ${{ steps.install.outcome }}
109+
RUFF: ${{ steps.ruff.outcome }}
110+
MYPY: ${{ steps.mypy.outcome }}
111+
VALIDATE: ${{ steps.validate.outcome }}
112+
PYTEST: ${{ steps.pytest.outcome }}
113+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
114+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
115+
run: |
116+
clean() { tr -d '`@' | cut -c1-300; }
117+
fence() { # fence <log> <lines>
118+
echo '```text'
119+
tail -n "$2" "$1" 2>/dev/null | clean
120+
echo '```'
121+
}
122+
verdict='✅ Final check passed'
123+
[ "$INSTALL/$RUFF/$MYPY/$VALIDATE/$PYTEST" = success/success/success/success/success ] \
124+
|| verdict='❌ Final check failed'
125+
{
126+
echo '<!-- techengine-final-check -->'
127+
echo "### $verdict"
128+
echo
129+
echo "Head \`${HEAD_SHA::7}\`, checked as merged into the base · [run]($RUN_URL)"
130+
echo
131+
echo '| Step | Result |'
132+
echo '| --- | --- |'
133+
echo "| Install | ${INSTALL:-skipped} |"
134+
echo "| Lint (ruff) | ${RUFF:-skipped} |"
135+
echo "| Type check (mypy) | ${MYPY:-skipped} |"
136+
echo "| Validate seed data | ${VALIDATE:-skipped} |"
137+
echo "| Tests + coverage (60%) | ${PYTEST:-skipped} |"
138+
if [ -s "$RUNNER_TEMP/pytest.log" ]; then
139+
echo
140+
echo '<details><summary>pytest summary</summary>'
141+
echo
142+
fence "$RUNNER_TEMP/pytest.log" 5
143+
echo '</details>'
144+
fi
145+
[ "$INSTALL" = failure ] && { echo; echo '<details><summary>Install log</summary>'; echo; fence "$RUNNER_TEMP/install.log" 20; echo '</details>'; }
146+
[ "$RUFF" = failure ] && { echo; echo '<details><summary>ruff log</summary>'; echo; fence "$RUNNER_TEMP/ruff.log" 30; echo '</details>'; }
147+
[ "$MYPY" = failure ] && { echo; echo '<details><summary>mypy log</summary>'; echo; fence "$RUNNER_TEMP/mypy.log" 30; echo '</details>'; }
148+
[ "$VALIDATE" = failure ] && { echo; echo '<details><summary>Validation log</summary>'; echo; fence "$RUNNER_TEMP/validate.log" 30; echo '</details>'; }
149+
[ "$PYTEST" = failure ] && { echo; echo '<details><summary>pytest log</summary>'; echo; fence "$RUNNER_TEMP/pytest.log" 40; echo '</details>'; }
150+
true
151+
} > "$RUNNER_TEMP/body.md"
152+
cat "$RUNNER_TEMP/body.md" >> "$GITHUB_STEP_SUMMARY"
153+
delim="BODY_$(openssl rand -hex 16)"
154+
{ echo "body<<$delim"; cat "$RUNNER_TEMP/body.md"; echo "$delim"; } >> "$GITHUB_OUTPUT"
155+
[ "$verdict" = '✅ Final check passed' ]
156+
157+
comment:
158+
needs: check
159+
if: >-
160+
always() && github.event.label.name == 'final-check' &&
161+
(needs.check.result == 'success' || needs.check.result == 'failure') &&
162+
github.event.pull_request.head.repo.full_name == github.repository
163+
runs-on: ubuntu-24.04
164+
timeout-minutes: 5
165+
permissions: {}
166+
steps:
167+
- name: Check the token
168+
id: token
169+
env:
170+
TOKEN: ${{ secrets.TECHENGINEBOT_TOKEN }}
171+
run: |
172+
if [ -z "$TOKEN" ]; then
173+
echo "::warning::TECHENGINEBOT_TOKEN is not set; the verdict is in the check job's summary only."
174+
echo "ok=false" >> "$GITHUB_OUTPUT"
175+
exit 0
176+
fi
177+
case "$TOKEN" in
178+
github_pat_*) echo "ok=true" >> "$GITHUB_OUTPUT" ;;
179+
*) echo "::error::TECHENGINEBOT_TOKEN must be a fine-grained token (github_pat_...) of TechEngineBot with Pull requests: read and write."
180+
exit 1 ;;
181+
esac
182+
183+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
184+
if: steps.token.outputs.ok == 'true'
185+
env:
186+
BODY: ${{ needs.check.outputs.body }}
187+
PR: ${{ github.event.pull_request.number }}
188+
with:
189+
github-token: ${{ secrets.TECHENGINEBOT_TOKEN }}
190+
script: |
191+
const marker = '<!-- techengine-final-check -->';
192+
const body = process.env.BODY || '';
193+
if (!body.startsWith(marker)) {
194+
core.setFailed('The check job produced no report; nothing posted.');
195+
return;
196+
}
197+
const { data: me } = await github.rest.users.getAuthenticated();
198+
if (me.login !== 'TechEngineBot') {
199+
core.setFailed(`TECHENGINEBOT_TOKEN belongs to ${me.login}, not TechEngineBot; nothing posted.`);
200+
return;
201+
}
202+
const { owner, repo } = context.repo;
203+
const issue_number = Number(process.env.PR);
204+
const comments = await github.paginate(github.rest.issues.listComments, {
205+
owner, repo, issue_number, per_page: 100,
206+
});
207+
const mine = comments.find((c) => c.user?.login === me.login && c.body?.startsWith(marker));
208+
if (!mine) {
209+
const { data } = await github.rest.issues.createComment({ owner, repo, issue_number, body });
210+
core.info(`Posted ${data.html_url}`);
211+
} else if (mine.body !== body) {
212+
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
213+
core.info(`Updated ${mine.html_url}`);
214+
} else {
215+
core.info(`Unchanged: ${mine.html_url}`);
216+
}

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ TechAPI/ # submodule -> GetTechAPI/TechAPI (clickable @ <sha> link
4040
ingest.yml # workflow_call: draft new SKUs -> one PR on TechAPI
4141
refresh.yml # workflow_call: re-scrape benchmarks -> one PR on TechAPI
4242
verify-promote.yml # workflow_call: Tier 1-3 checks + verified promotion PR
43+
final-check.yml # `final-check` label: test.yml's checks, one TechEngineBot comment
4344
```
4445

4546
## How The Two Repos Connect
4647

4748
Both repos live in the **GetTechAPI** org and each includes the other as a git
4849
**submodule** (a clickable `@ <sha>` pin). Nothing syncs them automatically.
4950

50-
No workflow here runs on a schedule, posts comments, or uses a personal or
51-
bot-account token (ADR-017). Collection runs only when a maintainer dispatches
51+
No workflow here runs on a schedule or uses a personal access token (ADR-017).
52+
The one comment any workflow posts is TechEngineBot's final check, and only
53+
when a maintainer adds the `final-check` label to a PR (`final-check.yml`). Collection runs only when a maintainer dispatches
5254
TechAPI's `collect.yml`, which calls `ingest.yml`, `refresh.yml` or
5355
`verify-promote.yml` with TechAPI's own `GITHUB_TOKEN`. Each task keeps at most
5456
one open PR on a fixed `collect/*` branch, updated in place, after

‎docs/DEVELOPMENT.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ scripts/
6161
dump.py DB → static JSON dump (replays API in-process) → ./dump
6262
tests/ unit/ + integration/ (conftest seeds a temp SQLite from data/)
6363
docs/ SPEC.md, DATA_PIPELINE.md, DEVELOPMENT.md
64-
.github/workflows/ test.yml, validate-data.yml, deploy-pages.yml, coverage-report.yml, and
64+
.github/workflows/ test.yml, validate-data.yml, deploy-pages.yml, coverage-report.yml,
65+
final-check.yml (`final-check` label -> one TechEngineBot comment), and
6566
the manual PR generators TechAPI's collect.yml calls: ingest.yml,
6667
refresh.yml, verify-promote.yml (nothing scheduled; ADR-017)
6768
```

‎docs/SPEC.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@ chore: bump fastapi to 0.115
14551455
| `coverage-report.yml` | 수동 | 커버리지 갭 리포트를 Job Summary와 아티팩트로 (이슈 작성 없음) |
14561456
| `ingest.yml` | `workflow_call` (TechAPI `collect.yml`, 수동) | 신규 SKU 수집 → `collect/ingest-<category>` PR 1개 |
14571457
| `refresh.yml` | `workflow_call` (TechAPI `collect.yml`, 수동) | 벤치마크 재수집 → `collect/refresh` PR 1개 (덤프 없음) |
1458+
| `final-check.yml` | PR에 `final-check` 라벨 (`main` 대상) | `test.yml`과 같은 검사(ruff, mypy, 시드 검증, pytest) 후 TechEngineBot 댓글 1개 |
14581459
| `verify-promote.yml` | `workflow_call` (TechAPI `collect.yml`, 수동, `apply` 입력) | Tier 1-3 검증 + `verified` 승급 → `collect/promote` PR 1개 |
14591460

14601461
모든 action은 GitHub 소유 action만, 전체 커밋 SHA로 고정한다.
@@ -2004,7 +2005,7 @@ TechAPI의 **핵심 목표 중 하나**. TechPicks 외에 다양한 앱·플랫
20042005
3. 레포 간 `repository_dispatch` 중계와 서브모듈 자동 bump(`notify-*`, `bump-*`)는 없앤다.
20052006
4. `verify-status`는 TechAPI `dump-develop`에 합치고, `dump-refresh`는 `dump-develop`의 수동 전체 재생성으로 대체한다. 커버리지 리포트는 주 1회, TechEngine 레포 이슈만 갱신한다.
20062007
5. 스크레이핑 워크플로(`weekly-data.yml`)는 수동 실행으로 검증한 뒤에 cron을 켠다.
2007-
6. **예외 — TechEngineBot 최종 확인**: TechEngineBot 계정은 중요한 PR의 최종 확인자 역할로만 쓴다. 메인테이너가 PR에 `final-check` 라벨을 붙일 때만 TechAPI `final-check.yml`이 돌고, PR 하나에 댓글 하나(재실행 시 같은 댓글 수정)만 남긴다. 조회는 `GITHUB_TOKEN`으로 하고, `TECHENGINEBOT_TOKEN`(TechAPI 레포 한정 fine-grained 토큰, Pull requests: write)은 그 댓글에만 쓴다. 스케줄·push·PR 이벤트로 TechEngineBot이 움직이는 워크플로는 만들지 않는다.
2008+
6. **예외 — TechEngineBot 최종 확인**: TechEngineBot 계정은 중요한 PR의 최종 확인자 역할로만 쓴다. 메인테이너가 PR에 `final-check` 라벨을 붙일 때만 각 레포의 `final-check.yml`(TechAPI, TechEngine)이 돌고, PR 하나에 댓글 하나(재실행 시 같은 댓글 수정)만 남긴다. 검사 작업은 시크릿 없이 읽기 전용 토큰으로 돌고, `TECHENGINEBOT_TOKEN`(TechEngineBot의 fine-grained 토큰, Pull requests: read and write)은 그 댓글 작업에만 쓴다. Copilot 리뷰는 쓰지 않는다. 스케줄·push·PR 이벤트로 TechEngineBot이 움직이는 워크플로는 만들지 않는다.
20082009
- **개정 (2026-11-01)**: TechAPI는 PR 검증을 자체 검증기로 직접 돌리므로, 2·4·5번을 다음으로 대체한다.
20092010
- PR 댓글 워크플로(`techapi-pr-validation-comment`, `techapi-verify-comment`)와 `refresh-data`를 삭제한다. `coverage-report`는 수동 실행·읽기 전용으로 바꾼다.
20102011
- 수집·검증(`weekly-ingest`, `weekly-refresh`, `verify-network`)은 `ingest.yml`, `refresh.yml`, `verify-promote.yml`로 바꾼다. TechAPI `collect.yml`이 `workflow_dispatch`로만 호출하는 PR 생성기다. 작업마다 고정 브랜치(`collect/*`)에 열린 PR을 최대 1개만 두고 제자리에서 갱신한다. 댓글은 달지 않으며, 게이트(`app.validate`, `integrity_check.py --strict`) 결과와 Tier 0 보고서는 PR 본문에 남긴다. 덤프는 커밋하지 않는다. 실행되는 엔진은 TechAPI가 서브모듈로 고정한 TechEngine 커밋이다.

0 commit comments

Comments
 (0)