Merge pull request #123 from codex-bot-andresmgsl/build/115-changelog-armed-fragment-mode
feat: arm changelogs in fragment mode
This commit is contained in:
commit
736733ebf8
4 changed files with 243 additions and 3 deletions
|
|
@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop.
|
||||||
|
|
||||||
## Unreleased
|
## 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).
|
||||||
- `actions/changelog-assembled` — a release PR's stamped section must be byte-for-byte what the fragments it consumed assemble to, replayed from the merge base; inapplicable trees pass with a NOTICE (#116).
|
- `actions/changelog-assembled` — a release PR's stamped section must be byte-for-byte what the fragments it consumed assemble to, replayed from the merge base; inapplicable trees pass with a NOTICE (#116).
|
||||||
- `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).
|
- `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 directed hold is the parked claim's fifth shape, its attention demand is acknowledged in the declaration comment, and its board bookkeeping covers in-flight work; TRIAGE.md no longer excludes it (#113).
|
- BUILDER.md — the directed hold is the parked claim's fifth shape, its attention demand is acknowledged in the declaration comment, and its board bookkeeping covers in-flight work; TRIAGE.md no longer excludes it (#113).
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ inputs:
|
||||||
description: Path to the changelog, relative to the workspace
|
description: Path to the changelog, relative to the workspace
|
||||||
required: false
|
required: false
|
||||||
default: CHANGELOG.md
|
default: CHANGELOG.md
|
||||||
|
fragments-dir:
|
||||||
|
description: Fragment directory whose presence selects fragment mode
|
||||||
|
required: false
|
||||||
|
default: changelog.d
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -21,4 +25,5 @@ runs:
|
||||||
env:
|
env:
|
||||||
CHANGELOG: ${{ inputs.changelog }}
|
CHANGELOG: ${{ inputs.changelog }}
|
||||||
VERSION_SOURCE: ${{ inputs.version-source }}
|
VERSION_SOURCE: ${{ inputs.version-source }}
|
||||||
|
FRAGMENTS_DIR: ${{ inputs.fragments-dir }}
|
||||||
run: bash "$GITHUB_ACTION_PATH/changelog-armed.sh"
|
run: bash "$GITHUB_ACTION_PATH/changelog-armed.sh"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# changelog-armed.sh [<changelog>] [<version-source>] — assert that the
|
# changelog-armed.sh [<changelog>] [<version-source>] [<fragments-dir>] —
|
||||||
# changelog is ARMED: that there is a heading for the next PR's entry to
|
# assert that the changelog is ARMED: that there is a place for the next PR's
|
||||||
# land under, and that it is the right one for the state this tree is in.
|
# 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
|
# 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
|
# 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
|
# tempted to simplify this back to the unconditional form should read
|
||||||
# those two reverts first.
|
# 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
|
# 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
|
# 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
|
# 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}}"
|
changelog="${1:-${CHANGELOG:-CHANGELOG.md}}"
|
||||||
version_source="${2:-${VERSION_SOURCE:-file}}"
|
version_source="${2:-${VERSION_SOURCE:-file}}"
|
||||||
|
fragments_dir="${3:-${FRAGMENTS_DIR:-changelog.d}}"
|
||||||
|
|
||||||
# The shared libs travel with this action: a consumer's
|
# The shared libs travel with this action: a consumer's
|
||||||
# `uses: heavy-duty/ceremony/actions/changelog-armed@<tag>` downloads this
|
# `uses: heavy-duty/ceremony/actions/changelog-armed@<tag>` downloads this
|
||||||
|
|
@ -67,6 +76,68 @@ ver="$(version_read "$version_source")" || {
|
||||||
exit 1
|
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/<issue>.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\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
|
||||||
|
|
||||||
|
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 <<EOF
|
||||||
|
changelog-armed: the version is '$ver' but the top section of $changelog is:
|
||||||
|
|
||||||
|
$top
|
||||||
|
|
||||||
|
Fragment mode has no re-arm step. A bare version means this tree is a
|
||||||
|
release, so the top section must be the stamped section for '$ver' itself.
|
||||||
|
A different version means the ceremony stamped the wrong number.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "changelog-armed: version '$ver' agrees with fragment mode ($fragments_dir)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# The TOP section: the first '## ' heading in the file. Everything above it is
|
# The TOP section: the first '## ' heading in the file. Everything above it is
|
||||||
# the changelog's own preamble and belongs to no section.
|
# the changelog's own preamble and belongs to no section.
|
||||||
top="$(grep -m1 '^## ' "$changelog" || true)"
|
top="$(grep -m1 '^## ' "$changelog" || true)"
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,159 @@ EOF
|
||||||
check "package-json: bare + armed passes" 0 "agrees" \
|
check "package-json: bare + armed passes" 0 "agrees" \
|
||||||
in_tree pkg-bare-armed CHANGELOG.md package-json
|
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' section 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.
|
||||||
|
|
||||||
|
## 1.2.3 — 2026-07-19
|
||||||
|
|
||||||
|
- The right release was not stamped on top.
|
||||||
|
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 --------------------------
|
# --- the action's wiring: inputs arrive as env vars --------------------------
|
||||||
|
|
||||||
mkdir -p "$TMP/env-tree"
|
mkdir -p "$TMP/env-tree"
|
||||||
|
|
@ -259,4 +412,14 @@ env_tree() {
|
||||||
}
|
}
|
||||||
check "env vars drive the script the way action.yml does" 0 "agrees" 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
|
summary
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue