Przeglądaj źródła

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 miesiąc temu
rodzic
commit
31d72c2e4e
4 zmienionych plików z 136 dodań i 115 usunięć
  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
 
-# 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:
-  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:
   contents: write   # create the GitHub Release + tag
 
 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:
-    name: Publish release
-    needs: build
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
@@ -58,42 +26,41 @@ jobs:
           node-version: 22
           registry-url: https://registry.npmjs.org
       - 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: |
-          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: |
-          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
+          echo "----- release notes -----"; cat notes.md
 
       - name: Create GitHub Release
         env:
           GH_TOKEN: ${{ github.token }}
         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
-        if: ${{ inputs.publish_npm }}
         env:
           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
         run: |

+ 26 - 16
BUNDLING.md

@@ -16,24 +16,28 @@ platform, identical recipe (only the Node download differs):
 
 ```
 codegraph-<target>/
-  node                     # official Node runtime for <target>
+  node | node.exe          # official Node runtime for <target>
   lib/
     dist/                  # compiled app (+ tree-sitter .wasm grammars, schema.sql)
     node_modules/          # production deps only (pure JS / wasm — portable)
   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
 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)
 
@@ -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
    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.
-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
   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
 # 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
-# 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:
 #   scripts/build-bundle.sh <target> [node-version]
 #     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)
 #
-# 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
 
 TARGET="${1:?usage: build-bundle.sh <target> [node-version]}"
@@ -25,46 +28,71 @@ OUT="$ROOT/release"
 WORK="$(mktemp -d)"
 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}"
 
 # 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).
 echo "[bundle] building app"
 ( 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}"
 mkdir -p "$STAGE/lib" "$STAGE/bin"
-cp "$NODE_BIN" "$STAGE/node"
 cp -R "$ROOT/dist" "$STAGE/lib/dist"
 cp "$ROOT/package.json" "$ROOT/package-lock.json" "$STAGE/lib/"
 echo "[bundle] installing production dependencies"
 ( cd "$STAGE/lib" && npm ci --omit=dev --ignore-scripts >/dev/null 2>&1 )
 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
 DIR="$(cd "$(dirname "$0")/.." && pwd)"
 exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
 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"
-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))"

+ 24 - 8
scripts/pack-npm.sh

@@ -27,19 +27,35 @@ rm -rf "$NPM"
 mkdir -p "$NPM/main"
 
 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; }
 
 targets=()
 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"
   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 '
       const fs=require("fs");
       fs.writeFileSync(process.argv[1], JSON.stringify({
@@ -47,7 +63,7 @@ for archive in "${archives[@]}"; do
         version: process.env.VERSION,
         description: `CodeGraph self-contained bundle for ${process.env.TARGET}`,
         os: [process.env.OSV], cpu: [process.env.ARCHV],
-        files: ["node","lib","bin"],
+        files: [process.env.NODEFILE, "lib", "bin"],
         license: "MIT"
       }, null, 2) + "\n");
     ' "$pkgdir/package.json"