Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/create-release-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,171 @@ jobs:
- name: Prepare release changes
run: |
architect prepare-release ${{ env.architect_flags }} --version "${{ needs.gather_facts.outputs.version }}"
- name: Aggregate release-candidate changelogs on promotion
env:
version: "${{ needs.gather_facts.outputs.version }}"
is_rc: "${{ needs.gather_facts.outputs.is_rc }}"
run: |
set -euo pipefail
changelog="CHANGELOG.md"
note_prefix="This release aggregates all changes from release candidate"

# Only stable releases can be a promotion of an RC series. architect has
# already moved the "Unreleased" delta into "## [<version>]"; for a stable
# promotion we additionally merge every "## [<version>-rc.N]" section into
# it, so the GitHub release page (built solely from the version's section)
# shows the complete set of changes rather than just the (often empty) delta.
if [ "${is_rc}" = "true" ]; then
echo "RC release (${version}); no aggregation needed."
exit 0
fi

if [ ! -f "${changelog}" ]; then
echo "No ${changelog}; nothing to aggregate."
exit 0
fi

core="${version}"
core_re="${core//./\\.}" # escape dots for use in a regex

# Release-candidate entries for this exact core, ordered oldest -> newest.
rc_sorted="$(grep -oE "^## \[${core_re}-rc\.[0-9]+\]" "${changelog}" \
| sed -E 's/^## \[(.*)\]$/\1/' \
| sort -V || true)"

if [ -z "${rc_sorted}" ]; then
echo "No release-candidate entries found for ${core}; not a promotion."
exit 0
fi

echo "Promotion detected for ${core}. Aggregating release candidates:"
echo "${rc_sorted}" | sed 's/^/ - /'

# Body of a "## [<ver>]" section: lines until the next "## [" header,
# skipping link-reference definitions. Mirrors the extractor in
# create-release.yaml so we produce exactly what the release body reads.
extract_section() {
awk -v ver="$1" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]")) { found = 1; next }
}
found {
if ($0 ~ /^\[[^]]+\]:[ \t]*https?:\/\//) next
if (!started && $0 ~ /^[[:space:]]*$/) next
started = 1
buf[++n] = $0
}
END {
while (n > 0 && buf[n] ~ /^[[:space:]]*$/) n--
for (i = 1; i <= n; i++) print buf[i]
}
' "${changelog}"
}

# Idempotency guard (defence-in-depth on top of the check_skip job): if the
# stable section already carries the note, this has already run.
if extract_section "${version}" | grep -qF "${note_prefix}"; then
echo "Stable section already aggregated; skipping."
exit 0
fi

# Source stream: the "Unreleased" delta (current stable section body) first,
# then each RC oldest -> newest. Each section begins with its own H3 headers,
# so simple concatenation is safe for the category bucketing below.
sources="$(mktemp)"
extract_section "${version}" >> "${sources}"
while IFS= read -r rcv; do
[ -n "${rcv}" ] || continue
extract_section "${rcv}" >> "${sources}"
done <<< "${rc_sorted}"

# The RC sources already passed validate-changelog.yaml (which accepts only the
# six Keep a Changelog categories), but the "Unreleased" delta -- the first
# source -- is not gated by that workflow (it runs only on release branches), so
# a non-canonical H3 there would be silently dropped by the category merge below.
# Refuse to drop content: fail loudly and point at the offending heading.
if grep -E '^### ' "${sources}" \
| grep -qvE '^### (Added|Changed|Deprecated|Removed|Fixed|Security)[[:space:]]*$'; then
echo "::error::Aggregated changelog for ${version} has a non-Keep-a-Changelog H3 heading (see below); fix it and re-run." >&2
echo "Allowed categories: Added, Changed, Deprecated, Removed, Fixed, Security (the Unreleased delta is not gated by validate-changelog.yaml)." >&2
grep -E '^### ' "${sources}" \
| grep -vE '^### (Added|Changed|Deprecated|Removed|Fixed|Security)[[:space:]]*$' \
| sed 's/^/ offending heading: /' >&2
exit 1
fi

first_rc="$(printf '%s\n' "${rc_sorted}" | head -1)"
last_rc="$(printf '%s\n' "${rc_sorted}" | tail -1)"
if [ "${first_rc}" = "${last_rc}" ]; then
note="${note_prefix} ${first_rc}."
else
note="${note_prefix}s ${first_rc} through ${last_rc}."
fi

# Merge bullets by Keep a Changelog category, preserving insertion order
# (Unreleased first, then RCs oldest -> newest). Emit categories in the
# canonical order and prepend the note as the first bullet under "Changed".
# Every H3 here is canonical (RCs passed the validator; the Unreleased delta was
# checked just above), so the category merge drops no content.
merged="$(mktemp)"
awk -v note="${note}" '
/^### / {
cat = $0
sub(/^### /, "", cat)
sub(/[[:space:]]+$/, "", cat)
current = cat
next
}
{
if (current == "") next
if ($0 ~ /^[[:space:]]*$/) next
content[current] = content[current] $0 "\n"
}
END {
n = split("Added Changed Deprecated Removed Fixed Security", canon, " ")
for (i = 1; i <= n; i++) {
c = canon[i]
body = content[c]
if (c == "Changed") body = "- " note "\n" body
if (body != "") printf "### %s\n\n%s\n", c, body
}
}
' "${sources}" > "${merged}"

# The splice below lands the merged body inside the "## [<version>]" header that
# architect created. If that header is absent, the merged content would be
# silently discarded, leaving the changelog unchanged while this step logs
# success -- fail instead so the problem is visible.
if ! grep -qE "^## \[${core_re}\]" "${changelog}"; then
echo "::error::Stable section \"## [${version}]\" not found in ${changelog}; cannot splice (did 'architect prepare-release' run?)." >&2
exit 1
fi

# Splice the merged content back into the "## [<version>]" section, replacing
# only its body; the RC sections, link references and the "Unreleased" heading
# are left untouched.
updated="$(mktemp)"
awk -v ver="${version}" -v mfile="${merged}" '
BEGIN { while ((getline line < mfile) > 0) merged = merged line "\n" }
/^## \[/ {
if (inSection) inSection = 0
if (index($0, "[" ver "]")) {
print
printf "\n%s", merged
inSection = 1
next
}
}
inSection { next }
{ print }
' "${changelog}" > "${updated}"

mv "${updated}" "${changelog}"
rm -f "${sources}" "${merged}"

echo "Aggregated changelog for ${version}:"
extract_section "${version}"
- name: Update version field in Chart.yaml
run: |
# Define chart_dir
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
however this project does not use Semantic Versioning and there are no releases.
Instead this file uses a date-based structure.

## 2026-07-09

### Added

- `create-release-pr.yaml` — when promoting a release candidate to a stable release (a stable version that already has `-rc.N` entries in `CHANGELOG.md`), the stable version's changelog section now aggregates all changes from its release candidates (oldest → newest) plus any `Unreleased` delta, merged by Keep a Changelog category, with a note under `### Changed`. Because a GitHub release page shows only that version's own section, the promoted stable release now lists the full set of changes instead of the (often empty) delta left after the RCs were cut.

## 2026-07-07

### Fixed
Expand Down