Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions BUNDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ scripts/build-bundle.sh linux-x64 # -> release/codegraph-linux-x64.ta
scripts/build-bundle.sh win32-x64 # -> release/codegraph-win32-x64.zip
```

The Node runtime is the official nodejs.org release named by the script's
default `NODE_VERSION` (or its second argument). The download is refused
unless it matches the SHA-256 listed in that release's `SHASUMS256.txt`.

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
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- The shared background server is no longer killed with "Main thread unresponsive" while it brings another project's index up to date, such as one a session names with `projectPath`: only its own project's index counted as progress, so a long update of any other looked like a hang, and the server was stopped and restarted again and again. Every index the server keeps up to date now counts, and so does the one a session updates on its own when the background server is unavailable. Thanks @mradwankhalil for the report and the first fix. (#2404)
- An index left incomplete by an indexing run that was stopped partway, by a crash, an out-of-memory kill or the server's own hang check, no longer gets the background server killed with "Main thread unresponsive" every time it starts: on a large project, restoring the database indexes the run had set aside took longer than the hang check allows, so each restart died at the same point and the index was never repaired. They are now restored in the background, and the project is served once they are back. Thanks @cbeaulieu-gt for the report and the first fix. (#1887)
- A new agent session no longer waits up to about 12 minutes for its first answer when the background server is stuck: a server whose process still holds its connection open but never responds made the session wait out a long series of retries. The session now gives up within seconds and answers on its own, and switches back to the background server once that has been restarted.
- On Windows, a command like `codegraph sync` no longer sometimes crashes as it exits, after its work is done, printing `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`, which made a script or CI run that had succeeded fail. The bundled Node runtime moves to 24.21.0, which fixes that crash in Node itself. Thanks @Ancienttwo for the report and the first fix. (#2617)
- The bundled Node runtime moves to 24.22.1, Node's October security release, and every release now checks the Node it bundles against the checksums nodejs.org publishes. The new runtime also fixes a crash on Windows as a command like `codegraph sync` exits, after its work is done, printing `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`, which made a script or CI run that had succeeded fail. Thanks @Ancienttwo for the report and the first fix, and @IdealBoy709 for the report on the security release. (#2617, #2780)
- When CodeGraph can't open a project's database, it now says what went wrong: SQLite's error, the file's path, and what to check, such as another program holding the file open, a folder you can't write to, or a network share. Before, every such failure read "CodeGraph requires node:sqlite (Node.js 22.5+)", and an agent was told no project was loaded. That hint now appears only when Node really can't load its built-in SQLite, a case that used to end in an unhandled error instead.
- `codegraph upgrade` no longer wipes your agent instructions file, such as a global `AGENTS.md`, `CLAUDE.md` or `GEMINI.md`, when it refreshes CodeGraph's section in it: a stray `<!-- CODEGRAPH_START -->` line above that section, like one left behind when part of an older section was deleted by hand, made the refresh replace everything in between, often the whole file. CodeGraph now rewrites only its own section, in `codegraph install` and `codegraph uninstall` too, and keeps everything around it exactly as it was, Windows line endings included. Thanks @pzaj2 for the report. (#2506)
- `codegraph install`, `upgrade` and `uninstall` now write through an instructions or config file that is a symbolic link, such as a `~/.codex/AGENTS.md` or `~/.claude.json` linked from your dotfiles, instead of replacing the link with a copy: the shared file gets CodeGraph's settings, uninstall takes them out again, and the link stays in place. A link CodeGraph can't write through, like one to a read-only file or into a folder that doesn't exist, is now reported by name and left alone, and an upgrade still refreshes your other agents. Thanks @jguerra6 for the report and @danusha2345 for the first fix. (#2721)
Expand Down
74 changes: 74 additions & 0 deletions __tests__/bundle-launcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it } from 'vitest';
import { spawnSync } from 'child_process';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -142,3 +143,76 @@ describe('bundled Node runtime (#2617)', () => {
expect(minor).toBeGreaterThanOrEqual(20);
});
});

describe('bundled Node security release (#2780)', () => {
const archive = 'node-v24.22.1-linux-x64.tar.gz';

// Step 1 of the release recipe, run against a stand-in for nodejs.org: `curl`
// copies the requested file out of `site/` and logs the URL it was asked for.
function download(sums: (sha: string) => string) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-node-download-'));
roots.push(root);
const site = path.join(root, 'site');
const work = path.join(root, 'work');
fs.mkdirSync(path.join(site, 'node-v24.22.1-linux-x64', 'bin'), { recursive: true });
fs.mkdirSync(work);
fs.writeFileSync(path.join(site, 'node-v24.22.1-linux-x64', 'bin', 'node'), '#!/bin/sh\n');
const arg = (n: number) => windows ? `$(cygpath -u "$${n}")` : `$${n}`;
// Packed the way nodejs.org packs a release: one top-level folder.
const pack = shell(`set -eu\ncd "${arg(1)}"\ntar -czf ${archive} node-v24.22.1-linux-x64`, [site]);
expect(pack.status, pack.stderr).toBe(0);
const sha = crypto.createHash('sha256').update(fs.readFileSync(path.join(site, archive))).digest('hex');
fs.writeFileSync(path.join(site, 'SHASUMS256.txt'), sums(sha));
const step = recipe.slice(recipe.indexOf('# 1. Download'), recipe.indexOf('# 2. Build'));
const result = shell(`set -euo pipefail
WORK="${arg(1)}"
SITE="${arg(2)}"
TARGET=linux-x64 OSFAM=linux ARCH=x64 NODE_VERSION=v24.22.1
curl() {
local url='' out=''
while [ $# -gt 0 ]; do
case "$1" in -o) out="$2"; shift 2 ;; -*) shift ;; *) url="$1"; shift ;; esac
done
printf '%s\\n' "$url" >> "$WORK/urls"
cp "$SITE/\${url##*/}" "$out"
}
${step}
printf 'NODE_BIN=%s\\n' "$NODE_BIN"`, [work, site]);
const urls = fs.existsSync(path.join(work, 'urls'))
? fs.readFileSync(path.join(work, 'urls'), 'utf8').trim().split('\n')
: [];
return { result, work, urls };
}

it('ships Node 24.22.1, the October 2026 security release, or a later 24.x', () => {
const pin = /^NODE_VERSION="\$\{2:-v(\d+)\.(\d+)\.(\d+)\}"\r?$/m.exec(recipe);
expect(pin, 'default NODE_VERSION in scripts/build-bundle.sh').not.toBeNull();
const [major, minor, patch] = pin!.slice(1, 4).map(Number);
expect(major).toBe(24);
// 24.22.1 fixes eight CVEs (four rated High) and ships OpenSSL 3.5.9.
expect(minor > 22 || (minor === 22 && patch >= 1), `v${major}.${minor}.${patch}`).toBe(true);
});

it('unpacks the Node download once it matches the SHA-256 nodejs.org publishes for it', () => {
const other = 'f'.repeat(64);
const { result, work, urls } = download((sha) =>
`${other} node-v24.22.1-darwin-arm64.tar.gz\n${sha} ${archive}\n${other} node-v24.22.1-win-x64.zip\n`);
expect(result.status, result.stderr).toBe(0);
expect(urls).toEqual([
`https://nodejs.org/dist/v24.22.1/${archive}`,
'https://nodejs.org/dist/v24.22.1/SHASUMS256.txt',
]);
expect(fs.existsSync(path.join(work, 'node-v24.22.1-linux-x64', 'bin', 'node'))).toBe(true);
expect(result.stdout).toMatch(/^NODE_BIN=.*node-v24\.22\.1-linux-x64\/bin\/node\r?$/m);
});

