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
Evelyn Chen 2f18a5fd5b Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): require scoped titles' (#10) from architect/bug-yhg8dqypwmar/require-conventional-titles into main
test / test (push) Successful in 10s
2026-08-27 18:54:17 +00:00
Evelyn Chen ca4dccfbbf Document safe edited-event wiring
test / test (pull_request) Successful in 9s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:43:32 +00:00
Evelyn Chen 0789210d4e fix(check-naming): address title review feedback
test / test (pull_request) Successful in 9s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:41:42 +00:00
Evelyn Chen 20a4bcce9d Require scoped Bug titles
test / test (pull_request) Successful in 11s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:36:13 +00:00
Evelyn Chen a8ed3aadae Merge pull request '[bug-yhg8dqypwmar] fix(check-naming): document edited trigger' (#9) from architect/bug-yhg8dqypwmar/document-edited-trigger into main
test / test (push) Successful in 8s
2026-08-27 18:34:22 +00:00
Evelyn Chen 86076048bb Document edited-body trigger
test / test (pull_request) Successful in 8s
Authored-By: Codex (GPT-5) <noreply@openai.com>
2026-08-27 18:30:36 +00:00
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
Evelyn Chen 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
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
5 changed files with 610 additions and 57 deletions

No files matched your search

+64 -15
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, PR-description AI authorship, and commit range against the fritzlab
warn-only until the Bugs tracking system is live (see Git standard. The current 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
@@ -10,7 +11,7 @@ warn-only until the Bugs tracking system is live (see
``` ```
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 resize loss (chore PRs: no [bug-id] prefix) Title: [bug-x7k2m9] fix(terminal): preserve resize state
``` ```
- `<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`
@@ -22,19 +23,37 @@ Title: [bug-x7k2m9] Fix terminal resize loss (chore PRs: no [bug-id] prefix
## Usage ## Usage
```yaml ```yaml
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs: 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 }}
``` ```
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
job — the step still fails, but the job cannot block the PR.
## Inputs ## Inputs
| Name | Required | Description | | Name | Required | Description |
@@ -42,23 +61,53 @@ 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 be
`[bug-<id>] `. For a `chore` branch the title should have no `[bug-id]` `[bug-<id>] type(scope): description`. The type starts with a lowercase
prefix. If both carry a bug-id they must match. letter and the type and one-component scope contain only lowercase letters,
digits, and hyphens. An optional `!` may follow the scope, and the description
must contain a non-whitespace character. For a `chore` branch the title must
have no `[bug-id]` 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** — 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 range** — `base-sha` and `head-sha` must identify available commits,
and `base-sha..head-sha` must contain at least one PR commit.
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
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 — requires Bug tracking and visible PR attribution, without commit-message watermarks.
- `v2` — enforces branch and title naming.
- `v1` — legacy warn-only: logs `WARN` lines, always exits 0.
## Tests ## Tests
+24 -5
View File
@@ -1,13 +1,14 @@
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 PR-description 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
chore: chore/<kebab> bug-less trivia only chore: chore/<kebab> bug-less trivia only
title: [bug-<id>] Description (chore PRs: no [bug-id] prefix) title: [bug-<id>] type(scope): description
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,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"
+156 -13
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-range 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 ----
@@ -40,19 +123,79 @@ 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}] fix(scope): description"
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
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 '
/<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 elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
echo "WARN[check-naming]: chore branch should not carry a [bug-id] title prefix" echo "FAIL[check-naming]: chore branch should not carry a [bug-id] title prefix"
WARN=1 FAILED=1
fi fi
if [ "${WARN}" -eq 0 ]; then 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
# Attribution belongs only in the PR description, not commit messages.
# Keep the existing nonempty-range gate independently of message content.
commit_count=$(git rev-list --count "${BASE}..${HEAD}" 2>/dev/null) || commit_count=0
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
}
}
'
+256 -24
View File
@@ -1,28 +1,118 @@
#!/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"
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] fix(check): 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] fix(check): 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"
FAIL=$((FAIL + 1))
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" echo " output: $out"
FAIL=$((FAIL + 1)) FAIL=$((FAIL + 1))
fi fi
@@ -33,28 +123,35 @@ check "dfritz exempt — invalid branch" 0 "totally/wrong-branch" "no prefix"
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 resize" "dev" check "dev/bug matching title" 0 "dev/bug-x7k2m9/fix-terminal-resize" "[bug-x7k2m9] fix(terminal): preserve resize" "dev"
check "architect/bug matching title" 0 "architect/bug-abc123/refactor-auth" "[bug-abc123] Refactor auth" "architect" check "architect/bug matching title" 0 "architect/bug-abc123/refactor-auth" "[bug-abc123] refactor(auth): simplify boundary" "architect"
check "ops/bug matching title" 0 "ops/bug-zz9/deploy-tweak" "[bug-zz9] Deploy tweak" "ops" check "ops/bug matching title" 0 "ops/bug-zz9/deploy-tweak" "[bug-zz9] fix(delivery): preserve rollout" "ops"
check "security/bug matching title" 0 "security/bug-s1a2/patch-cve" "[bug-s1a2] Patch CVE" "security" check "security/bug matching title" 0 "security/bug-s1a2/patch-cve" "[bug-s1a2] fix(auth): close bypass" "security"
check "perf/bug matching title" 0 "perf/bug-p0p0/reduce-latency" "[bug-p0p0] Reduce latency" "perf" check "perf/bug matching title" 0 "perf/bug-p0p0/reduce-latency" "[bug-p0p0] perf(checker): reduce latency" "perf"
check "ux/bug matching title" 0 "ux/bug-u1u1/polish-modal" "[bug-u1u1] Polish modal" "ux" check "ux/bug matching title" 0 "ux/bug-u1u1/polish-modal" "[bug-u1u1] fix(modal): preserve focus" "ux"
check "support/bug matching title" 0 "support/bug-sup9/clarify-error" "[bug-sup9] Clarify error" "support" check "support/bug matching title" 0 "support/bug-sup9/clarify-error" "[bug-sup9] docs(error): clarify recovery" "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"
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" 0 "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 missing scope" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix: fix resize" "dev"
check "role/bug path scope" 1 "dev/bug-x7k2m9/fix-resize" "[bug-x7k2m9] fix(hub/bugs): fix 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"
# 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 +159,141 @@ 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] fix(check): 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"
PLAIN_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)
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"
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)
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)
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 "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"
[ "${FAIL}" -eq 0 ] [ "${FAIL}" -eq 0 ]