From 0da887bb3b061b5f5fa9d0c741f5eaedb6cded3a Mon Sep 17 00:00:00 2001 From: rebel-mskim Date: Mon, 14 Sep 2026 15:59:55 +0900 Subject: [PATCH 1/3] feat(release): add chart_repos, release_notes_extra and notify inputs --- .github/workflows/release.yaml | 41 +++++++++++++++++++++++++++++++- rulesets/protect-main.json | 1 + scripts/release-notes.sh | 15 ++++++++---- templates/workflows/release.yaml | 9 +++++++ tests/release-notes.bats | 20 ++++++++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fab6256..08fb745 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,6 +21,10 @@ defaults: # before anything is published: what was validated is what ships. Tags are # created only by cut-release / tag-release with the release App token. # +# A caller that adds jobs of its own after this workflow (needs: release) reads +# kind/version/branch/last_rc/sha/digests/draft_url from the outputs and sets +# notify: false to post one Slack summary for the whole run itself. +# # Harbor is reachable only from the in-network runner (`runner` input); every # job that pushes to or pulls from Harbor runs there, and arm64 is built under # QEMU on that runner. GitHub-hosted runners do the rest. @@ -53,6 +57,21 @@ on: required: false default: "" type: string + chart_repos: + description: "Helm repositories the chart's dependencies come from, name=url space separated, e.g. 'nfd=https://kubernetes-sigs.github.io/node-feature-discovery/charts'; empty adds none" + required: false + default: "" + type: string + release_notes_extra: + description: "Executable (or markdown file) in the caller repository whose output is appended to the release notes, e.g. a table of the images this component pins; empty appends nothing" + required: false + default: "" + type: string + notify: + description: "Post the kit's Slack summaries (rc published, GA). Set false when the caller adds jobs after this workflow and posts one summary for the whole run itself" + required: false + default: true + type: boolean runner: description: "Runner label with network access to Harbor" required: false @@ -203,6 +222,14 @@ jobs: mkdir -p "$HOME/.local/bin" && echo "$HOME/.local/bin" >> "$GITHUB_PATH" curl -sSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "$HOME/.local/bin/yq" chmod +x "$HOME/.local/bin/yq" + # Chart.lock records dependency repositories by URL, but on a fresh runner + # helm dependency build still refuses a URL it has no named repository for. + - name: Add chart repositories + if: inputs.chart_repos != '' + env: + CHART_REPOS: ${{ inputs.chart_repos }} + run: | + for r in $CHART_REPOS; do helm repo add "${r%%=*}" "${r#*=}"; done # The rc chart must resolve this component's image from Harbor; the caller # says how through chart_rc_values because values layouts differ per chart. - name: Point the rc chart at Harbor @@ -246,6 +273,7 @@ jobs: - name: Release notes since the previous rc (or the previous GA) env: VERSION: ${{ needs.classify.outputs.version }} + RELEASE_NOTES_EXTRA: ${{ inputs.release_notes_extra }} run: | set -euo pipefail . .release-kit/scripts/lib.sh @@ -276,7 +304,7 @@ jobs: rc-notify: name: rc summary -> Slack needs: [classify, rc-images, rc-chart, rc-prerelease] - if: always() && needs.classify.outputs.kind == 'rc' + if: always() && needs.classify.outputs.kind == 'rc' && inputs.notify runs-on: ubuntu-latest steps: - name: Check out the release kit @@ -362,6 +390,14 @@ jobs: mkdir -p "$HOME/.local/bin" && echo "$HOME/.local/bin" >> "$GITHUB_PATH" curl -sSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "$HOME/.local/bin/yq" chmod +x "$HOME/.local/bin/yq" + # Chart.lock records dependency repositories by URL, but on a fresh runner + # helm dependency build still refuses a URL it has no named repository for. + - name: Add chart repositories + if: inputs.chart_repos != '' + env: + CHART_REPOS: ${{ inputs.chart_repos }} + run: | + for r in $CHART_REPOS; do helm repo add "${r%%=*}" "${r#*=}"; done - name: Package chart (pristine values) run: | set -euo pipefail @@ -401,6 +437,8 @@ jobs: # The previous GA is the highest GA tag below this one, not "the tag before # this one in history": release branches make git describe unreliable. - name: Generate release notes + env: + RELEASE_NOTES_EXTRA: ${{ inputs.release_notes_extra }} run: | set -euo pipefail . .release-kit/scripts/lib.sh @@ -443,6 +481,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh label delete "backport ${{ needs.classify.outputs.branch }}" --yes --repo "$GITHUB_REPOSITORY" || echo "::notice::label already gone" - name: Notify Slack + if: inputs.notify env: SLACK_OAUTH_TOKEN: ${{ secrets.SLACK_OAUTH_TOKEN }} SLACK_CHANNEL_ID: ${{ vars.CLOUD_COMPONENT_SLACK_CHANNEL_ID }} diff --git a/rulesets/protect-main.json b/rulesets/protect-main.json index 37ec295..ccc642a 100644 --- a/rulesets/protect-main.json +++ b/rulesets/protect-main.json @@ -12,6 +12,7 @@ { "type": "pull_request", "parameters": { + "require_extra_approval_for_unattributed_changes": true, "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": false, "require_code_owner_review": false, diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh index d3b95de..fd1d401 100755 --- a/scripts/release-notes.sh +++ b/scripts/release-notes.sh @@ -13,8 +13,9 @@ # - a main commit whose sha appears as a trailer in the previous GA's own # backports: that fix already shipped in the previous release. # Commits are grouped by Conventional Commit type. If RELEASE_NOTES_EXTRA names -# a file, its content is appended after Known Issues (repositories that pin -# other images, like the operator, put their component table there). +# an executable, its output is appended after Known Issues; a plain file is +# appended as-is (repositories that pin other images, like the operator, +# generate their component table there). A path that is neither fails. set -euo pipefail # shellcheck source=lib.sh @@ -83,8 +84,14 @@ improvements=$(for t in docs refactor perf style build chore test ci; do get "$t echo "## Known Issues" echo "- TBD" echo - if [ -n "${RELEASE_NOTES_EXTRA:-}" ] && [ -f "$RELEASE_NOTES_EXTRA" ]; then - cat "$RELEASE_NOTES_EXTRA" + if [ -n "${RELEASE_NOTES_EXTRA:-}" ]; then + if [ -x "$RELEASE_NOTES_EXTRA" ]; then + "$RELEASE_NOTES_EXTRA" + elif [ -f "$RELEASE_NOTES_EXTRA" ]; then + cat "$RELEASE_NOTES_EXTRA" + else + fail "RELEASE_NOTES_EXTRA '$RELEASE_NOTES_EXTRA' is neither an executable nor a file" + fi echo fi if [ -n "$prev" ]; then diff --git a/templates/workflows/release.yaml b/templates/workflows/release.yaml index 07d104f..0b49fda 100644 --- a/templates/workflows/release.yaml +++ b/templates/workflows/release.yaml @@ -28,4 +28,13 @@ jobs: # yq expression that points the rc chart's image at Harbor, e.g. # '.image.registry = env(HARBOR_HOST)'. chart_rc_values: "" + # Helm repositories the chart's dependencies come from, "name=url" space + # separated; empty when the chart has no dependencies. + chart_repos: "" + # Executable (or markdown file) whose output is appended to the release + # notes, e.g. a table of the images this component pins. Empty for none. + release_notes_extra: "" + # Set false when this repository adds its own jobs after `release` and + # posts one Slack summary for the whole run itself. + notify: true secrets: inherit diff --git a/tests/release-notes.bats b/tests/release-notes.bats index ba5e864..2abb77a 100644 --- a/tests/release-notes.bats +++ b/tests/release-notes.bats @@ -69,3 +69,23 @@ teardown() { teardown_repo; } grep -q 'feat: first' "$TEST_TMP/notes.md" grep -q '_Generated for v0.1.0._' "$TEST_TMP/notes.md" } + +@test "an executable RELEASE_NOTES_EXTRA is run and its output appended" { + commit "feat: first" a.txt >/dev/null + cut v0.1.0 + git tag -a v0.1.0 -m ga + printf '#!/usr/bin/env bash\necho "| generated | $GITHUB_REPOSITORY |"\n' >"$TEST_TMP/extra.sh" + chmod +x "$TEST_TMP/extra.sh" + RELEASE_NOTES_EXTRA="$TEST_TMP/extra.sh" run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" + [ "$status" -eq 0 ] + grep -q '| generated | acme/widget |' "$TEST_TMP/notes.md" +} + +@test "a RELEASE_NOTES_EXTRA that is neither executable nor file fails" { + commit "feat: first" a.txt >/dev/null + cut v0.1.0 + git tag -a v0.1.0 -m ga + RELEASE_NOTES_EXTRA="$TEST_TMP/missing.md" run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" + [ "$status" -ne 0 ] + [[ "$output" == *"neither an executable nor a file"* ]] +} From 9457b0bb84bb6a8a888827b97d0c745a074a531f Mon Sep 17 00:00:00 2001 From: rebel-mskim Date: Mon, 14 Sep 2026 16:22:26 +0900 Subject: [PATCH 2/3] fix: pick release_notes_extra mode by shebang, helm repo add --force-update release-notes.sh chose by the executable bit: a bare filename was looked up on PATH (command not found), a markdown file that happened to be +x was executed, and a script committed without +x was pasted into the notes as text. A file is now a script iff its first line is "#!": a script without the executable bit fails with a chmod hint, a bare name runs from the checkout, anything else is appended as-is. helm repo add gets --force-update so a repository name already registered on the runner with another URL is replaced instead of failing the job. Template: "remove both chart lines" -> the chart_* lines (chart_repos is a third one now). Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yaml | 6 +++--- scripts/release-notes.sh | 19 ++++++++++------- templates/workflows/release.yaml | 7 ++++--- tests/release-notes.bats | 36 ++++++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 08fb745..74fe2f3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,7 +63,7 @@ on: default: "" type: string release_notes_extra: - description: "Executable (or markdown file) in the caller repository whose output is appended to the release notes, e.g. a table of the images this component pins; empty appends nothing" + description: "File in the caller repository appended to the release notes: a script (#!, executable) is run and its output appended, a markdown file is appended as-is, e.g. a table of the images this component pins; empty appends nothing" required: false default: "" type: string @@ -229,7 +229,7 @@ jobs: env: CHART_REPOS: ${{ inputs.chart_repos }} run: | - for r in $CHART_REPOS; do helm repo add "${r%%=*}" "${r#*=}"; done + for r in $CHART_REPOS; do helm repo add --force-update "${r%%=*}" "${r#*=}"; done # The rc chart must resolve this component's image from Harbor; the caller # says how through chart_rc_values because values layouts differ per chart. - name: Point the rc chart at Harbor @@ -397,7 +397,7 @@ jobs: env: CHART_REPOS: ${{ inputs.chart_repos }} run: | - for r in $CHART_REPOS; do helm repo add "${r%%=*}" "${r#*=}"; done + for r in $CHART_REPOS; do helm repo add --force-update "${r%%=*}" "${r#*=}"; done - name: Package chart (pristine values) run: | set -euo pipefail diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh index fd1d401..32fd1ef 100755 --- a/scripts/release-notes.sh +++ b/scripts/release-notes.sh @@ -13,9 +13,10 @@ # - a main commit whose sha appears as a trailer in the previous GA's own # backports: that fix already shipped in the previous release. # Commits are grouped by Conventional Commit type. If RELEASE_NOTES_EXTRA names -# an executable, its output is appended after Known Issues; a plain file is -# appended as-is (repositories that pin other images, like the operator, -# generate their component table there). A path that is neither fails. +# a file, it is appended after Known Issues: a script (first line "#!") is run +# and its output appended, anything else is appended as-is (repositories that +# pin other images, like the operator, generate their component table there). +# A script without the executable bit, or a path that is not a file, fails. set -euo pipefail # shellcheck source=lib.sh @@ -85,12 +86,14 @@ improvements=$(for t in docs refactor perf style build chore test ci; do get "$t echo "- TBD" echo if [ -n "${RELEASE_NOTES_EXTRA:-}" ]; then - if [ -x "$RELEASE_NOTES_EXTRA" ]; then - "$RELEASE_NOTES_EXTRA" - elif [ -f "$RELEASE_NOTES_EXTRA" ]; then - cat "$RELEASE_NOTES_EXTRA" + extra=$RELEASE_NOTES_EXTRA + [ -f "$extra" ] || fail "RELEASE_NOTES_EXTRA '$extra' is not a file" + if [ "$(head -c 2 "$extra")" = '#!' ]; then + [ -x "$extra" ] || fail "RELEASE_NOTES_EXTRA '$extra' is a script but not executable (chmod +x)" + # A bare name would be looked up on PATH instead of in the checkout. + case $extra in */*) "$extra" ;; *) "./$extra" ;; esac else - fail "RELEASE_NOTES_EXTRA '$RELEASE_NOTES_EXTRA' is neither an executable nor a file" + cat "$extra" fi echo fi diff --git a/templates/workflows/release.yaml b/templates/workflows/release.yaml index 0b49fda..996182e 100644 --- a/templates/workflows/release.yaml +++ b/templates/workflows/release.yaml @@ -23,7 +23,7 @@ jobs: images: CHANGE-ME # Prefix of the GitHub release title. release_name: CHANGE-ME - # Helm chart directory, or remove both chart lines if there is no chart. + # Helm chart directory, or remove the chart_* lines if there is no chart. chart_dir: "" # yq expression that points the rc chart's image at Harbor, e.g. # '.image.registry = env(HARBOR_HOST)'. @@ -31,8 +31,9 @@ jobs: # Helm repositories the chart's dependencies come from, "name=url" space # separated; empty when the chart has no dependencies. chart_repos: "" - # Executable (or markdown file) whose output is appended to the release - # notes, e.g. a table of the images this component pins. Empty for none. + # Script (#!, executable) whose output, or markdown file whose content, is + # appended to the release notes, e.g. a table of the images this component + # pins. Empty for none. release_notes_extra: "" # Set false when this repository adds its own jobs after `release` and # posts one Slack summary for the whole run itself. diff --git a/tests/release-notes.bats b/tests/release-notes.bats index 2abb77a..0d3f195 100644 --- a/tests/release-notes.bats +++ b/tests/release-notes.bats @@ -81,11 +81,43 @@ teardown() { teardown_repo; } grep -q '| generated | acme/widget |' "$TEST_TMP/notes.md" } -@test "a RELEASE_NOTES_EXTRA that is neither executable nor file fails" { +@test "a RELEASE_NOTES_EXTRA that is not a file fails" { commit "feat: first" a.txt >/dev/null cut v0.1.0 git tag -a v0.1.0 -m ga RELEASE_NOTES_EXTRA="$TEST_TMP/missing.md" run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" [ "$status" -ne 0 ] - [[ "$output" == *"neither an executable nor a file"* ]] + [[ "$output" == *"is not a file"* ]] +} + +@test "a script given as a bare name runs from the checkout, not from PATH" { + commit "feat: first" a.txt >/dev/null + cut v0.1.0 + git tag -a v0.1.0 -m ga + printf '#!/usr/bin/env bash\necho "| bare | name |"\n' >extra.sh + chmod +x extra.sh + RELEASE_NOTES_EXTRA=extra.sh run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" + [ "$status" -eq 0 ] + grep -q '| bare | name |' "$TEST_TMP/notes.md" +} + +@test "a script without the executable bit fails instead of being pasted" { + commit "feat: first" a.txt >/dev/null + cut v0.1.0 + git tag -a v0.1.0 -m ga + printf '#!/usr/bin/env bash\necho nope\n' >"$TEST_TMP/extra.sh" + RELEASE_NOTES_EXTRA="$TEST_TMP/extra.sh" run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" + [ "$status" -ne 0 ] + [[ "$output" == *"not executable"* ]] +} + +@test "a markdown file with the executable bit is appended, not run" { + commit "feat: first" a.txt >/dev/null + cut v0.1.0 + git tag -a v0.1.0 -m ga + echo "| plain | markdown |" >"$TEST_TMP/extra.md" + chmod +x "$TEST_TMP/extra.md" + RELEASE_NOTES_EXTRA="$TEST_TMP/extra.md" run "$SCRIPTS/release-notes.sh" v0.1.0 "" "$TEST_TMP/notes.md" + [ "$status" -eq 0 ] + grep -q '| plain | markdown |' "$TEST_TMP/notes.md" } From 3e54de015fbf8383ccc3d09de8aa498e260c2e70 Mon Sep 17 00:00:00 2001 From: rebel-mskim Date: Mon, 14 Sep 2026 16:30:12 +0900 Subject: [PATCH 3/3] fix: give the GA release-notes step the rc step's VERSION, drop no-op ruleset line The release_notes_extra script saw VERSION on every rc pre-release but not on the GA draft, so a script that used it would fail only after ga-promote had already pushed to docker.io. Both steps now export it, and the input description says which variables the script may rely on. require_extra_approval_for_unattributed_changes is GitHub's server-side default and has no effect on a ruleset that requires zero approvals, so protect-main.json goes back to what main has. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yaml | 3 ++- rulesets/protect-main.json | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 74fe2f3..44e766e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,7 +63,7 @@ on: default: "" type: string release_notes_extra: - description: "File in the caller repository appended to the release notes: a script (#!, executable) is run and its output appended, a markdown file is appended as-is, e.g. a table of the images this component pins; empty appends nothing" + description: "File in the caller repository appended to the release notes: a script (#!, executable) is run in the checkout with VERSION, HARBOR_REGISTRY, DOCKERHUB_REGISTRY, IMAGES and CHART_DIR set and its output appended, a markdown file is appended as-is, e.g. a table of the images this component pins; empty appends nothing" required: false default: "" type: string @@ -438,6 +438,7 @@ jobs: # this one in history": release branches make git describe unreliable. - name: Generate release notes env: + VERSION: ${{ needs.classify.outputs.version }} RELEASE_NOTES_EXTRA: ${{ inputs.release_notes_extra }} run: | set -euo pipefail diff --git a/rulesets/protect-main.json b/rulesets/protect-main.json index ccc642a..37ec295 100644 --- a/rulesets/protect-main.json +++ b/rulesets/protect-main.json @@ -12,7 +12,6 @@ { "type": "pull_request", "parameters": { - "require_extra_approval_for_unattributed_changes": true, "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": false, "require_code_owner_review": false,