Repository navigation
ci: add label-triggered TechEngineBot final-check #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 -->'; | |
| const body = process.env.BODY || ''; | |
| 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}`); | |
| } |