Skip to content

ci(release): scope release notes to each module#131

Open
arnaugiralt wants to merge 1 commit into
masterfrom
ci/per-module-release-notes
Open

ci(release): scope release notes to each module#131
arnaugiralt wants to merge 1 commit into
masterfrom
ci/per-module-release-notes

Conversation

@arnaugiralt

Copy link
Copy Markdown
Member

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. the admin/v0.1.1 draft listed core cert-renewal and forward-routing PRs).

This replaces the auto-notes with .github/scripts/release-notes.sh, which:

  1. finds the previous tag of the same module (via the tag glob),
  2. asks GitHub to generate notes for that range, then
  3. keeps only the PRs whose changed files touched the released module's directory.

The pretty * title by @author in #PR format and the New Contributors section are preserved; the latter only survives if its PR did. Output is fed to action-gh-release via body_path.

Module boundaries

Module Kept when a PR touches…
core (root) anything except sdk/, plugins/contrib/, admin/, .github/, docs/, .ai/ — so plugins/reference/ (no own go.mod) correctly counts as core
sdk sdk/
contrib plugins/contrib/ but not plugins/contrib/microsoft/keyvault/ (separate module)
admin admin/

PRs touching only .github/ (CI action bumps) or only the keyvault submodule drop out of all release notes.

Other changes

  • permissions: pull-requests: read added — the script reads changed files via the PRs API, and an explicit permissions: block sets unlisted scopes to none.
  • fetch-depth: 0 added 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

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>
@arnaugiralt arnaugiralt requested a review from qarlosh June 29, 2026 15:00
@arnaugiralt arnaugiralt marked this pull request as ready for review June 29, 2026 15:00
- name: Generate release notes
env:
GH_TOKEN: ${{ github.token }}
run: bash .github/scripts/release-notes.sh "${{ github.ref_name }}" 'v*' core > release-notes.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

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_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants