From bf2197417d343ac3dbfa2f0d6eb3de90f2fa1c8d Mon Sep 17 00:00:00 2001 From: Thib_C Date: Tue, 27 Oct 2026 16:36:17 +0100 Subject: [PATCH 1/2] ci(release): GitLab component notes only list what changed in the component propagate.yml copied the GitHub release body into the component's RELEASE_NOTES.md, so every GitLab Catalog release carried the whole CLI changelog: GitHub-only controls, dependency bumps, tests. scripts/release-component-notes.sh now builds those notes, in the Keep a Changelog 1.1.0 format: the image the release pins, one entry per commit touching templates/ since the previous tag (release commits skipped), a link to the GitHub release for the full CLI changelog, and a compare link between the component tags. Closes #569 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/propagate.yml | 10 +- .github/workflows/scripts-test.yml | 1 + scripts/release-component-notes.sh | 107 +++++++++++++++++++++ scripts/release-component-notes.test.sh | 120 ++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 scripts/release-component-notes.sh create mode 100644 scripts/release-component-notes.test.sh diff --git a/.github/workflows/propagate.yml b/.github/workflows/propagate.yml index 4131a0c3..b0f47820 100644 --- a/.github/workflows/propagate.yml +++ b/.github/workflows/propagate.yml @@ -3,7 +3,9 @@ name: Propagate Release # Pushes a published release out to the repos that reference it: # - gitlab.com/getplumber/plumber (CI/CD Catalog component): sync # templates/plumber.yml, rewrite "pinned plumber version" marked README -# lines, write RELEASE_NOTES.md, commit to main and tag v${VERSION}. The +# lines, write RELEASE_NOTES.md (the component's own changes, built by +# scripts/release-component-notes.sh; the GitHub release keeps the full +# CLI changelog), commit to main and tag v${VERSION}. The # GitLab tag pipeline then creates the release (Catalog publish requires # a CI-created release, so the tag pipeline is the only valid path). # Two commits land on main: the content sync (untagged), then a @@ -77,7 +79,7 @@ jobs: env: VERSION: ${{ inputs.version }} DIGEST_INPUT: ${{ inputs.digest }} - GH_TOKEN: ${{ github.token }} + CLI_URL: ${{ github.server_url }}/${{ github.repository }} run: | set -euo pipefail SHA="$(git rev-parse "v${VERSION}^{commit}")" @@ -86,7 +88,9 @@ jobs: echo "digest mismatch: docker job says ${DIGEST_INPUT}, template has ${DIGEST}" >&2 exit 1 fi - gh release view "v${VERSION}" --json body --jq .body > /tmp/release-notes.md + # Not the GitHub release body: component users only need what + # changed in templates/plumber.yml, plus a link to the CLI release. + bash scripts/release-component-notes.sh "$VERSION" "$DIGEST" > /tmp/release-notes.md test -s /tmp/release-notes.md { echo "sha=${SHA}"; echo "digest=${DIGEST}"; } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/scripts-test.yml b/.github/workflows/scripts-test.yml index dc52f20d..91ffc9cf 100644 --- a/.github/workflows/scripts-test.yml +++ b/.github/workflows/scripts-test.yml @@ -46,3 +46,4 @@ jobs: bash scripts/release-template-digest.test.sh bash scripts/release-bump-version.test.sh bash scripts/release-plan.test.sh + bash scripts/release-component-notes.test.sh diff --git a/scripts/release-component-notes.sh b/scripts/release-component-notes.sh new file mode 100644 index 00000000..c814d59f --- /dev/null +++ b/scripts/release-component-notes.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Print the GitLab component release notes for one release, in the Keep a +# Changelog 1.1.0 format (https://keepachangelog.com/en/1.1.0/). +# +# The component is templates/plumber.yml, so the notes only describe what +# changed in it: the image the release pins, plus one entry per commit +# touching templates/ since the previous release tag. The CLI changelog +# (GitHub-only controls, CI bumps, tests...) is left to the GitHub release, +# which the notes link to. +# +# Commits are read from (the CLI repository, where templates/ +# lives), between the highest vX.Y.Z tag below v and v. +# Release commits (any "(release)" scope: chore(release) today, +# ci(release) in older releases) are skipped: their only template change is the +# digest bump, already covered by the image entry. Conventional commit +# subjects map to sections: +# security type or scope -> Security +# subject saying deprecate -> Deprecated +# subject starting remove/drop -> Removed +# feat -> Added +# fix -> Fixed +# anything else -> Changed +# A breaking change ("!" or a BREAKING CHANGE footer) gets a "Breaking:" +# prefix, wherever it lands. Empty sections are left out. +# +# Usage: release-component-notes.sh [repo-dir] +# Env: COMPONENT_URL GitLab component project (default gitlab.com/getplumber/plumber) +# CLI_URL GitHub CLI repository (default github.com/getplumber/plumber) +set -euo pipefail +ver="${1:?version required}" +digest="${2:?digest required}" +dir="${3:-.}" +component_url="${COMPONENT_URL:-https://gitlab.com/getplumber/plumber}" +cli_url="${CLI_URL:-https://github.com/getplumber/plumber}" + +[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "invalid digest: '${digest}'" >&2; exit 1; } +tag="v${ver}" +git -C "$dir" rev-parse -q --verify "refs/tags/${tag}" >/dev/null || { echo "tag ${tag} not found in ${dir}" >&2; exit 1; } + +# The tag right before ours in version order. Empty for the first release. +prev="$(git -C "$dir" tag --list 'v*' --sort=v:refname \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | grep -B1 -x "$tag" | grep -vx "$tag" || true)" +range="$tag" +if [ -n "$prev" ]; then range="${prev}..${tag}"; fi + +added=() changed=() deprecated=() removed=() fixed=() security=() +changed+=("Image bumped to \`getplumber/plumber:${ver}\` (\`${digest}\`).") + +re='^([a-zA-Z]+)(\(([^)]*)\))?(!)?: (.+)$' +while IFS= read -r sha; do + [ -n "$sha" ] || continue + subject="$(git -C "$dir" log -1 --format=%s "$sha")" + body="$(git -C "$dir" log -1 --format=%b "$sha")" + type="" scope="" bang="" desc="$subject" + if [[ "$subject" =~ $re ]]; then + type="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')" + scope="${BASH_REMATCH[3]}" + bang="${BASH_REMATCH[4]}" + desc="${BASH_REMATCH[5]}" + fi + if [ "$scope" = "release" ]; then continue; fi + + entry="$(printf '%s' "${desc:0:1}" | tr '[:lower:]' '[:upper:]')${desc:1}" + if ! [[ "$entry" =~ [.!?]$ ]]; then entry="${entry}."; fi + if [ -n "$bang" ] || printf '%s\n' "$body" | grep -qE '^BREAKING[ -]CHANGE:'; then + entry="**Breaking:** ${entry}" + fi + + if [ "$type" = "security" ] || [[ ",${scope}," =~ ,[[:space:]]*security[[:space:]]*, ]]; then + security+=("$entry") + elif printf '%s' "$desc" | grep -qi 'deprecat'; then + deprecated+=("$entry") + elif printf '%s' "$desc" | grep -qiE '^(remove|drop)'; then + removed+=("$entry") + elif [ "$type" = "feat" ]; then + added+=("$entry") + elif [ "$type" = "fix" ]; then + fixed+=("$entry") + else + changed+=("$entry") + fi +done < <(git -C "$dir" log --no-merges --reverse --format=%H "$range" -- templates/) + +section() { + local title="$1"; shift + [ "$#" -gt 0 ] || return 0 + printf '\n### %s\n\n' "$title" + printf -- '- %s\n' "$@" +} + +date="$(git -C "$dir" log -1 --format=%cs "${tag}^{commit}")" +if [ -n "$prev" ]; then + link="${component_url}/-/compare/${prev}...${tag}" +else + link="${component_url}/-/tags/${tag}" +fi + +printf '## [%s] - %s\n' "$ver" "$date" +section Added ${added[@]+"${added[@]}"} +section Changed "${changed[@]}" +section Deprecated ${deprecated[@]+"${deprecated[@]}"} +section Removed ${removed[@]+"${removed[@]}"} +section Fixed ${fixed[@]+"${fixed[@]}"} +section Security ${security[@]+"${security[@]}"} +printf '\nThe full CLI changelog is in the [%s GitHub release](%s/releases/tag/%s).\n' "$tag" "$cli_url" "$tag" +printf '\n[%s]: %s\n' "$ver" "$link" diff --git a/scripts/release-component-notes.test.sh b/scripts/release-component-notes.test.sh new file mode 100644 index 00000000..f851a9bf --- /dev/null +++ b/scripts/release-component-notes.test.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Unit tests for release-component-notes.sh. +# Run: bash scripts/release-component-notes.test.sh +set -euo pipefail +here="$(cd "$(dirname "$0")" && pwd)" +script="$here/release-component-notes.sh" +fails=0 +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +DIGEST="sha256:$(printf 'c%.0s' {1..64})" +repo="$tmp/repo" +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.com GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.com +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 +git init -q "$repo" +mkdir -p "$repo/templates" "$repo/control" + +# commit [body]: append a line to and commit it. +commit() { + echo "$2" >> "$repo/$1" + git -C "$repo" add -A + if [ -n "${3:-}" ]; then + git -C "$repo" commit -q -m "$2" -m "$3" + else + git -C "$repo" commit -q -m "$2" + fi +} +release() { + commit templates/plumber.yml "chore(release): $1 [skip ci]" + git -C "$repo" tag "v$1" +} + +check() { # check + if printf '%s\n' "$2" | grep -qF -- "$3"; then echo "ok: $1"; else echo "FAIL: $1 (missing: $3)"; fails=$((fails+1)); fi +} +check_not() { + if printf '%s\n' "$2" | grep -qF -- "$3"; then echo "FAIL: $1 (unexpected: $3)"; fails=$((fails+1)); else echo "ok: $1"; fi +} + +# v0.7.9: first release, no previous tag. +commit templates/plumber.yml "feat(component): initial template" +release 0.7.9 +out="$(bash "$script" 0.7.9 "$DIGEST" "$repo")" +check "heading with ISO date" "$out" "## [0.7.9] - $(git -C "$repo" log -1 --format=%cs v0.7.9)" +check "first release links the tag page" "$out" "[0.7.9]: https://gitlab.com/getplumber/plumber/-/tags/v0.7.9" +check "first release lists every template commit" "$out" "- Initial template." + +# v0.7.10: digest-only release, with a CLI-only change in between. +commit control/x.go "feat(github): a GitHub-only control" +release 0.7.10 +out="$(bash "$script" 0.7.10 "$DIGEST" "$repo")" +check "previous tag sorts by version (0.7.9 < 0.7.10)" "$out" "[0.7.10]: https://gitlab.com/getplumber/plumber/-/compare/v0.7.9...v0.7.10" +check "image entry under Changed" "$out" "- Image bumped to \`getplumber/plumber:0.7.10\` (\`${DIGEST}\`)." +check "full CLI changelog link" "$out" "The full CLI changelog is in the [v0.7.10 GitHub release](https://github.com/getplumber/plumber/releases/tag/v0.7.10)." +check_not "CLI-only commit left out" "$out" "GitHub-only control" +check_not "release commit left out" "$out" "skip ci" +check_not "no empty Added section" "$out" "### Added" +if [ "$(printf '%s\n' "$out" | grep -c '^### ')" = 1 ]; then echo "ok: digest-only release has only Changed"; else echo "FAIL: digest-only release has only Changed"; fails=$((fails+1)); fi + +# v0.8.0: every section, in order. +commit templates/plumber.yml "feat(component): add a report_format input" +commit templates/plumber.yml "fix(component): quote the threshold" +commit templates/plumber.yml "feat(component)!: rename the config input" +commit templates/plumber.yml "refactor: split the script" "BREAKING CHANGE: the job name changed" +commit templates/plumber.yml "feat(component): deprecate the legacy input" +commit templates/plumber.yml "feat(component): remove the verbose input" +commit templates/plumber.yml "fix(component,security): do not echo the token" +commit templates/plumber.yml "docs(component): explain the inputs" +commit templates/plumber.yml "Tweak the rules ordering" +commit templates/plumber.yml "ci(release): pin v0.7.11 refs [skip ci]" +commit control/x.go "fix(github): a GitHub-only fix" +release 0.8.0 +out="$(bash "$script" 0.8.0 "$DIGEST" "$repo")" +check "feat -> Added" "$out" "- Add a report_format input." +check "fix -> Fixed" "$out" "- Quote the threshold." +check "bang marks breaking" "$out" "- **Breaking:** Rename the config input." +check "footer marks breaking" "$out" "- **Breaking:** Split the script." +check "deprecate -> Deprecated" "$out" "### Deprecated + +- Deprecate the legacy input." +check "remove -> Removed" "$out" "### Removed + +- Remove the verbose input." +check "security scope -> Security" "$out" "### Security + +- Do not echo the token." +check "other types -> Changed" "$out" "- Explain the inputs." +check "non-conventional subject -> Changed, capitalized" "$out" "- Tweak the rules ordering." +check_not "ci(release) commit left out" "$out" "Pin v0.7.11" +check_not "CLI-only fix left out" "$out" "GitHub-only fix" +order="$(printf '%s\n' "$out" | grep '^### ' | tr '\n' ' ')" +if [ "$order" = "### Added ### Changed ### Deprecated ### Removed ### Fixed ### Security " ]; then + echo "ok: Keep a Changelog section order" +else + echo "FAIL: Keep a Changelog section order (got: $order)"; fails=$((fails+1)) +fi +if printf '%s\n' "$out" | grep -qE '^- (Image|Explain|Tweak|\*\*Breaking:\*\* Split)' \ + && [ "$(printf '%s\n' "$out" | sed -n '/^### Changed/,/^### /p' | grep -c '^- ')" = 4 ]; then + echo "ok: Changed holds the image and the three other changes" +else + echo "FAIL: Changed holds the image and the three other changes"; fails=$((fails+1)) +fi + +# URL overrides (the e2e lab points at its own projects). +out="$(COMPONENT_URL=https://gitlab.example.com/lab CLI_URL=https://github.com/example/lab bash "$script" 0.8.0 "$DIGEST" "$repo")" +check "COMPONENT_URL override" "$out" "[0.8.0]: https://gitlab.example.com/lab/-/compare/v0.7.10...v0.8.0" +check "CLI_URL override" "$out" "(https://github.com/example/lab/releases/tag/v0.8.0)" + +# Bad input: fail with a diagnostic. +set +e +out="$(bash "$script" 0.8.0 "sha256:nothex" "$repo" 2>&1)"; rc=$? +set -e +if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -q 'invalid digest'; then echo "ok: bad digest rejected"; else echo "FAIL: bad digest rejected (rc=$rc out=$out)"; fails=$((fails+1)); fi +set +e +out="$(bash "$script" 9.9.9 "$DIGEST" "$repo" 2>&1)"; rc=$? +set -e +if [ "$rc" -ne 0 ] && printf '%s' "$out" | grep -q 'tag v9.9.9 not found'; then echo "ok: missing tag rejected"; else echo "FAIL: missing tag rejected (rc=$rc out=$out)"; fails=$((fails+1)); fi + +if [ "$fails" -ne 0 ]; then echo "$fails test(s) failed"; exit 1; fi +echo "all tests passed" From bf25ebeef000def07ddcf8bbad42eabae24113f1 Mon Sep 17 00:00:00 2001 From: Thib_C Date: Tue, 27 Oct 2026 17:19:26 +0100 Subject: [PATCH 2/2] fix(release): only file a remove/drop first word under Removed "dropdown" or "drop-in" subjects were listed as removals. Test the security type route too, and check entries inside their section. Co-Authored-By: Claude Opus 5.5 --- scripts/release-component-notes.sh | 4 ++-- scripts/release-component-notes.test.sh | 26 ++++++++++++++----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/scripts/release-component-notes.sh b/scripts/release-component-notes.sh index c814d59f..59069664 100644 --- a/scripts/release-component-notes.sh +++ b/scripts/release-component-notes.sh @@ -16,7 +16,7 @@ # subjects map to sections: # security type or scope -> Security # subject saying deprecate -> Deprecated -# subject starting remove/drop -> Removed +# first word remove/drop -> Removed # feat -> Added # fix -> Fixed # anything else -> Changed @@ -71,7 +71,7 @@ while IFS= read -r sha; do security+=("$entry") elif printf '%s' "$desc" | grep -qi 'deprecat'; then deprecated+=("$entry") - elif printf '%s' "$desc" | grep -qiE '^(remove|drop)'; then + elif printf '%s' "$desc" | grep -qiE '^(remov(e|es|ed|ing)|drop(s|ped|ping)?)([^[:alnum:]_-]|$)'; then removed+=("$entry") elif [ "$type" = "feat" ]; then added+=("$entry") diff --git a/scripts/release-component-notes.test.sh b/scripts/release-component-notes.test.sh index f851a9bf..a22e5ce2 100644 --- a/scripts/release-component-notes.test.sh +++ b/scripts/release-component-notes.test.sh @@ -36,6 +36,8 @@ check() { # check check_not() { if printf '%s\n' "$2" | grep -qF -- "$3"; then echo "FAIL: $1 (unexpected: $3)"; fails=$((fails+1)); else echo "ok: $1"; fi } +# section : the entries of one "### <title>" section. +section() { printf '%s\n' "$1" | awk -v t="### $2" '$0 == t {on=1; next} /^### / {on=0} on && /^- /'; } # v0.7.9: first release, no previous tag. commit templates/plumber.yml "feat(component): initial template" @@ -63,27 +65,29 @@ commit templates/plumber.yml "fix(component): quote the threshold" commit templates/plumber.yml "feat(component)!: rename the config input" commit templates/plumber.yml "refactor: split the script" "BREAKING CHANGE: the job name changed" commit templates/plumber.yml "feat(component): deprecate the legacy input" +commit templates/plumber.yml "feat(component): dropdown selector for the report" +commit templates/plumber.yml "feat(component): drop-in replacement for the job" commit templates/plumber.yml "feat(component): remove the verbose input" commit templates/plumber.yml "fix(component,security): do not echo the token" +commit templates/plumber.yml "security: patch the token leak" commit templates/plumber.yml "docs(component): explain the inputs" commit templates/plumber.yml "Tweak the rules ordering" commit templates/plumber.yml "ci(release): pin v0.7.11 refs [skip ci]" commit control/x.go "fix(github): a GitHub-only fix" release 0.8.0 out="$(bash "$script" 0.8.0 "$DIGEST" "$repo")" -check "feat -> Added" "$out" "- Add a report_format input." -check "fix -> Fixed" "$out" "- Quote the threshold." +check "feat -> Added" "$(section "$out" Added)" "- Add a report_format input." +check "fix -> Fixed" "$(section "$out" Fixed)" "- Quote the threshold." check "bang marks breaking" "$out" "- **Breaking:** Rename the config input." check "footer marks breaking" "$out" "- **Breaking:** Split the script." -check "deprecate -> Deprecated" "$out" "### Deprecated - -- Deprecate the legacy input." -check "remove -> Removed" "$out" "### Removed - -- Remove the verbose input." -check "security scope -> Security" "$out" "### Security - -- Do not echo the token." +check "deprecate -> Deprecated" "$(section "$out" Deprecated)" "- Deprecate the legacy input." +check "remove -> Removed" "$(section "$out" Removed)" "- Remove the verbose input." +check "dropdown -> Added" "$(section "$out" Added)" "- Dropdown selector for the report." +check "drop-in -> Added" "$(section "$out" Added)" "- Drop-in replacement for the job." +check_not "dropdown is not a removal" "$(section "$out" Removed)" "Dropdown" +check_not "drop-in is not a removal" "$(section "$out" Removed)" "Drop-in" +check "security scope -> Security" "$(section "$out" Security)" "- Do not echo the token." +check "security type -> Security" "$(section "$out" Security)" "- Patch the token leak." check "other types -> Changed" "$out" "- Explain the inputs." check "non-conventional subject -> Changed, capitalized" "$out" "- Tweak the rules ordering." check_not "ci(release) commit left out" "$out" "Pin v0.7.11"