forked from heavy-duty/box
fix(changelog-monotonic): report containment vacuous when the base IS HEAD
Dropping the pull_request gate made merge_base == HEAD a routine path rather than a degradation, and the success line did not follow. On every push to main the step printed "all N release heading(s) at the merge base are still present" — a containment claim on the one event where deletion is undetectable, since the comparison is the file against itself. That is the dishonesty this PR fixed in the skip messages, surviving in the success message. The line now has two forms: containment vacuous, naming uniqueness as the half that ran, or the existing containment wording when a real base exists. Both pinned. Also scopes the ci.yml negative pin to the monotonic step's own block. As a file-wide grep it forbade any FUTURE step from being pull_request-gated and would have failed citing #143 when one legitimately was. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
94f6ed0047
commit
94d830cdb3
3 changed files with 54 additions and 6 deletions
15
.github/scripts/changelog-monotonic.sh
vendored
15
.github/scripts/changelog-monotonic.sh
vendored
|
|
@ -209,4 +209,17 @@ EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
count="$(printf '%s\n' "$base_headings" | grep -c . || true)"
|
count="$(printf '%s\n' "$base_headings" | grep -c . || true)"
|
||||||
echo "changelog-monotonic: all $count release heading(s) at the merge base ($(git rev-parse --short "$merge_base")) are still present in $changelog"
|
head_count="$(printf '%s\n' "$head_headings" | grep -c . || true)"
|
||||||
|
|
||||||
|
# The success line has two honest forms, because this step now runs on two
|
||||||
|
# shapes of event. On a push to main the merge base IS HEAD: containment
|
||||||
|
# compared the file against itself and asserted nothing, and deletion is
|
||||||
|
# undetectable on that event by construction. Reporting "all N still present"
|
||||||
|
# there would be the same dishonesty the skip messages were fixed for — a log
|
||||||
|
# claiming a check that did no work. Uniqueness is the half that actually ran,
|
||||||
|
# so that is the half the line names.
|
||||||
|
if [ "$merge_base" = "$(git rev-parse HEAD)" ]; then
|
||||||
|
echo "changelog-monotonic: containment vacuous (the merge base IS HEAD, so nothing could have been deleted between them) — uniqueness on HEAD checked $head_count release heading(s)."
|
||||||
|
else
|
||||||
|
echo "changelog-monotonic: all $count release heading(s) at the merge base ($(git rev-parse --short "$merge_base")) are still present in $changelog"
|
||||||
|
fi
|
||||||
|
|
|
||||||
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -120,8 +120,15 @@ which records not just what changed but what each drill run proved.
|
||||||
Fixed by moving, not rewriting: uniqueness now runs directly after the file
|
Fixed by moving, not rewriting: uniqueness now runs directly after the file
|
||||||
exists, before any git access. The skip messages say *containment* skipped and
|
exists, before any git access. The skip messages say *containment* skipped and
|
||||||
that uniqueness already passed, so a skip no longer claims nothing was
|
that uniqueness already passed, so a skip no longer claims nothing was
|
||||||
checked. The guard is also no longer gated to `pull_request` — deletion is
|
checked — and the success line got the same treatment, because dropping the
|
||||||
vacuous on a push to main, but duplication is vacuous on no tree, so a
|
gate made `merge_base == HEAD` a routine path rather than a degradation. On a
|
||||||
|
push to main containment compares the file against itself and asserts nothing,
|
||||||
|
so the line now reports containment *vacuous* and names uniqueness as the half
|
||||||
|
that ran, instead of claiming N headings were verified present by a comparison
|
||||||
|
that could not have detected their absence.
|
||||||
|
|
||||||
|
The guard is also no longer gated to `pull_request` — deletion is vacuous on
|
||||||
|
a push to main, but duplication is vacuous on no tree, so a
|
||||||
duplicate reaching main by any other route went unasserted. That gate could
|
duplicate reaching main by any other route went unasserted. That gate could
|
||||||
not simply be dropped: `github.base_ref` is empty on a push, and a bare
|
not simply be dropped: `github.base_ref` is empty on a push, and a bare
|
||||||
`origin/` under `STRICT=1` is a hard failure on every push to main, so the
|
`origin/` under `STRICT=1` is a hard failure on every push to main, so the
|
||||||
|
|
|
||||||
|
|
@ -457,12 +457,31 @@ G="$(grepo mono-143-nobase '## 0.8.0 — 2026-07-19' '' '- **Shipped**')"
|
||||||
head_changelog "$G" '## 0.8.0 — 2026-07-19' '' '- **a**' '' '## 0.8.0 — 2026-07-19' '' '- **b**'
|
head_changelog "$G" '## 0.8.0 — 2026-07-19' '' '- **a**' '' '## 0.8.0 — 2026-07-19' '' '- **b**'
|
||||||
check "monotonic: a duplicate is caught even when the base ref will not resolve (#143)" 1 "DUPLICATE release heading" mono "$G" no-such-ref
|
check "monotonic: a duplicate is caught even when the base ref will not resolve (#143)" 1 "DUPLICATE release heading" mono "$G" no-such-ref
|
||||||
|
|
||||||
|
# --- the push-to-main shape: containment vacuous, uniqueness real ----------
|
||||||
|
# With the pull_request gate gone (#143), merge_base == HEAD is a ROUTINE path,
|
||||||
|
# not a degradation. Containment compares the file against itself and asserts
|
||||||
|
# nothing, so a line reading "all N still present" would claim a check that did
|
||||||
|
# no work — the same dishonesty the skip messages were fixed for. The success
|
||||||
|
# line therefore has two forms, and this pins which one each event gets.
|
||||||
|
G="$(grepo mono-vacuous '## 0.8.0 — 2026-07-19' '' '- **Shipped**')"
|
||||||
|
check "monotonic: HEAD as its own base reports containment VACUOUS, not verified" 0 "containment vacuous" mono "$G" HEAD
|
||||||
|
check "monotonic: ...and names uniqueness as the half that actually ran" 0 "uniqueness on HEAD checked" mono "$G" HEAD
|
||||||
|
check "monotonic: ...and does NOT claim headings were still present" 1 "" \
|
||||||
|
bash -c 'cd "$1" && bash "$2" HEAD | grep -q "are still present"' _ "$G" "$MONO"
|
||||||
|
# The PR shape keeps the containment wording — the two must not collapse.
|
||||||
|
head_changelog "$G" '## 0.8.0 — 2026-07-19' '' '- **Shipped**' '' '## 0.9.0 — 2026-07-20' '' '- **New**'
|
||||||
|
check "monotonic: a real base still reports containment, naming the count" 0 "still present" mono "$G" main
|
||||||
|
|
||||||
# --- and the real tree, through the real script ----------------------------
|
# --- and the real tree, through the real script ----------------------------
|
||||||
# HEAD as its own base: the merge base is HEAD, so the sets are identical by
|
# HEAD as its own base: the merge base is HEAD, so the sets are identical by
|
||||||
# construction. Proves the script runs against the actual CHANGELOG.md and
|
# construction. Proves the script runs against the actual CHANGELOG.md and
|
||||||
# parses its real headings, without depending on an `origin/main` that a
|
# parses its real headings, without depending on an `origin/main` that a
|
||||||
# fresh clone or a detached CI checkout may not have.
|
# fresh clone or a detached CI checkout may not have.
|
||||||
check "monotonic: THIS tree passes against itself (the parser meets reality)" 0 "still present" \
|
# HEAD as its own base is now the VACUOUS-containment path (#143), so the
|
||||||
|
# assertion moved to uniqueness's count — which is the stronger proof of the
|
||||||
|
# original intent anyway: it says the parser read the REAL CHANGELOG.md and
|
||||||
|
# found real headings in it, rather than that a self-comparison came out equal.
|
||||||
|
check "monotonic: THIS tree passes against itself (the parser meets reality)" 0 "uniqueness on HEAD checked" \
|
||||||
mono "$ROOT" HEAD
|
mono "$ROOT" HEAD
|
||||||
|
|
||||||
# The guard is only a guard if CI runs it — and only if CI runs it with the
|
# The guard is only a guard if CI runs it — and only if CI runs it with the
|
||||||
|
|
@ -478,8 +497,17 @@ check "ci.yml: ...and STRICT, so a skip is a red run and not a green one" 0 "" \
|
||||||
# tree — and dropping that gate is only safe with the base-ref fallback, since
|
# tree — and dropping that gate is only safe with the base-ref fallback, since
|
||||||
# `github.base_ref` is empty on a push and a bare `origin/` under STRICT is a
|
# `github.base_ref` is empty on a push and a bare `origin/` under STRICT is a
|
||||||
# hard failure on every push to main.
|
# hard failure on every push to main.
|
||||||
check "ci.yml: the monotonic step is not gated to pull_request (#143)" 1 "" \
|
# Scoped to the step's OWN block, deliberately. A file-wide negative would
|
||||||
grep -qF "if: github.event_name == 'pull_request'" "$ROOT/.github/workflows/ci.yml"
|
# forbid any FUTURE step in ci.yml from being pull_request-gated and would fail
|
||||||
|
# citing #143 when one legitimately is — #143 constrains this step, not the file.
|
||||||
|
mono_step_block() {
|
||||||
|
awk '/^ - name: no shipped changelog heading/ {f=1; print; next}
|
||||||
|
f && /^ - name: / {exit}
|
||||||
|
f {print}' "$ROOT/.github/workflows/ci.yml"
|
||||||
|
}
|
||||||
|
mono_step_gated() { mono_step_block | grep -q 'if:'; }
|
||||||
|
check "ci.yml: the monotonic step itself is not pull_request-gated (#143)" 1 "" mono_step_gated
|
||||||
|
check "ci.yml: ...and the block was actually found (guards the awk above)" 0 "changelog-monotonic" mono_step_block
|
||||||
check "ci.yml: ...and falls back to ref_name, so a push has a base to resolve" 0 "" \
|
check "ci.yml: ...and falls back to ref_name, so a push has a base to resolve" 0 "" \
|
||||||
grep -qF 'github.base_ref || github.ref_name' "$ROOT/.github/workflows/ci.yml"
|
grep -qF 'github.base_ref || github.ref_name' "$ROOT/.github/workflows/ci.yml"
|
||||||
check "CONTRIBUTING: names the append-only rule for release headings" 0 "" \
|
check "CONTRIBUTING: names the append-only rule for release headings" 0 "" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue