diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fab6256..44e766e 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: "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 + 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 --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 @@ -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 --force-update "${r%%=*}" "${r#*=}"; done - name: Package chart (pristine values) run: | set -euo pipefail @@ -401,6 +437,9 @@ 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: + VERSION: ${{ needs.classify.outputs.version }} + RELEASE_NOTES_EXTRA: ${{ inputs.release_notes_extra }} run: | set -euo pipefail . .release-kit/scripts/lib.sh @@ -443,6 +482,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/scripts/release-notes.sh b/scripts/release-notes.sh index d3b95de..32fd1ef 100755 --- a/scripts/release-notes.sh +++ b/scripts/release-notes.sh @@ -13,8 +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 -# a file, its content is appended after Known Issues (repositories that pin -# other images, like the operator, put their component table there). +# 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 @@ -83,8 +85,16 @@ 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 + 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 + cat "$extra" + fi echo fi if [ -n "$prev" ]; then diff --git a/templates/workflows/release.yaml b/templates/workflows/release.yaml index 07d104f..996182e 100644 --- a/templates/workflows/release.yaml +++ b/templates/workflows/release.yaml @@ -23,9 +23,19 @@ 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)'. chart_rc_values: "" + # Helm repositories the chart's dependencies come from, "name=url" space + # separated; empty when the chart has no dependencies. + chart_repos: "" + # 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. + notify: true secrets: inherit diff --git a/tests/release-notes.bats b/tests/release-notes.bats index ba5e864..0d3f195 100644 --- a/tests/release-notes.bats +++ b/tests/release-notes.bats @@ -69,3 +69,55 @@ 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 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" == *"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" +}