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
11 changes: 3 additions & 8 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,13 @@ jobs:
if [[ "${{ inputs.version }}" =~ -nightly\.[0-9]{8}\.[0-9]+$ ]]; then brand=nightly; fi
node scripts/apply-web-brand-assets.ts "$brand" apps/server/dist/client
# The single-executable is built with a Node that supports --build-sea
# (25.7+); the repo itself stays on the engines.node version. A native
# build injects into the runner's own Node; a Windows target downloads its
# node.exe.
# Setup Vite+ selects the exact engines.node version from package.json.
# Native builds embed that executable; cross-target builds download the
# same version for their target.
- name: Build CLI single-executable
if: inputs.cli_archive
shell: bash
env:
# The exact version, not a major: vp downloads it from nodejs.org/dist on
# the runner, and only exact versions have a dist directory. Keep in
# step with SEA_NODE_VERSION in apps/server/vite.config.ts.
VP_NODE_VERSION: "26.8.2"
# The executable bundles the server again, so it needs the same stamp
# as release.yml's JS bundle.
T3CODE_TELEMETRY_CHANNEL: ${{ github.repository == 'pingdotgg/t3code' && inputs.release_channel || 'dev' }}
Expand Down
3 changes: 2 additions & 1 deletion apps/server/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process";

import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";
import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-executable-imports.ts";
import rootPackageJson from "../../../package.json" with { type: "json" };
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import {
ServerCliBuildAssetMissingError,
Expand Down Expand Up @@ -149,7 +150,7 @@ const buildExeCmd = Command.make(
}),
).pipe(
Command.withDescription(
"Build the server as a Node single-executable (needs a Node 25.7+ host for --build-sea). The binary still resolves native packages from a node_modules tree beside it.",
`Build the server as a Node single-executable. Native builds require Node ${rootPackageJson.engines.node}; explicit cross-target builds need a Node 25.7+ host for --build-sea. The binary still resolves native packages from a node_modules tree beside it.`,
),
);

Expand Down
10 changes: 8 additions & 2 deletions apps/server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus";

import baseConfig from "../../vite.config.ts";
import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
import rootPackageJson from "../../package.json" with { type: "json" };
import packageJson from "./package.json" with { type: "json" };
import { WeightedShardSequencer } from "./src/testUtils/weightedShardSequencer.ts";

Expand Down Expand Up @@ -34,7 +35,6 @@ if (!TELEMETRY_CHANNELS.includes(telemetryChannel)) {
// executable needs a host Node that supports `--build-sea` (25.7+), so this is
// a separate mode rather than a second entry in the default build.
const packExecutable = process.env.T3CODE_PACK_EXE === "1";
const SEA_NODE_VERSION = "26.8.2";
const SEA_TARGETS = {
"darwin-arm64": { platform: "darwin", arch: "arm64" },
"darwin-x64": { platform: "darwin", arch: "x64" },
Expand All @@ -44,6 +44,12 @@ const SEA_TARGETS = {
"win-x64": { platform: "win", arch: "x64" },
} as const;
const packExecutableTarget = process.env.T3CODE_PACK_EXE_TARGET?.trim();
const cliNodeVersion = rootPackageJson.engines.node;
if (packExecutable && !packExecutableTarget && process.versions.node !== cliNodeVersion) {
throw new Error(
`Native CLI executables must be built with Node ${cliNodeVersion}; running ${process.versions.node}.`,
);
}
if (packExecutableTarget && !Object.hasOwn(SEA_TARGETS, packExecutableTarget)) {
throw new Error(
`T3CODE_PACK_EXE_TARGET must be one of ${Object.keys(SEA_TARGETS).join(", ")}, got "${packExecutableTarget}".`,
Expand All @@ -53,7 +59,7 @@ const packExecutableTargets = packExecutableTarget
? [
{
...SEA_TARGETS[packExecutableTarget as keyof typeof SEA_TARGETS],
nodeVersion: SEA_NODE_VERSION,
nodeVersion: cliNodeVersion,
},
]
: undefined;
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ the version.
- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.tar.gz`, `.zip` on Windows) in the same job as that target's desktop artifact and attaches them to the GitHub Release with a `SHA256SUMS` file, on every channel, for five targets: macOS arm64, Linux x64 and arm64, Windows x64 and arm64. Every archive is smoke-tested on hardware of its own architecture, and every one but the Windows archives is also built and signed there. There is no macOS x64 archive: Node single-executables are unsupported on x64 macOS (the SEA docs list macOS as arm64 only) and the binary segfaults on start; the x64 desktop app is Electron and unaffected.
- The archive holds the server as a Node single-executable (`scripts/build-cli-archive.ts`), so unpacking it needs neither Node, npm, nor a compiler. It is the only form in which T3 Code manages a runtime: the desktop's SSH environments, the boot service, `t3 update`, and the install scripts all download and verify this archive against `SHA256SUMS`. The `t3` npm package exists for people who run `npx t3` or `npm install -g t3` themselves, and it downloads this same archive too; nothing in the product installs from npm. The `curl | sh` installers are `scripts/install.sh` and `scripts/install.ps1`; the marketing site copies them into its `public/` at build time (`apps/marketing/scripts/stage-install-scripts.mjs`) and serves them at `t3.codes/install.sh` and `/install.ps1`.
- The executable is built with a Node that supports `--build-sea` (`VP_NODE_VERSION=26.8.2`, kept in step with `SEA_NODE_VERSION` in `apps/server/vite.config.ts`), while the repo stays on `engines.node`.
- The root `package.json` pins `engines.node` to the exact Node version used by development, CI and standalone executables. Keep it exact: cross-target builds download that version, and native executable builds reject a different host Node version. Runtime changes need a new T3 release version because installed runtimes are cached by that version.
- macOS archives are signed with the Developer ID certificate and notarized when the Apple secrets are present (ad hoc otherwise, which still runs from `curl`/`tar` installs). Windows executables use the same Azure Trusted Signing setup as the installer. Every native addon in the macOS archive is signed too, since the hardened runtime refuses unsigned libraries.
- Each archive is extracted and executed (`scripts/smoke-cli-archive.ts`) on its build runner, or on the `verify_runner` for Windows.
- After the GitHub Release is published, publishes the `t3` npm package with OIDC trusted publishing from the same workflow file. It is only a launcher (`scripts/npm-launcher/t3.mjs`): on first run it downloads its exact version's CLI archive from the GitHub Release, verifies it against `SHA256SUMS`, and unpacks it into `<T3 home>/runtime/versions/<version>`, the layout the install scripts and `t3 update` share. The executables are not on npm. They used to be, as `@t3code/t3-<platform>-<arch>` optional dependencies, but npm's publish-time scanning held those 80-250 MB packages back for up to an hour after the launcher was live, and every install in that window got a launcher with no executable. Those packages stay on npm at their old versions and get no new ones. Because the launcher reads from the release, `publish_cli` runs after `release`, never before.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"vite-plus": "catalog:"
},
"engines": {
"node": "^24.13.1"
"node": "26.12.1"
},
"packageManager": "pnpm@12.14.0"
}
Loading