Просмотр исходного кода

feat(dist): Windows bundles + single-trigger release workflow

- build-bundle.sh: add win32-x64 / win32-arm64 targets — download Node's Windows
  zip, bundle node.exe + a .cmd launcher, output a .zip. Verified structurally
  (PE32+ node.exe, CRLF .cmd, portable node_modules). Since there are no native
  addons, any target builds on any OS, so the whole matrix builds on one runner.
- pack-npm.sh: handle .zip bundles and win32 packages (os: win32, node.exe).
- release.yml: simplified to your spec — manual trigger reads the version from
  package.json, builds all platform bundles, creates the GitHub Release with notes
  pulled from CHANGELOG.md, and publishes the npm shim + platform packages.
- BUNDLING.md: Windows + build-anywhere notes; release pipeline documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 1 месяц назад
Родитель
Сommit
31d72c2e4e
4 измененных файлов с 136 добавлено и 115 удалено
  1. 33 66
      .github/workflows/release.yml
  2. 26 16
      BUNDLING.md
  3. 53 25
      scripts/build-bundle.sh
  4. 24 8
      scripts/pack-npm.sh

+ 33 - 66
.github/workflows/release.yml

@@ -1,55 +1,23 @@
 name: Release
 name: Release
 
 
-# Manually triggered ("Run workflow"). Builds a self-contained bundle for each
-# platform on its own runner (no cross-compile), publishes a GitHub Release with
-# all archives, and publishes the npm thin-installer (shim + per-platform
-# packages). Bump package.json + finalize the CHANGELOG before triggering.
+# Manually triggered ("Run workflow"). On trigger it:
+#   1. reads the version from package.json,
+#   2. builds a self-contained bundle for every platform (one runner — there's no
+#      native compilation, so cross-packaging is fine),
+#   3. creates the GitHub Release (tag v<version>) with all archives, using the
+#      release notes from CHANGELOG.md,
+#   4. publishes the npm thin-installer (shim + per-platform packages).
+#
+# Before triggering: bump package.json and make sure CHANGELOG.md has the matching
+# section (## [<version>], or ## [Unreleased]). Set the NPM_TOKEN repo secret.
 on:
 on:
-  workflow_dispatch:
-    inputs:
-      version:
-        description: "Version to release (default: package.json version)"
-        required: false
-        type: string
-      publish_npm:
-        description: "Publish to npm"
-        required: false
-        type: boolean
-        default: true
+  workflow_dispatch: {}
 
 
 permissions:
 permissions:
   contents: write   # create the GitHub Release + tag
   contents: write   # create the GitHub Release + tag
 
 
 jobs:
 jobs:
-  build:
-    name: Build ${{ matrix.target }}
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - { target: darwin-arm64, os: macos-14 }
-          - { target: darwin-x64,   os: macos-13 }
-          - { target: linux-x64,    os: ubuntu-latest }
-          - { target: linux-arm64,  os: ubuntu-24.04-arm }
-          # Windows (win32-x64 / win32-arm64): TODO — build-bundle.sh is unix-only.
-    steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-node@v4
-        with:
-          node-version: 22
-      - run: npm ci
-      - name: Build bundle
-        run: bash scripts/build-bundle.sh ${{ matrix.target }}
-      - uses: actions/upload-artifact@v4
-        with:
-          name: bundle-${{ matrix.target }}
-          path: release/codegraph-${{ matrix.target }}.tar.gz
-          if-no-files-found: error
-
   release:
   release:
-    name: Publish release
-    needs: build
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -58,42 +26,41 @@ jobs:
           node-version: 22
           node-version: 22
           registry-url: https://registry.npmjs.org
           registry-url: https://registry.npmjs.org
       - run: npm ci
       - 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
+      - name: Build all platform bundles
         run: |
         run: |