it.each([
['whose SHA-256 differs from the published one', () => `${'0'.repeat(64)} ${archive}\n`],
['that the published list leaves out', (sha: string) => `${sha} node-v24.22.1-linux-arm64.tar.gz\n`],
])('refuses a Node download %s, before unpacking it', (_, sums) => {
const { result, work } = download(sums);
expect(result.status).toBe(1);
expect(result.stderr).toContain(`${archive} does not match the SHA-256 that nodejs.org lists for v24.22.1`);
expect(fs.existsSync(path.join(work, 'node-v24.22.1-linux-x64'))).toBe(false);
});
});
54 changes: 36 additions & 18 deletions scripts/build-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# 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.21.0 (default below; pin for reproducible builds)
# node-version: e.g. v24.22.1 (default below; pin for reproducible builds)
#
# Output:
# unix: release/codegraph-<target>.tar.gz (launcher: bin/codegraph)
Expand All @@ -22,12 +22,13 @@ set -euo pipefail

TARGET="${1:?usage: build-bundle.sh <target> [node-version]}"
# The release workflow passes no version, so this default is the Node every
# release bundles. Keep it on 24.x (the CLI refuses Node 25+) and at 24.20.0 or
# later: older 24.x releases can abort on Windows just as the process exits,
# after its work is done, with 0xC0000409 and "Assertion failed:
# !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c" (nodejs/node#56645,
# fixed by nodejs/node#61999 in 24.20.0; issue #2617).
NODE_VERSION="${2:-v24.21.0}"
# release bundles. Keep it on 24.x (the CLI refuses Node 25+) and at 24.22.1 or
# later. 24.22.1 is Node's October 2026 security release (issue #2780). Before
# 24.20.0, Node 24 can abort on Windows just as the process exits, after its
# work is done, with 0xC0000409 and "Assertion failed: !(handle->flags &
# UV_HANDLE_CLOSING), file src\win\async.c" (nodejs/node#56645, fixed by
# nodejs/node#61999; issue #2617).
NODE_VERSION="${2:-v24.22.1}"

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$ROOT/release"
Expand All @@ -39,24 +40,41 @@ OSFAM="${TARGET%-*}" # darwin | linux | win32

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. The
# archive must match the SHA-256 that nodejs.org publishes for the release
# before anything is unpacked, so a bundle ships exactly that release's
# official build, never a truncated or substituted download (issue #2780).
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"
NODE_ARCHIVE="${NODE_DIST}.zip"
else
NODE_DIST="node-${NODE_VERSION}-${TARGET}"
NODE_ARCHIVE="${NODE_DIST}.tar.gz"
fi
NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_ARCHIVE}"
echo "[bundle] downloading ${NODE_URL}"
curl -fsSL "$NODE_URL" -o "$WORK/$NODE_ARCHIVE"
curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt" -o "$WORK/SHASUMS256.txt"
EXPECTED_SHA="$(awk -v name="$NODE_ARCHIVE" '$2 == name { print $1 }' "$WORK/SHASUMS256.txt")"
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL_SHA="$(sha256sum < "$WORK/$NODE_ARCHIVE" | cut -d' ' -f1)"
else
ACTUAL_SHA="$(shasum -a 256 < "$WORK/$NODE_ARCHIVE" | cut -d' ' -f1)" # macOS
fi
if [ -z "$EXPECTED_SHA" ] || [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "[bundle] error: ${NODE_ARCHIVE} does not match the SHA-256 that nodejs.org lists for ${NODE_VERSION} (listed: ${EXPECTED_SHA:-nothing}, downloaded: ${ACTUAL_SHA})" >&2
exit 1
fi
echo "[bundle] verified ${NODE_ARCHIVE} against nodejs.org's SHASUMS256.txt (sha256 ${ACTUAL_SHA})"
if [ "$OSFAM" = "win32" ]; then
if command -v unzip >/dev/null 2>&1; then
unzip -q "$WORK/node.zip" -d "$WORK"
unzip -q "$WORK/$NODE_ARCHIVE" -d "$WORK"
else
tar -xf "$WORK/node.zip" -C "$WORK" # bsdtar can read zip
tar -xf "$WORK/$NODE_ARCHIVE" -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"
tar -xzf "$WORK/$NODE_ARCHIVE" -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; }
Expand Down