Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/scripts/final_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Render TechEngineBot's final-check comment for a PR.

Reads the validator / test outcomes from the environment and summarises the
ES records the PR touches. Prints Markdown to stdout. Stdlib only.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
MARKER = "<!-- techenginebot:final-check -->"
ICON = {"success": "✅", "failure": "❌", "skipped": "⏭️", "cancelled": "⏹️"}


def git(*args: str) -> str:
return subprocess.run(["git", *args], cwd=ROOT, check=True, capture_output=True, text=True).stdout


def changed_records(base: str, head: str) -> list[tuple[str, str]]:
"""(status, path) for every data/cpu JSON the PR adds, modifies, or removes."""
out = git("diff", "--name-status", "--no-renames", f"{base}...{head}", "--", "data/cpu")
rows = []
for line in out.splitlines():
status, path = line.split("\t", 1)
if path.endswith(".json"):
rows.append((status[0], path))
return rows


def describe(path: str) -> str:
try:
rec = json.loads((ROOT / path).read_text(encoding="utf-8-sig"))
except (OSError, ValueError) as exc:
return f"`{path}` (unreadable: {exc})"
ident = rec.get("qspec") or rec.get("opn") or "—"
sources = len(rec.get("source_urls") or [])
return (
f"`{rec.get('slug')}` · {ident} · {rec.get('architecture')} · "
f"{rec.get('cores')}C/{rec.get('threads')}T · class `{rec.get('sample_class')}` · "
f"{sources} source(s)"
)


def main() -> int:
validate = os.environ.get("VALIDATE_OUTCOME", "skipped")
tests = os.environ.get("TESTS_OUTCOME", "skipped")
base = os.environ["BASE_SHA"]
head = os.environ["HEAD_SHA"]
passed = validate == "success" and tests == "success"

rows = changed_records(base, head)
added = [p for s, p in rows if s == "A"]
modified = [p for s, p in rows if s == "M"]
removed = [p for s, p in rows if s == "D"]

lines = [
MARKER,
"## Final check",
"",
"**✅ Ready to merge**" if passed else "**❌ Not ready — fix the failing checks below**",
"",
f"- {ICON.get(validate, '❔')} `python -m app.validate`",
f"- {ICON.get(tests, '❔')} `python -m pytest -q`",
"",
f"**Records:** +{len(added)} added · ~{len(modified)} modified · −{len(removed)} removed",
]
for title, paths in (("Added", added), ("Modified", modified)):
if paths:
lines += ["", f"<details><summary>{title} ({len(paths)})</summary>", ""]
lines += [f"- {describe(p)}" for p in paths]
lines += ["", "</details>"]
if removed:
lines += ["", f"<details><summary>Removed ({len(removed)})</summary>", ""]
lines += [f"- `{p}`" for p in removed]
lines += ["", "</details>"]
lines += ["", f"<sub>Checked at {head[:7]}. This comment is updated in place on every push.</sub>"]
print("\n".join(lines))
return 0


if __name__ == "__main__":
sys.exit(main())
70 changes: 70 additions & 0 deletions .github/workflows/final-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: final-check

# TechEngineBot leaves one final-check comment on important PRs:
# releases into main, or any PR labeled `final-check`. The comment is
# edited in place on each push, never duplicated.

on:
pull_request:
branches: [develop, main]
types: [opened, synchronize, reopened, ready_for_review, labeled]

permissions:
contents: read

concurrency:
group: final-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
final-check:
if: >-
!github.event.pull_request.draft &&
(github.base_ref == 'main' || contains(github.event.pull_request.labels.*.name, 'final-check'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # diff against the base branch
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Validate ES catalog
id: validate
continue-on-error: true
run: python -m app.validate
- name: Tests
id: tests
continue-on-error: true
run: |
python -m pip install pytest
python -m pytest -q
- name: Render comment
env:
VALIDATE_OUTCOME: ${{ steps.validate.outcome }}
TESTS_OUTCOME: ${{ steps.tests.outcome }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python .github/scripts/final_check.py > final-check.md
- name: Post as TechEngineBot
env:
GH_TOKEN: ${{ secrets.TECHENGINEBOT_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
shell: bash # -eo pipefail: a failed lookup must stop here, not feed an error body into PATCH
run: |
# Bot account only: never fall back to a personal token.
if [ -z "$GH_TOKEN" ]; then
echo "::warning::TECHENGINEBOT_TOKEN is not set; skipping the comment"
exit 0
fi
id=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \
--jq '.[] | select(.user.login == "TechEngineBot" and (.body | startswith("<!-- techenginebot:final-check -->"))) | .id' | head -n1)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$id" -F body=@final-check.md > /dev/null
else
gh api "repos/$REPO/issues/$PR/comments" -F body=@final-check.md > /dev/null
fi
- name: Fail on failed checks
if: steps.validate.outcome != 'success' || steps.tests.outcome != 'success'
run: exit 1
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ Crawlers come later. This classifier is the gate.

A release is a PR from `develop` to `main`. Open work PRs **against `develop`**.

## Final check

Important PRs get one final-check comment from **TechEngineBot**: every release PR into `main`, and any PR labeled `final-check`. The comment reports the validator and test results plus the records the PR adds, modifies, or removes. It is edited in place on each push, and the job fails when a check fails. The comment is posted with the `TECHENGINEBOT_TOKEN` secret (a TechEngineBot token, never a personal one); without it the job still runs and skips the comment.

## Contributing

Open a PR against `develop` with a new/updated JSON file. `sample_class` must be `es`. Include `qspec` (Intel) and/or `opn` (AMD) and at least one public `source_url`.
Expand Down
Loading