ci: enforce the commit format CLAUDE.md said was enforced
CLAUDE.md has claimed since the file was written that commitlint gates the commit format in CI and that semantic-release runs off it. There was no commitlint config, no workflow running one, and nothing invoking .releaserc.yml — so the first thing every contributor and every agent reads about this repo was false in two places. scripts/commit-check.sh is the smaller honest answer: the grammar is one regex, and commitlint would mean a Node dependency tree at the root of a Go repo to run it. It is a commit-msg hook locally and a CI step over every commit in a push, and its type list is .releaserc.yml's so the check and the release rules cannot drift. The semantic-release half is recorded as configured-but-not-wired rather than implied to run.
This commit is contained in:
@@ -88,6 +88,24 @@ jobs:
|
||||
# has to be willing to operate on a directory it does not own.
|
||||
git config --global --add safe.directory /src
|
||||
|
||||
# Conventional Commits. `.releaserc.yml` has always derived the
|
||||
# version from the commit type; until now nothing checked that the
|
||||
# type was one it recognises, so a malformed subject silently meant
|
||||
# "no release". BEFORE is the push's previous tip and is absent or
|
||||
# all-zeros for a new branch, in which case only the tip is linted.
|
||||
- name: Commit messages
|
||||
working-directory: /src
|
||||
env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ -n "${BEFORE:-}" ] && [ "${BEFORE#0000000}" = "$BEFORE" ] \
|
||||
&& git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
|
||||
make commit-check RANGE="$BEFORE..$SHA"
|
||||
else
|
||||
make commit-check
|
||||
fi
|
||||
|
||||
- name: Go toolchain
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
@@ -128,6 +128,11 @@ first four are lefthook hooks, so skipping them locally only defers the
|
||||
failure; the typecheck is a hook too but only CI runs it over the test
|
||||
tree, which is where it has actually broken.
|
||||
|
||||
The **message** is gated too: `make commit-check` (a `commit-msg` hook,
|
||||
and a CI step over every commit in a push) rejects a subject that is not
|
||||
`type(scope): subject`, is over 72 chars, or ends with a period. A
|
||||
`--no-verify` commit skips it locally and meets it in CI.
|
||||
|
||||
Two things about the e2e tier that are not obvious until they bite.
|
||||
**The 36 specs share one backend process in file order**, so a spec
|
||||
that leaves the app somewhere passes alone and fails the suite — leave
|
||||
|
||||
@@ -41,6 +41,7 @@ make ui-visual # Same, including toMatchScreenshot comparisons
|
||||
make ui-setup # Install the Vitest provider's own Chromium (once)
|
||||
make bindings-check # Fail if frontend/wailsjs is stale vs the Go bindings
|
||||
make skill-check # Fail if .pi/ documents a make target that doesn't exist
|
||||
make commit-check # Fail if a commit subject is not a Conventional Commit
|
||||
make lint # golangci-lint v2 (strict), all three build configurations
|
||||
make test # All tests with race detector, all three build configurations
|
||||
make vulncheck # govulncheck for CVEs
|
||||
@@ -636,7 +637,21 @@ Pre-commit hooks verify generated code is fresh — always run `make generate` a
|
||||
|
||||
- **Go**: golangci-lint v2 with strict linters including `err113` (static errors), `nlreturn`, `wsl_v5` (whitespace), `godot` (comment periods), `sloglint`, `perfsprint`. Imports grouped: stdlib → third-party → `yellowjacket/...` (enforced by gci).
|
||||
- **TypeScript**: Strict mode, no implicit any, no unused locals/parameters.
|
||||
- **Commits**: Conventional commits format (enforced by commitlint in CI). Semantic release uses these for versioning.
|
||||
- **Commits**: Conventional Commits, enforced by `scripts/commit-check.sh` —
|
||||
a lefthook `commit-msg` hook locally, and a CI step over every commit in
|
||||
a push. It is twenty lines of shell rather than commitlint, because the
|
||||
grammar is one regex and commitlint would mean a Node dependency tree at
|
||||
the root of a Go repo. **Its type list is `.releaserc.yml`'s**; keep the
|
||||
two in step or semantic-release will decline to release something the
|
||||
check accepted.
|
||||
|
||||
`.releaserc.yml` is a complete semantic-release config that **nothing
|
||||
currently runs** — no workflow invokes it, and `CHANGELOG.md` is not
|
||||
being written by it. That is deliberate for now (wiring it means pushing
|
||||
tags, committing a changelog back, and interacting with the three
|
||||
publish workflows); it is recorded here rather than implied, because
|
||||
this file claimed for five phases that commitlint gated CI and that
|
||||
semantic release ran, and neither was true.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -656,9 +671,10 @@ push was healthy.
|
||||
|
||||
Two jobs, both in an `ubuntu:24.04` container:
|
||||
|
||||
- **`check`** — no display: `make lint` and `make test` (three build
|
||||
configurations each), `tsc --noEmit`, `make ui-test`,
|
||||
`make bindings-check`, `make skill-check`.
|
||||
- **`check`** — no display: `make commit-check` over the push's commits,
|
||||
`make lint` and `make test` (three build configurations each),
|
||||
`tsc --noEmit`, `make ui-test`, `make bindings-check`,
|
||||
`make skill-check`.
|
||||
- **`e2e`** — under Xvfb and a private D-Bus: fixtures, a seed built by
|
||||
running the app, `make dev-headless`, then the Playwright suite
|
||||
against **both** Chromium and WebKit. Playwright's Linux WebKit links
|
||||
|
||||
@@ -113,6 +113,11 @@ bindings-check: ## Fail if frontend/wailsjs is stale against the Go bindings
|
||||
skill-check: ## Fail if .pi/ documents a make target that does not exist
|
||||
@./scripts/skill-check.sh
|
||||
|
||||
# Conventional Commits, which CLAUDE.md claimed CI enforced for a long
|
||||
# time before anything did. RANGE=A..B lints a push; bare lints HEAD.
|
||||
commit-check: ## Fail if a commit subject is not a Conventional Commit
|
||||
@./scripts/commit-check.sh $(if $(RANGE),--range $(RANGE))
|
||||
|
||||
bindings: ## Regenerate frontend/wailsjs from the bound Go structs
|
||||
go tool wails generate module -tags webkit2_41
|
||||
@chmod 644 frontend/wailsjs/runtime/runtime.js \
|
||||
@@ -123,7 +128,7 @@ bindings: ## Regenerate frontend/wailsjs from the bound Go structs
|
||||
sandbox-seed sandbox-seed-bulk sandbox-seeds e2e e2e-setup e2e-report \
|
||||
perf perf-compare \
|
||||
ui-test ui-watch ui-visual ui-visual-update ui-setup \
|
||||
bindings bindings-check skill-check
|
||||
bindings bindings-check skill-check commit-check
|
||||
|
||||
# Base directory for fresh-install sandboxes. Deliberately NOT $TMPDIR:
|
||||
# on most Linux distros /tmp is tmpfs (RAM-backed) and only a few GB, so
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
# Install with: lefthook install
|
||||
# Docs: https://github.com/evilmartians/lefthook
|
||||
|
||||
# Conventional Commits. CI runs the same script over every commit in a
|
||||
# push, so skipping this locally only defers the failure.
|
||||
commit-msg:
|
||||
commands:
|
||||
commit-check:
|
||||
run: ./scripts/commit-check.sh {1}
|
||||
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Conventional Commits, enforced. CLAUDE.md has claimed for a long time
|
||||
# that commitlint gates this in CI; there was no config and no workflow
|
||||
# running one, so the claim was a lie in the file every contributor and
|
||||
# every agent reads first. This is the smaller of the two honest
|
||||
# answers to that: the grammar is twenty lines of shell, and adding
|
||||
# commitlint would mean a Node dependency tree at the root of a Go repo
|
||||
# purely to regex one line.
|
||||
#
|
||||
# The type list is the one .releaserc.yml's commit-analyzer knows about
|
||||
# — keep the two in step, or semantic-release will silently decline to
|
||||
# release something this accepts.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/commit-check.sh lint HEAD's message
|
||||
# scripts/commit-check.sh <file> lint a message file (commit-msg hook)
|
||||
# scripts/commit-check.sh --range A..B lint every commit in a range
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
TYPES='build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test'
|
||||
MAX_SUBJECT=72
|
||||
|
||||
# A subject is `type(optional-scope)!: text`. The `!` is Conventional
|
||||
# Commits' breaking-change marker and is allowed with or without a scope.
|
||||
SUBJECT_RE="^(${TYPES})(\([a-z0-9._/-]+\))?!?: .+$"
|
||||
|
||||
fail=0
|
||||
|
||||
check_subject() {
|
||||
local subject="$1" label="$2"
|
||||
|
||||
# Exemptions, all of them things git or a tool writes rather than a
|
||||
# person: merges, reverts of a revert, and the autosquash markers,
|
||||
# which carry the *original* subject and are rewritten by the rebase
|
||||
# that consumes them.
|
||||
case "$subject" in
|
||||
Merge\ * | Revert\ * | fixup!* | squash!* | amend!*) return 0 ;;
|
||||
esac
|
||||
|
||||
if ! printf '%s' "$subject" | grep -qE "$SUBJECT_RE"; then
|
||||
echo "commit-check: $label" >&2
|
||||
echo " $subject" >&2
|
||||
echo " is not 'type(scope): subject'." >&2
|
||||
echo " Types: $(printf '%s' "$TYPES" | tr '|' ' ')" >&2
|
||||
fail=1
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "${#subject}" -gt "$MAX_SUBJECT" ]; then
|
||||
echo "commit-check: $label" >&2
|
||||
echo " $subject" >&2
|
||||
echo " subject is ${#subject} chars; the limit is ${MAX_SUBJECT}." >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# godot for commits: the Go side of this repo lints comments for a
|
||||
# trailing period, and a subject is the one line that must not have
|
||||
# one.
|
||||
case "$subject" in
|
||||
*.)
|
||||
echo "commit-check: $label" >&2
|
||||
echo " $subject" >&2
|
||||
echo " subject ends with a period." >&2
|
||||
fail=1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--range)
|
||||
range="${2:?--range needs A..B}"
|
||||
count=0
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
sha="${line%% *}"
|
||||
check_subject "${line#* }" "$sha"
|
||||
count=$((count + 1))
|
||||
done <<<"$(git log --no-merges --format='%H %s' "$range")"
|
||||
[ "$fail" -eq 0 ] && echo "commit-check: $count commits in $range, all well-formed"
|
||||
;;
|
||||
"")
|
||||
check_subject "$(git log -1 --format='%s')" "HEAD"
|
||||
[ "$fail" -eq 0 ] && echo "commit-check: HEAD is well-formed"
|
||||
;;
|
||||
*)
|
||||
# The commit-msg hook hands us a file that also contains the body and
|
||||
# git's own comment lines; the subject is its first line.
|
||||
check_subject "$(head -n 1 "$1")" "$1"
|
||||
[ "$fail" -eq 0 ] && echo "commit-check: message is well-formed"
|
||||
;;
|
||||
esac
|
||||
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user