release.yml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. name: Release
  2. # Manually triggered ("Run workflow"). On trigger it:
  3. # 1. reads the version from package.json,
  4. # 2. promotes `## [Unreleased]` content into `## [<version>]` in
  5. # CHANGELOG.md (and commits + pushes that change back to main), so
  6. # the published release notes are never sparse just because the
  7. # maintainer didn't pre-stage the [<version>] block by hand,
  8. # 3. builds a self-contained bundle for every platform (one runner — there's no
  9. # native compilation, so cross-packaging is fine),
  10. # 4. creates the GitHub Release (tag v<version>) with all archives, using the
  11. # release notes from CHANGELOG.md,
  12. # 5. publishes the npm thin-installer (shim + per-platform packages).
  13. #
  14. # Before triggering: bump package.json. CHANGELOG.md entries can live under
  15. # `## [Unreleased]` — step 2 takes care of moving them.
  16. #
  17. # npm auth is OIDC trusted publishing (no NPM_TOKEN): every published package
  18. # (@colbymchenry/codegraph + the per-platform bundles) has this repo +
  19. # release.yml configured as its trusted publisher on npmjs.com. Adding a new
  20. # platform package means configuring its trusted publisher there before the
  21. # first release that includes it.
  22. on:
  23. workflow_dispatch: {}
  24. permissions:
  25. contents: write # create the GitHub Release + tag, push the CHANGELOG promote
  26. id-token: write # OIDC token for npm --provenance and Sigstore signing
  27. attestations: write # store the GitHub artifact attestations for the bundles
  28. jobs:
  29. # Native extraction-kernel prebuilds (docs/design/rust-kernel-migration-plan.md).
  30. # The kernel is an OPTIONAL per-language speedup: a bundle without a .node
  31. # runs the wasm extraction path unchanged. continue-on-error keeps a Rust
  32. # toolchain flake from ever blocking a release — the release job runs with
  33. # whatever prebuilds succeeded. (Runner images ship rustup; build-kernel.sh
  34. # adds each cross target itself.)
  35. kernel:
  36. continue-on-error: true
  37. strategy:
  38. fail-fast: false
  39. matrix:
  40. include:
  41. - runner: macos-14
  42. targets: aarch64-apple-darwin x86_64-apple-darwin
  43. - runner: ubuntu-22.04 # oldest glibc runner → widest compatibility
  44. targets: x86_64-unknown-linux-gnu
  45. - runner: ubuntu-22.04-arm
  46. targets: aarch64-unknown-linux-gnu
  47. - runner: windows-latest
  48. targets: x86_64-pc-windows-msvc aarch64-pc-windows-msvc
  49. runs-on: ${{ matrix.runner }}
  50. steps:
  51. - uses: actions/checkout@v6
  52. - name: Build kernel prebuilds
  53. shell: bash
  54. run: |
  55. for t in ${{ matrix.targets }}; do
  56. bash scripts/build-kernel.sh --target "$t"
  57. done
  58. ls -R codegraph-kernel/prebuilds
  59. - uses: actions/upload-artifact@v4
  60. with:
  61. name: kernel-${{ matrix.runner }}
  62. path: codegraph-kernel/prebuilds/
  63. if-no-files-found: error
  64. release:
  65. runs-on: ubuntu-latest
  66. needs: kernel
  67. steps:
  68. - uses: actions/checkout@v6
  69. with:
  70. # Default checkout is detached at a SHA; we need an actual branch
  71. # so the CHANGELOG-promote commit knows where to push.
  72. ref: ${{ github.ref }}
  73. # Authenticate as the maintainer (admin), not as github-actions[bot].
  74. # The "Require PR approval for main branch" ruleset only lets the
  75. # Admin repo role bypass — and GitHub blocks adding the GitHub
  76. # Actions integration to bypass_actors on user-owned (non-org)
  77. # repos with "Actor GitHub Actions integration must be part of
  78. # the ruleset source or owner organization." So the auto-promote
  79. # and auto-sync `git push origin HEAD:main` steps below both fail
  80. # under the default GITHUB_TOKEN. Using a fine-grained PAT owned
  81. # by the admin makes the push go through cleanly. Set the
  82. # RELEASE_PAT secret with: contents:write on this repo, no other
  83. # scopes. Rotate per your token policy; the workflow only runs
  84. # on manual dispatch so the blast radius is small.
  85. token: ${{ secrets.RELEASE_PAT }}
  86. - uses: actions/setup-node@v6
  87. with:
  88. node-version: 22
  89. # No registry-url here: it writes an .npmrc that requires a
  90. # NODE_AUTH_TOKEN env var to exist, and we publish via OIDC
  91. # trusted publishing instead of a token.
  92. - name: Upgrade npm for OIDC trusted publishing
  93. # Trusted publishing needs npm >= 11.5; Node 22 bundles npm 10.
  94. run: npm install -g npm@11 && npm --version
  95. - name: Sync package-lock.json if version drifted
  96. # When the maintainer bumps the version on package.json only — for
  97. # example via a GitHub web-UI edit — `npm ci` would refuse to run
  98. # with `EUSAGE: npm ci can only install packages when your
  99. # package.json and package-lock.json … are in sync`. This step
  100. # rewrites just the lock-file's version fields (top-level + the
  101. # `packages.""` entry) to match package.json, then auto-commits
  102. # and pushes the result so on-disk truth on `main` stays
  103. # consistent. Idempotent: if the lock file already matches, no
  104. # commit is made.
  105. run: |
  106. set -euo pipefail
  107. PKG_V=$(node -p "require('./package.json').version")
  108. LOCK_V=$(node -p "require('./package-lock.json').version")
  109. if [ "$PKG_V" = "$LOCK_V" ]; then
  110. echo "package-lock.json already at $PKG_V — nothing to sync."
  111. exit 0
  112. fi
  113. echo "Lock-file version drift: lock=$LOCK_V, package=$PKG_V. Syncing."
  114. # `--package-lock-only` rewrites only the lock file, doesn't
  115. # touch node_modules or actually install anything. Cheap.
  116. npm install --package-lock-only --ignore-scripts
  117. # Sanity: lockfile should now report the package version.
  118. NEW_LOCK_V=$(node -p "require('./package-lock.json').version")
  119. if [ "$NEW_LOCK_V" != "$PKG_V" ]; then
  120. echo "::error::lock-file still at $NEW_LOCK_V after sync attempt; expected $PKG_V"; exit 1
  121. fi
  122. if git diff --quiet -- package-lock.json; then
  123. echo "lock file unchanged after sync? bailing"; exit 1
  124. fi
  125. git config user.name "github-actions[bot]"
  126. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  127. git add package-lock.json
  128. git commit -m "release: sync package-lock.json to ${PKG_V}" -m "[skip ci] Auto-generated by Release workflow."
  129. git push origin "HEAD:${GITHUB_REF#refs/heads/}"
  130. - run: npm ci
  131. - name: Ensure zip/unzip
  132. run: sudo apt-get update -qq && sudo apt-get install -y -qq zip unzip
  133. - name: Resolve version
  134. id: ver
  135. run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
  136. - name: Promote [Unreleased] → [<version>] in CHANGELOG.md
  137. # Idempotent: a no-op if [Unreleased] is empty OR if the previous
  138. # run already moved everything. Auto-commit + push the change back
  139. # so the version block on main is the source of truth going
  140. # forward (and so subsequent extract-release-notes.mjs calls
  141. # surface the full content even if this run is re-triggered).
  142. run: |
  143. set -euo pipefail
  144. V="${{ steps.ver.outputs.version }}"
  145. before=$(git rev-parse HEAD)
  146. node scripts/prepare-release.mjs "$V"
  147. if git diff --quiet -- CHANGELOG.md; then
  148. echo "CHANGELOG.md unchanged — nothing to commit."
  149. else
  150. git config user.name "github-actions[bot]"
  151. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  152. git add CHANGELOG.md
  153. git commit -m "docs(changelog): promote [Unreleased] into [${V}]" -m "[skip ci] Auto-generated by Release workflow."
  154. # Push to the branch the workflow was triggered on (main).
  155. git push origin "HEAD:${GITHUB_REF#refs/heads/}"
  156. fi
  157. - name: Download kernel prebuilds
  158. # Best-effort: whatever platform legs succeeded land in release/kernel/
  159. # (<target>/codegraph-kernel.node); build-bundle.sh includes a target's
  160. # kernel when present and falls back to the wasm path when not.
  161. continue-on-error: true
  162. uses: actions/download-artifact@v4
  163. with:
  164. pattern: kernel-*
  165. merge-multiple: true
  166. path: release/kernel/
  167. - name: Kernel contract + grammar-parity gate
  168. # Asserts the native grammars and the vendored wasm grammars are built
  169. # from the same grammar revisions (node-kind/field tables compared id
  170. # by id) and that the .node speaks the expected wire contract.
  171. # CODEGRAPH_KERNEL_EXPECT=1 turns a missing binary into a FAILURE here
  172. # so the gate can't silently pass by not building the kernel.
  173. run: |
  174. if [ -f release/kernel/linux-x64/codegraph-kernel.node ]; then
  175. mkdir -p codegraph-kernel/prebuilds/linux-x64
  176. cp release/kernel/linux-x64/codegraph-kernel.node codegraph-kernel/prebuilds/linux-x64/
  177. CODEGRAPH_KERNEL_EXPECT=1 npx vitest run __tests__/kernel-scaffold.test.ts __tests__/kernel-grammar-parity.test.ts
  178. else
  179. echo "::warning::no linux-x64 kernel prebuild — skipping kernel gate (bundles ship wasm-only)"
  180. fi
  181. - name: Build all platform bundles
  182. run: |
  183. for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64 win32-arm64; do
  184. bash scripts/build-bundle.sh "$t"
  185. done
  186. ls -lh release
  187. - name: Generate SHA256SUMS
  188. # Published as a release asset; the npm launcher verifies downloaded
  189. # bundles against it (basenames only, so its path.basename match works).
  190. run: |
  191. ( cd release && sha256sum codegraph-* > SHA256SUMS )
  192. cat release/SHA256SUMS
  193. - name: Attest build provenance for release bundles
  194. # Signed, publicly-verifiable proof that each bundle (and SHA256SUMS)
  195. # was built by this workflow from this repo — SHA256SUMS alone only
  196. # proves integrity, not origin, since it ships next to the bundles.
  197. # Verify any downloaded artifact with:
  198. # gh attestation verify <file> -R colbymchenry/codegraph
  199. uses: actions/attest-build-provenance@v4
  200. with:
  201. subject-path: |
  202. release/codegraph-*
  203. release/SHA256SUMS
  204. - name: Release notes from CHANGELOG.md
  205. # The [<version>] block was guaranteed-populated by the
  206. # "Promote" step above, so the [Unreleased] fallback should
  207. # never be needed in practice. Kept for defense-in-depth.
  208. run: |
  209. V="${{ steps.ver.outputs.version }}"
  210. node scripts/extract-release-notes.mjs "$V" > notes.md 2>/dev/null \
  211. || node scripts/extract-release-notes.mjs Unreleased > notes.md 2>/dev/null || true
  212. if [ ! -s notes.md ]; then
  213. echo "::error::No release notes in CHANGELOG.md for [$V] or [Unreleased]."
  214. exit 1
  215. fi
  216. echo "----- release notes -----"; cat notes.md
  217. - name: Create GitHub Release
  218. env:
  219. GH_TOKEN: ${{ github.token }}
  220. run: |
  221. TAG="v${{ steps.ver.outputs.version }}"
  222. # Idempotent: create the release once, otherwise (re-run) refresh assets.
  223. if gh release view "$TAG" >/dev/null 2>&1; then
  224. gh release upload "$TAG" release/codegraph-* release/SHA256SUMS --clobber
  225. else
  226. gh release create "$TAG" release/codegraph-* release/SHA256SUMS --title "$TAG" --notes-file notes.md
  227. fi
  228. - name: Publish to npm
  229. # Auth is OIDC trusted publishing (id-token: write above) — npm mints
  230. # a short-lived credential from the workflow's identity; there is no
  231. # NPM_TOKEN. Provenance is generated automatically on this path; the
  232. # explicit --provenance keeps the intent visible and fails loudly if
  233. # OIDC is ever unavailable.
  234. run: |
  235. V="${{ steps.ver.outputs.version }}"
  236. bash scripts/pack-npm.sh "$V"
  237. # Platform packages first, then the main shim (which depends on them).
  238. # Skip any already on the registry so a re-run only fills in gaps.
  239. for dir in release/npm/codegraph-* release/npm/main; do
  240. name=$(node -p "require('./$dir/package.json').name")
  241. if npm view "$name@$V" version >/dev/null 2>&1; then
  242. echo "skip $name@$V (already published)"
  243. else
  244. echo "publishing $name@$V"
  245. # --provenance: publish with an npm provenance attestation
  246. # (needs the id-token: write permission above and the
  247. # repository field pack-npm.sh writes into each package.json).
  248. ( cd "$dir" && npm publish --access public --provenance )
  249. fi
  250. done
  251. - name: Verify every package is actually on the registry
  252. run: |
  253. V="${{ steps.ver.outputs.version }}"
  254. # npm publish can print success without persisting; confirm against the
  255. # registry (with retries for propagation) so green means really shipped.
  256. for dir in release/npm/codegraph-* release/npm/main; do
  257. name=$(node -p "require('./$dir/package.json').name")
  258. ok=
  259. for i in 1 2 3 4 5 6; do
  260. if npm view "$name@$V" version >/dev/null 2>&1; then ok=1; break; fi
  261. echo "waiting for $name@$V to appear ($i)…"; sleep 10
  262. done
  263. [ -n "$ok" ] || { echo "::error::$name@$V never appeared on the registry"; exit 1; }
  264. echo "verified $name@$V"
  265. done
  266. - name: Sync packages to npmmirror
  267. # npmmirror/cnpm mirror lazily and frequently never pull the per-platform
  268. # optionalDependencies on their own, so `npm i` there fails with
  269. # "no prebuilt bundle" (issue #303). Nudge a sync now so mirror users get
  270. # the bundle without waiting. Best-effort — the launcher also self-heals
  271. # from GitHub Releases — so a mirror hiccup never fails the release.
  272. continue-on-error: true
  273. run: |
  274. for dir in release/npm/codegraph-* release/npm/main; do
  275. name=$(node -p "require('./$dir/package.json').name")
  276. enc=$(node -p "encodeURIComponent(require('./$dir/package.json').name)")
  277. echo "sync $name"
  278. curl -s -X PUT "https://registry.npmmirror.com/-/package/$enc/syncs" || true
  279. echo
  280. done