From a31a8cf5806e9b807ce5b7a41894ed1677eb4054 Mon Sep 17 00:00:00 2001 From: Graffioh Date: Wed, 23 Sep 2026 14:01:01 +0000 Subject: [PATCH 1/2] ci(commits): check conventional commit style and dispatch the compaction bot Commit style (commit-style.yml) checks every non-merge commit in a pull request. Subjects must look like `type(scope): summary`, with a type from CONTRIBUTING.md plus build and revert and a lowercase scope. They must be at most 100 characters with no trailing period, and a blank line must separate the subject from the body. GitHub's `Revert "..."` subjects pass as they are. Compact commits (compact-commits.yml) forwards a pull request to the commit compaction bot when a maintainer adds the `compact-commits` label; it checks out and runs nothing. The bot folds the commits into a few conventional commits without changing the code, validates the messages with this repository's check_commit_messages.py, force-pushes, and comments the old-to-new mapping. It never merges. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/ci/check_commit_messages.py | 107 +++++++++++++++++++++++ .github/ci/test_check_commit_messages.py | 40 +++++++++ .github/workflows/ci.yml | 5 ++ .github/workflows/commit-style.yml | 29 ++++++ .github/workflows/compact-commits.yml | 40 +++++++++ 5 files changed, 221 insertions(+) create mode 100644 .github/ci/check_commit_messages.py create mode 100644 .github/ci/test_check_commit_messages.py create mode 100644 .github/workflows/commit-style.yml create mode 100644 .github/workflows/compact-commits.yml diff --git a/.github/ci/check_commit_messages.py b/.github/ci/check_commit_messages.py new file mode 100644 index 000000000..9ec13230a --- /dev/null +++ b/.github/ci/check_commit_messages.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Check that a pull request's commit messages follow the conventional commit style. + + type(scope): summary + + Optional body, separated by a blank line. + +Types follow CONTRIBUTING.md plus `build` and `revert`, which main already uses. +The scope is required and lowercase. Merge commits and GitHub's `Revert "..."` +subjects are accepted as they are. + +Usage: check_commit_messages.py --base origin/main --head HEAD +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys + +TYPES = ( + "feat", + "fix", + "perf", + "refactor", + "docs", + "test", + "bench", + "build", + "ci", + "chore", + "revert", +) +SUBJECT = re.compile(rf"^(?:{'|'.join(TYPES)})\([a-z0-9._/,-]+\)!?: \S") +GITHUB_REVERT = re.compile(r'^Revert ".+"$') +MAX_SUBJECT = 100 + + +def problems(message: str) -> list[str]: + lines = message.rstrip("\n").split("\n") + subject = lines[0] + if GITHUB_REVERT.match(subject): + return [] + found = [] + if not SUBJECT.match(subject): + found.append( + "the subject must look like `type(scope): summary` with type one of " + + ", ".join(TYPES) + + " and a required lowercase scope" + ) + if len(subject) > MAX_SUBJECT: + found.append(f"the subject is {len(subject)} characters (at most {MAX_SUBJECT})") + if subject.endswith("."): + found.append("the subject ends with a period") + if len(lines) > 1 and lines[1].strip(): + found.append("a blank line must separate the subject from the body") + return found + + +def git(*args: str) -> str: + return subprocess.run(["git", *args], check=True, capture_output=True, text=True).stdout + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument("--base", required=True) + parser.add_argument("--head", required=True) + args = parser.parse_args(argv) + + base = git("merge-base", args.base, args.head).strip() + shas = git("rev-list", "--reverse", "--no-merges", f"{base}..{args.head}").split() + bad = 0 + report = ["| Commit | Subject | Problem |", "|---|---|---|"] + for sha in shas: + message = git("log", "-1", "--format=%B", sha) + subject = message.split("\n", 1)[0] + for problem in problems(message): + bad += 1 + print(f"::error title=Commit {sha[:10]}::{subject}: {problem}") + report.append(f"| `{sha[:10]}` | {subject.replace('|', '/')} | {problem} |") + + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if bad: + text = "\n".join( + [ + f"## Commit style: {bad} problem(s)", + "", + *report, + "", + "Fix them with `git rebase -i` (reword), or add the `compact-commits` label: the bot " + "folds the commits and rewrites every message in this style.", + ] + ) + print(text) + else: + text = f"## Commit style: all {len(shas)} commit(s) OK" + print(text) + if summary: + with open(summary, "a") as fh: + fh.write(text + "\n") + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/ci/test_check_commit_messages.py b/.github/ci/test_check_commit_messages.py new file mode 100644 index 000000000..c5514c621 --- /dev/null +++ b/.github/ci/test_check_commit_messages.py @@ -0,0 +1,40 @@ +"""Tests for check_commit_messages.py.""" + +from __future__ import annotations + +import pytest +from check_commit_messages import problems + + +@pytest.mark.parametrize( + "message", + [ + "feat(ds4): add sparse prefill", + "fix(server): clamp the rollback window", + "perf(qwen35/gdn): fuse the gate", + "refactor(server,harness)!: rename engine\n\nBody text.\n", + "docs(contributing): explain the compact label\n\nCo-Authored-By: X \n", + 'Revert "feat(ds4): add sparse prefill"', + ], +) +def test_accepts(message: str) -> None: + assert problems(message) == [] + + +@pytest.mark.parametrize( + ("message", "expected"), + [ + ("Add sparse prefill", "type(scope): summary"), + ("feature(ds4): add sparse prefill", "type(scope): summary"), + ("feat(DS4): add sparse prefill", "lowercase scope"), + ("feat(ds4):add sparse prefill", "type(scope): summary"), + ("fixup! feat(ds4): add sparse prefill", "type(scope): summary"), + ("fix: clamp the rollback window", "required lowercase scope"), + ("ci(): empty scope", "required lowercase scope"), + ("fix(server): clamp the rollback window.", "ends with a period"), + ("fix(server): " + "x" * 100, "at most 100"), + ("fix(server): clamp\nbody without a blank line", "blank line"), + ], +) +def test_rejects(message: str, expected: str) -> None: + assert any(expected in p for p in problems(message)) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0559f3cc1..ae4389afe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,11 @@ jobs: - name: Test DS4 benchmark tools run: uv run --frozen --extra dev pytest -q harness/tests/test_ds4_benchmark_tools.py + - name: Lint and test CI helper scripts + run: | + find .github/ci -name '*.py' -print0 | xargs -0 uv run --frozen --extra dev ruff check + uv run --frozen --extra dev pytest -q .github/ci + build: name: Build (cmake + uv sync --extra megakernel) runs-on: ubuntu-latest diff --git a/.github/workflows/commit-style.yml b/.github/workflows/commit-style.yml new file mode 100644 index 000000000..7cefa4cb7 --- /dev/null +++ b/.github/workflows/commit-style.yml @@ -0,0 +1,29 @@ +name: Commit style + +# Every non-merge commit in a pull request must follow the conventional commit +# style: `type(scope): summary`. See .github/ci/check_commit_messages.py for the +# rules. The `compact-commits` label (compact-commits.yml) rewrites messages to match. + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + commit-style: + name: Commit messages + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check commit messages + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: python3 .github/ci/check_commit_messages.py --base "$BASE" --head "$HEAD" diff --git a/.github/workflows/compact-commits.yml b/.github/workflows/compact-commits.yml new file mode 100644 index 000000000..07595b087 --- /dev/null +++ b/.github/workflows/compact-commits.yml @@ -0,0 +1,40 @@ +name: Compact commits + +# Adding the `compact-commits` label to a pull request asks the commit compaction +# bot to fold its commits into a few conventional commits before merge. The bot +# never merges and never changes the code: it only rewrites the branch's history, +# then comments the old-to-new commit mapping and removes the label. +# +# This workflow only forwards the request; it checks out and runs nothing. + +on: + pull_request_target: + types: [labeled] + +permissions: {} + +jobs: + dispatch: + if: github.event.label.name == 'compact-commits' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Hand the pull request to the compaction bot + env: + GH_TOKEN: ${{ secrets.LUCEBOX_BOTS_DISPATCH_TOKEN }} + BOT_REPO: ${{ secrets.LUCEBOX_BOTS_DISPATCH_REPO }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + SENDER: ${{ github.event.sender.login }} + run: | + set -euo pipefail + if [ -z "$GH_TOKEN" ] || [ -z "$BOT_REPO" ]; then + echo "::error::LUCEBOX_BOTS_DISPATCH_TOKEN or LUCEBOX_BOTS_DISPATCH_REPO is not set" + exit 1 + fi + gh api "repos/$BOT_REPO/dispatches" \ + -f event_type=compact-commits \ + -f "client_payload[repository]=$REPO" \ + -F "client_payload[pr]=$PR" \ + -f "client_payload[sender]=$SENDER" >/dev/null + echo "Sent PR #$PR to the compaction bot." >> "$GITHUB_STEP_SUMMARY" From 1c7be1eb605f5792f5b6c7a48f192583b7ffb920 Mon Sep 17 00:00:00 2001 From: Graffioh Date: Wed, 23 Sep 2026 13:16:32 +0000 Subject: [PATCH 2/2] docs(contributing): explain the commit style check and compact-commits label Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd3213c13..2abd1691f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,19 @@ fix(dflash): clamp int8 DeltaNet state update before dequant docs(hub): add DVFS methodology link ``` -Allowed types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `bench`, `chore`, `ci`. +Allowed types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `bench`, `build`, `chore`, `ci`, `revert`. +The scope is required and lowercase, and the subject stays under 100 characters with no +trailing period. CI checks every commit in a pull request +([`commit-style.yml`](.github/workflows/commit-style.yml)). + +## Compacting commits before merge + +When a pull request is ready, a maintainer can add the `compact-commits` label. A bot then +folds a pull request with 2 or more commits into a few conventional commits (usually +one per type), checks the code is unchanged, and force-pushes them to the branch. It +never merges: press Merge as usual once CI passes on the new head. On a fork, keep +"Allow edits by maintainers" ticked. See +[`compact-commits.yml`](.github/workflows/compact-commits.yml). ## Hardware access