From 386ce67bd1f368909ce1e0e95f7698be4a83e420 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:19:18 +0000 Subject: [PATCH 1/3] test: specify changelog-armed fragment mode --- test/changelog-armed.test.sh | 159 +++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/test/changelog-armed.test.sh b/test/changelog-armed.test.sh index b495528..940f2c8 100644 --- a/test/changelog-armed.test.sh +++ b/test/changelog-armed.test.sh @@ -248,6 +248,155 @@ EOF check "package-json: bare + armed passes" 0 "agrees" \ in_tree pkg-bare-armed CHANGELOG.md package-json +# --- fragment mode: changelog.d/ is the arming ------------------------------- + +fragment_tree() { + local name="$1" version="$2" + shift 2 + tree "$name" "$version" + mkdir -p "$TMP/$name/changelog.d" + printf '%s\n' "# Changelog fragments" >"$TMP/$name/changelog.d/README.md" +} + +fragment_tree fragments-dev-empty 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +check "fragment -dev + marker + no fragments passes" 0 "fragment mode" \ + in_tree fragments-dev-empty + +fragment_tree fragments-dev-flat 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +printf '%s\n' "- Added fragment mode." >"$TMP/fragments-dev-flat/changelog.d/115.md" +check "fragment -dev + well-formed flat fragment passes" 0 "fragment mode" \ + in_tree fragments-dev-flat + +fragment_tree fragments-dev-grouped 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +### Fixed + +- The shipped entry. +EOF +cat >"$TMP/fragments-dev-grouped/changelog.d/115.md" <<'EOF' +### Changed + +- Added fragment mode. +EOF +check "fragment -dev + well-formed grouped fragment passes" 0 "fragment mode" \ + in_tree fragments-dev-grouped + +fragment_tree fragments-unreleased 1.2.4-dev <<'EOF' +# Changelog + +## Unreleased + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +check "fragment mode refuses even an empty Unreleased section" 1 \ + "Unreleased.*survived the adoption" in_tree fragments-unreleased + +fragment_tree fragments-no-marker 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +rm "$TMP/fragments-no-marker/changelog.d/README.md" +check "fragment mode requires the generated marker" 1 "README.md" \ + in_tree fragments-no-marker + +fragment_tree fragments-bad-name 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +printf '%s\n' "- An entry." >"$TMP/fragments-bad-name/changelog.d/notes.md" +check "fragment mode quotes malformed-fragment diagnosis and file" 1 \ + "fragment 'changelog.d/notes.md' is not named for its issue" \ + in_tree fragments-bad-name + +fragment_tree fragments-dangling-group 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +printf '%s\n' "### Changed" >"$TMP/fragments-dangling-group/changelog.d/115.md" +check "fragment mode refuses a dangling fragment heading" 1 \ + "fragment 'changelog.d/115.md' has no entries" \ + in_tree fragments-dangling-group + +fragment_tree fragments-bare-stamped 1.2.3 <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. + +## 1.2.2 — 2026-07-01 + +- The older entry. +EOF +check "fragment bare + stamped section + consumed directory passes" 0 \ + "fragment mode" in_tree fragments-bare-stamped + +cp -R "$TMP/fragments-bare-stamped" "$TMP/fragments-bare-survivor" +printf '%s\n' "- This entry was not consumed." \ + >"$TMP/fragments-bare-survivor/changelog.d/115.md" +check "fragment bare refuses and lists surviving fragments" 1 \ + "these fragments were not consumed: changelog.d/115.md" \ + in_tree fragments-bare-survivor + +fragment_tree fragments-bare-wrong 1.2.3 <<'EOF' +# Changelog + +## 9.9.9 — 2026-07-20 + +- The wrong release. +EOF +check "fragment bare refuses a stamp for another version" 1 \ + "stamped the wrong number" in_tree fragments-bare-wrong + +fragment_tree fragments-bare-missing 1.2.3 <<'EOF' +# Changelog + +## 1.2.2 — 2026-07-01 + +- The older entry. +EOF +check "fragment bare refuses a missing stamp via section diagnosis" 1 \ + "no section for '1.2.3'" in_tree fragments-bare-missing + +fragment_tree fragments-cross-mode 1.2.4-dev <<'EOF' +# Changelog + +## 1.2.3 — 2026-07-20 + +- The shipped entry. +EOF +check "same changelog passes in fragment mode" 0 "fragment mode" \ + in_tree fragments-cross-mode +rm -rf "$TMP/fragments-cross-mode/changelog.d" +check "same changelog fails in legacy mode" 1 "development tree" \ + in_tree fragments-cross-mode + # --- the action's wiring: inputs arrive as env vars -------------------------- mkdir -p "$TMP/env-tree" @@ -259,4 +408,14 @@ env_tree() { } check "env vars drive the script the way action.yml does" 0 "agrees" env_tree +mkdir -p "$TMP/env-fragments/custom.d" +printf '1.2.4-dev\n' >"$TMP/env-fragments/VERSION" +printf '# Changelog\n\n## 1.2.3 — 2026-07-20\n\n- Shipped.\n' \ + >"$TMP/env-fragments/CHANGELOG.md" +printf '%s\n' "# Changelog fragments" >"$TMP/env-fragments/custom.d/README.md" +env_fragments() { + (cd "$TMP/env-fragments" && FRAGMENTS_DIR=custom.d bash "$SCRIPT") +} +check "fragments-dir env var selects fragment mode" 0 "fragment mode" env_fragments + summary -- 2.45.2 From 21492ffeb959cea926a0f6b2738a283541b7a9c8 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:20:27 +0000 Subject: [PATCH 2/3] feat: arm changelogs in fragment mode --- actions/changelog-armed/action.yml | 5 ++ actions/changelog-armed/changelog-armed.sh | 74 +++++++++++++++++++++- test/changelog-armed.test.sh | 6 +- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/actions/changelog-armed/action.yml b/actions/changelog-armed/action.yml index 6a63684..9ebff84 100644 --- a/actions/changelog-armed/action.yml +++ b/actions/changelog-armed/action.yml @@ -13,6 +13,10 @@ inputs: description: Path to the changelog, relative to the workspace required: false default: CHANGELOG.md + fragments-dir: + description: Fragment directory whose presence selects fragment mode + required: false + default: changelog.d runs: using: composite steps: @@ -21,4 +25,5 @@ runs: env: CHANGELOG: ${{ inputs.changelog }} VERSION_SOURCE: ${{ inputs.version-source }} + FRAGMENTS_DIR: ${{ inputs.fragments-dir }} run: bash "$GITHUB_ACTION_PATH/changelog-armed.sh" diff --git a/actions/changelog-armed/changelog-armed.sh b/actions/changelog-armed/changelog-armed.sh index 4e6130b..feaa427 100644 --- a/actions/changelog-armed/changelog-armed.sh +++ b/actions/changelog-armed/changelog-armed.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# changelog-armed.sh [] [] — assert that the -# changelog is ARMED: that there is a heading for the next PR's entry to -# land under, and that it is the right one for the state this tree is in. +# changelog-armed.sh [] [] [] — +# assert that the changelog is ARMED: that there is a place for the next PR's +# entry to land, and that it is the right one for the state this tree is in. # # Ported from box .github/scripts/changelog-armed.sh (box#108, confirmed # cross-repo as rig#66) — box is the only repo that carries this guard @@ -15,6 +15,14 @@ set -euo pipefail # tempted to simplify this back to the unconditional form should read # those two reverts first. # +# Fragment mode makes the directory itself the arming (#115): every PR gets +# its own issue-named file, so the box#108 clean-mismerge cannot happen because +# there is no shared heading to disappear under an open PR. The guard instead +# proves that the marker exists, Unreleased is gone, and every fragment is +# publishable before its author lets go of the PR. A bare release has no +# re-armed shape in this mode — there is nothing to re-arm — so it must have +# consumed every fragment and stamped its exact publishable section. +# # The failure it exists to catch (box#108, rig#66) leaves no trace: the # ceremony PR stamps '## Unreleased' into '## X.Y.Z — DATE' by hand, and # nothing puts the heading back. A PR authored BEFORE the release wrote its @@ -47,6 +55,7 @@ set -euo pipefail changelog="${1:-${CHANGELOG:-CHANGELOG.md}}" version_source="${2:-${VERSION_SOURCE:-file}}" +fragments_dir="${3:-${FRAGMENTS_DIR:-changelog.d}}" # The shared libs travel with this action: a consumer's # `uses: heavy-duty/ceremony/actions/changelog-armed@` downloads this @@ -67,6 +76,65 @@ ver="$(version_read "$version_source")" || { exit 1 } +if [ -d "$fragments_dir" ]; then + [ -f "$fragments_dir/README.md" ] || { + echo "changelog-armed: fragment mode requires the generated marker '$fragments_dir/README.md' — restore it so the empty directory remains tracked" >&2 + exit 1 + } + + if awk '$1 == "##" && $2 == "Unreleased" { found = 1 } END { exit !found }' "$changelog"; then + echo "changelog-armed: a '## Unreleased' section survived the adoption — move its entries into '$fragments_dir/.md' and delete the heading" >&2 + exit 1 + fi + + fragments="$(changelog_fragments "$fragments_dir")" + while IFS= read -r fragment; do + [ -n "$fragment" ] || continue + if ! diagnosis="$(changelog_fragment_problem "$fragment")"; then + printf 'changelog-armed: %s\n' "$diagnosis" >&2 + exit 1 + fi + done <<<"$fragments" + + if version_is_dev "$ver"; then + echo "changelog-armed: version '$ver' agrees with fragment mode ($fragments_dir)" + exit 0 + fi + + if [ -n "$fragments" ]; then + surviving="$(printf '%s' "$fragments" | paste -sd ', ' -)" + echo "changelog-armed: these fragments were not consumed: $surviving — re-run 'changelog-assemble $ver'" >&2 + exit 1 + fi + + top="$(grep -m1 '^## ' "$changelog" || true)" + [ -n "$top" ] || { + echo "changelog-armed: $changelog has no '## ' section at all — the release stamp for '$ver' is missing" >&2 + exit 1 + } + if ! diagnosis="$(changelog_section_problem "$changelog" "$ver")"; then + printf "changelog-armed: the stamped section for '%s' is not publishable: %s\n" \ + "$ver" "$diagnosis" >&2 + exit 1 + fi + top_ver="$(printf '%s\n' "$top" | awk '{ print $2 }')" + if [ "$top_ver" != "$ver" ]; then + cat >&2 < Date: Fri, 24 Jul 2026 09:21:24 +0000 Subject: [PATCH 3/3] docs: record fragment arming guard --- CHANGELOG.md | 1 + actions/changelog-armed/changelog-armed.sh | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e33fc8e..da37dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop. ## Unreleased +- `changelog-armed` — treat `changelog.d/` as the arming, validate every development fragment, and require bare releases to consume the directory into their exact publishable section (#115). - `lib/changelog.sh` + `bin/changelog-assemble` — read the `changelog.d/` fragments, assemble one release section (canonical group order, one shape per repo), and consume exactly what was published (#114). - BUILDER.md — the handed-off PR is the parked claim's fourth shape, its handoff is its declaration, and shape 2 covers the round awaiting its first verdicts (#109). - `labels-reconcile` — a degraded mergeability/checks read now logs gh's actual stderr (collapsed, bounded) beside the byte-identical counted line, and the blind-sweep warning leads with the observed reason instead of asserting the permissions cause (#101). diff --git a/actions/changelog-armed/changelog-armed.sh b/actions/changelog-armed/changelog-armed.sh index feaa427..0e4b0ba 100644 --- a/actions/changelog-armed/changelog-armed.sh +++ b/actions/changelog-armed/changelog-armed.sh @@ -102,7 +102,10 @@ if [ -d "$fragments_dir" ]; then fi if [ -n "$fragments" ]; then - surviving="$(printf '%s' "$fragments" | paste -sd ', ' -)" + surviving="$(printf '%s\n' "$fragments" | awk ' + BEGIN { separator = "" } + { printf "%s%s", separator, $0; separator = ", " } + ')" echo "changelog-armed: these fragments were not consumed: $surviving — re-run 'changelog-assemble $ver'" >&2 exit 1 fi -- 2.45.2