-          V="${{ inputs.version }}"
-          [ -n "$V" ] || V="$(node -p "require('./package.json').version")"
-          echo "version=$V" >> "$GITHUB_OUTPUT"
+          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: Collect bundles
-        uses: actions/download-artifact@v4
-        with:
-          path: artifacts
-          pattern: bundle-*
-          merge-multiple: true
-      - run: mkdir -p release && cp artifacts/*.tar.gz release/ && ls -lh release
+      - name: Resolve version
+        id: ver
+        run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
 
 
-      - name: Release notes
-        id: notes
+      - name: Release notes from CHANGELOG.md
         run: |
         run: |
-          if node scripts/extract-release-notes.mjs "${{ steps.ver.outputs.version }}" > /tmp/notes.md 2>/dev/null && [ -s /tmp/notes.md ]; then
-            echo "file=/tmp/notes.md" >> "$GITHUB_OUTPUT"
+          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
           fi
+          echo "----- release notes -----"; cat notes.md
 
 
       - name: Create GitHub Release
       - name: Create GitHub Release
         env:
         env:
           GH_TOKEN: ${{ github.token }}
           GH_TOKEN: ${{ github.token }}
         run: |
         run: |
-          TAG="v${{ steps.ver.outputs.version }}"
-          if [ -n "${{ steps.notes.outputs.file }}" ]; then
-            gh release create "$TAG" release/*.tar.gz --title "$TAG" --notes-file "${{ steps.notes.outputs.file }}"
-          else
-            gh release create "$TAG" release/*.tar.gz --title "$TAG" --generate-notes
-          fi
+          gh release create "v${{ steps.ver.outputs.version }}" \
+            release/codegraph-* \
+            --title "v${{ steps.ver.outputs.version }}" \
+            --notes-file notes.md
 
 
       - name: Publish to npm
       - name: Publish to npm
-        if: ${{ inputs.publish_npm }}
         env:
         env:
           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
         run: |
         run: |

+ 26 - 16
BUNDLING.md

@@ -16,24 +16,28 @@ platform, identical recipe (only the Node download differs):
 
 
 ```
 ```
 codegraph-<target>/
 codegraph-<target>/
-  node                     # official Node runtime for <target>
+  node | node.exe          # official Node runtime for <target>
   lib/
   lib/
     dist/                  # compiled app (+ tree-sitter .wasm grammars, schema.sql)
     dist/                  # compiled app (+ tree-sitter .wasm grammars, schema.sql)
     node_modules/          # production deps only (pure JS / wasm — portable)
     node_modules/          # production deps only (pure JS / wasm — portable)
   bin/
   bin/
-    codegraph              # launcher: exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
+    codegraph | codegraph.cmd   # launcher → runs the bundled Node with the app
 ```
 ```
 
 
-Targets: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64` (Windows: TODO).
+Targets: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`,
+`win32-arm64`. Unix targets produce `.tar.gz` (shell launcher); Windows produces
+`.zip` (`node.exe` + a `.cmd` launcher).
 
 
 ```bash
 ```bash
 scripts/build-bundle.sh linux-x64            # -> release/codegraph-linux-x64.tar.gz
 scripts/build-bundle.sh linux-x64            # -> release/codegraph-linux-x64.tar.gz
-scripts/build-bundle.sh darwin-arm64 v24.16.0
+scripts/build-bundle.sh win32-x64            # -> release/codegraph-win32-x64.zip
 ```
 ```
 
 
-Note: the script does **not** cross-compile — it downloads the official Node
-binary for `<target>`, but to *run-test* a bundle you must be on that platform
-(or emulate it, e.g. `docker run --platform linux/amd64`).
+Because dropping better-sqlite3 left **zero native addons**, building a bundle is
+pure file-packaging — **any** target builds on **any** OS (the whole matrix builds
+on one Linux runner). Cross-compilation isn't a concern; only *run-testing* a
+bundle needs the target platform (or emulation, e.g. `docker run --platform
+linux/amd64`).
 
 
 ## Install channels (all deliver the same bundle)
 ## Install channels (all deliver the same bundle)
 
 
@@ -47,16 +51,22 @@ binary for `<target>`, but to *run-test* a bundle you must be on that platform
    (`@colbymchenry/codegraph-<target>` with `os`/`cpu`), so npm installs only the
    (`@colbymchenry/codegraph-<target>` with `os`/`cpu`), so npm installs only the
    matching one. The shim — run by the user's Node — execs the bundle, so the
    matching one. The shim — run by the user's Node — execs the bundle, so the
    real work runs on the bundled Node 24. Works even on old Node.
    real work runs on the bundled Node 24. Works even on old Node.
-3. **Homebrew / Scoop** — TODO (tap + cask pointing at the Release archives).
+3. **Windows** ([`install.ps1`](install.ps1)) — `irm … | iex`; same flow as
+   install.sh (detect arch, pull the `.zip` from Releases, add to PATH).
+4. **Homebrew / Scoop** — TODO (tap + cask pointing at the Release archives).
 
 
-## Release pipeline (TODO)
+## Release pipeline
 
 
-- CI matrix (one runner per os/arch) runs `build-bundle.sh`, uploads each archive
-  to the GitHub Release.
-- Publish the npm main shim package + the per-platform packages.
-- **Code signing** is the main gap for "download & run": macOS Gatekeeper needs a
+[`.github/workflows/release.yml`](.github/workflows/release.yml) — manually
+triggered. Reads the version from `package.json`, builds every platform bundle on
+one runner, creates the GitHub Release (notes from `CHANGELOG.md`), and publishes
+the npm shim + per-platform packages. Requires the `NPM_TOKEN` repo secret.
+
+Still TODO:
+- **Code signing** — the main gap for "download & run": macOS Gatekeeper needs a
   Developer ID + notarization; Windows needs Authenticode. Homebrew softens the
   Developer ID + notarization; Windows needs Authenticode. Homebrew softens the
   macOS case (handles quarantine).
   macOS case (handles quarantine).
-- Once bundles ship, retire the Node-version gate in `src/bin/codegraph.ts` — the
-  bundle always runs Node 24, and the npm shim does no tree-sitter work, so no
-  version check is needed.
+- Retire the now-vestigial Node-version gate in `src/bin/codegraph.ts` — the
+  bundle always runs Node 24, and the npm shim does no tree-sitter work.
+- Re-wire `npm uninstall` cleanup (the agent-config `preuninstall`) through the
+  shim — the generated main package doesn't carry it.

+ 53 - 25
scripts/build-bundle.sh

@@ -3,18 +3,21 @@
 # Build a self-contained CodeGraph bundle: an official Node runtime + the
 # Build a self-contained CodeGraph bundle: an official Node runtime + the
 # compiled app + its production deps, so CodeGraph runs with NO system Node and
 # compiled app + its production deps, so CodeGraph runs with NO system Node and
 # NO native build — node:sqlite is built into the bundled Node. One archive per
 # NO native build — node:sqlite is built into the bundled Node. One archive per
-# platform; the recipe is identical across platforms (only the Node download
-# differs), so a CI matrix produces all of them.
+# platform.
+#
+# Because dropping better-sqlite3 left zero native addons, the recipe is pure
+# file-packaging (download the target's Node, copy the app, archive) — so any
+# platform's bundle can be built on any OS. No cross-compile, no native runners.
 #
 #
 # Usage:
 # Usage:
 #   scripts/build-bundle.sh <target> [node-version]
 #   scripts/build-bundle.sh <target> [node-version]
 #     target:        darwin-arm64 | darwin-x64 | linux-x64 | linux-arm64
 #     target:        darwin-arm64 | darwin-x64 | linux-x64 | linux-arm64
+#                  | win32-x64 | win32-arm64
 #     node-version:  e.g. v24.16.0 (default below; pin for reproducible builds)
 #     node-version:  e.g. v24.16.0 (default below; pin for reproducible builds)
 #
 #
-# Output: release/codegraph-<target>.tar.gz  (extracts to codegraph-<target>/)
-#
-# NOTE: does not cross-compile — the bundled Node binary is the official build
-# for <target>, but to *run-test* a bundle you must be on that platform.
+# Output:
+#   unix:    release/codegraph-<target>.tar.gz   (launcher: bin/codegraph)
+#   windows: release/codegraph-<target>.zip      (launcher: bin/codegraph.cmd)
 set -euo pipefail
 set -euo pipefail
 
 
 TARGET="${1:?usage: build-bundle.sh <target> [node-version]}"
 TARGET="${1:?usage: build-bundle.sh <target> [node-version]}"
@@ -25,46 +28,71 @@ OUT="$ROOT/release"
 WORK="$(mktemp -d)"
 WORK="$(mktemp -d)"
 trap 'rm -rf "$WORK"' EXIT
 trap 'rm -rf "$WORK"' EXIT
 
 
-NODE_DIST="node-${NODE_VERSION}-${TARGET}"
-NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.tar.gz"
+ARCH="${TARGET##*-}"   # x64 | arm64
+OSFAM="${TARGET%-*}"   # darwin | linux | win32
 
 
 echo "[bundle] target=${TARGET} node=${NODE_VERSION}"
 echo "[bundle] target=${TARGET} node=${NODE_VERSION}"
 
 
 # 1. Download + extract the official Node runtime for the target platform.
 # 1. Download + extract the official Node runtime for the target platform.
-echo "[bundle] downloading ${NODE_URL}"
-curl -fsSL "$NODE_URL" -o "$WORK/node.tar.gz"
-tar -xzf "$WORK/node.tar.gz" -C "$WORK"
-NODE_BIN="$WORK/${NODE_DIST}/bin/node"
-[ -f "$NODE_BIN" ] || { echo "[bundle] error: node binary not found in tarball" >&2; exit 1; }
+if [ "$OSFAM" = "win32" ]; then
+  NODE_DIST="node-${NODE_VERSION}-win-${ARCH}"
+  NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.zip"
+  echo "[bundle] downloading ${NODE_URL}"
+  curl -fsSL "$NODE_URL" -o "$WORK/node.zip"
+  if command -v unzip >/dev/null 2>&1; then
+    unzip -q "$WORK/node.zip" -d "$WORK"
+  else
+    tar -xf "$WORK/node.zip" -C "$WORK"   # bsdtar can read zip
+  fi
+  NODE_BIN="$WORK/${NODE_DIST}/node.exe"
+else
+  NODE_DIST="node-${NODE_VERSION}-${TARGET}"
+  NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.tar.gz"
+  echo "[bundle] downloading ${NODE_URL}"
+  curl -fsSL "$NODE_URL" -o "$WORK/node.tar.gz"
+  tar -xzf "$WORK/node.tar.gz" -C "$WORK"
+  NODE_BIN="$WORK/${NODE_DIST}/bin/node"
+fi
+[ -f "$NODE_BIN" ] || { echo "[bundle] error: node binary not found ($NODE_BIN)" >&2; exit 1; }
 
 
 # 2. Build the app (compiled JS + copied wasm/schema assets).
 # 2. Build the app (compiled JS + copied wasm/schema assets).
 echo "[bundle] building app"
 echo "[bundle] building app"
 ( cd "$ROOT" && npm run build >/dev/null )
 ( cd "$ROOT" && npm run build >/dev/null )
 
 
-# 3. Stage: vendored node + app + production-only deps + launcher.
+# 3. Stage: app + production-only deps (pure JS/wasm → portable across platforms).
 STAGE="$WORK/codegraph-${TARGET}"
 STAGE="$WORK/codegraph-${TARGET}"
 mkdir -p "$STAGE/lib" "$STAGE/bin"
 mkdir -p "$STAGE/lib" "$STAGE/bin"
-cp "$NODE_BIN" "$STAGE/node"
 cp -R "$ROOT/dist" "$STAGE/lib/dist"
 cp -R "$ROOT/dist" "$STAGE/lib/dist"
 cp "$ROOT/package.json" "$ROOT/package-lock.json" "$STAGE/lib/"
 cp "$ROOT/package.json" "$ROOT/package-lock.json" "$STAGE/lib/"
 echo "[bundle] installing production dependencies"
 echo "[bundle] installing production dependencies"
 ( cd "$STAGE/lib" && npm ci --omit=dev --ignore-scripts >/dev/null 2>&1 )
 ( cd "$STAGE/lib" && npm ci --omit=dev --ignore-scripts >/dev/null 2>&1 )
 rm -f "$STAGE/lib/package-lock.json"
 rm -f "$STAGE/lib/package-lock.json"
 
 
-# 4. Launcher: exec the vendored Node with the app entry. `exec` replaces the
-#    shell so there's a single process, and the absolute path means the bundled
-#    Node is used regardless of what's (or isn't) on the user's PATH.
-cat > "$STAGE/bin/codegraph" <<'LAUNCH'
+# 4. Vendored Node + launcher (the launcher uses the bundled Node by relative
+#    path, so no system Node is ever needed).
+if [ "$OSFAM" = "win32" ]; then
+  cp "$NODE_BIN" "$STAGE/node.exe"
+  printf '@"%%~dp0..\\node.exe" "%%~dp0..\\lib\\dist\\bin\\codegraph.js" %%*\r\n' \
+    > "$STAGE/bin/codegraph.cmd"
+else
+  cp "$NODE_BIN" "$STAGE/node"
+  cat > "$STAGE/bin/codegraph" <<'LAUNCH'
 #!/bin/sh
 #!/bin/sh
 DIR="$(cd "$(dirname "$0")/.." && pwd)"
 DIR="$(cd "$(dirname "$0")/.." && pwd)"
 exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
 exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
 LAUNCH
 LAUNCH
-chmod +x "$STAGE/bin/codegraph"
+  chmod +x "$STAGE/bin/codegraph"
+fi
 
 
-# 5. Archive.
+# 5. Archive (.zip for Windows, .tar.gz otherwise).
 mkdir -p "$OUT"
 mkdir -p "$OUT"
-ARCHIVE="$OUT/codegraph-${TARGET}.tar.gz"
-# --no-xattrs: don't embed macOS extended attributes (com.apple.provenance),
-# which make GNU tar warn noisily when the archive is extracted on Linux.
-tar --no-xattrs -czf "$ARCHIVE" -C "$WORK" "codegraph-${TARGET}"
+if [ "$OSFAM" = "win32" ]; then
+  ARCHIVE="$OUT/codegraph-${TARGET}.zip"
+  rm -f "$ARCHIVE"
+  ( cd "$WORK" && zip -rqX "$ARCHIVE" "codegraph-${TARGET}" )
+else
+  ARCHIVE="$OUT/codegraph-${TARGET}.tar.gz"
+  # --no-xattrs: don't embed macOS xattrs that make GNU tar warn on Linux.
+  tar --no-xattrs -czf "$ARCHIVE" -C "$WORK" "codegraph-${TARGET}"
+fi
 echo "[bundle] wrote ${ARCHIVE} ($(du -h "$ARCHIVE" | cut -f1))"
 echo "[bundle] wrote ${ARCHIVE} ($(du -h "$ARCHIVE" | cut -f1))"

+ 24 - 8
scripts/pack-npm.sh

@@ -27,19 +27,35 @@ rm -rf "$NPM"
 mkdir -p "$NPM/main"
 mkdir -p "$NPM/main"
 
 
 shopt -s nullglob
 shopt -s nullglob
-archives=("$REL"/codegraph-*.tar.gz)
+archives=("$REL"/codegraph-*.tar.gz "$REL"/codegraph-*.zip)
 [ ${#archives[@]} -gt 0 ] || { echo "[pack-npm] no bundles in $REL — run build-bundle.sh first" >&2; exit 1; }
 [ ${#archives[@]} -gt 0 ] || { echo "[pack-npm] no bundles in $REL — run build-bundle.sh first" >&2; exit 1; }
 
 
 targets=()
 targets=()
 for archive in "${archives[@]}"; do
 for archive in "${archives[@]}"; do
-  base="$(basename "$archive" .tar.gz)"   # codegraph-<target>
-  target="${base#codegraph-}"             # <target>, e.g. darwin-arm64
-  os="${target%-*}"                       # darwin
-  arch="${target##*-}"                    # arm64
+  fname="$(basename "$archive")"
+  case "$fname" in
+    *.tar.gz) base="${fname%.tar.gz}" ;;   # codegraph-<target>
+    *.zip)    base="${fname%.zip}" ;;
+  esac
+  target="${base#codegraph-}"             # <target>, e.g. darwin-arm64 / win32-x64
+  os="${target%-*}"                       # darwin | linux | win32
+  arch="${target##*-}"                    # arm64 | x64
   pkgdir="$NPM/$base"
   pkgdir="$NPM/$base"
   mkdir -p "$pkgdir"
   mkdir -p "$pkgdir"
-  tar -xzf "$archive" -C "$pkgdir" --strip-components=1
-  VERSION="$VERSION" SCOPE="$SCOPE" TARGET="$target" OSV="$os" ARCHV="$arch" \
+  case "$fname" in
+    *.zip)
+      tmpx="$(mktemp -d)"
+      unzip -q "$archive" -d "$tmpx"
+      mv "$tmpx/codegraph-${target}"/* "$pkgdir"/
+      rm -rf "$tmpx"
+      nodefile="node.exe"
+      ;;
+    *)
+      tar -xzf "$archive" -C "$pkgdir" --strip-components=1
+      nodefile="node"
+      ;;
+  esac
+  VERSION="$VERSION" SCOPE="$SCOPE" TARGET="$target" OSV="$os" ARCHV="$arch" NODEFILE="$nodefile" \
     node -e '
     node -e '
       const fs=require("fs");
       const fs=require("fs");
       fs.writeFileSync(process.argv[1], JSON.stringify({
       fs.writeFileSync(process.argv[1], JSON.stringify({
@@ -47,7 +63,7 @@ for archive in "${archives[@]}"; do
         version: process.env.VERSION,
         version: process.env.VERSION,
         description: `CodeGraph self-contained bundle for ${process.env.TARGET}`,
         description: `CodeGraph self-contained bundle for ${process.env.TARGET}`,
         os: [process.env.OSV], cpu: [process.env.ARCHV],
         os: [process.env.OSV], cpu: [process.env.ARCHV],
-        files: ["node","lib","bin"],
+        files: [process.env.NODEFILE, "lib", "bin"],
         license: "MIT"
         license: "MIT"
       }, null, 2) + "\n");
       }, null, 2) + "\n");
     ' "$pkgdir/package.json"
     ' "$pkgdir/package.json"