Release #31
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). | |
| # | |
| # Before triggering: bump package.json. CHANGELOG.md entries can live under | |
| # `## [Unreleased]` — step 2 takes care of moving them. | |
| # | |
| # npm auth is OIDC trusted publishing (no NPM_TOKEN): every published package | |
| # (@colbymchenry/codegraph + the per-platform bundles) has this repo + | |
| # release.yml configured as its trusted publisher on npmjs.com. Adding a new | |
| # platform package means configuring its trusted publisher there before the | |
| # first release that includes it. | |
| on: | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: write # create the GitHub Release + tag, push the CHANGELOG promote | |
| id-token: write # OIDC token for npm --provenance and Sigstore signing | |
| attestations: write # store the GitHub artifact attestations for the bundles | |
| jobs: | |
| # Native extraction-kernel prebuilds (docs/design/rust-kernel-migration-plan.md). | |
| # As of 1.5.0 the kernel is the release's HEADLINE, not an optional extra, so | |
| # this matrix is REQUIRED: a failed kernel build blocks the release instead of | |
| # silently shipping wasm-only bundles under a Rust-engine banner. Per-file | |
| # wasm fallback still exists at runtime for erroring files and unsupported | |
| # platforms — but every published bundle must carry its .node. Note the | |
| # vendored-grammar-C languages (kotlin/lua/scala/dart) compile parser.c via | |
| # the cc crate, so each leg needs its platform C toolchain (runner images | |
| # ship one). (Runner images ship rustup; build-kernel.sh | |
| # adds each cross target itself.) | |
| kernel: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: macos-14 | |
| targets: aarch64-apple-darwin x86_64-apple-darwin | |
| - runner: ubuntu-22.04 # oldest glibc runner → widest compatibility | |
| targets: x86_64-unknown-linux-gnu | |
| - runner: ubuntu-22.04-arm | |
| targets: aarch64-unknown-linux-gnu | |
| - runner: windows-latest | |
| targets: x86_64-pc-windows-msvc aarch64-pc-windows-msvc | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Build kernel prebuilds | |
| shell: bash | |
| run: | | |
| for t in ${{ matrix.targets }}; do | |
| bash scripts/build-kernel.sh --target "$t" | |
| done | |
| ls -R codegraph-kernel/prebuilds | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: kernel-${{ matrix.runner }} | |
| path: codegraph-kernel/prebuilds/ | |
| if-no-files-found: error | |
| release: | |
| runs-on: ubuntu-latest | |
| needs: kernel | |
| 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 }} | |
| # Authenticate as the maintainer (admin), not as github-actions[bot]. | |
| # The "Require PR approval for main branch" ruleset only lets the | |
| # Admin repo role bypass — and GitHub blocks adding the GitHub | |
| # Actions integration to bypass_actors on user-owned (non-org) | |
| # repos with "Actor GitHub Actions integration must be part of | |
| # the ruleset source or owner organization." So the auto-promote | |
| # and auto-sync `git push origin HEAD:main` steps below both fail | |
| # under the default GITHUB_TOKEN. Using a fine-grained PAT owned | |
| # by the admin makes the push go through cleanly. Set the | |
| # RELEASE_PAT secret with: contents:write on this repo, no other | |
| # scopes. Rotate per your token policy; the workflow only runs | |
| # on manual dispatch so the blast radius is small. | |
| token: ${{ secrets.RELEASE_PAT }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| # No registry-url here: it writes an .npmrc that requires a | |
| # NODE_AUTH_TOKEN env var to exist, and we publish via OIDC | |
| # trusted publishing instead of a token. | |
| - name: Upgrade npm for OIDC trusted publishing | |
| # Trusted publishing needs npm >= 11.5; Node 22 bundles npm 10. | |
| run: npm install -g npm@11 && npm --version | |
| - name: Sync package-lock.json if version drifted | |
| # When the maintainer bumps the version on package.json only — for | |
| # example via a GitHub web-UI edit — `npm ci` would refuse to run | |
| # with `EUSAGE: npm ci can only install packages when your | |
| # package.json and package-lock.json … are in sync`. This step | |
| # rewrites just the lock-file's version fields (top-level + the | |
| # `packages.""` entry) to match package.json, then auto-commits | |
| # and pushes the result so on-disk truth on `main` stays | |
| # consistent. Idempotent: if the lock file already matches, no | |
| # commit is made. | |
| run: | | |
| set -euo pipefail | |
| PKG_V=$(node -p "require('./package.json').version") | |
| LOCK_V=$(node -p "require('./package-lock.json').version") | |
| if [ "$PKG_V" = "$LOCK_V" ]; then | |
| echo "package-lock.json already at $PKG_V — nothing to sync." | |
| exit 0 | |
| fi | |
| echo "Lock-file version drift: lock=$LOCK_V, package=$PKG_V. Syncing." | |
| # `--package-lock-only` rewrites only the lock file, doesn't | |
| # touch node_modules or actually install anything. Cheap. | |
| npm install --package-lock-only --ignore-scripts | |
| # Sanity: lockfile should now report the package version. | |
| NEW_LOCK_V=$(node -p "require('./package-lock.json').version") | |
| if [ "$NEW_LOCK_V" != "$PKG_V" ]; then | |
| echo "::error::lock-file still at $NEW_LOCK_V after sync attempt; expected $PKG_V"; exit 1 | |
| fi | |
| if git diff --quiet -- package-lock.json; then | |
| echo "lock file unchanged after sync? bailing"; exit 1 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add package-lock.json | |
| git commit -m "release: sync package-lock.json to ${PKG_V}" -m "[skip ci] Auto-generated by Release workflow." | |
| git push origin "HEAD:${GITHUB_REF#refs/heads/}" | |
| - run: npm ci | |
| - 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: Download kernel prebuilds | |
| # All legs are required (see the kernel job), so every target's | |
| # <target>/codegraph-kernel.node must be present in release/kernel/. | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: kernel-* | |
| merge-multiple: true | |
| path: release/kernel/ | |
| - name: Kernel contract + full walker-parity gate | |
| # Runs EVERY kernel suite (__tests__/kernel-*.test.ts — the wire | |
| # contract, the grammar-source parity table checks, and all 20 | |
| # languages' walker byte-parity suites incl. torture/CRLF/defer pins) | |
| # against the freshly built linux-x64 .node. The glob keeps this gate | |
| # current as languages are added. CODEGRAPH_KERNEL_EXPECT=1 turns a | |
| # missing binary into a FAILURE, and a missing prebuild fails outright | |
| # — the matrix is required, so absence here means a wiring bug. | |
| run: | | |
| if [ -f release/kernel/linux-x64/codegraph-kernel.node ]; then | |
| mkdir -p codegraph-kernel/prebuilds/linux-x64 | |
| cp release/kernel/linux-x64/codegraph-kernel.node codegraph-kernel/prebuilds/linux-x64/ | |
| CODEGRAPH_KERNEL_EXPECT=1 npx vitest run __tests__/kernel-*.test.ts | |
| else | |
| echo "::error::linux-x64 kernel prebuild missing despite required matrix" | |
| exit 1 | |
| 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-* > SHA256SUMS ) | |
| cat release/SHA256SUMS | |
| - name: Attest build provenance for release bundles | |
| # Signed, publicly-verifiable proof that each bundle (and SHA256SUMS) | |
| # was built by this workflow from this repo — SHA256SUMS alone only | |
| # proves integrity, not origin, since it ships next to the bundles. | |
| # Verify any downloaded artifact with: | |
| # gh attestation verify <file> -R colbymchenry/codegraph | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: | | |
| release/codegraph-* | |
| 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 }}" | |
| # 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-* release/SHA256SUMS --clobber | |
| else | |
| gh release create "$TAG" release/codegraph-* release/SHA256SUMS --title "$TAG" --notes-file notes.md | |
| fi | |
| - name: Publish to npm | |
| # Auth is OIDC trusted publishing (id-token: write above) — npm mints | |
| # a short-lived credential from the workflow's identity; there is no | |
| # NPM_TOKEN. Provenance is generated automatically on this path; the | |
| # explicit --provenance keeps the intent visible and fails loudly if | |
| # OIDC is ever unavailable. | |
| 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. | |
| for dir in release/npm/codegraph-* release/npm/main; do | |
| 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" | |
| # --provenance: publish with an npm provenance attestation | |
| # (needs the id-token: write permission above and the | |
| # repository field pack-npm.sh writes into each package.json). | |
| ( cd "$dir" && npm publish --access public --provenance ) | |
| fi | |
| done | |
| - name: Verify every package is actually on the registry | |
| 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. | |
| for dir in release/npm/codegraph-* release/npm/main; do | |
| name=$(node -p "require('./$dir/package.json').name") | |
| ok= | |
| for i in 1 2 3 4 5 6; do | |
| if npm view "$name@$V" version >/dev/null 2>&1; then ok=1; break; fi | |
| echo "waiting for $name@$V to appear ($i)…"; sleep 10 | |
| done | |
| [ -n "$ok" ] || { echo "::error::$name@$V never appeared on the registry"; exit 1; } | |
| echo "verified $name@$V" | |
| done | |
| - name: Sync packages to npmmirror | |
| # 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: | | |
| for dir in release/npm/codegraph-* release/npm/main; do | |
| 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 |