-
Notifications
You must be signed in to change notification settings - Fork 0
ci(release): scope release notes to each module #131
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright 2026 CloudBlue LLC | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Generate PR-level release notes scoped to a single module. | ||
| # | ||
| # Usage: release-notes.sh <current_tag> <tag_glob> <module_key> | ||
| # current_tag e.g. sdk/v0.3.0 | ||
| # tag_glob e.g. 'sdk/v*' (finds the previous tag of the SAME module) | ||
| # module_key one of: core | sdk | contrib | admin | ||
| # | ||
| # Emits a "## What's Changed" list containing only the PRs whose changed files | ||
| # touched the given module's directory, followed by a filtered | ||
| # "## New Contributors" section and the module-scoped Full Changelog link. | ||
| # Pure path-based filtering: a PR that edits a root-level file (e.g. Makefile) | ||
| # counts as a "core" change even if its intent was another module. | ||
| # | ||
| # Requires: gh (authenticated via GH_TOKEN), git (full history + tags), jq. | ||
| set -euo pipefail | ||
|
|
||
| cur="$1"; glob="$2"; key="$3" | ||
| repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}" | ||
|
|
||
| # Previous tag of the same module (newest tag matching the glob that isn't cur). | ||
| prev="$(git tag --list "$glob" --sort=-version:refname | grep -vFx "$cur" | head -n1 || true)" | ||
|
|
||
| # Raw GitHub-generated notes for the module's commit range. | ||
| if [ -n "$prev" ]; then | ||
| raw="$(gh api "repos/$repo/releases/generate-notes" \ | ||
| -f tag_name="$cur" -f previous_tag_name="$prev" --jq '.body')" | ||
| else | ||
| raw="$(gh api "repos/$repo/releases/generate-notes" -f tag_name="$cur" --jq '.body')" | ||
| fi | ||
|
|
||
| # Returns 0 if the file list on stdin contains a file belonging to $key. | ||
| module_match() { | ||
| case "$key" in | ||
| sdk) grep -qE '^sdk/' ;; | ||
| admin) grep -qE '^admin/' ;; | ||
| contrib) grep -E '^plugins/contrib/' | grep -qvE '^plugins/contrib/microsoft/keyvault/' ;; | ||
| # Exclude only the sibling modules (separate go.mod). plugins/reference has | ||
| # no go.mod, so it belongs to the root module and must count as core. | ||
| core) grep -qvE '^(sdk|plugins/contrib|admin|\.github|docs|\.ai)/' ;; | ||
| *) echo "unknown module key: $key" >&2; exit 2 ;; | ||
| esac | ||
| } | ||
|
|
||
| # Decide which PRs to keep (space-separated list of surviving PR numbers). | ||
| keepers="" | ||
| for pr in $(printf '%s\n' "$raw" | grep -oE 'pull/[0-9]+' | sed 's#pull/##' | sort -un); do | ||
| if gh api "repos/$repo/pulls/$pr/files" --paginate --jq '.[].filename' | module_match; then | ||
| keepers="$keepers $pr" | ||
| fi | ||
| done | ||
|
|
||
| # Re-emit the raw notes, dropping bullet lines whose PR wasn't kept, and | ||
| # dropping the "New Contributors" header if nothing survives under it. | ||
| printf '%s\n' "$raw" | awk -v keepers="$keepers" ' | ||
| BEGIN { split(keepers, a, " "); for (i in a) ok[a[i]] = 1 } | ||
| # Capture a section header; buffer until we know it has content. | ||
| /^## / { flush(); header = $0; have = 0; next } | ||
| # A bullet referencing a PR: keep only if that PR survived. | ||
| /pull\/[0-9]+/ { | ||
| n = $0; sub(/.*pull\//, "", n); sub(/[^0-9].*/, "", n) | ||
| if (ok[n]) { buffer = buffer (buffer ? "\n" : "") $0; have = 1 } | ||
| next | ||
| } | ||
| # Full Changelog (and any other prose) — pass through, flushing first. | ||
| { flush(); print } | ||
| END { flush() } | ||
| function flush() { | ||
| if (header != "") { | ||
| if (have) { print header; print buffer } | ||
| header = ""; buffer = ""; have = 0 | ||
| } else if (buffer != "") { print buffer; buffer = "" } | ||
| } | ||
| ' | cat -s | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ concurrency: | |
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: read # release-notes.sh reads changed files via the PRs API | ||
|
|
||
| env: | ||
| GOVULNCHECK_VERSION: 'v1.1.4' | ||
|
|
@@ -127,12 +128,18 @@ jobs: | |
| cd dist | ||
| sha256sum * > checksums.txt | ||
|
|
||
| - name: Generate release notes | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'v*' core > release-notes.md | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Info] Tag name is interpolated directly into a These new steps expand Suggested fix: pass the values via |
||
|
|
||
| - name: Create GitHub Release | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | ||
| with: | ||
| draft: true | ||
| prerelease: ${{ contains(github.ref_name, '-') }} | ||
| generate_release_notes: true | ||
| generate_release_notes: false | ||
| body_path: release-notes.md | ||
| files: | | ||
| dist/chaperone-* | ||
| dist/checksums.txt | ||
|
|
@@ -145,6 +152,8 @@ jobs: | |
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | ||
|
|
@@ -166,21 +175,30 @@ jobs: | |
| id: version | ||
| run: echo "sdk_version=${GITHUB_REF_NAME#sdk/}" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Generate release notes | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| { | ||
| echo "## SDK Release ${{ github.ref_name }}" | ||
| echo | ||
| echo "This is a release of the Chaperone SDK module." | ||
| echo | ||
| echo "### Installation" | ||
| echo '```bash' | ||
| echo "go get github.com/cloudblue/chaperone/sdk@${{ steps.version.outputs.sdk_version }}" | ||
| echo '```' | ||
| echo | ||
| bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'sdk/v*' sdk | ||
| } > release-notes.md | ||
|
|
||
| - name: Create GitHub Release for SDK | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | ||
| with: | ||
| draft: true | ||
| prerelease: ${{ contains(github.ref_name, '-') }} | ||
| generate_release_notes: true | ||
| body: | | ||
| ## SDK Release ${{ github.ref_name }} | ||
|
|
||
| This is a release of the Chaperone SDK module. | ||
|
|
||
| ### Installation | ||
| ```bash | ||
| go get github.com/cloudblue/chaperone/sdk@${{ steps.version.outputs.sdk_version }} | ||
| ``` | ||
| generate_release_notes: false | ||
| body_path: release-notes.md | ||
|
|
||
| # Notify on Contrib releases (no binaries to build) | ||
| contrib-release: | ||
|
|
@@ -190,6 +208,8 @@ jobs: | |
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | ||
|
|
@@ -212,21 +232,30 @@ jobs: | |
| id: version | ||
| run: echo "contrib_version=${GITHUB_REF_NAME#plugins/contrib/}" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Generate release notes | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| { | ||
| echo "## Contrib Release ${{ github.ref_name }}" | ||
| echo | ||
| echo "This is a release of the Chaperone Contrib Plugins module." | ||
| echo | ||
| echo "### Installation" | ||
| echo '```bash' | ||
| echo "go get github.com/cloudblue/chaperone/plugins/contrib@${{ steps.version.outputs.contrib_version }}" | ||
| echo '```' | ||
| echo | ||
| bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'plugins/contrib/v*' contrib | ||
| } > release-notes.md | ||
|
|
||
| - name: Create GitHub Release for Contrib | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | ||
| with: | ||
| draft: true | ||
| prerelease: ${{ contains(github.ref_name, '-') }} | ||
| generate_release_notes: true | ||
| body: | | ||
| ## Contrib Release ${{ github.ref_name }} | ||
|
|
||
| This is a release of the Chaperone Contrib Plugins module. | ||
|
|
||
| ### Installation | ||
| ```bash | ||
| go get github.com/cloudblue/chaperone/plugins/contrib@${{ steps.version.outputs.contrib_version }} | ||
| ``` | ||
| generate_release_notes: false | ||
| body_path: release-notes.md | ||
|
|
||
| # Build admin portal binaries (UI embedded into chaperone-admin) | ||
| admin-release: | ||
|
|
@@ -317,17 +346,26 @@ jobs: | |
| cd dist | ||
| sha256sum * > checksums.txt | ||
|
|
||
| - name: Generate release notes | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| { | ||
| echo "## Admin Portal Release ${{ steps.version.outputs.admin_version }}" | ||
| echo | ||
| echo "Ships the \`chaperone-admin\` binary with the Vue UI embedded." | ||
| echo "Use \`chaperone-admin create-user\` to seed the initial admin user." | ||
| echo | ||
| bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'admin/v*' admin | ||
| } > release-notes.md | ||
|
|
||
| - name: Create GitHub Release for Admin | ||
| uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 | ||
| with: | ||
| draft: true | ||
| prerelease: ${{ contains(github.ref_name, '-') }} | ||
| generate_release_notes: true | ||
| generate_release_notes: false | ||
| body_path: release-notes.md | ||
| files: | | ||
| dist/chaperone-admin-* | ||
| dist/checksums.txt | ||
| body: | | ||
| ## Admin Portal Release ${{ steps.version.outputs.admin_version }} | ||
|
|
||
| Ships the `chaperone-admin` binary with the Vue UI embedded. | ||
| Use `chaperone-admin create-user` to seed the initial admin user. | ||
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.
[Info] A transient PRs-API failure silently drops a PR from the notes
In the keepers loop,
if gh api ".../pulls/$pr/files" ... | module_matchruns underset -o pipefailinside anif. If the API call fails transiently (rate limit, 5xx), the condition evaluates false and the PR is silently excluded from the release notes instead of surfacing an error. Low impact since releases are created as drafts and reviewed by hand, but worth knowing.Optional hardening: capture the file list into a variable first and fail loudly (or retry) on a non-zero
gh apiexit, so an API hiccup can't quietly shrink the changelog.