Skip to content
Open
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
19 changes: 8 additions & 11 deletions .github/workflows/build-and-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ on:
# Permissions are intentionally omitted here so callers can choose
# least-privilege (e.g. dev CI uses read-only; releases grant write).

env:
NODE_VERSION: 20

jobs:
build-macos:
runs-on: macos-15-intel
Expand All @@ -71,7 +68,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Set workspace versions
Expand Down Expand Up @@ -242,7 +239,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Set workspace versions
Expand Down Expand Up @@ -292,7 +289,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Set workspace versions
Expand Down Expand Up @@ -426,7 +423,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Setup Rust (Tauri)
Expand Down Expand Up @@ -499,7 +496,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Setup Rust (Tauri)
Expand Down Expand Up @@ -572,7 +569,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Setup Rust (Tauri)
Expand Down Expand Up @@ -649,7 +646,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Setup Rust (Tauri)
Expand Down Expand Up @@ -836,7 +833,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Setup Rust (Tauri)
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/manual-npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ jobs:
publish:
runs-on: ubuntu-latest
env:
NODE_VERSION: 22
PUBLISH_NPM_VERSION: 11.5.1
steps:
- name: Checkout
Expand All @@ -57,7 +56,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
registry-url: https://registry.npmjs.org

- name: Prepare pinned npm CLI
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .node-version
cache: npm
- run: npm ci --ignore-scripts
- name: Install browser for native pruning UI checks
Expand Down Expand Up @@ -130,7 +130,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .node-version
cache: npm

- name: Setup Rust
Expand All @@ -157,7 +157,7 @@ jobs:
run: npm run typecheck

- name: Test desktop packaging invariants
run: node --test scripts/desktop-server-resources.test.cjs
run: node --test scripts/desktop-server-resources.test.cjs scripts/prepare-node-runtime.test.cjs

- name: Test Electron client state
env:
Expand Down Expand Up @@ -274,7 +274,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .node-version
cache: npm

- name: Setup Rust
Expand Down Expand Up @@ -314,7 +314,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .node-version
cache: npm

- name: Install dependencies
Expand Down Expand Up @@ -345,7 +345,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: .node-version
cache: npm

- name: Setup Rust
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/release-ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ on:
permissions:
contents: read

env:
NODE_VERSION: 20

jobs:
release-ui:
# Automated via reusable call (main releases); manual runs allowed on dev/main.
Expand All @@ -30,7 +27,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version
cache: npm

- name: Install dependencies
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/reusable-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ permissions:
id-token: write
contents: write

env:
NODE_VERSION: 20

jobs:
prepare-release:
runs-on: ubuntu-latest
Expand All @@ -62,7 +59,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version-file: .node-version

- name: Compute release versions
id: versions
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-winget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version-file: .node-version

- name: Wait for Windows Tauri release asset
id: release_asset
Expand Down
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.20.0
22 changes: 19 additions & 3 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,25 @@ This guide explains how to build distributable binaries for CodeNomad.
## Prerequisites

- **Bun** - Package manager and runtime
- **Node.js** - For electron-builder
- **Node.js 24 LTS** - Use the exact release pinned in `.node-version` for builds and tests
- **Electron Builder** - Installed via devDependencies

### Bundled server runtime

Both Tauri and Electron ship a separate Node.js executable for the CodeNomad
server. `scripts/prepare-node-runtime.cjs` reads the same `.node-version` pin as
CI, downloads the official target archive, and verifies its SHA-256 before
packaging. Only the executable is bundled, not npm or Corepack. Updating this
pin does not update Electron's internal Node/Chromium, WebView2, the system Node
installation, or the independently managed OpenCode daemon.

For a runtime update, run the server and desktop tests with the pinned Node,
then rebuild and smoke-test the packaged applications on Windows, macOS, and
Linux. Check the upstream Node platform requirements before changing the pin;
Node 24 requires macOS 13.5 or later. Keep both desktop bundle minimum versions
aligned. Keep the version exact rather than resolving `latest` or `lts/*` at
build time so release inputs remain reproducible.

## Quick Start

All commands now run inside the workspace packages. From the repo root you can target the Electron app package directly:
Expand Down Expand Up @@ -111,8 +127,8 @@ The Tauri build directory uses Tauri's native Debian filename. CI renames the pa

### macOS

- **Build on:** macOS 10.13+
- **Run on:** macOS 10.13+
- **Build on:** macOS 13.5+ with the required Xcode tools (CI uses macOS 15)
- **Run on:** macOS 13.5+ (Intel and Apple Silicon; required by bundled Node 24)
- **Code signing:** Optional (recommended for distribution)

