diff --git a/.github/scripts/marker-check.sh b/.github/scripts/marker-check.sh index 65c1be6..b973f8f 100755 --- a/.github/scripts/marker-check.sh +++ b/.github/scripts/marker-check.sh @@ -3,7 +3,10 @@ # outlived the releases that shipped their machinery. A release candidate must # therefore reject a marker its assembled changelog makes false, while every # tree rejects an untraceable marker. Cross-repo citations are traceable but -# are not compared with this repository's changelog. +# are not compared with this repository's changelog; a marker for this repo's +# own issue uses bare #N, never a self-qualified repository citation (#238 D8). +# CHANGELOG.md is the release oracle and immutable shipped prose, so it and the +# fragments that feed it are excluded from the documentation scan (#238 D5). # # Usage: marker-check.sh [tree-dir] (default: the repository root) set -euo pipefail @@ -26,25 +29,36 @@ trap 'rm -f "$marker_records"' EXIT mapfile -d '' markdown_files < <(git -C "$tree" ls-files -z -- '*.md') for relative in "${markdown_files[@]}"; do case "$relative" in - changelog.d/*) continue ;; + CHANGELOG.md|changelog.d/*) continue ;; esac if ! awk -v file="$relative" ' - { lines[NR] = $0 } + function without_inline_code(text, before, after) { + while (match(text, /`[^`]*`/)) { + before = substr(text, 1, RSTART - 1) + after = substr(text, RSTART + RLENGTH) + text = before after + } + return text + } + { + lines[NR] = $0 + scan_lines[NR] = without_inline_code($0) + } END { token = "**unreleased**" citation_re = "^[[:space:]]*\\((([[:alnum:]_.-]+/)?[[:alnum:]_.-]+)?#[0-9]+\\)" bad = 0 for (line_no = 1; line_no <= NR; line_no++) { - remaining = lines[line_no] + remaining = scan_lines[line_no] offset = 0 while ((at = index(remaining, token)) != 0) { rest = substr(remaining, at + length(token)) candidate = rest next_line = line_no + 1 while (candidate ~ /^[[:space:]]*$/ && next_line <= NR) { - candidate = candidate " " lines[next_line] + candidate = candidate " " scan_lines[next_line] next_line++ } @@ -60,7 +74,7 @@ for relative in "${markdown_files[@]}"; do } offset += at + length(token) - 1 - remaining = substr(lines[line_no], offset + 1) + remaining = substr(scan_lines[line_no], offset + 1) } } exit bad diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 38c9fc0..cb3f30b 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -113,14 +113,14 @@ the machinery at all: self-hosted runner still wants it: the guard's value is the day somebody adds one. - This guide documents `main`. New machinery is marked with the lowercase - word `unreleased` in bold, immediately followed by its issue citation - (for example, `(#238)`); whitespace between them may include a line break. - A citation is mandatory, because a marker the guard cannot trace is a - marker it cannot prove false. Cross-repo citations such as `(crew#293)` - satisfy that traceability rule but are not compared with this repository's - release section. The ceremony-only `marker-check.sh` guard enforces both - rules. The release PR that ships the machinery clears, in that same PR, + This guide documents `main`. A marker is the literal token + `**unreleased**` immediately followed by its issue citation (for example, + `(#238)`); whitespace between them may include a line break. A citation is + mandatory, because a marker the guard cannot trace is a marker it cannot + prove false. A marker for this repository's own issue uses bare `#N`. + Cross-repo citations such as `(crew#293)` satisfy the traceability rule but + are not compared with this repository's release section. The ceremony-only + `marker-check.sh` guard enforces these rules. The release PR that ships the machinery clears, in that same PR, every marker its own assembled section makes false: the section cites its issues, each marker cites the same issue, and the release PR's diff is the one place both halves are visible at once (#221). If an action does not exist at the @@ -153,7 +153,7 @@ the machinery at all: - uses: heavy-duty/ceremony/actions/refs-not-closing@ ``` - `refs-not-closing` is **unreleased** until the first tag carrying #218. + `refs-not-closing` is **unreleased** (#218) until the first tag carrying it. Adopt this caller with that ordinary pin bump; never point only this file at a moving or newer ref. 7. **Labels automation** (optional but recommended): the two callers from @@ -592,7 +592,7 @@ mirror), `--check` re-diffs it in CI on every PR, so a hand edit or a stale pin goes red instead of quietly governing. `RELEASES.md` joins that mirror with the first tag carrying ceremony#248. -It is **unreleased** until that tag exists: consumers add +It is **unreleased** (#248) until that tag exists: consumers add `.ceremony/RELEASES.md` only with the ordinary pin bump and re-sync, never by copying it ahead of their pinned doctrine set. diff --git a/test/marker-check.test.sh b/test/marker-check.test.sh index 7017e84..ca373fd 100644 --- a/test/marker-check.test.sh +++ b/test/marker-check.test.sh @@ -84,11 +84,37 @@ EOF check "a cross-repo citation is valid and ignored by release comparison" 0 \ "agree with the tree" run_check cross-repo +fixture self-qualified 0.6.0 +printf 'Ceremony work remains **unreleased** (ceremony#248).\n' \ + >"$TMP/self-qualified/docs/CONSUMERS.md" +cat >"$TMP/self-qualified/CHANGELOG.md" <<'EOF' +# Changelog + +## 0.6.0 — 2026-08-03 + +- Ceremony work shipped (#248). +EOF +check "a self-qualified citation is ignored; local markers must use bare #N" 0 \ + "agree with the tree" run_check self-qualified + fixture exclusions 0.6.0-dev -printf '# Notes\n\n## Unreleased\n' >"$TMP/exclusions/NOTES.md" +cat >"$TMP/exclusions/NOTES.md" <<'EOF' +# Notes + +## Unreleased + +The marker token is `**unreleased**`. +EOF printf -- '- A fragment may say **unreleased** without being documentation.\n' \ >"$TMP/exclusions/changelog.d/999.md" -check "headings and changelog fragments do not trip the guard" 0 \ +cat >"$TMP/exclusions/CHANGELOG.md" <<'EOF' +# Changelog + +## 0.5.0 — 2026-08-03 + +- Shipped prose may discuss **unreleased** markers without becoming one. +EOF +check "headings, inline mentions, changelog entries, and fragments are excluded" 0 \ "agree with the tree" run_check exclusions summary