ceremony-ci-probe/test/changelog-assembled.test.sh

326 lines
13 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Contract tests for actions/changelog-assembled (issue #116). Like the
# monotonic guard's suite, every applicable case is a constructed git repo —
# "the section matches the fragments it consumed" is a property of a DIFF:
# the fragments are gone from HEAD's tree by construction, so the fixture is
# a history: a base commit holding the fragments and a -dev version, a HEAD
# commit holding the ceremony's edit, and a 'base' branch standing in for
# origin/main. set -u, not -e: failing commands are behavior for the
# harness to inspect.
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=test/harness.sh
. "$ROOT/test/harness.sh"
SCRIPT="$ROOT/actions/changelog-assembled/changelog-assembled.sh"
ARMED="$ROOT/actions/changelog-armed/changelog-armed.sh"
MONOTONIC="$ROOT/actions/changelog-monotonic/changelog-monotonic.sh"
ASSEMBLE="$ROOT/bin/changelog-assemble"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
init_repo() {
local dir="$TMP/$1"
mkdir -p "$dir"
git -C "$dir" init -q -b main
git -C "$dir" config user.email ci@example.invalid
git -C "$dir" config user.name ci
}
# commit_base <name> — commit the tree as the merge base and pin 'base' there.
commit_base() {
git -C "$TMP/$1" add -A
git -C "$TMP/$1" commit -qm base
git -C "$TMP/$1" branch base
}
commit_head() {
git -C "$TMP/$1" add -A
git -C "$TMP/$1" commit -qm head
}
# seed_flat <name> — the shared pre-ceremony tree: an armed changelog, two
# flat fragments (numeric order will put 12 before 9), a -dev version.
seed_flat() {
local name="$1" dir="$TMP/$1"
init_repo "$name"
mkdir -p "$dir/changelog.d"
printf 'Machine-assembled; see heavy-duty/ceremony#112.\n' >"$dir/changelog.d/README.md"
cat >"$dir/CHANGELOG.md" <<'EOF'
# Changelog
Preamble prose belongs to no section.
## 0.1.0 — 2026-07-01
- The shipped entry.
EOF
printf '0.1.1-dev\n' >"$dir/VERSION"
printf -- '- Twelve landed.\n' >"$dir/changelog.d/12.md"
printf -- '- Nine landed, and its prose wraps onto a\n continuation line.\n' >"$dir/changelog.d/9.md"
commit_base "$name"
}
# ceremony <name> <ver> <date> — the faithful release edit on the working
# tree, exactly as a human runs it: the real assembler in write mode, then
# the bare version stamp. Deliberately does NOT commit, so a case can break
# the tree before committing HEAD.
ceremony() {
local name="$1" ver="$2" stamp="$3"
(cd "$TMP/$name" && "$ASSEMBLE" "$ver" "$stamp" >/dev/null 2>&1) || return 1
printf '%s\n' "$ver" >"$TMP/$name/VERSION"
}
run() { local name="$1"; shift; (cd "$TMP/$name" && bash "$SCRIPT" "$@"); }
run_strict() { local name="$1"; shift; (cd "$TMP/$name" && CHANGELOG_ASSEMBLED_STRICT=1 bash "$SCRIPT" "$@"); }
# --- the documented flow passes ----------------------------------------------
seed_flat faithful-flat
ceremony faithful-flat 0.2.0 2026-07-24
commit_head faithful-flat
check "faithful flat ceremony: the section is byte-for-byte the assembly" 0 \
"byte-for-byte" run faithful-flat base
seed_flat faithful-grouped
printf -- '### Fixed\n\n- Fixed twenty-one.\n' >"$TMP/faithful-grouped/changelog.d/21.md"
printf -- '### Added\n\n- Added twenty.\n\n### Docs\n\n- Docs twenty.\n' >"$TMP/faithful-grouped/changelog.d/20.md"
rm "$TMP/faithful-grouped/changelog.d/12.md" "$TMP/faithful-grouped/changelog.d/9.md"
git -C "$TMP/faithful-grouped" add -A
git -C "$TMP/faithful-grouped" commit -qm regroup
git -C "$TMP/faithful-grouped" branch -f base
ceremony faithful-grouped 0.2.0 2026-07-24
commit_head faithful-grouped
check "faithful grouped ceremony passes" 0 "byte-for-byte" run faithful-grouped base
# The date is HEAD's to choose: a stamp nowhere near today must not read as
# a prose difference — the comparison is body against body, headings (and
# so dates) never enter it.
seed_flat old-date
ceremony old-date 0.2.0 2020-01-01
commit_head old-date
check "the stamp's date never enters the comparison" 0 "byte-for-byte" \
run old-date base
# --- inapplicable trees: green NOTICE, never a silent skip -------------------
seed_flat ordinary-add
printf -- '- Thirteen incoming.\n' >"$TMP/ordinary-add/changelog.d/13.md"
commit_head ordinary-add
check "-dev PR adding a fragment: green NOTICE" 0 "NOTICE" run ordinary-add base
seed_flat ordinary-none
printf 'code\n' >"$TMP/ordinary-none/code.txt"
commit_head ordinary-none
check "-dev PR touching no fragment: green NOTICE" 0 "NOTICE" run ordinary-none base
seed_flat ordinary-del
rm "$TMP/ordinary-del/changelog.d/12.md"
commit_head ordinary-del
check "-dev PR even deleting a fragment: green NOTICE" 0 "NOTICE" run ordinary-del base
# Legacy mode: no changelog.d/ at the merge base — always a NOTICE, even on
# a release tree, because the mid-adoption ceremony edits the changelog by
# hand and there is no fragment set for its section to answer to.
init_repo legacy
printf '# Changelog\n\n## Unreleased\n\n- An entry.\n' >"$TMP/legacy/CHANGELOG.md"
printf '0.1.1-dev\n' >"$TMP/legacy/VERSION"
commit_base legacy
printf '# Changelog\n\n## 0.2.0 — 2026-07-24\n\n- An entry.\n' >"$TMP/legacy/CHANGELOG.md"
printf '0.2.0\n' >"$TMP/legacy/VERSION"
commit_head legacy
check "legacy repo (no changelog.d at base): green NOTICE, even on a release tree" 0 \
"NOTICE" run legacy base
# The un-rearmed window: a PR branched right after a release merges sits on
# a bare version whose section was stamped at its MERGE BASE — it is not
# the ceremony and must not be asked to answer for one.
seed_flat post-release
ceremony post-release 0.2.0 2026-07-24
git -C "$TMP/post-release" add -A
git -C "$TMP/post-release" commit -qm release
git -C "$TMP/post-release" branch -f base
printf 'code\n' >"$TMP/post-release/code.txt"
commit_head post-release
check "a PR atop the un-rearmed release: green NOTICE (this branch did not stamp)" 0 \
"NOTICE" run post-release base
# --- the refusals ------------------------------------------------------------
# The issue's headline failure: one fragment kept out of the ceremony — it
# survives at HEAD and its entry is absent from the section. Both refusals
# fire: the diff names the missing entry, the survivor list names the file.
seed_flat dropped
mv "$TMP/dropped/changelog.d/9.md" "$TMP/dropped/9.md.hold"
ceremony dropped 0.2.0 2026-07-24
mv "$TMP/dropped/9.md.hold" "$TMP/dropped/changelog.d/9.md"
commit_head dropped
check "a fragment kept out of the ceremony fails" 1 "" run dropped base
check "the dropped-entry diff names the missing entry" 1 "Nine landed" \
run dropped base
check "the surviving fragment is listed by path" 1 "changelog.d/9.md" \
run dropped base
# The same drop, but the fragment was deleted anyway: its prose vanished
# without ever being published. Only the diff can say so.
seed_flat vanished
mv "$TMP/vanished/changelog.d/9.md" "$TMP/vanished/9.md.hold"
ceremony vanished 0.2.0 2026-07-24
rm "$TMP/vanished/9.md.hold"
commit_head vanished
check "a deleted fragment whose entry never landed fails" 1 "Nine landed" \
run vanished base
seed_flat edited
ceremony edited 0.2.0 2026-07-24
sed -i 's/Twelve landed/Twelve allegedly landed/' "$TMP/edited/CHANGELOG.md"
commit_head edited
check "a hand-edited entry fails with a unified diff" 1 "+++" run edited base
check "the edit is visible in the diff" 1 "allegedly" run edited base
check "the diff failure teaches redo-with-the-tool, never hand-edit" 1 \
"never to hand-edit" run edited base
# Re-ordering away from the canonical order: the section is hand-built with
# the right entries in the wrong order (the assembler puts 12 before 9).
seed_flat reordered
rm "$TMP/reordered/changelog.d/12.md" "$TMP/reordered/changelog.d/9.md"
cat >"$TMP/reordered/CHANGELOG.md" <<'EOF'
# Changelog
Preamble prose belongs to no section.
## 0.2.0 — 2026-07-24
- Nine landed, and its prose wraps onto a
continuation line.
- Twelve landed.
## 0.1.0 — 2026-07-01
- The shipped entry.
EOF
printf '0.2.0\n' >"$TMP/reordered/VERSION"
commit_head reordered
check "re-ordered entries fail" 1 "NOT what the fragments" run reordered base
# A faithful assembly that forgot one deletion: the section is right, the
# directory is not — only the survivor refusal fires.
seed_flat survivor
ceremony survivor 0.2.0 2026-07-24
printf -- '- Nine landed, and its prose wraps onto a\n continuation line.\n' >"$TMP/survivor/changelog.d/9.md"
commit_head survivor
check "a surviving fragment with its entry present fails" 1 "STILL PRESENT" \
run survivor base
check "the survivor refusal names the file" 1 "changelog.d/9.md" \
run survivor base
# Fragments consumed, section never stamped: the prose went nowhere.
seed_flat halfdone
rm "$TMP/halfdone/changelog.d/12.md" "$TMP/halfdone/changelog.d/9.md"
printf '0.2.0\n' >"$TMP/halfdone/VERSION"
commit_head halfdone
check "fragments consumed but no section stamped fails" 1 \
"non-empty section for '0.2.0'" run halfdone base
# A release stamped out of a fragment-free directory: the replay refuses the
# way the real assembler would have — an empty release.
seed_flat empty-release
rm "$TMP/empty-release/changelog.d/12.md" "$TMP/empty-release/changelog.d/9.md"
git -C "$TMP/empty-release" add -A
git -C "$TMP/empty-release" commit -qm consume-early
git -C "$TMP/empty-release" branch -f base
cat >"$TMP/empty-release/CHANGELOG.md" <<'EOF'
# Changelog
Preamble prose belongs to no section.
## 0.2.0 — 2026-07-24
- Prose from nowhere.
## 0.1.0 — 2026-07-01
- The shipped entry.
EOF
printf '0.2.0\n' >"$TMP/empty-release/VERSION"
commit_head empty-release
check "a section stamped from zero fragments fails on the replay's refusal" 1 \
"zero fragments" run empty-release base
check "missing changelog fails" 1 "no such file" run faithful-flat base NOPE.md
# --- the trio interaction row (the issue's whole argument) -------------------
# On the faithful tree all three guards are green; on the dropped-entry tree
# armed is green (the section exists and has prose), monotonic is green (no
# heading was deleted) — this guard is the ONLY one that goes red.
run_armed() { (cd "$TMP/$1" && bash "$ARMED"); }
run_monotonic() { local name="$1"; shift; (cd "$TMP/$name" && bash "$MONOTONIC" "$@"); }
check "trio, faithful tree: changelog-armed green" 0 "agrees" run_armed faithful-flat
check "trio, faithful tree: changelog-monotonic green" 0 "still present" \
run_monotonic faithful-flat base
check "trio, faithful tree: changelog-assembled green" 0 "byte-for-byte" \
run faithful-flat base
check "trio, dropped-entry tree: changelog-armed stays green" 0 "agrees" \
run_armed dropped
check "trio, dropped-entry tree: changelog-monotonic stays green" 0 "still present" \
run_monotonic dropped base
check "trio, dropped-entry tree: changelog-assembled is the only red" 1 "" \
run dropped base
# --- degradation: the loud skip and the STRICT refusal -----------------------
seed_flat no-base
ceremony no-base 0.2.0 2026-07-24
commit_head no-base
check "base ref missing, STRICT=0: loud skip, exit 0" 0 "SKIPPED" \
run no-base does-not-exist
check "base ref missing, STRICT=1: hard failure" 1 "FAILURE" \
run_strict no-base does-not-exist
check "the STRICT failure names the checkout fix, not the script" 1 "fetch-depth: 0" \
run_strict no-base does-not-exist
mkdir -p "$TMP/plain"
printf '# Changelog\n\n## Unreleased\n' >"$TMP/plain/CHANGELOG.md"
check "not a git repo, STRICT=0: loud skip" 0 "not inside a git work tree" \
run plain
check "not a git repo, STRICT=1: hard failure" 1 "FAILURE" run_strict plain
# --- the honest edges --------------------------------------------------------
# Push-to-main shape: the merge base IS HEAD, nothing was consumed between
# them, and the success line must say so instead of claiming a comparison.
seed_flat vacuous
ceremony vacuous 0.2.0 2026-07-24
commit_head vacuous
check "merge base IS HEAD: vacuous, named honestly" 0 "vacuous" run vacuous HEAD
# --- the action's wiring: inputs arrive as env vars --------------------------
# Non-default names for everything action.yml passes, STRICT included, prove
# the env vars are honored the way the composite sets them.
init_repo env-tree
mkdir -p "$TMP/env-tree/frags"
printf '# Changelog\n\n## 0.1.0 — 2026-07-01\n\n- Shipped.\n' >"$TMP/env-tree/NOTES.md"
printf '0.1.1-dev\n' >"$TMP/env-tree/VERSION"
printf -- '- Flagged entry.\n' >"$TMP/env-tree/frags/2.md"
git -C "$TMP/env-tree" add -A
git -C "$TMP/env-tree" commit -qm base
git -C "$TMP/env-tree" branch fixture-base
(cd "$TMP/env-tree" && "$ASSEMBLE" 0.2.0 2026-07-24 --changelog NOTES.md --dir frags >/dev/null 2>&1)
printf '0.2.0\n' >"$TMP/env-tree/VERSION"
git -C "$TMP/env-tree" add -A
git -C "$TMP/env-tree" commit -qm head
env_tree() {
(cd "$TMP/env-tree" && \
CHANGELOG_ASSEMBLED_BASE=fixture-base CHANGELOG=NOTES.md \
CHANGELOG_ASSEMBLED_DIR=frags VERSION_SOURCE=file \
CHANGELOG_ASSEMBLED_STRICT=1 bash "$SCRIPT")
}
check "env vars drive the script the way action.yml does" 0 "byte-for-byte" env_tree
summary