feat(release): auto-promote [Unreleased] into [<version>] on release workflow run (#436)
Fixes the silent-sparse-release-notes failure mode that surfaced on
v0.9.5: the Release workflow used to do a literal
`extract-release-notes.mjs <version>` lookup with an `[Unreleased]`
fallback. The fallback only triggered when the `[<version>]` block
DIDN'T exist at all — and in practice maintainers sometimes had a
sparse `[<version>]` block pre-populated (e.g. one early fix
documented before the rest of the work landed). The workflow then
extracted that sparse block, ignoring the much-larger `[Unreleased]`
section above it. Result: the published v0.9.5 release notes were
missing the shared MCP daemon, the per-file staleness banner, the
Objective-C indexing, AND the Mixed iOS/RN/Expo bridging.
The fix is a new `scripts/prepare-release.mjs` step that runs at the
start of the workflow:
Case A — `[<version>]` does not yet exist:
Rename `[Unreleased]` → `[<version>] - <today>`. Add a fresh
empty `[Unreleased]` above. The common path.
Case B — `[<version>]` exists AND `[Unreleased]` has content:
Merge `[Unreleased]`'s sub-sections (### Added / ### Fixed /
### Changed / ### Removed / ### Deprecated / ### Security) into
the corresponding sub-sections of `[<version>]`. Unmatched
sub-sections are appended. Then empty `[Unreleased]`.
Case C — `[Unreleased]` is empty:
No-op. Re-runs of the workflow are safe.
After the script runs, the workflow auto-commits + pushes the
CHANGELOG.md change back to main (with a `[skip ci]` tag in the
commit body) so future runs and human eyes both see the same
on-disk truth.
9 unit tests (`__tests__/prepare-release.test.ts`) cover all three
cases, idempotency, version-source precedence, and an
extract-release-notes.mjs integration check.
Workflow comment header rewritten to reflect the new flow.
Trigger reminder going forward: bump package.json. CHANGELOG entries
can live under `[Unreleased]` — the workflow takes care of moving
them.
937/939 existing tests pass (2 pre-existing skips); +9 new tests.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5a4fcd56b7
commit
b77af782c5
@@ -2,25 +2,33 @@ name: Release
|
||||
|
||||
# Manually triggered ("Run workflow"). On trigger it:
|
||||
# 1. reads the version from package.json,
|
||||
# 2. builds a self-contained bundle for every platform (one runner — there's no
|
||||
# 2. promotes `## [Unreleased]` content into `## [<version>]` in
|
||||
# CHANGELOG.md (and commits + pushes that change back to main), so
|
||||
# the published release notes are never sparse just because the
|
||||
# maintainer didn't pre-stage the [<version>] block by hand,
|
||||
# 3. builds a self-contained bundle for every platform (one runner — there's no
|
||||
# native compilation, so cross-packaging is fine),
|
||||
# 3. creates the GitHub Release (tag v<version>) with all archives, using the
|
||||
# 4. creates the GitHub Release (tag v<version>) with all archives, using the
|
||||
# release notes from CHANGELOG.md,
|
||||
# 4. publishes the npm thin-installer (shim + per-platform packages).
|
||||
# 5. publishes the npm thin-installer (shim + per-platform packages).
|
||||
#
|
||||
# Before triggering: bump package.json and make sure CHANGELOG.md has the matching
|
||||
# section (## [<version>], or ## [Unreleased]). Set the NPM_TOKEN repo secret.
|
||||
# Before triggering: bump package.json. CHANGELOG.md entries can live under
|
||||
# `## [Unreleased]` — step 2 takes care of moving them. Set the NPM_TOKEN secret.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: write # create the GitHub Release + tag
|
||||
contents: write # create the GitHub Release + tag, push the CHANGELOG promote
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# Default checkout is detached at a SHA; we need an actual branch
|
||||
# so the CHANGELOG-promote commit knows where to push.
|
||||
ref: ${{ github.ref }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -29,6 +37,32 @@ jobs:
|
||||
- name: Ensure zip/unzip
|
||||
run: sudo apt-get update -qq && sudo apt-get install -y -qq zip unzip
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Promote [Unreleased] → [<version>] in CHANGELOG.md
|
||||
# Idempotent: a no-op if [Unreleased] is empty OR if the previous
|
||||
# run already moved everything. Auto-commit + push the change back
|
||||
# so the version block on main is the source of truth going
|
||||
# forward (and so subsequent extract-release-notes.mjs calls
|
||||
# surface the full content even if this run is re-triggered).
|
||||
run: |
|
||||
set -euo pipefail
|
||||
V="${{ steps.ver.outputs.version }}"
|
||||
before=$(git rev-parse HEAD)
|
||||
node scripts/prepare-release.mjs "$V"
|
||||
if git diff --quiet -- CHANGELOG.md; then
|
||||
echo "CHANGELOG.md unchanged — nothing to commit."
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs(changelog): promote [Unreleased] into [${V}]" -m "[skip ci] Auto-generated by Release workflow."
|
||||
# Push to the branch the workflow was triggered on (main).
|
||||
git push origin "HEAD:${GITHUB_REF#refs/heads/}"
|
||||
fi
|
||||
|
||||
- name: Build all platform bundles
|
||||
run: |
|
||||
for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64 win32-arm64; do
|
||||
@@ -43,11 +77,10 @@ jobs:
|
||||
( cd release && sha256sum codegraph-* > SHA256SUMS )
|
||||
cat release/SHA256SUMS
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Release notes from CHANGELOG.md
|
||||
# The [<version>] block was guaranteed-populated by the
|
||||
# "Promote" step above, so the [Unreleased] fallback should
|
||||
# never be needed in practice. Kept for defense-in-depth.
|
||||
run: |
|
||||
V="${{ steps.ver.outputs.version }}"
|
||||
node scripts/extract-release-notes.mjs "$V" > notes.md 2>/dev/null \
|
||||
|
||||
Reference in New Issue
Block a user