Skip to content
Merged
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
107 changes: 107 additions & 0 deletions .github/ci/check_commit_messages.py
Original file line number Diff line number Diff line change
@@ -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())
40 changes: 40 additions & 0 deletions .github/ci/test_check_commit_messages.py
Original file line number Diff line number Diff line change
@@ -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 <x@y.z>\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))
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/commit-style.yml
Original file line number Diff line number Diff line change
@@ -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"
40 changes: 40 additions & 0 deletions .github/workflows/compact-commits.yml
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading