Repository navigation
ci(release): GitLab component notes only list what changed in the component #578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <repo-dir> (the CLI repository, where templates/ | ||
| # lives), between the highest vX.Y.Z tag below v<version> and v<version>. | ||
| # 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 | ||
| # first word 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 <version> <digest> [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 '^(remov(e|es|ed|ing)|drop(s|ped|ping)?)([^[:alnum:]_-]|$)'; 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| #!/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 <file> <subject> [body]: append a line to <file> 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 <name> <output> <grep -F needle> | ||
| 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 | ||
| } | ||
| # section <output> <title>: 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" | ||
| 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): 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" "$(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" "$(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" | ||
| 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" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Test coverage
Low: the security-by-type classification branch is never exercised by a test — only the security-by-scope branch is. (Low)
The section classifier in
scripts/release-component-notes.sh:70routes a commit to Security when either its conventional-commit type issecurityor its scope containssecurity:The test suite only covers the scope branch —
scripts/release-component-notes.test.sh:230commitsfix(component,security): do not echo the tokenand asserts it lands under### Security. There is no test for asecurity-typed commit (e.g.security: patch the token leakorsecurity(component): rotate keys), even though the script's own header comment documents this mapping ("security type or scope -> Security",scripts/release-component-notes.sh:17).Because the
[ "$type" = "security" ]disjunct is never executed, a regression affecting only that branch — a typo, or a refactor that collapses the condition to scope-only — would silently misclassify security-typed commits into the Changed section, producing incorrect GitLab component release notes, and the full test run would still reportall tests passed.Suggested test: add a commit
commit templates/plumber.yml "security: patch the token leak"(plainsecuritytype, no scope) before thev0.8.0release in the section-coverage block, and assert its description appears under### Security(and does not appear under### Changed). This closes the second, currently-unverified half of the Security routing logic.Control:
.github/claude-prompts/test-coverage.md· resolve this conversation once addressed.