Compare commits

2 Commits
Author SHA1 Message Date
Evelyn Chen 1fba1e06b2 Normalize Bug title whitespace
test / test (pull_request) Successful in 9s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:24:37 +00:00
Evelyn Chen c09fc72500 Reject Conventional Commit PR titles
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:24:37 +00:00
4 changed files with 60 additions and 104 deletions

No files matched your search

+13 -25
View File
@@ -1,8 +1,8 @@
# action/check-naming # action/check-naming
Composite Gitea Action that validates a pull request's branch, title, Bug Composite Gitea Action that validates a pull request's branch, title, Bug
tracking, PR-description AI authorship, and commit range against the fritzlab tracking, and AI authorship against the fritzlab Git standard. The current
Git standard. The current action fails (exits 1) on any violation; `@v1` is the legacy warn-only release action fails (exits 1) on any violation; `@v1` is the legacy warn-only release
(Bugs program: see (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)).
@@ -11,7 +11,7 @@ Git standard. The current action fails (exits 1) on any violation; `@v1` is the
``` ```
Branch: <role>/bug-<id>/<kebab-description> e.g. dev/bug-x7k2m9/fix-terminal-resize Branch: <role>/bug-<id>/<kebab-description> e.g. dev/bug-x7k2m9/fix-terminal-resize
Chore: chore/<kebab-description> bug-less trivia only (dep bumps, typos, CI tweaks) Chore: chore/<kebab-description> bug-less trivia only (dep bumps, typos, CI tweaks)
Title: [bug-x7k2m9] fix(terminal): preserve resize state Title: [bug-x7k2m9] Fix terminal resize loss (chore PRs: no [bug-id] prefix)
``` ```
- `<role>` must be a roster handle: `dev` | `ux` | `ops` | `security` | `perf` | `architect` | `support` - `<role>` must be a roster handle: `dev` | `ux` | `ops` | `security` | `perf` | `architect` | `support`
@@ -23,10 +23,6 @@ Title: [bug-x7k2m9] fix(terminal): preserve resize state
## Usage ## Usage
```yaml ```yaml
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs: jobs:
naming: naming:
runs-on: fritzlab runs-on: fritzlab
@@ -46,11 +42,6 @@ jobs:
head-sha: ${{ github.event.pull_request.head.sha }} head-sha: ${{ github.event.pull_request.head.sha }}
``` ```
The explicit `edited` activity is part of the enforcement contract: changing
the PR description must issue a new naming status for the same commit.
Keep required code-validation jobs in a separate workflow that does not run on
`edited`; Gitea treats a skipped required context as passing.
To wire a new repo warn-only first, add `continue-on-error: true` to the 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. job — the step still fails, but the job cannot block the PR.
@@ -74,12 +65,11 @@ The check validates four things for every non-break-glass Agent PR:
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 `FAIL[check-naming]` log line. kebab descriptions all produce a `FAIL[check-naming]` log line.
2. **Title form** — for a `role/bug` branch the title must be 2. **Title form** — for a `role/bug` branch the title must start with
`[bug-<id>] type(scope): description`. The type starts with a lowercase `[bug-<id>] `. For a `chore` branch the title must have no `[bug-id]`
letter and the type and one-component scope contain only lowercase letters, prefix. If both carry a bug-id they must match. The description is a
digits, and hyphens. An optional `!` may follow the scope, and the description plain-language imperative; Conventional Commit forms such as `fix(scope):`
must contain a non-whitespace character. For a `chore` branch the title must are rejected.
have no `[bug-id]` prefix. If both carry a bug-id they must match.
3. **Tracking and attribution** — on Bug-backed work, `## Tracking` contains both the literal 3. **Tracking and attribution** — on Bug-backed work, `## Tracking` contains both the literal
`Fixes bug-<id>` automation token and the matching navigable `Fixes bug-<id>` automation token and the matching navigable
@@ -91,12 +81,10 @@ The check validates four things for every non-break-glass Agent PR:
`<h2>` sections outside collapsed `<details>` content. Fenced, commented, `<h2>` sections outside collapsed `<details>` content. Fenced, commented,
scripted, or collapsed copies do not satisfy the visible provenance contract. scripted, or collapsed copies do not satisfy the visible provenance contract.
4. **Commit range** — `base-sha` and `head-sha` must identify available commits, 4. **Commit attribution** — every commit in `base-sha..head-sha`, including
and `base-sha..head-sha` must contain at least one PR commit. commits on `chore/` branches, ends with
The naming job must check out full history before this action. the canonical `Authored-By` trailer, separated from the message body by a
Attribution is required only in the PR description; commit messages are not blank line. The naming job must check out full history before this action.
inspected. Existing commit trailers remain valid history, but no trailer is
required for a new commit.
Every violation prints a `FAIL[check-naming]: ...` line and the step Every violation prints a `FAIL[check-naming]: ...` line and the step
exits 1. To make the check required on a repo: drop any exits 1. To make the check required on a repo: drop any
@@ -105,7 +93,7 @@ context to the repo's `status_check_contexts` in agenthub `hub/hub.yaml`.
## Versions ## Versions
- unreleased — requires Bug tracking and visible PR attribution, without commit-message watermarks. - unreleased — adds Bug tracking plus PR and commit attribution enforcement.
- `v2` — enforces branch and title naming. - `v2` — enforces branch and title naming.
- `v1` — legacy warn-only: logs `WARN` lines, always exits 0. - `v1` — legacy warn-only: logs `WARN` lines, always exits 0.
+2 -2
View File
@@ -1,6 +1,6 @@
name: Check PR contract name: Check PR contract
description: | description: |
Validates that a PR's branch, title, Bug tracking, and PR-description AI attribution Validates that a PR's branch, title, Bug tracking, and AI attribution
follow the fritzlab Git standard. Fails (exits 1) on any violation, with follow the fritzlab Git standard. Fails (exits 1) on any violation, with
FAIL log lines naming it. Warn-only wiring is the consumer's choice: set 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). continue-on-error: true on the job (or pin @v1, which never fails).
@@ -8,7 +8,7 @@ description: |
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
chore: chore/<kebab> bug-less trivia only chore: chore/<kebab> bug-less trivia only
title: [bug-<id>] type(scope): description title: [bug-<id>] Description (chore PRs: no [bug-id] prefix)
Role must be a roster handle: dev | ux | ops | security | perf | architect | support Role must be a roster handle: dev | ux | ops | security | perf | architect | support
Break-glass: PRs authored by dfritz are exempt. Break-glass: PRs authored by dfritz are exempt.
+25 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Branch, PR body, and commit-range contract checker. # Branch, PR body, and commit contract checker.
# v2: exits 1 on violation. Consumers that want warn-only set # v2: exits 1 on violation. Consumers that want warn-only set
# continue-on-error: true on the job (or stay on @v1). # continue-on-error: true on the job (or stay on @v1).
set -euo pipefail set -euo pipefail
@@ -121,18 +121,21 @@ if echo "${TITLE}" | grep -qE "^\[bug-[a-z0-9]+\] ."; then
TITLE_BUG=$(echo "${TITLE}" | sed -E 's|^\[(bug-[a-z0-9]+)\].*|\1|') TITLE_BUG=$(echo "${TITLE}" | sed -E 's|^\[(bug-[a-z0-9]+)\].*|\1|')
fi fi
TITLE_DESCRIPTION=$(printf '%s\n' "${TITLE}" | sed -E 's/^\[bug-[a-z0-9]+\][[:space:]]+//')
if printf '%s\n' "${TITLE_DESCRIPTION}" | grep -qE '^[[:alnum:]-]+(\([^)]*\))?!?:[[:space:]]'; then
echo "FAIL[check-naming]: title must use a plain-language imperative without Conventional Commit syntax"
FAILED=1
fi
if [ "${BRANCH_KIND}" = "role-bug" ]; then if [ "${BRANCH_KIND}" = "role-bug" ]; then
if [ -z "${TITLE_BUG}" ]; then if [ -z "${TITLE_BUG}" ]; then
echo "FAIL[check-naming]: title must lead with [${BRANCH_BUG}]" echo "FAIL[check-naming]: title must lead with [${BRANCH_BUG}]"
echo " have: ${TITLE}" echo " have: ${TITLE}"
echo " want: [${BRANCH_BUG}] fix(scope): description" echo " want: [${BRANCH_BUG}] ${TITLE}"
FAILED=1 FAILED=1
elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then
echo "FAIL[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}'"
FAILED=1 FAILED=1
elif ! printf '%s\n' "${TITLE}" | grep -qE "^\\[${BRANCH_BUG}\\] [a-z][a-z0-9-]*\\([a-z0-9][a-z0-9-]*\\)!?: .*[^[:space:]]$"; then
echo "FAIL[check-naming]: Bug-backed title must use '[${BRANCH_BUG}] type(scope): description'"
FAILED=1
fi fi
tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk ' tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
@@ -180,9 +183,23 @@ if [ "${BRANCH_KIND}" != "invalid" ]; then
fi fi
done done
if git cat-file -e "${BASE}^{commit}" 2>/dev/null && git cat-file -e "${HEAD}^{commit}" 2>/dev/null; then if git cat-file -e "${BASE}^{commit}" 2>/dev/null && git cat-file -e "${HEAD}^{commit}" 2>/dev/null; then
# Attribution belongs only in the PR description, not commit messages. commit_count=0
# Keep the existing nonempty-range gate independently of message content. while IFS= read -r commit; do
commit_count=$(git rev-list --count "${BASE}..${HEAD}" 2>/dev/null) || commit_count=0 [ -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 if [ "${commit_count}" -eq 0 ]; then
echo "FAIL[check-naming]: base-sha..head-sha contains no PR commits" echo "FAIL[check-naming]: base-sha..head-sha contains no PR commits"
FAILED=1 FAILED=1
+20 -69
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Tests for check.sh — exercises naming, PR attribution, and commit ranges. # Tests for check.sh — exercises valid/invalid branch+title matrices.
set -euo pipefail set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh" SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
@@ -48,7 +48,7 @@ check_contract() {
local desc="$1" body="$2" base="$3" head="$4" diagnostic="$5" local desc="$1" body="$2" base="$3" head="$4" diagnostic="$5"
local out rc=0 local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \ out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] fix(check): enforce contract" PR_AUTHOR="architect" \ PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \ PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \
GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \ GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$? BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
@@ -66,7 +66,7 @@ check_contract_pass() {
local desc="$1" body="$2" base="$3" head="$4" local desc="$1" body="$2" base="$3" head="$4"
local out rc=0 local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \ out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] fix(check): enforce contract" PR_AUTHOR="architect" \ PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \ PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \
GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \ GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$? BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
@@ -98,38 +98,18 @@ check_chore_contract() {
fi fi
} }
check_author_contract() {
local desc="$1" want_fail="$2" author="$3" body="$4" base="$5" head="$6"
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="chore/contract" \
PR_TITLE="Align PR attribution policy" PR_AUTHOR="$author" \
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \
GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
local got_lines=0
printf '%s\n' "$out" | grep -qE '^FAIL' && got_lines=1 || true
if [ "$rc" -eq "$want_fail" ] && [ "$got_lines" -eq "$want_fail" ]; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
else
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
}
# break-glass # break-glass
check "dfritz exempt — invalid branch" 0 "totally/wrong-branch" "no prefix" "dfritz" check "dfritz exempt — invalid branch" 0 "totally/wrong-branch" "no prefix" "dfritz"
check "dfritz exempt — mismatch" 0 "dev/bug-abc/thing" "[bug-xyz] Thing" "dfritz" check "dfritz exempt — mismatch" 0 "dev/bug-abc/thing" "[bug-xyz] Thing" "dfritz"
# valid role/bug — all roster handles # valid role/bug — all roster handles
check "dev/bug matching title" 0 "dev/bug-x7k2m9/fix-terminal-resize" "[bug-x7k2m9] fix(terminal): preserve resize" "dev" check "dev/bug matching title" 0 "dev/bug-x7k2m9/fix-terminal-resize" "[bug-x7k2m9] Fix terminal resize" "dev"
check "architect/bug matching title" 0 "architect/bug-abc123/refactor-auth" "[bug-abc123] refactor(auth): simplify boundary" "architect" check "architect/bug matching title" 0 "architect/bug-abc123/refactor-auth" "[bug-abc123] Refactor auth" "architect"
check "ops/bug matching title" 0 "ops/bug-zz9/deploy-tweak" "[bug-zz9] fix(delivery): preserve rollout" "ops" check "ops/bug matching title" 0 "ops/bug-zz9/deploy-tweak" "[bug-zz9] Deploy tweak" "ops"
check "security/bug matching title" 0 "security/bug-s1a2/patch-cve" "[bug-s1a2] fix(auth): close bypass" "security" check "security/bug matching title" 0 "security/bug-s1a2/patch-cve" "[bug-s1a2] Patch CVE" "security"
check "perf/bug matching title" 0 "perf/bug-p0p0/reduce-latency" "[bug-p0p0] perf(checker): reduce latency" "perf" check "perf/bug matching title" 0 "perf/bug-p0p0/reduce-latency" "[bug-p0p0] Reduce latency" "perf"
check "ux/bug matching title" 0 "ux/bug-u1u1/polish-modal" "[bug-u1u1] fix(modal): preserve focus" "ux" check "ux/bug matching title" 0 "ux/bug-u1u1/polish-modal" "[bug-u1u1] Polish modal" "ux"
check "support/bug matching title" 0 "support/bug-sup9/clarify-error" "[bug-sup9] docs(error): clarify recovery" "support" check "support/bug matching title" 0 "support/bug-sup9/clarify-error" "[bug-sup9] Clarify error" "support"
# valid chore form # valid chore form
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"
@@ -143,13 +123,10 @@ check "bug-id mismatch" 1 "dev/bug-x7k2m9/fix-resize" "[bug-
# fail: 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"
check "role/bug Conventional title" 0 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev" check "role/bug Conventional title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev"
check "role/bug plain title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] Fix resize" "dev" check "role/bug Conventional title gap" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev"
check "role/bug missing scope" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix: fix resize" "dev" check "chore Conventional title" 1 "chore/bump-deps" "chore(deps): bump deps" "dev"
check "role/bug path scope" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub/bugs): fix resize" "dev" check "role/bug breaking title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix!: break resize" "dev"
check "role/bug empty description" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): " "dev"
check "role/bug whitespace description" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): " "dev"
check "chore Conventional title" 0 "chore/bump-deps" "chore(deps): bump deps" "dev"
# fail: 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"
@@ -239,7 +216,7 @@ canonical_body bug-x7k2m9 >> "${LARGE_BODY}"
large_out="" large_out=""
large_rc=0 large_rc=0
large_out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \ large_out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] fix(check): enforce contract" PR_AUTHOR="architect" \ PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
TEST_PR_BODY_FILE="${LARGE_BODY}" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \ TEST_PR_BODY_FILE="${LARGE_BODY}" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \
GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \ GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || large_rc=$? BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || large_rc=$?
@@ -253,46 +230,20 @@ else
fi fi
git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change" git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change"
PLAIN_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD) BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
check_contract_pass "Bug-backed commits need no watermark" "$BODY" "$GOOD_HEAD" "$PLAIN_HEAD" check_contract "agenthub 779 commits reject missing watermark" "$BODY" "$GOOD_HEAD" "$BAD_HEAD" "must end with an Authored-By"
check_chore_contract "chore requires PR attribution" "" "$BASE" "$GOOD_HEAD" "## Attribution must contain" check_chore_contract "chore requires PR attribution" "" "$BASE" "$GOOD_HEAD" "## Attribution must contain"
check_chore_contract "chore commits require watermark" "$(canonical_body bug-unused)" "$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>' 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) 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' 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) 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"
git -C "${FIXTURES}" commit --allow-empty -q -m "malformed legacy attribution" -m "Authored-By: incomplete"
MALFORMED_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
# Existing Human exemption is unchanged; other Humans and named Agents satisfy
# the same PR-description contract independently of any commit-message footer.
for author in dfritz human-contributor architect; do
for scenario in legacy plain misplaced nonfinal malformed; do
case "$scenario" in
legacy) head="$GOOD_HEAD" ;;
plain) head="$PLAIN_HEAD" ;;
misplaced) head="$MISPLACED_HEAD" ;;
nonfinal) head="$NONFINAL_HEAD" ;;
malformed) head="$MALFORMED_HEAD" ;;
esac
check_author_contract "$author accepts $scenario commit messages" 0 "$author" "$BODY" "$BASE" "$head"
done
done
for author in human-contributor architect; do
check_author_contract "$author requires PR-description attribution" 1 "$author" "" "$BASE" "$PLAIN_HEAD"
check_author_contract "$author rejects a missing PR model" 1 "$author" \
$'## Attribution\n- Authored-By: Codex <noreply@openai.com>' "$BASE" "$PLAIN_HEAD"
check_author_contract "$author rejects a non-noreply PR address" 1 "$author" \
$'## Attribution\n- Authored-By: Codex (GPT-5) <person@example.invalid>' "$BASE" "$PLAIN_HEAD"
done
check_author_contract "dfritz exemption still ignores PR attribution and commit range" 0 dfritz "" not-a-sha not-a-sha
check_contract "missing commit range rejects" "$BODY" "$GOOD_HEAD" "$GOOD_HEAD" "contains no PR commits" 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" check_contract "invalid SHA rejects before git" "$BODY" "not-a-sha" "$GOOD_HEAD" "lowercase hexadecimal"
check_contract "unavailable commit rejects" "$BODY" 0000000000000000000000000000000000000000 "$GOOD_HEAD" "must name available commits"
echo "" echo ""
echo "Results: ${PASS} passed, ${FAIL} failed" echo "Results: ${PASS} passed, ${FAIL} failed"