ceremony-ci-probe/test/release-chain.test.sh

209 lines
7.6 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# The merge door's script chain, composed end-to-end (issue #9): facts →
# decide → notes against a constructed fixture ceremony, exactly the way
# release.yml wires them (facts' $GITHUB_OUTPUT lines become decide's env;
# the notes come from the one canonical extractor). facts.test.sh proves the
# fact rows and decide's own suite proves the table; this file proves the
# HANDOFF between them. Also run by release-exercise.yml on dispatch. 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"
feat(forge): port every reconciler call site onto the shim Term 1 completed. All 52 runtime gh call sites in the three reconcilers and lib/ruling.sh now go through forge_* verbs; the three remaining matches in labels-reconcile are prose in comments. lib/facts.sh is deliberately untouched — it is the release door, and the ruling keeps release.yml out of this issue. The CEREMONY_FORGE_CLIENT:-gh wrappers die here, in the same commit as the sites they described, so the tree is never in a state where the declaration lies. main() now runs forge_preflight then forge_select "". Two sites needed judgment rather than substitution: - labels-scope's write is forge_labels_add, a genuine additive POST on both backends, NOT forge_issue_edit --add-label. ceremony#128 turns on that write not being a read-modify-PUT: the labeler action computed (labels-at-job-start union derived) and PUT the whole set, silently dropping a label applied while the job ran. Routing it through a generic edit verb would have quietly reopened that. - the human-review request is forge_request_reviewer. Contrary to my earlier reading, POST /pulls/{n}/requested_reviewers DOES exist on Forgejo — 422 naming the reviewer's access without it, 201 with it. The earlier 404 was a GET, which the endpoint does not serve, plus a username that did not exist. Test churn, all of it the term-5 boundary move: - the suites select the github backend, so their existing gh() stubs stay the boundary and keep intercepting; - stubs strip the paging the shim injects, so fixtures stay keyed on the logical endpoint (inlined in the PATH stub, which is a standalone executable and cannot see a shell function); - fixtures renamed off the per_page suffix for the same reason; - recorded-mutation assertions now match the verb, not the raw gh line; - gh() stubs carry SC2317: they are reached through the backend now, so shellcheck can no longer see the call path. Refs #188
2026-08-02 19:43:58 +00:00
# The suite drives the GITHUB backend: its gh() stubs ARE the forge boundary
# now, and forge_api/forge_issue_edit/... resolve to the gh invocations those
# stubs already intercept (#188). Without this the verbs are simply undefined.
# shellcheck source=lib/forge.sh
. "$ROOT/lib/forge.sh"
forge_select github
FACTS="$ROOT/lib/facts.sh"
DECIDE="$ROOT/lib/decide.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# A gh stub for the one API fact the ceremony path needs: the merged,
# release-labeled PR behind the commit.
mkdir -p "$TMP/stub"
cat >"$TMP/stub/gh" <<'EOF'
#!/usr/bin/env bash
if [ "$1" = api ]; then echo true; exit 0; fi
echo "gh stub: unexpected call: gh $*" >&2
exit 97
EOF
chmod +x "$TMP/stub/gh"
# The fixture: a base tree at 0.6.9-dev with an armed changelog, then the
# ceremony merge — VERSION bumped bare, Unreleased stamped, re-armed above.
git init -q "$TMP/repo"
git -C "$TMP/repo" config user.email fixture@example.invalid
git -C "$TMP/repo" config user.name fixture
printf '0.6.9-dev\n' >"$TMP/repo/VERSION"
cat >"$TMP/repo/CHANGELOG.md" <<'EOF'
# Changelog
## Unreleased
- The entry this release ships.
## 0.6.8 — 2026-07-01
- An older entry.
EOF
git -C "$TMP/repo" add VERSION CHANGELOG.md
git -C "$TMP/repo" commit -qm "base"
BASE_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
printf '0.7.0\n' >"$TMP/repo/VERSION"
cat >"$TMP/repo/CHANGELOG.md" <<'EOF'
# Changelog
## Unreleased
## 0.7.0 — 2026-07-21
- The entry this release ships.
## 0.6.8 — 2026-07-01
- An older entry.
EOF
git -C "$TMP/repo" add VERSION CHANGELOG.md
git -C "$TMP/repo" commit -qm "release: 0.7.0"
MERGE_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
# chain <merge_sha> <event_before> [repo_dir] [stub_dir] — facts, then
# decide fed from facts' output lines, then the notes extraction, printing
# each stage's result. The optional dirs default to the main fixture; the
# greenfield cases below (#134) bring their own repo and stub.
chain() {
(
cd "${3:-$TMP/repo}" || exit 1
facts_out="$(env PATH="${4:-$TMP/stub}:$PATH" GITHUB_REPOSITORY=fixture/fixture \
GH_TOKEN=stub VERSION_SOURCE=file MERGE_SHA="$1" EVENT_BEFORE="$2" \
bash "$FACTS")" || exit 1
printf '%s\n' "$facts_out"
ver="$(printf '%s\n' "$facts_out" | awk -F= '$1 == "ver" { print $2 }')"
base_ver="$(printf '%s\n' "$facts_out" | awk -F= '$1 == "base_ver" { print $2 }')"
released="$(printf '%s\n' "$facts_out" | awk -F= '$1 == "released" { print $2 }')"
labeled="$(printf '%s\n' "$facts_out" | awk -F= '$1 == "labeled" { print $2 }')"
decide_out="$(env VER="$ver" BASE_VER="$base_ver" RELEASED="$released" \
LABELED="$labeled" bash "$DECIDE")" || exit 1
printf '%s\n' "$decide_out"
case "$decide_out" in
*ceremony=yes*)
# shellcheck source=lib/changelog.sh
. "$ROOT/lib/changelog.sh"
2026-07-23 23:43:20 +00:00
diagnosis="$(changelog_section_problem CHANGELOG.md "$ver")" || {
printf 'chain: %s\n' "$diagnosis" >&2
exit 1
2026-07-23 23:43:20 +00:00
}
notes="$(changelog_section CHANGELOG.md "$ver")"
printf 'notes: %s\n' "$notes"
;;
esac
)
}
check "the ceremony merge decides ceremony=yes" 0 "ceremony=yes" \
chain "$MERGE_SHA" "$BASE_SHA"
check "the notes are the stamped section's prose" 0 \
"notes: - The entry this release ships." chain "$MERGE_SHA" "$BASE_SHA"
2026-07-23 23:43:20 +00:00
# The same ceremony facts with an entry-less stamped section must stop at the
# notes door, before any tag or release mutation could run.
git -C "$TMP/repo" reset -q --hard "$BASE_SHA"
printf '0.7.0\n' >"$TMP/repo/VERSION"
cat >"$TMP/repo/CHANGELOG.md" <<'EOF'
# Changelog
## Unreleased
## 0.7.0 — 2026-07-21
### Added
## 0.6.8 — 2026-07-01
- An older entry.
EOF
git -C "$TMP/repo" add VERSION CHANGELOG.md
git -C "$TMP/repo" commit -qm "release: entry-less 0.7.0"
EMPTY_MERGE_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
check "the notes door refuses an entry-less stamped section" 1 \
"section '0.7.0' has no entries" chain "$EMPTY_MERGE_SHA" "$BASE_SHA"
git -C "$TMP/repo" reset -q --hard "$MERGE_SHA"
# The same chain on an ordinary merge: -dev, unchanged — a green NOTICE
# no-op that never consults the API (the stub would refuse a release view).
printf 'ordinary work\n' >"$TMP/repo/notes.txt"
git -C "$TMP/repo" add notes.txt
git -C "$TMP/repo" commit -qm "ordinary work"
WORK_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
printf '0.7.1-dev\n' >"$TMP/repo/VERSION"
git -C "$TMP/repo" add VERSION
git -C "$TMP/repo" commit -qm "chore: bump main to 0.7.1-dev"
BUMP_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
printf 'more ordinary work\n' >"$TMP/repo/notes.txt"
git -C "$TMP/repo" add notes.txt
git -C "$TMP/repo" commit -qm "more ordinary work"
WORK2_SHA="$(git -C "$TMP/repo" rev-parse HEAD)"
check "the post-release bump decides ceremony=no" 0 "ceremony=no" \
chain "$BUMP_SHA" "$WORK_SHA"
check "an ordinary -dev merge decides ceremony=no" 0 "ceremony=no" \
chain "$WORK2_SHA" "$BUMP_SHA"
# --- the repository's first push to main: a root commit, no base (#134) ------
# event.before is all-zeros and the head has no first parent. facts reads
# base_ver=(none) instead of dying at rev-parse, and decide's table governs
# from there: the guided bootstrap (-dev first commit, what CONSUMERS.md
# tells a new repo to write) is a green NOTICE no-op — the doctrine's
# promise held at the exact moment a consumer adopts the ceremony.
ZEROS="0000000000000000000000000000000000000000"
git init -q "$TMP/greenfield"
git -C "$TMP/greenfield" config user.email fixture@example.invalid
git -C "$TMP/greenfield" config user.name fixture
printf '0.1.0-dev\n' >"$TMP/greenfield/VERSION"
git -C "$TMP/greenfield" add VERSION
git -C "$TMP/greenfield" commit -qm "root: adopt the ceremony at 0.1.0-dev"
GREEN_SHA="$(git -C "$TMP/greenfield" rev-parse HEAD)"
check "a greenfield -dev root commit decides ceremony=no, not a red run" 0 "ceremony=no" \
chain "$GREEN_SHA" "$ZEROS" "$TMP/greenfield"
check "the greenfield no-op is a NOTICE" 0 "NOTICE:" \
chain "$GREEN_SHA" "$ZEROS" "$TMP/greenfield"
# A bare first commit with no merged release-labeled PR must still refuse —
# the reason the crash could not be `|| true`-ed away: a greenfield adoption
# must never become a silent release. This stub answers the labeled query
# with false.
mkdir -p "$TMP/stub-unlabeled"
cat >"$TMP/stub-unlabeled/gh" <<'EOF'
#!/usr/bin/env bash
if [ "$1" = api ]; then echo false; exit 0; fi
echo "gh stub: unexpected call: gh $*" >&2
exit 97
EOF
chmod +x "$TMP/stub-unlabeled/gh"
git init -q "$TMP/greenfield-bare"
git -C "$TMP/greenfield-bare" config user.email fixture@example.invalid
git -C "$TMP/greenfield-bare" config user.name fixture
printf '0.1.0\n' >"$TMP/greenfield-bare/VERSION"
git -C "$TMP/greenfield-bare" add VERSION
git -C "$TMP/greenfield-bare" commit -qm "root: bare 0.1.0, nobody declared a release"
GREENB_SHA="$(git -C "$TMP/greenfield-bare" rev-parse HEAD)"
check "a bare unlabeled root commit still refuses, creating nothing" 1 \
"no merged, release-labeled PR" \
chain "$GREENB_SHA" "$ZEROS" "$TMP/greenfield-bare" "$TMP/stub-unlabeled"
summary