From 4b8d06092868267d885a5a5cb32268eed302416d Mon Sep 17 00:00:00 2001 From: satsukies Date: Wed, 7 Oct 2026 12:34:27 +0900 Subject: [PATCH 1/3] ci: run on Node 24 and smoke-test the bundle on Node 20 --- .github/workflows/ci.yml | 24 ++++++- .github/workflows/release.yml | 4 +- CLAUDE.md | 2 +- scripts/smoke-bundle.mjs | 132 ++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 scripts/smoke-bundle.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb0755..16db6e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 24 cache: npm - run: npm ci @@ -21,3 +21,25 @@ jobs: - run: npm run build - run: npm test + + bundle-compat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + + - run: npm ci + + - run: npm run bundle + + # Run the bundle on the minimum Node allowed by package.json "engines". + # No dependencies are installed for this step; the bundle is self-contained. + - uses: actions/setup-node@v7 + with: + node-version: 20 + + - run: node scripts/smoke-bundle.mjs plugin/scripts/bundle.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d8d2b3..1a56efa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: - if: ${{ steps.release.outputs.prs_created == 'true' }} uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 24 cache: npm - if: ${{ steps.release.outputs.prs_created == 'true' }} @@ -61,7 +61,7 @@ jobs: - if: ${{ steps.release.outputs.release_created == 'true' }} uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 24 cache: npm registry-url: 'https://registry.npmjs.org' diff --git a/CLAUDE.md b/CLAUDE.md index 008f38a..ccc8474 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ npm run dev # TypeScript watch mode npm start # Run the MCP server directly ``` -CI runs `npm run build && npm test` on every PR and push to main. `plugin/scripts/bundle.js` is regenerated and committed automatically by the release-please workflow when it opens/updates a release PR — do not run `npm run bundle` and commit the result manually. A pre-commit hook in `.githooks/` (installed by `npm install` via `prepare`) blocks accidental commits of the bundle. +CI runs `npm run build && npm test` on Node 24 on every PR and push to main. A separate `bundle-compat` job builds the bundle on Node 24 and runs `node scripts/smoke-bundle.mjs` against it on Node 20, the minimum in `engines`, so the published bundle keeps working there even though dev tooling (vitest 5) needs Node 22+. `plugin/scripts/bundle.js` is regenerated and committed automatically by the release-please workflow when it opens/updates a release PR — do not run `npm run bundle` and commit the result manually. A pre-commit hook in `.githooks/` (installed by `npm install` via `prepare`) blocks accidental commits of the bundle. ## Architecture diff --git a/scripts/smoke-bundle.mjs b/scripts/smoke-bundle.mjs new file mode 100644 index 0000000..742a79e --- /dev/null +++ b/scripts/smoke-bundle.mjs @@ -0,0 +1,132 @@ +// Smoke test for the bundled MCP server. Spawns the bundle with the current +// Node, performs the MCP handshake over stdio and checks that tools/list +// returns tools. Dependency-free so it can run on the minimum supported Node +// without installing dev dependencies. +// +// Usage: node scripts/smoke-bundle.mjs [path/to/bundle.js] + +import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const TIMEOUT_MS = 15_000; + +const bundle = resolve(process.argv[2] ?? "plugin/scripts/bundle.js"); + +// Point every config location at an empty directory so a token stored on the +// machine is never picked up. +const home = mkdtempSync(join(tmpdir(), "deploygate-smoke-")); + +const child = spawn(process.execPath, [bundle], { + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + XDG_CONFIG_HOME: home, + APPDATA: home, + }, + stdio: ["pipe", "pipe", "pipe"], +}); + +let stderr = ""; +child.stderr.on("data", (chunk) => (stderr += chunk)); + +const pending = new Map(); +let buffer = ""; +child.stdout.on("data", (chunk) => { + buffer += chunk; + let newline; + while ((newline = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, newline).trim(); + buffer = buffer.slice(newline + 1); + if (!line) continue; + let message; + try { + message = JSON.parse(line); + } catch { + fail(new Error(`bundle wrote a non JSON-RPC line to stdout: ${line}`)); + return; + } + pending.get(message.id)?.(message); + pending.delete(message.id); + } +}); + +const exited = new Promise((_, reject) => { + child.on("exit", (code, signal) => + reject(new Error(`bundle exited early (code=${code}, signal=${signal})`)), + ); +}); + +function send(message) { + child.stdin.write(JSON.stringify(message) + "\n"); +} + +function request(id, method, params = {}) { + const response = new Promise((resolve) => pending.set(id, resolve)); + send({ jsonrpc: "2.0", id, method, params }); + return Promise.race([response, exited]).then((message) => { + if (message.error) + throw new Error(`${method} failed: ${JSON.stringify(message.error)}`); + return message.result; + }); +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const timer = setTimeout( + () => fail(new Error(`timed out after ${TIMEOUT_MS}ms`)), + TIMEOUT_MS, +); + +function finish(code) { + clearTimeout(timer); + child.removeAllListeners("exit"); + child.kill(); + rmSync(home, { recursive: true, force: true }); + process.exit(code); +} + +function fail(error) { + console.error( + `smoke test failed on Node ${process.version}: ${error.message}`, + ); + if (stderr) console.error(`bundle stderr:\n${stderr}`); + finish(1); +} + +try { + const init = await request(1, "initialize", { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "smoke-bundle", version: "0.0.0" }, + }); + assert( + init.serverInfo?.name === "deploygate", + `unexpected serverInfo: ${JSON.stringify(init.serverInfo)}`, + ); + + send({ jsonrpc: "2.0", method: "notifications/initialized" }); + + const { tools } = await request(2, "tools/list"); + assert( + Array.isArray(tools) && tools.length > 0, + "tools/list returned no tools", + ); + for (const tool of tools) { + assert( + typeof tool.name === "string" && tool.inputSchema, + `malformed tool: ${JSON.stringify(tool)}`, + ); + } + + console.log( + `smoke test passed on Node ${process.version}: ${init.serverInfo.name} ${init.serverInfo.version}, ${tools.length} tools`, + ); + finish(0); +} catch (error) { + fail(error); +} From 8ebeef107032a4f55c421b04b9b978a8853b6dde Mon Sep 17 00:00:00 2001 From: satsukies Date: Wed, 7 Oct 2026 12:37:36 +0900 Subject: [PATCH 2/3] ci: run the bundle outside the checkout and require JSON-RPC output in the smoke test --- .github/workflows/ci.yml | 7 +++++-- scripts/smoke-bundle.mjs | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16db6e3..3d174fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,9 +37,12 @@ jobs: - run: npm run bundle # Run the bundle on the minimum Node allowed by package.json "engines". - # No dependencies are installed for this step; the bundle is self-contained. + # Copy it out of the checkout first so imports cannot resolve from + # node_modules; published users get only the self-contained bundle. - uses: actions/setup-node@v7 with: node-version: 20 - - run: node scripts/smoke-bundle.mjs plugin/scripts/bundle.js + - run: | + cp plugin/scripts/bundle.js "$RUNNER_TEMP/bundle.js" + node scripts/smoke-bundle.mjs "$RUNNER_TEMP/bundle.js" diff --git a/scripts/smoke-bundle.mjs b/scripts/smoke-bundle.mjs index 742a79e..9d8f19d 100644 --- a/scripts/smoke-bundle.mjs +++ b/scripts/smoke-bundle.mjs @@ -45,6 +45,14 @@ child.stdout.on("data", (chunk) => { try { message = JSON.parse(line); } catch { + message = undefined; + } + if ( + typeof message !== "object" || + message === null || + Array.isArray(message) || + message.jsonrpc !== "2.0" + ) { fail(new Error(`bundle wrote a non JSON-RPC line to stdout: ${line}`)); return; } From bcaae2616a8d863907050fe95225fd9e03bb8e50 Mon Sep 17 00:00:00 2001 From: satsukies Date: Wed, 7 Oct 2026 12:46:30 +0900 Subject: [PATCH 3/3] ci: smoke-test a copy of plugin/ on the exact engines minimum, Node 20.0.0 --- .github/workflows/ci.yml | 12 ++++++------ CLAUDE.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d174fc..25da66a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,13 @@ jobs: - run: npm run bundle - # Run the bundle on the minimum Node allowed by package.json "engines". - # Copy it out of the checkout first so imports cannot resolve from - # node_modules; published users get only the self-contained bundle. + # Run the bundle on the exact minimum Node allowed by package.json + # "engines". Copy plugin/ out of the checkout first, as the plugin cache + # does, so neither node_modules nor the root package.json is visible. - uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 20.0.0 - run: | - cp plugin/scripts/bundle.js "$RUNNER_TEMP/bundle.js" - node scripts/smoke-bundle.mjs "$RUNNER_TEMP/bundle.js" + cp -R plugin "$RUNNER_TEMP/plugin" + node scripts/smoke-bundle.mjs "$RUNNER_TEMP/plugin/scripts/bundle.js" diff --git a/CLAUDE.md b/CLAUDE.md index ccc8474..73eda4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ npm run dev # TypeScript watch mode npm start # Run the MCP server directly ``` -CI runs `npm run build && npm test` on Node 24 on every PR and push to main. A separate `bundle-compat` job builds the bundle on Node 24 and runs `node scripts/smoke-bundle.mjs` against it on Node 20, the minimum in `engines`, so the published bundle keeps working there even though dev tooling (vitest 5) needs Node 22+. `plugin/scripts/bundle.js` is regenerated and committed automatically by the release-please workflow when it opens/updates a release PR — do not run `npm run bundle` and commit the result manually. A pre-commit hook in `.githooks/` (installed by `npm install` via `prepare`) blocks accidental commits of the bundle. +CI runs `npm run build && npm test` on Node 24 on every PR and push to main. A separate `bundle-compat` job builds the bundle on Node 24 and runs `node scripts/smoke-bundle.mjs` against a copy of `plugin/` outside the checkout on Node 20.0.0, the exact minimum in `engines`, so the plugin as users install it keeps working there even though dev tooling (vitest 5) needs Node 22+. `plugin/scripts/bundle.js` is regenerated and committed automatically by the release-please workflow when it opens/updates a release PR — do not run `npm run bundle` and commit the result manually. A pre-commit hook in `.githooks/` (installed by `npm install` via `prepare`) blocks accidental commits of the bundle. ## Architecture