Skip to content

Release

Release #48

Workflow file for this run

name: Release
# Manually triggered ("Run workflow"). On trigger it:
# 1. reads the version from package.json,
# 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),
# 4. creates the GitHub Release (tag v<version>) with all archives, using the
# release notes from CHANGELOG.md,
# 5. publishes the npm thin-installer (shim + per-platform packages) — OPTIONAL.
#
# Before triggering: bump package.json on the default branch, and dispatch this
# workflow from that branch — a guard step rejects anything else, because step 2
# pushes its commit to whichever branch triggered the run. CHANGELOG.md entries
# can live under `## [Unreleased]`; step 2 moves them. Never pre-create a
# `## [<version>]` block.
#
# GitHub Releases is the required channel: install.sh / install.ps1 and
# `codegraph-vba upgrade` only need the release assets. Step 5 runs only when the
# NPM_TOKEN secret is set, and an npm failure never fails the release (#326).
on:
workflow_dispatch: {}
permissions:
contents: write # create the GitHub Release + tag, push the CHANGELOG promote
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Refuse to release from a non-default branch
# The promote step below pushes the CHANGELOG back to the branch
# this workflow was dispatched on. Dispatched from a short-lived
# `release/*` branch, that commit lands there and disappears with
# the branch — which is exactly how v1.16.0 shipped while `main`
# kept a stale CHANGELOG and package.json (#296). Releases come
# from the default branch or they do not happen.
env:
REF_NAME: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
if [ "$REF_NAME" != "$DEFAULT_BRANCH" ]; then
echo "::error::Release was dispatched from '$REF_NAME'. Run it from '$DEFAULT_BRANCH' so the CHANGELOG promotion is pushed to a branch that survives."
exit 1
fi
echo "Releasing from '$REF_NAME' (default branch)."
- 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 }}
# NOTE: the upstream workflow uses a fine-grained PAT
# (`secrets.RELEASE_PAT`) to push the auto-promote commit, on the
# assumption that the repo has a "Require PR approval for main
# branch" ruleset blocking the default GITHUB_TOKEN. This fork has
# no branch protection at all (`gh api ... /protection` returns 404),
# so the PAT is unnecessary — the default GITHUB_TOKEN works fine.
# If you ever add a ruleset that gates `main`, restore the
# `token: ${{ secrets.RELEASE_PAT }}` line and create a PAT with
# `contents:write` scoped to this repo.
- uses: actions/setup-node@v6
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Install pnpm
run: npm install -g pnpm@10.16.1
- run: pnpm install --frozen-lockfile
- name: Ensure zip/unzip
run: sudo apt-get update -qq && sudo apt-get install -y -qq zip unzip
- name: Sync pnpm-lock.yaml if version drifted
# Removed: the upstream's sync step assumed pnpm 9's lockfile format
# which carries the package's own version as a top-level
# ` version: '<X.Y.Z>'` line under the `importers:` block. This fork's
# pnpm-lock.yaml is at lockfileVersion: '9.0' but the importers block
# doesn't include the package's own version there, so the grep
# `^ version: '${PKG_V}'$` always fails to match and the step bails
# before the actual pnpm install runs. The next step (`pnpm install
# --frozen-lockfile`) already enforces lockfile integrity on its own
# — if the lockfile is genuinely out of date, that step fails with
# a clear error pointing at the drift, and the maintainer can
# regenerate the lockfile locally with `pnpm install`. No need for
# the broken sync step.
run: echo "sync step skipped (pnpm 9 lockfile format doesn't carry package version)"
- 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
bash scripts/build-bundle.sh "$t"
done
ls -lh release
- name: Generate SHA256SUMS
# Published as a release asset; the npm launcher verifies downloaded
# bundles against it (basenames only, so its path.basename match works).
run: |
( cd release && sha256sum codegraph-vba-* > SHA256SUMS )
cat release/SHA256SUMS
- 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 \
|| node scripts/extract-release-notes.mjs Unreleased > notes.md 2>/dev/null || true
if [ ! -s notes.md ]; then
echo "::error::No release notes in CHANGELOG.md for [$V] or [Unreleased]."
exit 1
fi
echo "----- release notes -----"; cat notes.md
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="v${{ steps.ver.outputs.version }}"
# Title is the plain semver tag (v<version>), matching `gh release view` output.
TITLE="$TAG"
# Idempotent: create the release once, otherwise (re-run) refresh assets.
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" release/codegraph-vba-* release/SHA256SUMS --clobber
else
gh release create "$TAG" release/codegraph-vba-* release/SHA256SUMS --title "$TITLE" --notes-file notes.md
fi
- name: Check npm credentials
# Secrets can't be read in `if:`, so expose only whether one is set.
id: npm
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -n "$NPM_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::NPM_TOKEN is not set — skipping npm publish. The GitHub Release is complete."
fi
- name: Publish to npm
# Optional channel (#326): a dead or revoked token must not turn a
# release red after its GitHub Release already shipped.
id: publish
if: steps.npm.outputs.enabled == 'true'
continue-on-error: true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
V="${{ steps.ver.outputs.version }}"
bash scripts/pack-npm.sh "$V"
# Platform packages first, then the main shim (which depends on them).
# Skip any already on the registry so a re-run only fills in gaps.
# `find` is used instead of a bash glob because paths containing `@`
# (scoped npm packages) don't always expand correctly inside the
# GitHub Actions bash subshell — node receives the literal pattern
# with `*` and `Cannot find module` errors out. `find` is portable.
while IFS= read -r dir; do
[ -d "$dir" ] || continue
name=$(node -p "require('./$dir/package.json').name")
if npm view "$name@$V" version >/dev/null 2>&1; then
echo "skip $name@$V (already published)"
else
echo "publishing $name@$V"
( cd "$dir" && npm publish --access public )
fi
done < <(find release/npm -name 'package.json' -not -path '*/node_modules/*' -not -path '*/lib/*' -not -path '*/bin/*' -exec dirname {} \; | sort -u)
- name: Verify every package is actually on the registry
if: steps.publish.outcome == 'success'
continue-on-error: true
env:
# Per-package patience. Each per-platform package bundles a Node
# runtime and unpacks to ~200 MB over ~1,000 files, and npm's
# registry can take MINUTES to make one of that size resolvable —
# on the v1.17.0 run (#307) the last one landed 3m09s after the
# publish step had already finished, so a 200s budget failed a
# release that had in fact shipped correctly. Only whichever package
# was published last tends to lag, so the worst case is one wait, not
# six. Do not trim this back without re-reading that run.
VERIFY_ATTEMPTS: 60
VERIFY_INTERVAL_SECONDS: 10
run: |
V="${{ steps.ver.outputs.version }}"
# npm publish can print success without persisting; confirm against the
# registry (with retries for propagation) so green means really shipped.
while IFS= read -r dir; do
[ -d "$dir" ] || continue
name=$(node -p "require('./$dir/package.json').name")
ok=
for i in $(seq 1 "$VERIFY_ATTEMPTS"); do
if npm view "$name@$V" version >/dev/null 2>&1; then ok=1; break; fi
echo "waiting for $name@$V to appear ($i/$VERIFY_ATTEMPTS)…"
sleep "$VERIFY_INTERVAL_SECONDS"
done
# The loop sleeps AFTER its last failed check, so without this the
# final wait is thrown away and a package that appears during it is
# reported missing — which is exactly how #307 went red.
if [ -z "$ok" ] && npm view "$name@$V" version >/dev/null 2>&1; then ok=1; fi
[ -n "$ok" ] || { echo "::error::$name@$V never appeared on the registry"; exit 1; }
echo "verified $name@$V"
done < <(find release/npm -name 'package.json' -not -path '*/node_modules/*' -not -path '*/lib/*' -not -path '*/bin/*' -exec dirname {} \; | sort -u)
- name: Sync packages to npmmirror
if: steps.publish.outcome == 'success'
# npmmirror/cnpm mirror lazily and frequently never pull the per-platform
# optionalDependencies on their own, so `npm i` there fails with
# "no prebuilt bundle" (issue #303). Nudge a sync now so mirror users get
# the bundle without waiting. Best-effort — the launcher also self-heals
# from GitHub Releases — so a mirror hiccup never fails the release.
continue-on-error: true
run: |
while IFS= read -r dir; do
[ -d "$dir" ] || continue
name=$(node -p "require('./$dir/package.json').name")
enc=$(node -p "encodeURIComponent(require('./$dir/package.json').name)")
echo "sync $name"
curl -s -X PUT "https://registry.npmmirror.com/-/package/$enc/syncs" || true
echo
done < <(find release/npm -name 'package.json' -not -path '*/node_modules/*' -not -path '*/lib/*' -not -path '*/bin/*' -exec dirname {} \; | sort -u)