Repository navigation
Release #48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |