ci(release): scope release notes to each module#131
Conversation
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) <noreply@anthropic.com>
| - name: Generate release notes | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'v*' core > release-notes.md |
There was a problem hiding this comment.
[Info] Tag name is interpolated directly into a run: shell block
These new steps expand ${{ github.ref_name }} (and ${{ steps.version.outputs.*_version }}) inside run: scripts, where the value is substituted before bash parses the line. Git tag names allow shell metacharacters (;, $(), backticks), so a crafted tag could inject commands into the release job. Blast radius is limited to whoever can push tags, and the same pattern already exists in the pre-existing VERSION="${{ github.ref_name }}" build step — so this is defense-in-depth, not a live hole. Same applies to the sdk/contrib/admin "Generate release notes" steps.
Suggested fix: pass the values via env: and reference them quoted, e.g. env: with TAG: ${{ github.ref_name }}, then run: bash .github/scripts/release-notes.sh "$TAG" 'v*' core. Ideally apply the same to the pre-existing VERSION= step for consistency.
| # 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 |
There was a problem hiding this comment.
[Info] A transient PRs-API failure silently drops a PR from the notes
In the keepers loop, if gh api ".../pulls/$pr/files" ... | module_match runs under set -o pipefail inside an if. 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 api exit, so an API hiccup can't quietly shrink the changelog.
What
The release workflow used
generate_release_notes: true, which lists every PR in the tag's commit range with no path filtering. In this monorepo that meant the SDK, Contrib, and Admin draft releases all repeated the same full cross-module changelog (e.g. theadmin/v0.1.1draft listed core cert-renewal and forward-routing PRs).This replaces the auto-notes with
.github/scripts/release-notes.sh, which:The pretty
* title by @author in #PRformat and theNew Contributorssection are preserved; the latter only survives if its PR did. Output is fed toaction-gh-releaseviabody_path.Module boundaries
core(root)sdk/,plugins/contrib/,admin/,.github/,docs/,.ai/— soplugins/reference/(no owngo.mod) correctly counts as coresdksdk/contribplugins/contrib/but notplugins/contrib/microsoft/keyvault/(separate module)adminadmin/PRs touching only
.github/(CI action bumps) or only the keyvault submodule drop out of all release notes.Other changes
permissions: pull-requests: readadded — the script reads changed files via the PRs API, and an explicitpermissions:block sets unlisted scopes tonone.fetch-depth: 0added to the SDK and Contrib checkouts so the previous module tag is resolvable.Testing
Ran the script locally against all four real
v0.3.0-era tags; output matches the manually-filtered drafts. Note this only affects the next release after merge — the current drafts were already filtered by hand.🤖 Generated with Claude Code