From bf8e309b19757cb82d19b9589591c38b27bb7004 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 22:58:55 +0200 Subject: [PATCH 1/6] ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three builder runs tonight carried a Co-Authored-By: Claude trailer despite bold instructions forbidding it. A rule in a prompt is not a control — this gate is. Flags commits in a PR's range carrying co-authored-by + (claude OR anthropic OR noreply@anthropic.com), case-insensitive, covering all observed spellings. Wired into CI (new attribution-trailer job) and pre-push hook. Mirrors existing lint scripts in structure and exit conventions. Pattern tested against all three variants plus a normal commit. --- .forgejo/workflows/ci.yml | 17 ++++++ scripts/check-attribution-trailers.sh | 85 +++++++++++++++++++++++++++ scripts/pre-push | 6 ++ 3 files changed, 108 insertions(+) create mode 100755 scripts/check-attribution-trailers.sh diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 60535005..c109794c 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -68,6 +68,23 @@ jobs: # their own. See scripts/check-doc-refs.sh. run: sh scripts/check-doc-refs.sh + attribution-trailers: + name: attribution-trailer lint + runs-on: [hive-ci] + # Pure git — seconds. + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: lint + # Flags Co-Authored-By trailers with Claude/Anthropic attribution + # in PR commits (hive convention in /knowledge/hive-rules.md forbids + # them — an attribution line in a prompt is not a control). Own + # job, and IS a required check on the forge (branch protection) — a + # hit blocks merge. See scripts/check-attribution-trailers.sh. + run: sh scripts/check-attribution-trailers.sh + prose-lint: name: prose lint (vale) runs-on: [hive-ci] diff --git a/scripts/check-attribution-trailers.sh b/scripts/check-attribution-trailers.sh new file mode 100755 index 00000000..4d589421 --- /dev/null +++ b/scripts/check-attribution-trailers.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# CI lint: flags Co-Authored-By trailers with Claude/Anthropic attribution in +# PR commits. The hive convention in /knowledge/hive-rules.md forbids these +# trailers — an attribution line in a prompt is not a control. This gate is. +# +# Emits a CI error annotation per offending commit and exits 1 if any commit +# in the PR's range carries a prohibited trailer. Runs as its own CI job and +# IS a required check on the forge (branch protection) — a hit blocks merge. +# +# Pattern: case-insensitive `co-authored-by` followed by `claude`, `anthropic`, +# or `noreply@anthropic.com` anywhere in the line. Covers variants seen in the +# wild: `Co-Authored-By: Claude`, `Co-Authored-By: Claude Sonnet 4.5 `. +# +# Scope: commits in the PR's range (BASE..HEAD), not the entire history — old +# commits must not fail the build. In CI, BASE is the target branch +# (GITHUB_BASE_REF / github.event.pull_request.base.ref in GH Actions; similar +# vars in Forgejo). Falls back to origin/main for local runs. +set -eu + +# Determine base ref: in CI (pull_request event), use the PR's base branch; +# locally, default to origin/main. CI env vars: GITHUB_BASE_REF (GitHub +# Actions), similar for Forgejo/Gitea (not standardized across runners, so we +# check both common patterns). +if [ -n "${GITHUB_BASE_REF:-}" ]; then + # GitHub Actions / Forgejo Actions pull_request context + base="origin/${GITHUB_BASE_REF}" +elif [ -n "${CI_COMMIT_REF_NAME:-}" ]; then + # GitLab-style variable (some Forgejo setups) + base="origin/main" +else + # Local run or unknown CI — default to origin/main + base="origin/main" +fi + +# Ensure the base ref exists (in CI, actions/checkout@v3 fetches the PR head +# but may not fetch the base branch by default — fetch it if missing). +if ! git rev-parse --verify "$base" >/dev/null 2>&1; then + # Extract branch name and fetch it + branch="${base#origin/}" + git fetch origin "$branch" 2>/dev/null || true + # If still missing, fall back to main + if ! git rev-parse --verify "$base" >/dev/null 2>&1; then + base="origin/main" + git fetch origin main 2>/dev/null || base="HEAD~10" + fi +fi + +# Get all commits in the PR's range. `--reverse` lists oldest first (clearer +# for annotation output). `--format=%H` gets the full commit sha. +commits="$(git log --reverse --format='%H' "${base}..HEAD" 2>/dev/null || true)" + +if [ -z "$commits" ]; then + # No commits in range — either not on a PR branch, or nothing new. Pass. + exit 0 +fi + +# Check each commit's full message for the prohibited trailer pattern. +# Case-insensitive grep for `co-authored-by` + any of `claude`, `anthropic`, +# `noreply@anthropic.com`. +hits="" +for sha in $commits; do + msg="$(git log -1 --format='%B' "$sha")" + # Match case-insensitive co-authored-by AND (claude OR anthropic OR noreply@anthropic.com) + # Use grep -E with -i for case-insensitive matching + if printf '%s\n' "$msg" | grep -qiE 'co-authored-by.*claude|co-authored-by.*anthropic|co-authored-by.*noreply@anthropic\.com'; then + # Found a hit — build annotation + subject="$(git log -1 --format='%s' "$sha")" + hits="${hits}${sha}|${subject} +" + fi +done + +if [ -n "$hits" ]; then + printf '%s' "$hits" | while IFS='|' read -r sha subject; do + [ -z "$sha" ] && continue + short_sha="$(printf '%s' "$sha" | cut -c1-8)" + printf '::error title=Attribution trailer found in %s::%s — commit carries a Co-Authored-By trailer with Claude/Anthropic attribution, forbidden by /knowledge/hive-rules.md\n' \ + "$short_sha" "$subject" + done + count="$(printf '%s' "$hits" | grep -c '|' || true)" + printf 'check-attribution-trailers: %s commit(s) with prohibited trailers found\n' "$count" >&2 + exit 1 +fi + +exit 0 diff --git a/scripts/pre-push b/scripts/pre-push index 62634252..6e812400 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -34,5 +34,11 @@ if ! sh "$repo_root/scripts/check-doc-refs.sh"; then exit 1 fi +echo "pre-push: running attribution-trailer lint..." >&2 +if ! sh "$repo_root/scripts/check-attribution-trailers.sh"; then + echo "pre-push: attribution-trailer lint FAILED — fix before pushing" >&2 + exit 1 +fi + echo "pre-push: lints passed ✓" >&2 exit 0 From 5bd68f9b8a70953ef68558d4e1be602088abccc4 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 23:07:00 +0200 Subject: [PATCH 2/6] ops: reduce comment density in PR #4433 Cut commentary from 104 to 24 lines across the new lint script, CI config, and pre-push hook. Kept only: policy reference, why pattern is loose, why scope is PR commits not history, and non-obvious CI behavior notes. No logic lines changed. Refs #4432 --- .forgejo/workflows/ci.yml | 61 ++++----------------------- scripts/check-attribution-trailers.sh | 42 ++++-------------- scripts/pre-push | 15 ++----- 3 files changed, 19 insertions(+), 99 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index c109794c..ee038c41 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -3,123 +3,78 @@ name: CI on: pull_request: branches: ["**"] - # Lets `hive-forge ci-rerun` re-trigger CI via the workflow-dispatch API - # without an empty commit. No effect on the PR-triggered runs above. + # Lets `hive-forge ci-rerun` re-trigger CI via workflow-dispatch API workflow_dispatch: jobs: check: name: nix flake check runs-on: [hive-ci] - # Bound the job so a wedged build fails in minutes instead of - # hanging until the runner's 3h cap (or, when the runner itself - # deadlocks, never). 30 min is well above a cold-cache rebuild - # (~15 min observed) and well under the 3h hard cap — tune if a - # legit cold build ever trips it. + # 30 min is well above a cold-cache rebuild (~15 min observed) and well + # under the runner's 3h cap timeout-minutes: 30 steps: - uses: actions/checkout@v3 - name: check - # Runs all flake checks: formatting (treefmt+rustfmt), cargo test, - # cargo clippy, and module evaluation. No --no-build: the checks - # derivations are the canonical source of truth. run: nix flake check tracker-tags: name: tracker-tag lint runs-on: [hive-ci] - # Pure git+grep — seconds normally; a few minutes is already a hang. timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: lint - # Flags hash-number tracker tags in source (hive convention is - # prose, not tags — /knowledge/hive-rules.md). Its own job, and - # IS a required check on the forge (branch protection) — a hit - # blocks merge. See scripts/check-issue-refs.sh. run: sh scripts/check-issue-refs.sh comment-blocks: name: comment-block lint runs-on: [hive-ci] - # Pure git+awk — seconds. timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: lint - # Flags contiguous comment blocks over 30 lines (a giant prose - # block belongs in docs/ as implementation notes, not in source). - # Own job, and IS a required check on the forge (branch - # protection) — a hit blocks merge. See - # scripts/check-comment-blocks.sh. run: sh scripts/check-comment-blocks.sh doc-refs: name: doc-pointer lint runs-on: [hive-ci] - # Pure git+grep — seconds. timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: lint - # Flags `docs/…md` paths and relative markdown links that no - # longer resolve. Pointing at a doc is what the comment-block - # lint above pushes people toward, so the pointers need a gate of - # their own. See scripts/check-doc-refs.sh. run: sh scripts/check-doc-refs.sh attribution-trailers: name: attribution-trailer lint runs-on: [hive-ci] - # Pure git — seconds. timeout-minutes: 5 steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: lint - # Flags Co-Authored-By trailers with Claude/Anthropic attribution - # in PR commits (hive convention in /knowledge/hive-rules.md forbids - # them — an attribution line in a prompt is not a control). Own - # job, and IS a required check on the forge (branch protection) — a - # hit blocks merge. See scripts/check-attribution-trailers.sh. run: sh scripts/check-attribution-trailers.sh prose-lint: name: prose lint (vale) runs-on: [hive-ci] - # A `vale sync` + full docs/ run took under a minute in testing. timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: lint - # Styles (Microsoft + alex + write-good, named in .vale.ini's - # `Packages` line) are fetched fresh from the Vale Package Hub each - # run rather than vendored, so this job always lints against - # upstream's current rules. Failing red on a hit is intentional -- - # the existing docs/ tree has a standing backlog of warning- and - # suggestion-level rule hits, so this job stays red on unrelated - # PRs until that backlog is worked down (tracked separately, as a - # follow-up to the error-only work below). Not wired into branch - # protection -- see the sibling job below for the error-only slice - # that is. + # Styles are fetched fresh from Vale Package Hub each run (not vendored). + # Not wired into branch protection — see prose-lint-errors below for the + # error-only slice that is. run: XDG_DATA_HOME="$PWD/.vale-data" nix shell nixpkgs#vale --command sh -c 'vale sync && vale docs' prose-lint-errors: name: prose lint (vale, errors) runs-on: [hive-ci] - # Own `vale sync` rather than reusing the sibling job's: each job gets - # a fresh checkout/environment, so there is nothing to share between - # them without an artifact-upload step this doesn't need. timeout-minutes: 5 steps: - uses: actions/checkout@v3 - name: lint - # The error-level-only slice of the sibling job above. - # `--minAlertLevel=error` changes what vale both reports AND exits - # non-zero on, so this job's pass/fail tracks only error-severity - # rule hits, not the warning/suggestion backlog the sibling job - # carries. Split out so this one alone can be a required check - # (forge branch protection) without blocking merges on the - # pre-existing backlog the sibling job still surfaces. + # Error-level-only slice. Split out so this job alone can be a required + # check without blocking merges on the warning/suggestion backlog. run: XDG_DATA_HOME="$PWD/.vale-data" nix shell nixpkgs#vale --command sh -c 'vale sync && vale --minAlertLevel=error docs' diff --git a/scripts/check-attribution-trailers.sh b/scripts/check-attribution-trailers.sh index 4d589421..152ca251 100755 --- a/scripts/check-attribution-trailers.sh +++ b/scripts/check-attribution-trailers.sh @@ -1,69 +1,43 @@ #!/bin/sh -# CI lint: flags Co-Authored-By trailers with Claude/Anthropic attribution in -# PR commits. The hive convention in /knowledge/hive-rules.md forbids these -# trailers — an attribution line in a prompt is not a control. This gate is. +# Flags Co-Authored-By trailers with Claude/Anthropic attribution in PR commits. +# The hive convention in /knowledge/hive-rules.md forbids these trailers. # -# Emits a CI error annotation per offending commit and exits 1 if any commit -# in the PR's range carries a prohibited trailer. Runs as its own CI job and -# IS a required check on the forge (branch protection) — a hit blocks merge. +# Pattern is loose (case-insensitive, matches `claude`, `anthropic`, or +# `noreply@anthropic.com` anywhere after `co-authored-by`) to cover variants +# like `Co-Authored-By: Claude Sonnet 4.5 `. # -# Pattern: case-insensitive `co-authored-by` followed by `claude`, `anthropic`, -# or `noreply@anthropic.com` anywhere in the line. Covers variants seen in the -# wild: `Co-Authored-By: Claude`, `Co-Authored-By: Claude Sonnet 4.5 `. -# -# Scope: commits in the PR's range (BASE..HEAD), not the entire history — old -# commits must not fail the build. In CI, BASE is the target branch -# (GITHUB_BASE_REF / github.event.pull_request.base.ref in GH Actions; similar -# vars in Forgejo). Falls back to origin/main for local runs. +# Scope is the PR's commits (BASE..HEAD), not entire history — old commits +# must not fail the build. set -eu -# Determine base ref: in CI (pull_request event), use the PR's base branch; -# locally, default to origin/main. CI env vars: GITHUB_BASE_REF (GitHub -# Actions), similar for Forgejo/Gitea (not standardized across runners, so we -# check both common patterns). if [ -n "${GITHUB_BASE_REF:-}" ]; then - # GitHub Actions / Forgejo Actions pull_request context base="origin/${GITHUB_BASE_REF}" elif [ -n "${CI_COMMIT_REF_NAME:-}" ]; then - # GitLab-style variable (some Forgejo setups) base="origin/main" else - # Local run or unknown CI — default to origin/main base="origin/main" fi -# Ensure the base ref exists (in CI, actions/checkout@v3 fetches the PR head -# but may not fetch the base branch by default — fetch it if missing). +# actions/checkout@v3 fetches the PR head but may not fetch the base branch if ! git rev-parse --verify "$base" >/dev/null 2>&1; then - # Extract branch name and fetch it branch="${base#origin/}" git fetch origin "$branch" 2>/dev/null || true - # If still missing, fall back to main if ! git rev-parse --verify "$base" >/dev/null 2>&1; then base="origin/main" git fetch origin main 2>/dev/null || base="HEAD~10" fi fi -# Get all commits in the PR's range. `--reverse` lists oldest first (clearer -# for annotation output). `--format=%H` gets the full commit sha. commits="$(git log --reverse --format='%H' "${base}..HEAD" 2>/dev/null || true)" if [ -z "$commits" ]; then - # No commits in range — either not on a PR branch, or nothing new. Pass. exit 0 fi -# Check each commit's full message for the prohibited trailer pattern. -# Case-insensitive grep for `co-authored-by` + any of `claude`, `anthropic`, -# `noreply@anthropic.com`. hits="" for sha in $commits; do msg="$(git log -1 --format='%B' "$sha")" - # Match case-insensitive co-authored-by AND (claude OR anthropic OR noreply@anthropic.com) - # Use grep -E with -i for case-insensitive matching if printf '%s\n' "$msg" | grep -qiE 'co-authored-by.*claude|co-authored-by.*anthropic|co-authored-by.*noreply@anthropic\.com'; then - # Found a hit — build annotation subject="$(git log -1 --format='%s' "$sha")" hits="${hits}${sha}|${subject} " diff --git a/scripts/pre-push b/scripts/pre-push index 6e812400..0f6bf3c2 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -1,17 +1,8 @@ #!/bin/sh -# Git pre-push hook: runs the tracker-tag, comment-block and doc-pointer -# lints against the working tree before any push lands on the remote. -# Catches issues that would fail CI and require a follow-up commit -# (common failure mode: a nix comment containing a hash-issue-number -# tag added mid-session). +# Install: ln -sf ../../scripts/pre-push .git/hooks/pre-push # -# Install (once per clone): -# ln -sf ../../scripts/pre-push .git/hooks/pre-push -# -# The hook runs against the full working tree (not just staged or -# pushed files) to match what CI sees: `nix flake check` builds from -# the committed tree, so an untracked hit on a staged file would still -# trip CI. +# Runs against the full working tree (not just staged or pushed files) to match +# what CI sees: `nix flake check` builds from the committed tree. set -eu repo_root="$(git rev-parse --show-toplevel)" From f45cecf08c81bb230c48adcb2230e96ce7f38c66 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 23:11:06 +0200 Subject: [PATCH 3/6] ops: address argus review findings on PR #4433 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add attribution-trailer lint row to the ci.md checks table (modeled on the vale-errors row: not yet a required check) so the doc doesn't assert stale completeness once attribution-trailers merges. Delete the dead CI_COMMIT_REF_NAME elif in check-attribution-trailers.sh — byte-identical to the else arm, never exercised, and its comment overclaimed GitLab/Forgejo support nothing here can test. Refs #4432 --- docs/scheduler/ci.md | 17 +++++++++-------- scripts/check-attribution-trailers.sh | 2 -- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/scheduler/ci.md b/docs/scheduler/ci.md index da1b9b57..74196cd5 100644 --- a/docs/scheduler/ci.md +++ b/docs/scheduler/ci.md @@ -41,14 +41,15 @@ gating on the full job's standing warning/suggestion backlog, but flipping that switch on the forge is a separate operator action, not something this table can assert ahead of it happening. -| Job | What it runs | Required? | -| ----------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------- | -| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes | -| **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) | yes | -| **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) | yes | -| **doc-pointer lint** | flags `docs/…md` paths and relative markdown links that no longer resolve (`scripts/check-doc-refs.sh`) | yes | -| **prose lint (vale, errors)** | vale, `docs/` tree, error-severity rule hits only | not yet — intended to be | -| **prose lint (vale)** | vale, `docs/` tree, full severity range (warnings + suggestions too) | no — advisory, standing backlog | +| Job | What it runs | Required? | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes | +| **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) | yes | +| **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) | yes | +| **doc-pointer lint** | flags `docs/…md` paths and relative markdown links that no longer resolve (`scripts/check-doc-refs.sh`) | yes | +| **attribution-trailer lint** | flags `Co-Authored-By` trailers with Claude/Anthropic attribution in PR commits (`scripts/check-attribution-trailers.sh`) | not yet — intended to be | +| **prose lint (vale, errors)** | vale, `docs/` tree, error-severity rule hits only | not yet — intended to be | +| **prose lint (vale)** | vale, `docs/` tree, full severity range (warnings + suggestions too) | no — advisory, standing backlog | `hive-forge ci-rerun --pr N` dispatches a `workflow_dispatch` retrigger without an empty commit. diff --git a/scripts/check-attribution-trailers.sh b/scripts/check-attribution-trailers.sh index 152ca251..1eb96dbc 100755 --- a/scripts/check-attribution-trailers.sh +++ b/scripts/check-attribution-trailers.sh @@ -12,8 +12,6 @@ set -eu if [ -n "${GITHUB_BASE_REF:-}" ]; then base="origin/${GITHUB_BASE_REF}" -elif [ -n "${CI_COMMIT_REF_NAME:-}" ]; then - base="origin/main" else base="origin/main" fi From 9e618b3b90284ed8f080a6ae915b5d558c6d71ba Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 23:16:20 +0200 Subject: [PATCH 4/6] ops: anchor attribution trailer lint to actual git trailer lines The previous regex matched any line containing both 'co-authored-by' and 'claude'/'anthropic' anywhere on the line, which flagged prose *about* the trailer, not just the trailer itself. This caught the commit that introduced the lint (subject: "ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers") as a false positive. Anchor the match to `^[[:space:]]*co-authored-by:` so it only fires on an actual trailer line (Key: value at line start), not a mention in a commit subject or body. Add a --self-test mode with fixtures covering three genuine-trailer spellings, an ordinary message, and the PR's own subject line as the prose-mention regression case. Refs #4432 --- scripts/check-attribution-trailers.sh | 45 ++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/scripts/check-attribution-trailers.sh b/scripts/check-attribution-trailers.sh index 1eb96dbc..300e7e88 100755 --- a/scripts/check-attribution-trailers.sh +++ b/scripts/check-attribution-trailers.sh @@ -2,14 +2,51 @@ # Flags Co-Authored-By trailers with Claude/Anthropic attribution in PR commits. # The hive convention in /knowledge/hive-rules.md forbids these trailers. # -# Pattern is loose (case-insensitive, matches `claude`, `anthropic`, or -# `noreply@anthropic.com` anywhere after `co-authored-by`) to cover variants -# like `Co-Authored-By: Claude Sonnet 4.5 `. +# Pattern is anchored to an actual git trailer line — `^[[:space:]]*co-authored-by:` +# — not any mention of the phrase, then requires the value name `claude`, +# `anthropic`, or `noreply@anthropic.com`. Matching is case-insensitive and +# per-line (git trailers are one `Key: value` per line) to cover variants like +# `Co-Authored-By: Claude Sonnet 4.5 ` while letting a +# commit subject/body that merely *discusses* the trailer (e.g. this script's +# own "add CI lint for Co-Authored-By/Claude/Anthropic trailers") pass. # # Scope is the PR's commits (BASE..HEAD), not entire history — old commits # must not fail the build. +# +# Run with --self-test to check the pattern against fixtures instead of +# scanning git history. set -eu +trailer_re='^[[:space:]]*co-authored-by:.*(claude|anthropic|noreply@anthropic\.com)' + +if [ "${1:-}" = "--self-test" ]; then + fail=0 + + assert_match() { + if ! printf '%s\n' "$2" | grep -qiE "$trailer_re"; then + echo "self-test FAILED (expected match): $1" >&2 + fail=1 + fi + } + assert_no_match() { + if printf '%s\n' "$2" | grep -qiE "$trailer_re"; then + echo "self-test FAILED (expected no match): $1" >&2 + fail=1 + fi + } + + assert_match "bare trailer" 'Co-Authored-By: Claude' + assert_match "full trailer with email" 'Co-Authored-By: Claude Sonnet 4.5 ' + assert_match "lowercase trailer" 'co-authored-by: claude' + assert_no_match "ordinary message" 'fix: correct off-by-one in parser' + assert_no_match "prose mention of the trailer" 'ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers' + + if [ "$fail" -eq 0 ]; then + echo "self-test OK" + fi + exit "$fail" +fi + if [ -n "${GITHUB_BASE_REF:-}" ]; then base="origin/${GITHUB_BASE_REF}" else @@ -35,7 +72,7 @@ fi hits="" for sha in $commits; do msg="$(git log -1 --format='%B' "$sha")" - if printf '%s\n' "$msg" | grep -qiE 'co-authored-by.*claude|co-authored-by.*anthropic|co-authored-by.*noreply@anthropic\.com'; then + if printf '%s\n' "$msg" | grep -qiE "$trailer_re"; then subject="$(git log -1 --format='%s' "$sha")" hits="${hits}${sha}|${subject} " From ed781b990e10a4ece48ab5b8307276b5f7bce0d3 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 23:22:31 +0200 Subject: [PATCH 5/6] ops: run attribution-trailer self-test in CI Self-test ran only when invoked by hand; wire it into the same job as the range scan so a broken checker fails the build. Self-test runs first so a broken checker doesn't get masked by the range scan. Refs #4432 --- .forgejo/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index ee038c41..769a8020 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - name: self-test + run: sh scripts/check-attribution-trailers.sh --self-test - name: lint run: sh scripts/check-attribution-trailers.sh From 91320fdd80b8f46988de8134e07df1ca674850f7 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 15 Sep 2026 23:59:39 +0200 Subject: [PATCH 6/6] ops: remove --self-test from attribution-trailer lint Operator ruling on PR #4433: the self-test doubled the script size without adding value. Drops the --self-test mode + fixtures from check-attribution-trailers.sh and the CI self-test step that invoked it. The anchored trailer regex and the real BASE..HEAD scan are unchanged. Refs #4432 --- .forgejo/workflows/ci.yml | 2 -- scripts/check-attribution-trailers.sh | 31 --------------------------- 2 files changed, 33 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 769a8020..ee038c41 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -53,8 +53,6 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: self-test - run: sh scripts/check-attribution-trailers.sh --self-test - name: lint run: sh scripts/check-attribution-trailers.sh diff --git a/scripts/check-attribution-trailers.sh b/scripts/check-attribution-trailers.sh index 300e7e88..c2fc2b73 100755 --- a/scripts/check-attribution-trailers.sh +++ b/scripts/check-attribution-trailers.sh @@ -12,41 +12,10 @@ # # Scope is the PR's commits (BASE..HEAD), not entire history — old commits # must not fail the build. -# -# Run with --self-test to check the pattern against fixtures instead of -# scanning git history. set -eu trailer_re='^[[:space:]]*co-authored-by:.*(claude|anthropic|noreply@anthropic\.com)' -if [ "${1:-}" = "--self-test" ]; then - fail=0 - - assert_match() { - if ! printf '%s\n' "$2" | grep -qiE "$trailer_re"; then - echo "self-test FAILED (expected match): $1" >&2 - fail=1 - fi - } - assert_no_match() { - if printf '%s\n' "$2" | grep -qiE "$trailer_re"; then - echo "self-test FAILED (expected no match): $1" >&2 - fail=1 - fi - } - - assert_match "bare trailer" 'Co-Authored-By: Claude' - assert_match "full trailer with email" 'Co-Authored-By: Claude Sonnet 4.5 ' - assert_match "lowercase trailer" 'co-authored-by: claude' - assert_no_match "ordinary message" 'fix: correct off-by-one in parser' - assert_no_match "prose mention of the trailer" 'ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers' - - if [ "$fail" -eq 0 ]; then - echo "self-test OK" - fi - exit "$fail" -fi - if [ -n "${GITHUB_BASE_REF:-}" ]; then base="origin/${GITHUB_BASE_REF}" else