Compare commits

26 Commits
Author SHA1 Message Date
architect 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
architect c09fc72500 Reject Conventional Commit PR titles
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:24:37 +00:00
architect 6fe42b2ad2 Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): fetch PR body' (#7) from architect/bug-yhg8dqypwmar/fetch-pr-body into main
test / test (push) Successful in 9s
2026-08-27 18:21:03 +00:00
architect 76c72b02e2 Fetch PR body inside checker
test / test (pull_request) Successful in 8s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:17:27 +00:00
architect f7daa3f025 Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): reject hidden media' (#5) from architect/bug-yhg8dqypwmar/reject-hidden-media into main
test / test (push) Successful in 7s
2026-08-27 18:08:54 +00:00
architect 93b6bf62bb Explain hidden provenance recovery
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:06:03 +00:00
architect cb30e5d7a6 Exclude hidden media fallback provenance
test / test (pull_request) Successful in 8s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:01:03 +00:00
architect 29c34f1527 Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): reject hidden provenance' (#4) from architect/bug-yhg8dqypwmar/reject-hidden-provenance into main
test / test (push) Successful in 8s
2026-08-27 17:53:56 +00:00
architect 79d6ba5be9 Tokenize open disclosure attributes
test / test (pull_request) Successful in 6s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:48:43 +00:00
architect b8580d2082 Preserve open provenance disclosures
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:42:51 +00:00
architect 1e207dfd76 Honor canonical PR title forms
test / test (pull_request) Successful in 6s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:36:56 +00:00
architect 131976a5f1 Scope rendered provenance checks
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:35:21 +00:00
architect c07ea023c0 Validate visible rendered provenance
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:26:13 +00:00
architect a97440968c Reject malformed closing fences
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:13:39 +00:00
architect 0d768ef75b Reject hidden PR body content
test / test (pull_request) Successful in 6s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:11:21 +00:00
architect 5ce83cdf13 Keep escape parsing linear
test / test (pull_request) Successful in 6s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:05:40 +00:00
architect 9c07c5f32d Align comment parsing with Markdown
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 17:01:29 +00:00
architect 8a1d71c4e9 Enforce provenance on every agent change
test / test (pull_request) Successful in 7s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 16:56:05 +00:00
architect cc0bee61a4 Preserve visible Markdown code examples
test / test (pull_request) Successful in 8s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 16:52:45 +00:00
architect 14a8813a7b Reject provenance hidden in PR comments
test / test (pull_request) Successful in 5s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 16:47:39 +00:00
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
architect 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
dev 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
devandClaude 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
architect 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
architectandClaude 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
5 changed files with 561 additions and 52 deletions
+51 -14
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,24 @@ 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 }}
server-url: ${{ github.server_url }}
token: ${{ github.token }}
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 +52,50 @@ 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 |
| `server-url` | yes | Gitea server URL — `github.server_url` |
| `token` | yes | Gitea Actions token — `github.token` |
| `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 every non-break-glass 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. The description is a
plain-language imperative; Conventional Commit forms such as `fix(scope):`
are rejected.
All violations print a `WARN[check-naming]: ...` line. The step always 3. **Tracking and attribution** — on Bug-backed work, `## 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. Every PR has a separate
and add the job's context to the repo's `status_check_contexts`. `## Attribution` section containing the canonical `Authored-By`
product/model watermark. The action loads the body from Gitea by repository
and PR number, then asks Gitea to render it; both calls have a bounded
5-second connection and 15-second total wait. It then checks visible
`<h2>` sections outside collapsed `<details>` content. Fenced, commented,
scripted, or collapsed copies do not satisfy the visible provenance contract.
4. **Commit attribution** — every commit in `base-sha..head-sha`, including
commits on `chore/` branches, 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
+23 -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,18 @@ inputs:
dfritz is exempt from all checks. dfritz is exempt from all checks.
required: false required: false
default: '' default: ''
server-url:
description: Gitea server URL — github.server_url.
required: true
token:
description: Gitea Actions token — github.token.
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 +48,10 @@ 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 }}
GITEA_SERVER_URL: ${{ inputs.server-url }}
GITEA_TOKEN: ${{ inputs.token }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_SHA: ${{ inputs.base-sha }}
HEAD_SHA: ${{ inputs.head-sha }}
run: bash "${{ github.action_path }}/check.sh" run: bash "${{ github.action_path }}/check.sh"
+178 -18
View File
@@ -1,11 +1,18 @@
#!/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:-}"
BASE="${BASE_SHA:-}"
HEAD="${HEAD_SHA:-}"
SERVER_URL="${GITEA_SERVER_URL:-}"
TOKEN="${GITEA_TOKEN:-}"
REPOSITORY="${GITEA_REPOSITORY:-}"
PR_NUMBER="${GITEA_PR_NUMBER:-}"
# 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,10 +20,86 @@ if [ "${AUTHOR}" = "dfritz" ]; then
exit 0 exit 0
fi fi
WARN=0 FAILED=0
BRANCH_KIND="invalid" BRANCH_KIND="invalid"
BRANCH_BUG="" BRANCH_BUG=""
# Gitea's renderer is the visibility contract. Validate its output instead of
# maintaining a second Markdown parser in this action.
RENDERED_BODY=""
BODY=""
if [ -z "${SERVER_URL}" ] || [ -z "${TOKEN}" ] ||
! printf '%s' "${REPOSITORY}" | grep -qE '^[[:alnum:]_.-]+/[[:alnum:]_.-]+$' ||
! printf '%s' "${PR_NUMBER}" | grep -qE '^[1-9][0-9]*$'; then
echo "FAIL[check-naming]: server-url, token, repository, and PR number are required to load the PR body"
FAILED=1
elif ! BODY=$(curl --fail --silent --show-error \
--connect-timeout 5 \
--max-time 15 \
--header "Authorization: token ${TOKEN}" \
"${SERVER_URL%/}/api/v1/repos/${REPOSITORY}/pulls/${PR_NUMBER}" |
jq -er '.body | if type == "string" then . else error("body is not a string") end'); then
echo "FAIL[check-naming]: Gitea could not load the PR body"
FAILED=1
elif ! RENDERED_BODY=$(printf '%s' "${BODY}" |
jq -Rs '{Text: ., Mode: "gfm"}' |
curl --fail --silent --show-error \
--connect-timeout 5 \
--max-time 15 \
--header "Authorization: token ${TOKEN}" \
--header "Content-Type: application/json" \
--data-binary @- "${SERVER_URL%/}/api/v1/markdown"); then
echo "FAIL[check-naming]: Gitea could not render the PR body"
FAILED=1
fi
# A collapsed disclosure is valid supporting content, but provenance inside one
# is not visible by default. Remove only collapsed details subtrees; an open
# disclosure remains visible unless one of its ancestors is collapsed. Media
# fallback subtrees are likewise hidden when the browser supports the element.
VISIBLE_BODY=$(printf '%s\n' "${RENDERED_BODY}" |
awk '
{
line=$0 "\n"
for (i=1; i<=length(line); i++) {
char=substr(line, i, 1)
if (in_tag) {
tag=tag char
if (char == ">") {
lower=tolower(tag)
if (lower ~ /^<(video|audio)([[:space:]>])/) media_depth++
else if (lower ~ /^<\/(video|audio)([[:space:]>])/) {
if (media_depth > 0) media_depth--
}
else if (lower ~ /^<details([[:space:]>])/) {
attributes=lower
gsub(/"[^"]*"/, "", attributes)
parent_hidden=(details_depth > 0 && hidden[details_depth])
details_depth++
hidden[details_depth]=(parent_hidden || attributes !~ /[[:space:]]open([[:space:]=>]|$)/)
}
else if (lower ~ /^<\/details([[:space:]>])/) {
if (details_depth > 0) {
delete hidden[details_depth]
details_depth--
}
}
else if (!hidden[details_depth] && media_depth == 0) printf "%s", tag
in_tag=0
tag=""
}
}
else if (char == "<") {
in_tag=1
tag=char
}
else if (!hidden[details_depth] && media_depth == 0) printf "%s", char
}
}
')
hidden_content_guidance='move ## Tracking and ## Attribution outside collapsed <details> or <video>/<audio> fallback content'
# ---- branch form ---- # ---- branch form ----
# <role>/bug-<id>/<kebab> # <role>/bug-<id>/<kebab>
if echo "${BRANCH}" | grep -qE "^(dev|ux|ops|security|perf|architect|support)/bug-[a-z0-9]+/[a-z0-9][a-z0-9-]*$"; then if echo "${BRANCH}" | grep -qE "^(dev|ux|ops|security|perf|architect|support)/bug-[a-z0-9]+/[a-z0-9][a-z0-9-]*$"; then
@@ -26,10 +109,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 ----
@@ -38,21 +121,98 @@ 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
if [ "${BRANCH_KIND}" = "role-bug" ]; then TITLE_DESCRIPTION=$(printf '%s\n' "${TITLE}" | sed -E 's/^\[bug-[a-z0-9]+\][[:space:]]+//')
if [ -z "${TITLE_BUG}" ]; then if printf '%s\n' "${TITLE_DESCRIPTION}" | grep -qE '^[[:alnum:]-]+(\([^)]*\))?!?:[[:space:]]'; then
echo "WARN[check-naming]: title missing [${BRANCH_BUG}] prefix for branch '${BRANCH}'" echo "FAIL[check-naming]: title must use a plain-language imperative without Conventional Commit syntax"
WARN=1 FAILED=1
elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then
echo "WARN[check-naming]: bug-id mismatch — branch carries '${BRANCH_BUG}' but title carries '${TITLE_BUG}'"
WARN=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 if [ "${BRANCH_KIND}" = "role-bug" ]; then
if [ -z "${TITLE_BUG}" ]; then
echo "FAIL[check-naming]: title must lead with [${BRANCH_BUG}]"
echo " have: ${TITLE}"
echo " want: [${BRANCH_BUG}] ${TITLE}"
FAILED=1
elif [ "${TITLE_BUG}" != "${BRANCH_BUG}" ]; then
echo "FAIL[check-naming]: bug-id mismatch — branch carries '${BRANCH_BUG}' but title carries '${TITLE_BUG}'"
FAILED=1
fi
tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/<h2[^>]*>Tracking<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit }
in_section { print }
')
if [ -z "${tracking}" ]; then
echo "FAIL[check-naming]: PR body must contain a non-empty ## Tracking section visible by default; ${hidden_content_guidance}"
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
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 [ "${BRANCH_KIND}" != "invalid" ]; then
attribution=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/<h2[^>]*>Attribution<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit }
in_section { print }
')
watermark_re='Authored-By: .+ \(.+\) <a href="mailto:noreply@[[:alnum:].-]+"[^>]*>noreply@[[:alnum:].-]+</a>'
if ! printf '%s\n' "${attribution}" | grep -qE "${watermark_re}"; then
echo "FAIL[check-naming]: visible ## Attribution must contain an Authored-By product/model watermark; ${hidden_content_guidance}"
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
fi
if [ "${FAILED}" -eq 0 ]; then
echo "check-naming: ok" echo "check-naming: ok"
fi fi
exit 0 exit "${FAILED}"
Executable
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Deterministic Gitea Markdown renderer stub for check.sh contract tests.
set -euo pipefail
connect_timeout=0
total_timeout=0
url=""
while [ "$#" -gt 0 ]; do
case "$1" in
--connect-timeout)
[ "${2:-}" = "5" ] || exit 2
connect_timeout=1
shift 2
;;
--max-time)
[ "${2:-}" = "15" ] || exit 2
total_timeout=1
shift 2
;;
--header|--data-binary)
shift 2
;;
http*)
url="$1"
shift
;;
*) shift ;;
esac
done
[ "${connect_timeout}" -eq 1 ] && [ "${total_timeout}" -eq 1 ] || exit 2
if [[ "${url}" == */api/v1/repos/action/check-naming/pulls/1 ]]; then
if [ -n "${TEST_PR_BODY_FILE:-}" ]; then
jq -Rs '{body: .}' < "${TEST_PR_BODY_FILE}"
else
printf '%s' "${PR_BODY:-}" | jq -Rs '{body: .}'
fi
exit 0
fi
BODY=$(jq -r '.Text')
if printf '%s\n' "${BODY}" | grep -Fq '[[stall-renderer]]'; then
exit 28
fi
if printf '%s\n' "${BODY}" | grep -Fq '<!--'; then
exit 0
fi
printf '%s\n' "${BODY}" | awk '
{
line=$0
lower=tolower(line)
pos=1
while (pos <= 4 && substr(line, pos, 1) == " ") pos++
char=substr(line, pos, 1)
run=0
if (char == "`" || char == "~") while (substr(line, pos + run, 1) == char) run++
rest=substr(line, pos + run)
if (in_fence) {
if (pos <= 4 && char == fence_char && run >= fence_run && rest ~ /^[[:blank:]]*$/) in_fence=0
next
}
if (in_script) {
if (lower ~ /<\/script[[:blank:]]*>/) in_script=0
next
}
if (pos <= 4 && run >= 3 && (char == "~" || index(rest, "`") == 0)) {
in_fence=1
fence_char=char
fence_run=run
next
}
if (lower ~ /^[ ]{0,3}<script([[:blank:]>])/) {
in_script=1
next
}
if (lower ~ /^[ ]{0,3}<\/details([[:blank:]>])/) {
print "</details>"
next
}
if (lower ~ /^[ ]{0,3}<details([[:blank:]>])/) {
attributes=lower
gsub(/"[^"]*"/, "", attributes)
if (attributes ~ /[[:blank:]]open([[:blank:]=>]|$)/) sub(/<details open>/, "<details open=\"\">", line)
print line
next
}
if (lower ~ /^[ ]{0,3}<\/?(video|audio)([[:blank:]>])/) {
print line
next
}
if (line ~ /^## Tracking[[:space:]]*$/) print "<h2>Tracking</h2>"
else if (line ~ /^## Attribution[[:space:]]*$/) print "<h2>Attribution</h2>"
else if (line ~ /^- Fixes[[:space:]]/) print "<li>" substr(line, 3) "</li>"
else if (line ~ /Authored-By:.*<noreply@[[:alnum:].-]+>/) {
email=line
sub(/^.*</, "", email)
sub(/>.*/, "", email)
sub(/ <noreply@[[:alnum:].-]+>.*/, "", line)
print line " <a href=\"mailto:" email "\">" email "</a>"
}
else {
gsub(/</, "\\&lt;", line)
gsub(/>/, "\\&gt;", line)
print line
}
}
'
+199 -16
View File
@@ -3,26 +3,96 @@
set -euo pipefail set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh" SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
TEST_BIN="$(cd "$(dirname "$0")" && pwd)/bin"
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" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" \
echo "FAIL [$desc]: script exited $rc (must always exit 0)" GITEA_REPOSITORY="action/check-naming" GITEA_PR_NUMBER=1 PATH="${TEST_BIN}:$PATH" \
echo " output: $out" BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
FAIL=$((FAIL + 1)) local got_lines=0
return echo "$out" | grep -qE "^FAIL" && got_lines=1 || true
fi if [ "$rc" -eq "$want_fail" ] && [ "$got_lines" -eq "$want_fail" ]; then
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" 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=$?
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"
FAIL=$((FAIL + 1))
fi
}
check_contract_pass() {
local desc="$1" body="$2" base="$3" head="$4"
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" 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=$?
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -Fq "check-naming: ok"; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
else
echo "FAIL [$desc]: expected exit=0 and check-naming: ok, got exit=$rc"
echo " output: $out"
FAIL=$((FAIL + 1))
fi
}
check_chore_contract() {
local desc="$1" body="$2" base="$3" head="$4" diagnostic="$5"
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="chore/contract" \
PR_TITLE="Improve delivery contract" PR_AUTHOR="dev" \
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=$?
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 +115,20 @@ 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"
check "role/bug Conventional title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev"
check "role/bug Conventional title gap" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub): fix resize" "dev"
check "chore Conventional title" 1 "chore/bump-deps" "chore(deps): bump deps" "dev"
check "role/bug breaking title" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix!: break resize" "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 +136,115 @@ 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"
check_contract "hidden provenance does not satisfy the visible contract" \
$'<!--\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "comment removal cannot synthesize section headings" \
$'<!-- hidden -->## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n<!-- hidden -->## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "four-space indented backticks do not expose comments" \
$' ````\n<!--\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "unequal backtick runs do not expose comments" \
$'`<!--``\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "invalid backtick fence info does not expose comments" \
$'```html`oops\n<!--\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "invalid fence closer does not expose comments" \
$'```html\n```oops\n```\n<!--\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "fences inside comments do not expose provenance" \
$'<!--\n```html\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "escaped backticks do not hide a comment opener" \
$'\\`<!--\\`\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n-->' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "fenced provenance does not satisfy the visible contract" \
$'```\n```oops\n## 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" "PR body must contain a non-empty ## Tracking section"
check_contract "indented fence closer does not expose provenance" \
$'```\n ```\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n```' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract_pass "fenced code examples remain available" \
$'```html\n<script>example only</script>\n```\n\n## 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"
check_contract "details cannot collapse provenance" \
$'<details><summary>Release notes</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract_pass "open details preserve visible provenance" \
$'<details open><summary>Release notes</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>' \
"$BASE" "$GOOD_HEAD"
check_contract "open details inside collapsed details remain hidden" \
$'<details><summary>Release notes</summary>\n<details open><summary>Visible only after expansion</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>\n</details>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "open text in another details attribute remains collapsed" \
$'<details title="x open y"><summary>Release notes</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "video fallback cannot hide provenance" \
$'<video controls>\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</video>' \
"$BASE" "$GOOD_HEAD" "move ## Tracking and ## Attribution outside collapsed <details> or <video>/<audio> fallback content"
check_contract "audio fallback cannot hide provenance" \
$'<audio controls>\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</audio>' \
"$BASE" "$GOOD_HEAD" "move ## Tracking and ## Attribution outside collapsed <details> or <video>/<audio> fallback content"
check_contract_pass "unrelated collapsed details remain available" \
$'<details><summary>Logs</summary>\n\nSupporting output\n</details>\n\n## 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"
check_contract "script cannot suppress provenance" \
$'<script>\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</script>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "stalled renderer fails closed at the curl timeout" \
"$(canonical_body bug-x7k2m9)"$'\n[[stall-renderer]]' \
"$BASE" "$GOOD_HEAD" "Gitea could not render the PR body"
LARGE_BODY="${FIXTURES}/large-pr-body"
awk 'BEGIN { for (i=0; i<150000; i++) printf "x"; print "" }' > "${LARGE_BODY}"
canonical_body bug-x7k2m9 >> "${LARGE_BODY}"
large_out=""
large_rc=0
large_out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
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" \
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=$?
if [ "${large_rc}" -eq 0 ] && printf '%s\n' "${large_out}" | grep -Fq "check-naming: ok"; then
echo "PASS [large PR body loads without environment transport]"
PASS=$((PASS + 1))
else
echo "FAIL [large PR body loads without environment transport]: expected exit=0 and check-naming: ok, got exit=${large_rc}"
echo " output: ${large_out}"
FAIL=$((FAIL + 1))
fi
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"
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>'
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 ]