Author SHA1 Message Date
dfritz 31b42b1851 fix: require authorship only in PR descriptions
test / test (pull_request) Successful in 9s
Align the shared naming checker with the Human-directed PR-only attribution policy. Preserve visible PR provenance, naming, tracking, current identity exemptions, available base/head commits and nonempty-range validation without inspecting commit messages.

Verify Human and named-Agent callers against legacy, plain, malformed, misplaced and nonfinal commit trailers; retain missing/malformed PR attribution and invalid-range rejection. All 79 offline contract tests, shell syntax and whitespace checks pass.

Ticket: https://agenthub.fritzlab.net/t-d12vqnp63v5d

Policy decision: websites/docs.fritzlab.net#454 (comment)
2026-10-18 15:43:30 +00:00
4 changed files with 66 additions and 32 deletions

No files matched your search

+9 -7
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, and AI authorship against the fritzlab Git standard. The current tracking, PR-description AI authorship, and commit range against the fritzlab
action fails (exits 1) on any violation; `@v1` is the legacy warn-only release Git standard. The current 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)).
@@ -91,10 +91,12 @@ 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 attribution** — every commit in `base-sha..head-sha`, including 4. **Commit range** — `base-sha` and `head-sha` must identify available commits,
commits on `chore/` branches, ends with and `base-sha..head-sha` must contain at least one PR commit.
the canonical `Authored-By` trailer, separated from the message body by a The naming job must check out full history before this action.
blank line. The naming job must check out full history before this action. Attribution is required only in the PR description; commit messages are not
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
@@ -103,7 +105,7 @@ context to the repo's `status_check_contexts` in agenthub `hub/hub.yaml`.
## Versions ## Versions
- unreleased — adds Bug tracking plus PR and commit attribution enforcement. - unreleased — requires Bug tracking and visible PR attribution, without commit-message watermarks.
- `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.
+1 -1
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 AI attribution Validates that a PR's branch, title, Bug tracking, and PR-description 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).
+4 -18
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Branch, PR body, and commit contract checker. # Branch, PR body, and commit-range 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
@@ -180,23 +180,9 @@ 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
commit_count=0 # Attribution belongs only in the PR description, not commit messages.
while IFS= read -r commit; do # Keep the existing nonempty-range gate independently of message content.
[ -n "${commit}" ] || continue commit_count=$(git rev-list --count "${BASE}..${HEAD}" 2>/dev/null) || commit_count=0
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
+52 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Tests for check.sh — exercises valid/invalid branch+title matrices. # Tests for check.sh — exercises naming, PR attribution, and commit ranges.
set -euo pipefail set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh" SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
@@ -98,6 +98,26 @@ 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"
@@ -233,20 +253,46 @@ else
fi fi
git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change" git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change"
BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD) PLAIN_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" check_contract_pass "Bug-backed commits need no watermark" "$BODY" "$GOOD_HEAD" "$PLAIN_HEAD"
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"