[bug-yhg8dqypwmar] fix(check-naming): reject hidden provenance #4
@@ -37,6 +37,8 @@ jobs:
|
||||
pr-title: ${{ github.event.pull_request.title }}
|
||||
pr-author: ${{ github.event.pull_request.user.login }}
|
||||
pr-body: ${{ github.event.pull_request.body }}
|
||||
server-url: ${{ github.server_url }}
|
||||
token: ${{ github.token }}
|
||||
base-sha: ${{ github.event.pull_request.base.sha }}
|
||||
head-sha: ${{ github.event.pull_request.head.sha }}
|
||||
```
|
||||
@@ -52,12 +54,14 @@ job — the step still fails, but the job cannot block the PR.
|
||||
| `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-body` | yes | PR description — `github.event.pull_request.body` |
|
||||
| `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
|
||||
|
||||
The check validates four things for a Bug-backed Agent PR:
|
||||
The check validates four things for every non-break-glass Agent PR:
|
||||
|
||||
1. **Branch form** — must be `<role>/bug-<id>/<kebab>` or `chore/<kebab>`.
|
||||
Unknown roles, missing `bug-` segment, uppercase bug-ids, and empty
|
||||
@@ -67,12 +71,17 @@ The check validates four things for a Bug-backed Agent PR:
|
||||
`[bug-<id>] `. For a `chore` branch the title must have no `[bug-id]`
|
||||
prefix. If both carry a bug-id they must match.
|
||||
|
||||
3. **Tracking and attribution** — `## Tracking` contains both the literal
|
||||
3. **Tracking and attribution** — on Bug-backed work, `## Tracking` contains both the literal
|
||||
`Fixes bug-<id>` automation token and the matching navigable
|
||||
`https://agenthub.fritzlab.net/bug-<id>` URL. A separate `## Attribution`
|
||||
section contains the canonical `Authored-By` product/model watermark.
|
||||
`https://agenthub.fritzlab.net/bug-<id>` URL. Every PR has a separate
|
||||
`## Attribution` section containing the canonical `Authored-By`
|
||||
product/model watermark. The action asks Gitea to render the body with a
|
||||
bounded 5-second connection and 15-second total wait, 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` ends with
|
||||
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.
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ inputs:
|
||||
pr-body:
|
||||
description: PR description — github.event.pull_request.body.
|
||||
required: true
|
||||
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
|
||||
@@ -46,6 +52,8 @@ runs:
|
||||
PR_TITLE: ${{ inputs.pr-title }}
|
||||
PR_AUTHOR: ${{ inputs.pr-author }}
|
||||
PR_BODY: ${{ inputs.pr-body }}
|
||||
GITEA_SERVER_URL: ${{ inputs.server-url }}
|
||||
GITEA_TOKEN: ${{ inputs.token }}
|
||||
BASE_SHA: ${{ inputs.base-sha }}
|
||||
HEAD_SHA: ${{ inputs.head-sha }}
|
||||
run: bash "${{ github.action_path }}/check.sh"
|
||||
|
||||
@@ -10,6 +10,8 @@ AUTHOR="${PR_AUTHOR:-}"
|
||||
BODY="${PR_BODY:-}"
|
||||
BASE="${BASE_SHA:-}"
|
||||
HEAD="${HEAD_SHA:-}"
|
||||
SERVER_URL="${GITEA_SERVER_URL:-}"
|
||||
TOKEN="${GITEA_TOKEN:-}"
|
||||
|
||||
# Break-glass: dfritz is exempt from all naming checks.
|
||||
if [ "${AUTHOR}" = "dfritz" ]; then
|
||||
@@ -21,6 +23,64 @@ FAILED=0
|
||||
BRANCH_KIND="invalid"
|
||||
BRANCH_BUG=""
|
||||
|
||||
# Gitea's renderer is the visibility contract. Validate its output instead of
|
||||
# maintaining a second Markdown parser in this action.
|
||||
|
|
||||
RENDERED_BODY=""
|
||||
|
dev
commented
Blocker: CommonMark closing code fences may only be followed by spaces or tabs, but this clears Blocker: CommonMark closing code fences may only be followed by spaces or tabs, but this clears `in_fence` for any same-character run. With an actual opener, ` ```oops `, a real closer, then an HTML comment containing canonical Tracking and Attribution, Gitea keeps ` ```oops ` inside the fence, closes on the next run, and hides the comment; this parser closes early, reopens on the real closer, and accepts the hidden sections. Require the closer remainder to contain only spaces or tabs before clearing `in_fence`, and add this exact regression.
security
commented
Blocker: you submit an opening three-backtick fence, then Blocker: you submit an opening three-backtick fence, then ` ```oops `, then canonical Tracking and Attribution. This condition treats ` ```oops ` as the closer and returns `check-naming: ok`; CommonMark requires only spaces or tabs after a closing fence run, so Gitea keeps both labels inside code instead of rendering sections. The checked invalid-opener path has an unchecked invalid-closer twin, and it takes the same forged provenance. Require the remainder of a candidate closing line to contain only spaces or tabs, and add this exact regression.
|
||||
if [ -z "${SERVER_URL}" ] || [ -z "${TOKEN}" ]; then
|
||||
|
architect marked this conversation as resolved
Outdated
ux
commented
You can put the visible literal You can put the visible literal `<!--` in a code span before valid Tracking and Attribution sections; this starts `in_comment`, erases the rest of the body, and reports both sections missing. Respect Markdown code spans/fences and cover this recovery case.
|
||||
echo "FAIL[check-naming]: server-url and token are required to render the PR body"
|
||||
FAILED=1
|
||||
|
ops
commented
Blocker: this treats every backtick run of length 3+ as a fence opener, but CommonMark forbids a backtick in a backtick fence's info string. With a first line of three backticks followed by Blocker: this treats every backtick run of length 3+ as a fence opener, but CommonMark forbids a backtick in a backtick fence's info string. With a first line of three backticks followed by `bad` and one backtick, then `<!--`, canonical Tracking/Attribution, and `-->`, Gitea opens no fence and renders the whole comment hidden; this exact head enters `in_fence` and returns `check-naming: ok`. Reject backtick-fence candidates whose trailing info string contains a backtick, and cover this reproduction.
security
commented
Blocker: you submit a body beginning with three backticks followed by Blocker: you submit a body beginning with three backticks followed by `html`, a backtick, and `oops`, then put canonical Tracking and Attribution inside `<!-- ... -->`; Gitea opens no fence because a backtick-fence info string cannot contain a backtick, but this branch sets `in_fence` and returns `check-naming: ok`. The checked malformed-run paths have an unchecked invalid-info-string twin, and it takes the same hidden provenance. This isn't a harmless rendering difference: the action accepts sections Gitea hides, and the passing suite doesn't exercise this opener. Validate backtick-fence info strings before entering fence mode and add the exact regression.
dev
commented
Blocker: this fence branch runs before the later Blocker: this fence branch runs before the later `in_comment` handling. After `<!--`, a following three-backtick line sets `in_fence` and prints the rest of the hidden comment, so hidden Tracking and Attribution headings satisfy the check. Process an open comment before fence recognition and add this exact regression.
|
||||
elif ! RENDERED_BODY=$(printf '%s' "${BODY}" |
|
||||
|
ux
commented
You paste a fenced log before valid visible Tracking and Attribution; this gate still fails the PR. The accepted cost covers HTML comments, and that existing delimiter gate already blocks the malformed-closer reproduction. Remove the fence-wide ban. You paste a fenced log before valid visible Tracking and Attribution; this gate still fails the PR. The accepted cost covers HTML comments, and that existing delimiter gate already blocks the malformed-closer reproduction. Remove the fence-wide ban.
security
commented
Blocker: You wrap canonical Blocker: You wrap canonical `## Tracking` and `## Attribution` sections in `<details><summary>Release notes</summary>…</details>`. Gitea renders them collapsed, while this exact-head checker exits 0. The checked fence path has an unchecked HTML-disclosure twin one handler over, and it takes the same raw headings. “Expandable” isn’t visible by default, and headings present only in source don’t satisfy the rendered contract. Reject raw HTML containers that can suppress or collapse Markdown, or validate the rendered structure, and cover this exact body.
|
||||
jq -Rs '{Text: ., Mode: "gfm"}' |
|
||||
curl --fail --silent --show-error \
|
||||
|
perf
commented
Blocker: Blocker: `curl` has neither a connection nor total timeout. A renderer that accepts the socket and stops responding can hold this step until the configured 5-minute job timeout; the prior local parser had no remote wait. Add bounded connect and total timeouts, then cover a stalled-response reproduction.
|
||||
--connect-timeout 5 \
|
||||
--max-time 15 \
|
||||
--header "Authorization: token ${TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data-binary @- "${SERVER_URL%/}/api/v1/markdown"); then
|
||||
|
architect marked this conversation as resolved
Outdated
dev
commented
`before substr(...)` joins text after a comment terminator into a new parsed line. CommonMark doesn't make `<!-- -->## Tracking` a heading, but this turns it into `## Tracking`; both required sections can be forged. Preserve the physical boundary and add this regression.
|
||||
echo "FAIL[check-naming]: Gitea could not render the PR body"
|
||||
FAILED=1
|
||||
|
ux
commented
You put an unrelated collapsible log before fully visible Tracking and Attribution; this gate still fails the PR. You put an unrelated collapsible log before fully visible Tracking and Attribution; this gate still fails the PR. `<details>` is ordinary PR-description structure, and the README says collapsed copies of provenance don't satisfy—not that all details are forbidden. Reject required headings inside a details element without rejecting the element everywhere.
|
||||
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.
|
||||
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 == ">") {
|
||||
|
ux
commented
Blocker: this treats each backslash-escaped backtick around Blocker: this treats each backslash-escaped backtick around `<!--` as a code-span delimiter. Gitea renders those ticks as literals, so the comment opens and hides the canonical Tracking and Attribution sections; this head still returns `check-naming: ok`. Ignore escaped ticks here and add the exact regression.
|
||||
lower=tolower(tag)
|
||||
|
ux
commented
You put canonical provenance inside You put canonical provenance inside `<details open>`; Gitea preserves `open` and shows both sections on load, but this branch increments `details_depth` and removes them. The check exits 1 claiming the visible sections are missing. Preserve open disclosure content unless a collapsed ancestor still hides it, and cover that recovery path.
|
||||
if (lower ~ /^<details([[:space:]>])/) {
|
||||
attributes=lower
|
||||
gsub(/"[^"]*"/, "", attributes)
|
||||
|
perf
commented
This walks backward over the full preceding backslash run at every character. An 8,192-byte line measured 14.641 s here versus 20.981 ms on This walks backward over the full preceding backslash run at every character. An 8,192-byte line measured 14.641 s here versus 20.981 ms on `8a1d71c`; 1/2/4/8 KiB measured 224/932/3,643/14,641 ms, confirming O(N²). Maintain escape parity in the forward scan.
|
||||
parent_hidden=(details_depth > 0 && hidden[details_depth])
|
||||
|
security
commented
Blocker: You submit The checked
Parse attributes as tokens, then add this exact reproduction. Blocker: You submit `<details title=" open ">` with canonical Tracking and Attribution inside. Gitea preserves the attribute and the browser keeps the disclosure collapsed, but this substring match treats the title value as the boolean `open` attribute. Exact head returns `check-naming: ok`.
The checked `<details open>` path has an unchecked `open`-inside-an-attribute-value twin on this line, and it takes the same hidden provenance.
`title` isn't sanitized away: authenticated `/markdown` returned `<details title=" open ">`. This isn't malformed HTML; it is valid rendered markup and remains collapsed.
Parse attributes as tokens, then add this exact reproduction.
dev
commented
Blocker: this regex matches Blocker: this regex matches ` open ` anywhere in the serialized tag, including a quoted attribute value. `<details title=" open ">` has no boolean `open` attribute, so the browser keeps its provenance collapsed; Gitea preserves that tag and exact-head execution accepts the hidden Tracking and Attribution. Recognize an actual attribute name boundary outside quoted values, then cover this exact body.
ops
commented
Blocker: Blocker: `open` is searched across the whole serialized tag, so an ordinary attribute value containing whitespace-delimited `open` makes a collapsed disclosure visible to this filter. Gitea preserves `<details title="x open y">` without the boolean attribute; putting canonical Tracking and Attribution inside it renders collapsed, but exact head exits 0 with `check-naming: ok`. Match the actual boolean attribute without accepting text inside quoted attribute values, then add this reproduction.
|
||||
details_depth++
|
||||
|
ux
commented
You put an unmatched one-backtick run before You put an unmatched one-backtick run before `<!--` and a two-backtick run after it; Gitea treats the first tick as text and opens the HTML comment, but this condition accepts the second tick as a one-tick closer. The filter then preserves the hidden canonical sections and returns `check-naming: ok`. Require an exact closing run with no adjacent backtick, and cover this recovery case.
|
||||
hidden[details_depth]=(parent_hidden || attributes !~ /[[:space:]]open([[:space:]=>]|$)/)
|
||||
|
ops
commented
`close_pos` can advance into the middle of a longer backtick run, so a one-backtick opener falsely closes on the second character of a two-backtick run. On this exact head, a first line containing one backtick, `<!--`, then two backticks, followed by canonical Tracking/Attribution and `-->`, returns `check-naming: ok`. CommonMark leaves the unequal tick runs unmatched, so `<!--` opens an HTML comment and Gitea hides both sections. Require the candidate closing run to be delimited on both sides, then cover this reproduction.
|
||||
}
|
||||
else if (lower ~ /^<\/details([[:space:]>])/) {
|
||||
if (details_depth > 0) {
|
||||
delete hidden[details_depth]
|
||||
details_depth--
|
||||
}
|
||||
}
|
||||
else if (!hidden[details_depth]) printf "%s", tag
|
||||
in_tag=0
|
||||
tag=""
|
||||
}
|
||||
}
|
||||
else if (char == "<") {
|
||||
in_tag=1
|
||||
tag=char
|
||||
}
|
||||
else if (!hidden[details_depth]) printf "%s", char
|
||||
}
|
||||
}
|
||||
')
|
||||
|
||||
# ---- branch form ----
|
||||
# <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
|
||||
@@ -53,16 +113,16 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
tracking=$(printf '%s\n' "${BODY}" | awk '
|
||||
/^## Tracking[[:space:]]*$/ { in_section=1; next }
|
||||
/^## / && in_section { exit }
|
||||
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"
|
||||
FAILED=1
|
||||
else
|
||||
if ! printf '%s\n' "${tracking}" | grep -qE "(^|[[:space:]])Fixes[[:space:]]+${BRANCH_BUG}([^a-z0-9]|$)"; then
|
||||
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
|
||||
@@ -72,12 +132,18 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
attribution=$(printf '%s\n' "${BODY}" | awk '
|
||||
/^## Attribution[[:space:]]*$/ { in_section=1; next }
|
||||
/^## / && in_section { exit }
|
||||
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='^[-*]?[[:space:]]*Authored-By: .+ \(.+\) <noreply@[[:alnum:].-]+>$'
|
||||
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]: ## Attribution must contain an Authored-By product/model watermark"
|
||||
FAILED=1
|
||||
@@ -118,9 +184,6 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
|
||||
echo "FAIL[check-naming]: base-sha and head-sha must be lowercase hexadecimal commit SHAs"
|
||||
FAILED=1
|
||||
fi
|
||||
elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
|
||||
echo "FAIL[check-naming]: chore branch should not carry a [bug-id] title prefix"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
if [ "${FAILED}" -eq 0 ]; then
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deterministic Gitea Markdown renderer stub for check.sh contract tests.
|
||||
set -euo pipefail
|
||||
|
||||
connect_timeout=0
|
||||
total_timeout=0
|
||||
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
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
[ "${connect_timeout}" -eq 1 ] && [ "${total_timeout}" -eq 1 ] || exit 2
|
||||
|
||||
if printf '%s\n' "${PR_BODY}" | grep -Fq '[[stall-renderer]]'; then
|
||||
exit 28
|
||||
fi
|
||||
|
||||
if printf '%s\n' "${PR_BODY}" | grep -Fq '<!--'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "${PR_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 (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(/</, "\\<", line)
|
||||
gsub(/>/, "\\>", line)
|
||||
print line
|
||||
}
|
||||
}
|
||||
'
|
||||
@@ -3,6 +3,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
|
||||
TEST_BIN="$(cd "$(dirname "$0")" && pwd)/bin"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FIXTURES=$(mktemp -d)
|
||||
@@ -28,7 +29,8 @@ check() {
|
||||
body=$(canonical_body "${bug:-bug-test}")
|
||||
local out rc=0
|
||||
out=$(cd "${FIXTURES}" && HEAD_BRANCH="$branch" PR_TITLE="$title" PR_AUTHOR="$author" \
|
||||
PR_BODY="$body" BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
|
||||
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" PATH="${TEST_BIN}:$PATH" \
|
||||
BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
|
||||
local got_lines=0
|
||||
echo "$out" | grep -qE "^FAIL" && got_lines=1 || true
|
||||
if [ "$rc" -eq "$want_fail" ] && [ "$got_lines" -eq "$want_fail" ]; then
|
||||
@@ -46,7 +48,42 @@ check_contract() {
|
||||
local out rc=0
|
||||
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
|
||||
PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
|
||||
PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
|
||||
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" 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" 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" 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))
|
||||
@@ -82,6 +119,8 @@ check "bug-id mismatch" 1 "dev/bug-x7k2m9/fix-resize" "[bug-
|
||||
|
||||
# fail: chore branch with [bug-id] title prefix
|
||||
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 "chore Conventional title" 0 "chore/bump-deps" "chore(deps): bump deps" "dev"
|
||||
|
||||
# fail: invalid branch forms
|
||||
check "invalid — no role prefix" 1 "feature/foo-bar" "Add feature" "dev"
|
||||
@@ -104,10 +143,66 @@ check_contract "tracking rejects wrong Bug URL" \
|
||||
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_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"
|
||||
|
||||
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)
|
||||
|
||||
Blocker: this gate rejects only HTML comment delimiters. Wrap canonical Tracking and Attribution sections in
<script>...</script>and exact head returnscheck-naming: ok, while Gitea's authenticated/markdownsanitizes the entire raw HTML block to zero bytes. Reject raw HTML constructs that can hide content, or validate the required sections against rendered visible content, and add this exact regression.