### Windows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function getProcessStartIdentity(pid: number): string | undefined {
"-NoProfile",
"-NonInteractive",
"-Command",
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" -ErrorAction Stop).CreationDate.ToUniversalTime().Ticks`,
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
],
"win32",
)
Expand Down Expand Up @@ -99,7 +99,7 @@ export async function getProcessStartIdentityAsync(
"-NoProfile",
"-NonInteractive",
"-Command",
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" -ErrorAction Stop).CreationDate.ToUniversalTime().Ticks`,
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
], "win32", timeoutMs)
}
} catch {
Expand Down
1 change: 1 addition & 0 deletions packages/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
}
],
"mac": {
"minimumSystemVersion": "13.5",
"entitlements": "electron/resources/entitlements.mac.plist",
"entitlementsInherit": "electron/resources/entitlements.mac.plist",
"extendInfo": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ fn process_start_identity(pid: u32) -> Option<String> {
"-NoProfile",
"-NonInteractive",
"-Command",
&format!("(Get-CimInstance Win32_Process -Filter \"ProcessId = {pid}\" -ErrorAction Stop).CreationDate.ToUniversalTime().Ticks"),
&format!("(Get-Process -Id {pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks"),
],
)
.map(|value| format!("win32:{value}"))
Expand Down
3 changes: 3 additions & 0 deletions packages/tauri-app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
},
"bundle": {
"active": true,
"macOS": {
"minimumSystemVersion": "13.5"
},
"linux": {
"appimage": {
"files": {
Expand Down
7 changes: 6 additions & 1 deletion scripts/prepare-node-runtime.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ const os = require("os")
const path = require("path")
const { spawnSync } = require("child_process")

const MANAGED_NODE_VERSION = "v22.22.2"
// Keep packaged runtimes and CI on the same exact LTS release.
const nodeVersion = fs.readFileSync(path.join(__dirname, "..", ".node-version"), "utf8").trim()
if (!/^\d+\.\d+\.\d+$/.test(nodeVersion)) {
throw new Error(".node-version must pin an exact Node.js release")
}
const MANAGED_NODE_VERSION = `v${nodeVersion}`

const ARTIFACTS = {
"darwin-x64": { archive: `node-${MANAGED_NODE_VERSION}-darwin-x64.tar.gz`, root: `node-${MANAGED_NODE_VERSION}-darwin-x64`, binary: path.join("bin", "node") },
Expand Down
42 changes: 42 additions & 0 deletions scripts/prepare-node-runtime.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const test = require("node:test")
const yaml = require("yaml")
const { MANAGED_NODE_VERSION } = require("./prepare-node-runtime.cjs")

const root = path.resolve(__dirname, "..")
const read = (file) => fs.readFileSync(path.join(root, file), "utf8")

test("pins the bundled runtime to the exact Node 24 LTS used by CI", () => {
const version = read(".node-version").trim()
assert.match(version, /^24\.\d+\.\d+$/)
assert.equal(MANAGED_NODE_VERSION, `v${version}`)
})

test("all Node setup steps use the checked-out runtime pin", () => {
const workflows = fs.readdirSync(path.join(root, ".github/workflows"))
.filter((file) => /\.ya?ml$/.test(file))
let checked = 0
for (const file of workflows) {
const workflow = yaml.parse(read(`.github/workflows/${file}`))
for (const job of Object.values(workflow.jobs ?? {})) {
const steps = job.steps ?? []
for (const [index, step] of steps.entries()) {
if (!step.uses?.startsWith("actions/setup-node@")) continue
assert.equal(step.with?.["node-version-file"], ".node-version", file)
assert.equal(step.with?.["node-version"], undefined, file)
assert.ok(steps.slice(0, index).some((earlier) => earlier.uses?.startsWith("actions/checkout@")), file)
checked++
}
}
}
assert.ok(checked > 0)
})

test("both macOS bundles require the OS supported by Node 24", () => {
const electron = JSON.parse(read("packages/electron-app/package.json"))
const tauri = JSON.parse(read("packages/tauri-app/src-tauri/tauri.conf.json"))
assert.equal(electron.build.mac.minimumSystemVersion, "13.5")
assert.equal(tauri.bundle.macOS.minimumSystemVersion, "13.5")
})
19 changes: 12 additions & 7 deletions scripts/smoke-packaged-resources.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
const fs = require("fs")
const path = require("path")
const { spawnSync } = require("child_process")
const { MANAGED_NODE_VERSION } = require("./prepare-node-runtime.cjs")

const requiredPackages = [
"yaml",
"fastify",
"@fastify/static",
"@fastify/cors",
"@fastify/reply-from",
"@opencode-ai/client",
"openai",
"pino",
"undici",
Expand Down Expand Up @@ -58,6 +60,7 @@ function run(command, args, options = {}) {
if (result.status !== 0) {
throw new Error(`${command} exited with code ${result.status ?? 1}`)
}
return result
}

function smokeServer(resourcesRoot, target) {
Expand All @@ -81,18 +84,20 @@ function smokeServer(resourcesRoot, target) {
return
}

const version = run(node, ["--version"], { stdio: "pipe", encoding: "utf8" }).stdout.trim()
if (version !== MANAGED_NODE_VERSION) {
throw new Error(`Packaged Node version ${version} does not match ${MANAGED_NODE_VERSION}`)
}
console.log(`packaged Node version ok: ${version}`)
run(node, [entrypoint, "--version"])

const requireScript = [
"import { createRequire } from 'module';",
"import path from 'path';",
`const root = ${JSON.stringify(serverRoot)};`,
"const req = createRequire(path.join(root, 'dist/bin.js'));",
`${JSON.stringify(requiredPackages)}.forEach((name) => req(name));`,
const importScript = [
`for (const name of ${JSON.stringify(requiredPackages)}) await import(name);`,
"console.log('packaged dependency imports ok');",
].join(" ")

run(node, ["--input-type=module", "-e", requireScript])
// Resolve from the packaged server, not the build checkout. The V2 client is ESM-only.
run(node, ["--input-type=module", "-e", importScript], { cwd: serverRoot })
}

function smokeLoadingAssets(loadingRoot) {
Expand Down
Loading