name: Release # Manually triggered ("Run workflow"). On trigger it: # 1. reads the version from package.json, # 2. promotes `## [Unreleased]` content into `## []` 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 [] 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) 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] → [] 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 # /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 -R colbymchenry/codegraph uses: actions/attest-build-provenance@v4 with: subject-path: | release/codegraph-* release/SHA256SUMS - name: Release notes from CHANGELOG.md # The [] 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