From 2a571cd145a70c56bd203c29d29af5ee2c8052e0 Mon Sep 17 00:00:00 2001 From: Arnau Giralt Date: Mon, 29 Jun 2026 13:52:27 +0200 Subject: [PATCH] ci(release): scope release notes to each module GitHub's generate_release_notes lists every PR in the commit range and has no path filtering, so the SDK/Contrib/Admin drafts all repeated the full cross-module changelog. Replace it with .github/scripts/release-notes.sh, which keeps only PRs whose changed files touched the released module's directory (carving the keyvault submodule out of contrib), and feed the result via body_path. Add fetch-depth: 0 to the SDK and Contrib checkouts so the previous module tag is resolvable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/release-notes.sh | 77 ++++++++++++++++++++++++++ .github/workflows/release.yml | 92 ++++++++++++++++++++++---------- 2 files changed, 142 insertions(+), 27 deletions(-) create mode 100755 .github/scripts/release-notes.sh diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh new file mode 100755 index 0000000..1e9d46a --- /dev/null +++ b/.github/scripts/release-notes.sh @@ -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 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ba3f42..a6e0f1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 + - 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.