| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- 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
|