1
0

release.yml 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. release:
  30. runs-on: ubuntu-latest
  31. steps:
  32. - uses: actions/checkout@v6
  33. with:
  34. # Default checkout is detached at a SHA; we need an actual branch
  35. # so the CHANGELOG-promote commit knows where to push.
  36. ref: ${{ github.ref }}
  37. # Authenticate as the maintainer (admin), not as github-actions[bot].
  38. # The "Require PR approval for main branch" ruleset only lets the
  39. # Admin repo role bypass — and GitHub blocks adding the GitHub
  40. # Actions integration to bypass_actors on user-owned (non-org)
  41. # repos with "Actor GitHub Actions integration must be part of
  42. # the ruleset source or owner organization." So the auto-promote
  43. # and auto-sync `git push origin HEAD:main` steps below both fail
  44. # under the default GITHUB_TOKEN. Using a fine-grained PAT owned
  45. # by the admin makes the push go through cleanly. Set the
  46. # RELEASE_PAT secret with: contents:write on this repo, no other
  47. # scopes. Rotate per your token policy; the workflow only runs
  48. # on manual dispatch so the blast radius is small.
  49. token: ${{ secrets.RELEASE_PAT }}
  50. - uses: actions/setup-node@v6
  51. with:
  52. node-version: 22
  53. # No registry-url here: it writes an .npmrc that requires a
  54. # NODE_AUTH_TOKEN env var to exist, and we publish via OIDC
  55. # trusted publishing instead of a token.
  56. - name: Upgrade npm for OIDC trusted publishing
  57. # Trusted publishing needs npm >= 11.5; Node 22 bundles npm 10.
  58. run: npm install -g npm@11 && npm --version
  59. - name: Sync package-lock.json if version drifted
  60. # When the maintainer bumps the version on package.json only — for
  61. # example via a GitHub web-UI edit — `npm ci` would refuse to run
  62. # with `EUSAGE: npm ci can only install packages when your
  63. # package.json and package-lock.json … are in sync`. This step
  64. # rewrites just the lock-file's version fields (top-level + the
  65. # `packages.""` entry) to match package.json, then auto-commits
  66. # and pushes the result so on-disk truth on `main` stays
  67. # consistent. Idempotent: if the lock file already matches, no
  68. # commit is made.
  69. run: |
  70. set -euo pipefail
  71. PKG_V=$(node -p "require('./package.json').version")
  72. LOCK_V=$(node -p "require('./package-lock.json').version")
  73. if [ "$PKG_V" = "$LOCK_V" ]; then
  74. echo "package-lock.json already at $PKG_V — nothing to sync."
  75. exit 0
  76. fi
  77. echo "Lock-file version drift: lock=$LOCK_V, package=$PKG_V. Syncing."
  78. # `--package-lock-only` rewrites only the lock file, doesn't
  79. # touch node_modules or actually install anything. Cheap.
  80. npm install --package-lock-only --ignore-scripts
  81. # Sanity: lockfile should now report the package version.
  82. NEW_LOCK_V=$(node -p "require('./package-lock.json').version")
  83. if [ "$NEW_LOCK_V" != "$PKG_V" ]; then
  84. echo "::error::lock-file still at $NEW_LOCK_V after sync attempt; expected $PKG_V"; exit 1
  85. fi
  86. if git diff --quiet -- package-lock.json; then
  87. echo "lock file unchanged after sync? bailing"; exit 1
  88. fi
  89. git config user.name "github-actions[bot]"
  90. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  91. git add package-lock.json
  92. git commit -m "release: sync package-lock.json to ${PKG_V}" -m "[skip ci] Auto-generated by Release workflow."
  93. git push origin "HEAD:${GITHUB_REF#refs/heads/}"
  94. - run: npm ci
  95. - name: Ensure zip/unzip
  96. run: sudo apt-get update -qq && sudo apt-get install -y -qq zip unzip
  97. - name: Resolve version
  98. id: ver
  99. run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
  100. - name: Promote [Unreleased] → [<version>] in CHANGELOG.md
  101. # Idempotent: a no-op if [Unreleased] is empty OR if the previous
  102. # run already moved everything. Auto-commit + push the change back
  103. # so the version block on main is the source of truth going
  104. # forward (and so subsequent extract-release-notes.mjs calls
  105. # surface the full content even if this run is re-triggered).
  106. run: |
  107. set -euo pipefail
  108. V="${{ steps.ver.outputs.version }}"
  109. before=$(git rev-parse HEAD)
  110. node scripts/prepare-release.mjs "$V"
  111. if git diff --quiet -- CHANGELOG.md; then
  112. echo "CHANGELOG.md unchanged — nothing to commit."
  113. else
  114. git config user.name "github-actions[bot]"
  115. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  116. git add CHANGELOG.md
  117. git commit -m "docs(changelog): promote [Unreleased] into [${V}]" -m "[skip ci] Auto-generated by Release workflow."
  118. # Push to the branch the workflow was triggered on (main).
  119. git push origin "HEAD:${GITHUB_REF#refs/heads/}"
  120. fi
  121. - name: Build all platform bundles
  122. run: |
  123. for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64 win32-arm64; do
  124. bash scripts/build-bundle.sh "$t"
  125. done
  126. ls -lh release
  127. - name: Generate SHA256SUMS
  128. # Published as a release asset; the npm launcher verifies downloaded
  129. # bundles against it (basenames only, so its path.basename match works).
  130. run: |
  131. ( cd release && sha256sum codegraph-* > SHA256SUMS )
  132. cat release/SHA256SUMS
  133. - name: Attest build provenance for release bundles
  134. # Signed, publicly-verifiable proof that each bundle (and SHA256SUMS)
  135. # was built by this workflow from this repo — SHA256SUMS alone only
  136. # proves integrity, not origin, since it ships next to the bundles.
  137. # Verify any downloaded artifact with:
  138. # gh attestation verify <file> -R colbymchenry/codegraph
  139. uses: actions/attest-build-provenance@v4
  140. with:
  141. subject-path: |
  142. release/codegraph-*
  143. release/SHA256SUMS
  144. - name: Release notes from CHANGELOG.md
  145. # The [<version>] block was guaranteed-populated by the
  146. # "Promote" step above, so the [Unreleased] fallback should
  147. # never be needed in practice. Kept for defense-in-depth.
  148. run: |
  149. V="${{ steps.ver.outputs.version }}"
  150. node scripts/extract-release-notes.mjs "$V" > notes.md 2>/dev/null \
  151. || node scripts/extract-release-notes.mjs Unreleased > notes.md 2>/dev/null || true
  152. if [ ! -s notes.md ]; then
  153. echo "::error::No release notes in CHANGELOG.md for [$V] or [Unreleased]."
  154. exit 1
  155. fi
  156. echo "----- release notes -----"; cat notes.md
  157. - name: Create GitHub Release
  158. env:
  159. GH_TOKEN: ${{ github.token }}
  160. run: |
  161. TAG="v${{ steps.ver.outputs.version }}"
  162. # Idempotent: create the release once, otherwise (re-run) refresh assets.
  163. if gh release view "$TAG" >/dev/null 2>&1; then
  164. gh release upload "$TAG" release/codegraph-* release/SHA256SUMS --clobber
  165. else
  166. gh release create "$TAG" release/codegraph-* release/SHA256SUMS --title "$TAG" --notes-file notes.md
  167. fi
  168. - name: Publish to npm
  169. # Auth is OIDC trusted publishing (id-token: write above) — npm mints
  170. # a short-lived credential from the workflow's identity; there is no
  171. # NPM_TOKEN. Provenance is generated automatically on this path; the
  172. # explicit --provenance keeps the intent visible and fails loudly if
  173. # OIDC is ever unavailable.
  174. run: |
  175. V="${{ steps.ver.outputs.version }}"
  176. bash scripts/pack-npm.sh "$V"
  177. # Platform packages first, then the main shim (which depends on them).
  178. # Skip any already on the registry so a re-run only fills in gaps.
  179. for dir in release/npm/codegraph-* release/npm/main; do
  180. name=$(node -p "require('./$dir/package.json').name")
  181. if npm view "$name@$V" version >/dev/null 2>&1; then
  182. echo "skip $name@$V (already published)"
  183. else
  184. echo "publishing $name@$V"
  185. # --provenance: publish with an npm provenance attestation
  186. # (needs the id-token: write permission above and the
  187. # repository field pack-npm.sh writes into each package.json).
  188. ( cd "$dir" && npm publish --access public --provenance )
  189. fi
  190. done
  191. - name: Verify every package is actually on the registry
  192. run: |
  193. V="${{ steps.ver.outputs.version }}"
  194. # npm publish can print success without persisting; confirm against the
  195. # registry (with retries for propagation) so green means really shipped.
  196. for dir in release/npm/codegraph-* release/npm/main; do
  197. name=$(node -p "require('./$dir/package.json').name")
  198. ok=
  199. for i in 1 2 3 4 5 6; do
  200. if npm view "$name@$V" version >/dev/null 2>&1; then ok=1; break; fi
  201. echo "waiting for $name@$V to appear ($i)…"; sleep 10
  202. done
  203. [ -n "$ok" ] || { echo "::error::$name@$V never appeared on the registry"; exit 1; }
  204. echo "verified $name@$V"
  205. done
  206. - name: Sync packages to npmmirror
  207. # npmmirror/cnpm mirror lazily and frequently never pull the per-platform
  208. # optionalDependencies on their own, so `npm i` there fails with
  209. # "no prebuilt bundle" (issue #303). Nudge a sync now so mirror users get
  210. # the bundle without waiting. Best-effort — the launcher also self-heals
  211. # from GitHub Releases — so a mirror hiccup never fails the release.
  212. continue-on-error: true
  213. run: |
  214. for dir in release/npm/codegraph-* release/npm/main; do
  215. name=$(node -p "require('./$dir/package.json').name")
  216. enc=$(node -p "encodeURIComponent(require('./$dir/package.json').name)")
  217. echo "sync $name"
  218. curl -s -X PUT "https://registry.npmmirror.com/-/package/$enc/syncs" || true
  219. echo
  220. done