Generate release notes from the changelog section

release-github.yml now checks out the tag and composes the release body by extracting that version's section from CHANGELOG.md, failing hard when it is missing, and appends the full-changelog link plus a sigit.si/code footnote backlink. The sigit-code-release skill records that the release body is generated, never hand-edited on GitHub. Co-Authored-By: siGit Code <noreply@sigit.si>

Seto Elkahfi committed Sep 13, 2026 at 16:36 UTC e563c96fccbe99d7c435287c19c920235017d7c4
2 files changed +37
.agents/skills/sigit-code-release/SKILL.md
+1
@@ -35,6 +35,7 @@ Use this skill when preparing a release for this repository.
35 - The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
36 - `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
37 - Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
38 +- The GitHub release body is generated, not hand-written: `release-github.yml`'s release job checks out the tag, awk-extracts the `## <version>` section from `CHANGELOG.md` (a missing section is a hard failure), appends a full-changelog link and a `sigit.si/code` footnote, and publishes it via `body_path: release_notes.md`. The only file a release-prep change needs to touch is `CHANGELOG.md` — never edit release notes in the GitHub UI, because the next tag push would overwrite them.
39 - npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
40 - The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
41 - The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
.github/workflows/release-github.yml
+36
@@ -240,6 +240,41 @@ jobs:
240 echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
241 fi
242
243 + - name: Checkout tag
244 + # The release job otherwise only downloads artifacts; the changelog
245 + # has to be read from the tree at the tag being released.
246 + uses: actions/checkout@v6
247 + with:
248 + ref: ${{ steps.tag.outputs.tag }}
249 +
250 + - name: Compose release notes
251 + # The release page carries the section of CHANGELOG.md for this
252 + # version, not a bare "Full changelog" default. A missing section is
253 + # a hard failure: publishing a release whose notes don't match its
254 + # changelog is worse than stopping the job.
255 + shell: bash
256 + run: |
257 + version="${{ steps.tag.outputs.tag }}"
258 + version="${version#v}"
259 + section="$(awk -v ver="${version}" '
260 + $0 == "## " ver { found = 1; next }
261 + found && /^## / { exit }
262 + found { print }
263 + ' CHANGELOG.md)"
264 + if [ -z "${section//[$' \t\n']/}" ]; then
265 + echo "No ## ${version} section found in CHANGELOG.md" >&2
266 + exit 1
267 + fi
268 + {
269 + printf '%s\n' "${section}"
270 + printf '\n---\n\n'
271 + printf 'Full changelog: https://github.com/%s/blob/%s/CHANGELOG.md\n' \
272 + "${GITHUB_REPOSITORY}" "${{ steps.tag.outputs.tag }}"
273 + printf '\n---\n\n'
274 + printf 'About siGit Code: [sigit.si/code](https://sigit.si/code)\n'
275 + } > release_notes.md
276 + cat release_notes.md
277 +
278 - name: Download binary artifacts
279 uses: actions/download-artifact@v4
280 with:
@@ -258,6 +293,7 @@ jobs:
293 uses: softprops/action-gh-release@v2
294 with:
295 tag_name: ${{ steps.tag.outputs.tag }}
296 + body_path: release_notes.md
297 files: release/*
298
299 # These channels all publish somewhere outside this repo (a tap, a Scoop