Compare commits

...
6 Commits
Author SHA1 Message Date
dfritz e6b9fcf3f9 Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): enforce tracking attribution' (#3) from architect/bug-yhg8dqypwmar/enforce-tracking-attribution into main
test / test (push) Successful in 5s
Reviewed-on: #3
2026-08-26 23:23:02 +00:00
Evelyn Chen 5749dfd238 fix(check-naming): enforce tracking attribution
test / test (pull_request) Successful in 4s
Authored-By: Codex (GPT-5.6) <noreply@openai.com>
2026-08-26 13:26:41 +00:00
Dave Kowalski fc993fccc3 Merge pull request '[bug-hvhthrwf9ry8] fix(check-naming): print corrected title on missing-prefix failure' (#2) from dev/bug-hvhthrwf9ry8/improve-failure-message into main
test / test (push) Successful in 4s
2026-08-25 02:07:28 +00:00
Dave KowalskiandClaude Sonnet 4.6 5d0c72201e fix(check-naming): print corrected title on missing-prefix failure
test / test (pull_request) Successful in 6s
When a role-bug branch has no [bug-id] prefix in the PR title, the check
now prints what the title should be:

  FAIL[check-naming]: title must lead with [bug-r095xkfgt2vz]
    have: fix(agenthub/bugs): budget unpark quote in bytes not runes
    want: [bug-r095xkfgt2vz] fix(agenthub/bugs): budget unpark quote in bytes not runes

The bug id is already parsed from the branch, so the corrected title is
mechanically derivable — no guesswork needed from the author.

Part of bug-hvhthrwf9ry8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-25 02:04:17 +00:00
Evelyn Chen a9c5282f16 Merge pull request '[bug-557] check-naming v2: fail on violation' (#1) from architect/bug-557/fail-on-violation into main
test / test (push) Successful in 4s
2026-07-26 18:06:08 +00:00
Evelyn ChenandClaude Fable 5 9bfbd24461 v2: fail (exit 1) on naming violation
test / test (pull_request) Successful in 4s
The four wired repos flip the naming job to a required status check
(agenthub#557 WS6); a check that always exits 0 can never gate. v2 makes
the script exit nonzero with FAIL lines; warn-only stays available via
job-level continue-on-error or the legacy @v1 tag. dfritz break-glass
exemption unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:05:26 +00:00
4 changed files with 217 additions and 48 deletions

No files matched your search

+40 -13
View File
@@ -1,8 +1,9 @@
# action/check-naming # action/check-naming
Composite Gitea Action that validates a pull request's head branch name Composite Gitea Action that validates a pull request's branch, title, Bug
and title against the fritzlab naming standard. Always exits 0 — tracking, and AI authorship against the fritzlab Git standard. The current
warn-only until the Bugs tracking system is live (see action fails (exits 1) on any violation; `@v1` is the legacy warn-only release
(Bugs program: see
[fritzlab/agenthub#557](https://code.fritzlab.net/fritzlab/agenthub/issues/557)). [fritzlab/agenthub#557](https://code.fritzlab.net/fritzlab/agenthub/issues/557)).
## Standard ## Standard
@@ -26,15 +27,23 @@ jobs:
naming: naming:
runs-on: fritzlab runs-on: fritzlab
timeout-minutes: 5 timeout-minutes: 5
continue-on-error: true # non-required until Bugs cutover
steps: steps:
- uses: https://code.fritzlab.net/action/check-naming@v1 - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: https://code.fritzlab.net/action/check-naming@<commit-sha>
with: with:
head-branch: ${{ github.head_ref }} head-branch: ${{ github.head_ref }}
pr-title: ${{ github.event.pull_request.title }} pr-title: ${{ github.event.pull_request.title }}
pr-author: ${{ github.event.pull_request.user.login }} pr-author: ${{ github.event.pull_request.user.login }}
pr-body: ${{ github.event.pull_request.body }}
base-sha: ${{ github.event.pull_request.base.sha }}
head-sha: ${{ github.event.pull_request.head.sha }}
``` ```
To wire a new repo warn-only first, add `continue-on-error: true` to the
job — the step still fails, but the job cannot block the PR.
## Inputs ## Inputs
| Name | Required | Description | | Name | Required | Description |
@@ -42,23 +51,41 @@ jobs:
| `head-branch` | yes | Head branch name — `github.head_ref` | | `head-branch` | yes | Head branch name — `github.head_ref` |
| `pr-title` | yes | PR title — `github.event.pull_request.title` | | `pr-title` | yes | PR title — `github.event.pull_request.title` |
| `pr-author` | no | PR author login — `github.event.pull_request.user.login`; `dfritz` is exempt | | `pr-author` | no | PR author login — `github.event.pull_request.user.login`; `dfritz` is exempt |
| `pr-body` | yes | PR description — `github.event.pull_request.body` |
| `base-sha` | yes | Base commit — `github.event.pull_request.base.sha` |
| `head-sha` | yes | Head commit — `github.event.pull_request.head.sha` |
## Behavior ## Behavior
The check validates two things: The check validates four things for a Bug-backed Agent PR:
1. **Branch form** — must be `<role>/bug-<id>/<kebab>` or `chore/<kebab>`. 1. **Branch form** — must be `<role>/bug-<id>/<kebab>` or `chore/<kebab>`.
Unknown roles, missing `bug-` segment, uppercase bug-ids, and empty Unknown roles, missing `bug-` segment, uppercase bug-ids, and empty
kebab descriptions all produce a `WARN[check-naming]` log line. kebab descriptions all produce a `FAIL[check-naming]` log line.
2. **Title form** — for a `role/bug` branch the title should start with 2. **Title form** — for a `role/bug` branch the title must start with
`[bug-<id>] `. For a `chore` branch the title should have no `[bug-id]` `[bug-<id>] `. For a `chore` branch the title must have no `[bug-id]`
prefix. If both carry a bug-id they must match. prefix. If both carry a bug-id they must match.
All violations print a `WARN[check-naming]: ...` line. The step always 3. **Tracking and attribution** — `## Tracking` contains both the literal
exits 0 so it cannot block a PR. To harden to required after the Bugs `Fixes bug-<id>` automation token and the matching navigable
cutover: remove `continue-on-error: true` from the consuming workflow `https://agenthub.fritzlab.net/bug-<id>` URL. A separate `## Attribution`
and add the job's context to the repo's `status_check_contexts`. section contains the canonical `Authored-By` product/model watermark.
4. **Commit attribution** — every commit in `base-sha..head-sha` ends with
the canonical `Authored-By` trailer, separated from the message body by a
blank line. The naming job must check out full history before this action.
Every violation prints a `FAIL[check-naming]: ...` line and the step
exits 1. To make the check required on a repo: drop any
`continue-on-error: true` from the consuming workflow and add the job's
context to the repo's `status_check_contexts` in agenthub `hub/hub.yaml`.
## Versions
- unreleased — adds Bug tracking plus PR and commit attribution enforcement.
- `v2` — enforces branch and title naming.
- `v1` — legacy warn-only: logs `WARN` lines, always exits 0.
## Tests ## Tests
+17 -4
View File
@@ -1,8 +1,9 @@
name: Check branch/PR naming name: Check PR contract
description: | description: |
Validates that a PR's head branch and title follow the fritzlab naming Validates that a PR's branch, title, Bug tracking, and AI attribution
standard. Always exits 0 — warn-only until the Bugs system is live. follow the fritzlab Git standard. Fails (exits 1) on any violation, with
Produces WARN log lines for any violation. FAIL log lines naming it. Warn-only wiring is the consumer's choice: set
continue-on-error: true on the job (or pin @v1, which never fails).
Standard: Standard:
branch: <role>/bug-<id>/<kebab> e.g. dev/bug-x7k2m9/fix-terminal-resize branch: <role>/bug-<id>/<kebab> e.g. dev/bug-x7k2m9/fix-terminal-resize
@@ -25,6 +26,15 @@ inputs:
dfritz is exempt from all checks. dfritz is exempt from all checks.
required: false required: false
default: '' default: ''
pr-body:
description: PR description — github.event.pull_request.body.
required: true
base-sha:
description: Base commit SHA — github.event.pull_request.base.sha.
required: true
head-sha:
description: Head commit SHA — github.event.pull_request.head.sha.
required: true
runs: runs:
using: composite using: composite
@@ -35,4 +45,7 @@ runs:
HEAD_BRANCH: ${{ inputs.head-branch }} HEAD_BRANCH: ${{ inputs.head-branch }}
PR_TITLE: ${{ inputs.pr-title }} PR_TITLE: ${{ inputs.pr-title }}
PR_AUTHOR: ${{ inputs.pr-author }} PR_AUTHOR: ${{ inputs.pr-author }}
PR_BODY: ${{ inputs.pr-body }}
BASE_SHA: ${{ inputs.base-sha }}
HEAD_SHA: ${{ inputs.head-sha }}
run: bash "${{ github.action_path }}/check.sh" run: bash "${{ github.action_path }}/check.sh"
+87 -15
View File
@@ -1,11 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Branch and PR title naming-standard checker. # Branch, PR body, and commit contract checker.
# Always exits 0 — warn-only until the Bugs system is live and STRICT=1 is set. # v2: exits 1 on violation. Consumers that want warn-only set
# continue-on-error: true on the job (or stay on @v1).
set -euo pipefail set -euo pipefail
BRANCH="${HEAD_BRANCH:-}" BRANCH="${HEAD_BRANCH:-}"
TITLE="${PR_TITLE:-}" TITLE="${PR_TITLE:-}"
AUTHOR="${PR_AUTHOR:-}" AUTHOR="${PR_AUTHOR:-}"
BODY="${PR_BODY:-}"
BASE="${BASE_SHA:-}"
HEAD="${HEAD_SHA:-}"
# Break-glass: dfritz is exempt from all naming checks. # Break-glass: dfritz is exempt from all naming checks.
if [ "${AUTHOR}" = "dfritz" ]; then if [ "${AUTHOR}" = "dfritz" ]; then
@@ -13,7 +17,7 @@ if [ "${AUTHOR}" = "dfritz" ]; then
exit 0 exit 0
fi fi
WARN=0 FAILED=0
BRANCH_KIND="invalid" BRANCH_KIND="invalid"
BRANCH_BUG="" BRANCH_BUG=""
@@ -26,10 +30,10 @@ if echo "${BRANCH}" | grep -qE "^(dev|ux|ops|security|perf|architect|support)/bu
elif echo "${BRANCH}" | grep -qE "^chore/[a-z0-9][a-z0-9-]*$"; then elif echo "${BRANCH}" | grep -qE "^chore/[a-z0-9][a-z0-9-]*$"; then
BRANCH_KIND="chore" BRANCH_KIND="chore"
else else
echo "WARN[check-naming]: branch '${BRANCH}' does not match convention" echo "FAIL[check-naming]: branch '${BRANCH}' does not match convention"
echo " expected: <role>/bug-<id>/<kebab> (role: dev|ux|ops|security|perf|architect|support)" echo " expected: <role>/bug-<id>/<kebab> (role: dev|ux|ops|security|perf|architect|support)"
echo " or: chore/<kebab>" echo " or: chore/<kebab>"
WARN=1 FAILED=1
fi fi
# ---- title form ---- # ---- title form ----
@@ -40,19 +44,87 @@ fi
if [ "${BRANCH_KIND}" = "role-bug" ]; then if [ "${BRANCH_KIND}" = "role-bug" ]; then
if [ -z "${TITLE_BUG}" ]; then if [ -z "${TITLE_BUG}" ]; then
echo "WARN[check-naming]: title missing [${BRANCH_BUG}] prefix for branch '${BRANCH}'" echo "FAIL[check-naming]: title must lead with [${BRANCH_BUG}]"
WARN=1 echo " have: ${TITLE}"
echo " want: [${BRANCH_BUG}] ${TITLE}"
FAILED=1
elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then
echo "WARN[check-naming]: bug-id mismatch — branch carries '${BRANCH_BUG}' but title carries '${TITLE_BUG}'" echo "FAIL[check-naming]: bug-id mismatch — branch carries '${BRANCH_BUG}' but title carries '${TITLE_BUG}'"
WARN=1 FAILED=1
fi
elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
echo "WARN[check-naming]: chore branch should not carry a [bug-id] title prefix"
WARN=1
fi fi
if [ "${WARN}" -eq 0 ]; then tracking=$(printf '%s\n' "${BODY}" | awk '
/^## Tracking[[:space:]]*$/ { in_section=1; next }
/^## / && in_section { exit }
in_section { print }
')
if [ -z "${tracking}" ]; then
echo "FAIL[check-naming]: PR body must contain a non-empty ## Tracking section"
FAILED=1
else
if ! printf '%s\n' "${tracking}" | grep -qE "(^|[[:space:]])Fixes[[:space:]]+${BRANCH_BUG}([^a-z0-9]|$)"; then
echo "FAIL[check-naming]: ## Tracking must contain the literal token 'Fixes ${BRANCH_BUG}'"
FAILED=1
fi
if ! printf '%s\n' "${tracking}" | grep -Fq "https://agenthub.fritzlab.net/${BRANCH_BUG}"; then
echo "FAIL[check-naming]: ## Tracking must link https://agenthub.fritzlab.net/${BRANCH_BUG}"
FAILED=1
fi
fi
attribution=$(printf '%s\n' "${BODY}" | awk '
/^## Attribution[[:space:]]*$/ { in_section=1; next }
/^## / && in_section { exit }
in_section { print }
')
watermark_re='^[-*]?[[:space:]]*Authored-By: .+ \(.+\) <noreply@[[:alnum:].-]+>$'
if ! printf '%s\n' "${attribution}" | grep -qE "${watermark_re}"; then
echo "FAIL[check-naming]: ## Attribution must contain an Authored-By product/model watermark"
FAILED=1
fi
if ! printf '%s\n%s\n' "${BASE}" "${HEAD}" | grep -qEv '^[0-9a-f]{40,64}$'; then
for revision in "${BASE}" "${HEAD}"; do
if ! git cat-file -e "${revision}^{commit}" 2>/dev/null; then
echo "FAIL[check-naming]: base-sha and head-sha must name available commits"
FAILED=1
break
fi
done
if git cat-file -e "${BASE}^{commit}" 2>/dev/null && git cat-file -e "${HEAD}^{commit}" 2>/dev/null; then
commit_count=0
while IFS= read -r commit; do
[ -n "${commit}" ] || continue
commit_count=$((commit_count + 1))
message=$(git log -1 --format=%B "${commit}")
if ! printf '%s\n' "${message}" | awk '
{ line[NR]=$0 }
END {
n=NR
while (n > 0 && line[n] == "") n--
if (n < 3 || line[n-1] != "" || line[n] !~ /^Authored-By: .+ \(.+\) <noreply@[[:alnum:].-]+>$/) exit 1
}
'; then
echo "FAIL[check-naming]: commit ${commit} must end with an Authored-By product/model trailer"
FAILED=1
fi
done < <(git rev-list --reverse "${BASE}..${HEAD}" 2>/dev/null)
if [ "${commit_count}" -eq 0 ]; then
echo "FAIL[check-naming]: base-sha..head-sha contains no PR commits"
FAILED=1
fi
fi
else
echo "FAIL[check-naming]: base-sha and head-sha must be lowercase hexadecimal commit SHAs"
FAILED=1
fi
elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
echo "FAIL[check-naming]: chore branch should not carry a [bug-id] title prefix"
FAILED=1
fi
if [ "${FAILED}" -eq 0 ]; then
echo "check-naming: ok" echo "check-naming: ok"
fi fi
exit 0 exit "${FAILED}"
+73 -16
View File
@@ -5,24 +5,53 @@ set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh" SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
PASS=0 PASS=0
FAIL=0 FAIL=0
FIXTURES=$(mktemp -d)
trap 'rm -rf "${FIXTURES}"' EXIT
git -C "${FIXTURES}" init -q
git -C "${FIXTURES}" config user.name "Test Agent"
git -C "${FIXTURES}" config user.email "test@noreply.fritzlab.net"
git -C "${FIXTURES}" commit --allow-empty -q -m "base"
BASE=$(git -C "${FIXTURES}" rev-parse HEAD)
git -C "${FIXTURES}" commit --allow-empty -q -m "valid change" -m "Authored-By: Codex (GPT-5) <noreply@openai.com>"
GOOD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
canonical_body() {
local bug="$1"
printf '## Tracking\n- Fixes %s — [%s](https://agenthub.fritzlab.net/%s)\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n' "$bug" "$bug" "$bug"
}
check() { check() {
local desc="$1" want_warn="$2" branch="$3" title="$4" author="$5" local desc="$1" want_fail="$2" branch="$3" title="$4" author="$5"
local bug body
bug=$(printf '%s\n' "$branch" | sed -nE 's|^[^/]+/(bug-[a-z0-9]+)/.*|\1|p')
body=$(canonical_body "${bug:-bug-test}")
local out rc=0 local out rc=0
out=$(HEAD_BRANCH="$branch" PR_TITLE="$title" PR_AUTHOR="$author" bash "$SCRIPT" 2>&1) || rc=$? out=$(cd "${FIXTURES}" && HEAD_BRANCH="$branch" PR_TITLE="$title" PR_AUTHOR="$author" \
if [ "$rc" -ne 0 ]; then PR_BODY="$body" BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
echo "FAIL [$desc]: script exited $rc (must always exit 0)" local got_lines=0
echo " output: $out" echo "$out" | grep -qE "^FAIL" && got_lines=1 || true
FAIL=$((FAIL + 1)) if [ "$rc" -eq "$want_fail" ] && [ "$got_lines" -eq "$want_fail" ]; then
return
fi
local got_warn=0
echo "$out" | grep -qE "^WARN" && got_warn=1 || true
if [ "$got_warn" -eq "$want_warn" ]; then
echo "PASS [$desc]" echo "PASS [$desc]"
PASS=$((PASS + 1)) PASS=$((PASS + 1))
else else
echo "FAIL [$desc]: expected warn=$want_warn got=$got_warn" echo "FAIL [$desc]: expected exit=$want_fail fail-lines=$want_fail, got exit=$rc fail-lines=$got_lines"
echo " output: $out"
FAIL=$((FAIL + 1))
fi
}
check_contract() {
local desc="$1" body="$2" base="$3" head="$4" diagnostic="$5"
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -Fq "$diagnostic"; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
else
echo "FAIL [$desc]: expected exit=1 and diagnostic '$diagnostic', got exit=$rc"
echo " output: $out" echo " output: $out"
FAIL=$((FAIL + 1)) FAIL=$((FAIL + 1))
fi fi
@@ -45,16 +74,16 @@ check "support/bug matching title" 0 "support/bug-sup9/clarify-error"
check "chore — no title prefix" 0 "chore/bump-deps" "Bump dependency versions" "dev" check "chore — no title prefix" 0 "chore/bump-deps" "Bump dependency versions" "dev"
check "chore — plain title" 0 "chore/fix-a-typo" "Fix typo in README" "ops" check "chore — plain title" 0 "chore/fix-a-typo" "Fix typo in README" "ops"
# warn: role/bug branch but no [bug-id] title prefix # fail: role/bug branch but no [bug-id] title prefix
check "role/bug no title prefix" 1 "dev/bug-x7k2m9/fix-resize" "Fix resize" "dev" check "role/bug no title prefix" 1 "dev/bug-x7k2m9/fix-resize" "Fix resize" "dev"
# warn: bug-id mismatch between branch and title # fail: bug-id mismatch between branch and title
check "bug-id mismatch" 1 "dev/bug-x7k2m9/fix-resize" "[bug-zzzzz1] Fix resize" "dev" check "bug-id mismatch" 1 "dev/bug-x7k2m9/fix-resize" "[bug-zzzzz1] Fix resize" "dev"
# warn: chore branch with [bug-id] title prefix # fail: chore branch with [bug-id] title prefix
check "chore with bug-id title" 1 "chore/bump-deps" "[bug-abc123] Bump deps" "dev" check "chore with bug-id title" 1 "chore/bump-deps" "[bug-abc123] Bump deps" "dev"
# warn: invalid branch forms # fail: invalid branch forms
check "invalid — no role prefix" 1 "feature/foo-bar" "Add feature" "dev" check "invalid — no role prefix" 1 "feature/foo-bar" "Add feature" "dev"
check "invalid — missing bug segment" 1 "dev/fix-something" "Fix something" "dev" check "invalid — missing bug segment" 1 "dev/fix-something" "Fix something" "dev"
check "invalid — unknown role" 1 "unknown/bug-abc/thing" "[bug-abc] Thing" "dev" check "invalid — unknown role" 1 "unknown/bug-abc/thing" "[bug-abc] Thing" "dev"
@@ -62,6 +91,34 @@ check "invalid — uppercase bug-id" 1 "dev/bug-ABC123/thing" "[b
check "invalid — empty kebab" 1 "dev/bug-x7k2m9/" "[bug-x7k2m9] No kebab" "dev" check "invalid — empty kebab" 1 "dev/bug-x7k2m9/" "[bug-x7k2m9] No kebab" "dev"
check "invalid — no bug prefix on seg" 1 "dev/x7k2m9/fix-resize" "Fix resize" "dev" check "invalid — no bug prefix on seg" 1 "dev/x7k2m9/fix-resize" "Fix resize" "dev"
# Bug-backed PR body contract. These mirror the omissions in agenthub#779.
BODY=$(canonical_body "bug-x7k2m9")
check_contract "raw Fixes only rejects missing link and attribution" \
$'## Tracking\n- Fixes bug-x7k2m9' "$BASE" "$GOOD_HEAD" "must link https://agenthub.fritzlab.net/bug-x7k2m9"
check_contract "tracking rejects linked Fixes without literal token" \
$'## Tracking\n- Fixes [bug-x7k2m9](https://agenthub.fritzlab.net/bug-x7k2m9)\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD" "must contain the literal token 'Fixes bug-x7k2m9'"
check_contract "tracking rejects wrong Bug URL" \
$'## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-other\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD" "must link https://agenthub.fritzlab.net/bug-x7k2m9"
check_contract "attribution must be in its own section" \
$'## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD" "## Attribution must contain"
git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change"
BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
check_contract "agenthub 779 commits reject missing watermark" "$BODY" "$GOOD_HEAD" "$BAD_HEAD" "must end with an Authored-By"
git -C "${FIXTURES}" commit --allow-empty -q -m $'misplaced trailer\nAuthored-By: Codex (GPT-5) <noreply@openai.com>'
MISPLACED_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
check_contract "commit watermark requires blank separator" "$BODY" "$BAD_HEAD" "$MISPLACED_HEAD" "must end with an Authored-By"
git -C "${FIXTURES}" commit --allow-empty -q -m "trailer not final" -m $'Authored-By: Codex (GPT-5) <noreply@openai.com>\nextra text'
NONFINAL_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
check_contract "commit watermark must be final trailer" "$BODY" "$MISPLACED_HEAD" "$NONFINAL_HEAD" "must end with an Authored-By"
check_contract "missing commit range rejects" "$BODY" "$GOOD_HEAD" "$GOOD_HEAD" "contains no PR commits"
check_contract "invalid SHA rejects before git" "$BODY" "not-a-sha" "$GOOD_HEAD" "lowercase hexadecimal"
echo "" echo ""
echo "Results: ${PASS} passed, ${FAIL} failed" echo "Results: ${PASS} passed, ${FAIL} failed"
[ "${FAIL}" -eq 0 ] [ "${FAIL}" -eq 0 ]