DOCS

Release

Build, verify, publish, install, and recover the Apple Silicon CLI release.

About 19 min read · 5 sections

On this page

Provider source pins can advance through the local stable-update gate before cutting a release. See provider maintenance for the daily schedule, generated inputs and blocked-update behavior. It never publishes or activates a release on its own.

The native macOS control panel has a separate local bundle command, npm run app:build. See macOS app packaging for its verified backend input, local installation, native tests, and distribution signing requirements. The CLI archive workflow below does not publish the app. Use the background identity and signing workflow when shipping a Developer ID signed app and its startup launcher.

Standard GitHub release

Every version is one standard release, vVERSION, marked Latest. It holds the notarized app ZIP, the command-line archive, app-update.json and one SHA256SUMS for all of them, with notes taken from the version's CHANGELOG.md section. The README and the gatefox.dev download button link to it.

Version Also publish App ZIP Command-line archive
1.7.6 app-v1.7.6 (by release-macos-app.mjs --publish, for 1.7.5 and older updaters) Harness-Gateway-1.7.6-macos-arm64.zip harness-gateway-1.7.6-macos-arm64.tar.gz
1.8.0 and later nothing else GateFox-VERSION-macos-arm64.zip gatefox-VERSION-macos-arm64.tar.gz

From 1.8.0 the app publisher creates vVERSION itself: it pushes nothing, so push the vVERSION tag at the release commit first, then run it with --publish. It uploads the app ZIP, the archive, app-update.json and SHA256SUMS to a draft, verifies every GitHub digest, publishes it as Latest and takes the notes from the version's CHANGELOG.md section. 1.7.6 to 1.7.8 used app-vVERSION from the publisher plus a hand-made vVERSION with the same files. Publish 1.8.0 only after every Mac runs 1.7.6, the first updater that reads vVERSION tags and GateFox names; see macOS app updates.

macOS app distribution

Build the matching canonical CLI archive with --with-native --with-gateway-sdk first. The app distributor refuses an archive missing either bundled component; a minimal CLI archive is not a complete desktop release. From a clean, committed local production checkout whose package.json and lockfile have the exact release version. After the source, backend archive, and local gates are reviewed, run the one-pass distribution command:

node scripts/release-macos-app.mjs \
  --backend-archive /absolute/gatefox-VERSION-macos-arm64.tar.gz \
  --backend-sha256 LOWERCASE_SHA256 \
  --identity DEVELOPER_ID_SHA1 \
  --keychain /absolute/signing.keychain-db \
  --profile asc-notary \
  --publish

It verifies the canonical backend archive and production source, builds, signs, notarizes, assesses Gatekeeper, extracts the final signed ZIP for the runtime check, validates the signed updater flow against the existing app and CLI release, then writes immutable release assets under .state/app-release/VERSION. The signed runtime fixture declares its own JavaScript module type so unrelated files in the temporary directory cannot change the synthetic CLI semantics.

Publishing requires origin/production and the release tag (vVERSION from 1.8.0, app-vVERSION before) to resolve exactly to the current HEAD, creates the release as a draft, uploads the ZIP, backend archive, app-update.json, and SHA256SUMS, verifies every GitHub-reported digest and size, then publishes the draft. An existing release or any failed draft verification is left untouched. This command never enables or dispatches GitHub Actions; follow the CI cost policy for the separate paid native workflow. See macOS app updates for daily client activation and rollback behavior.

For an explicitly selected-machine rollout, add --prerelease to --publish. The release is created as a prerelease before upload and stays a prerelease when its draft is published. The publisher verifies that state alongside asset digests and never marks it latest. Automatic app updaters ignore prereleases; deploy the verified assets only to the approved machines. Publish the CLI tag as a prerelease too. Do not expose a stable app release for a restricted rollout.

Omit --publish only to exercise the local build, signing, and archive gates. It leaves the versioned stage in place and deliberately cannot resume it as a later publication. For an actual distribution, rerun only after moving that failed or inspection-only stage to Trash, then use the one-pass command above. --publish uses ~/Applications/GateFox.app (or a Harness Gateway.app not yet renamed) and ~/.local/share/gatefox/current (or the pre-1.8.0 share/subscription-gateway) by default for the updater fixture; pass absolute --previous-app and --installed-release paths to select the intended existing signed app and CLI release.

The installer contains the compiled package and official Node 24.22.1 runtime. The runtime archive is pinned to its published SHA-256 digest. Node's license travels with it. No provider credentials, test output, or local configuration belongs in the release archive. When a signed native app registered the installation's app-runtime, the command wrapper uses that app-owned Node while continuing to load the selected installed JavaScript. Installing a CLI update preserves the registration. See Node and tool permissions.

  1. Choose the next version with npm run version:next. Minor and patch top out at 9: after x.y.9 increment the minor, and after x.9.9 increment the major. Thus 0.9.9 becomes 1.0.0 and 1.9.9 becomes 2.0.0, never x.10.x. A legacy patch above 9 such as 0.2.36 moves to 0.3.0. Set that value in package.json, confirm it with npm run version:next -- --check (it prints ok, or the problem and a non-zero exit when the version is invalid), then update the lockfile. Run the development gate.
  2. Download the official macOS ARM64 runtime into .state/release-runtime and verify it against Node's published checksums. Extract it in that directory.
  3. Run node scripts/build-release.mjs --allow-dirty only for preflight testing. Test the extracted installer on an isolated prefix, including a repeated install, paths containing spaces/quotes, and a conflicting command.
  4. Run the release validation checks against the installed candidate. Use isolated homes and the matching signed engine; the checklist identifies which checks use real provider inference and which use local mocks.
  5. Commit and push the final source after its full local gate passes. Hosted CI remains manual and opt-in; see the CI cost policy.
  6. Run node scripts/build-release.mjs from the clean release commit. It always compiles before packing, refuses generated source drift or missing package entry points, and verifies the complete backend manifest before archiving. Its final JSON line gives absolute asset and checksum paths; inspect the archive's file list and verify SHA256SUMS.
  7. Before publishing anything, run the forward check against the release the fleet runs now, so every Mac picks up the new release without a restart: node scripts/check-forward-compat.mjs --deployed ~/.local/share/subscription-gateway/current/package --candidate ARCHIVE. It runs the deployed release's own forwarding code against the candidate package in a private /tmp layout and exits non-zero (deployed_release_refuses_candidate, code_mode_differs, ...) when that release would refuse chats of the new one. Do not publish a release that fails it. same_gateway_sdk and forwards pass. The app publisher (scripts/release-macos-app.mjs --publish) runs the same check against --installed-release before its updater fixture and refuses to publish on failure.
  8. Publish the versioned archive and checksums to the standard GitHub release. Install exactly that asset on each device, then run gateway versions --check there and require a zero exit before the device is treated as updated. Run gateway rollout after each install and require ready before declaring the machine shipped. It ties immutable loaded process releases, the service's PID-frozen release, the inference response header, execution-host identity, and a current tool canary together; a moved current link or an HTTP 200 alone cannot pass it. Use gateway rollout --verify for a new isolated no-provider canary when a cached proof is pending. See rollout readiness. After every install, also run gateway health on every Mac; its verdict must be ok or warn (see the watchdog). A fail means this Mac is broken or half-moved, and the alert push is not configured until <state>/alert-push-url exists with mode 0600. Run it as gateway health --smoke on the machine that took the new release first: that is the one pass that proves the app's own engine still chats, and its answer is remembered against the app build, so the ten-minute check is paid once per update.

Updating without quitting the app

After publication, refresh the public version and changelog with npm run docs:review, then npm run docs:deploy -- --remote https://github.com/edihasaj/gatefox.git --push. The publish step selects complete GitHub app releases, so an unreleased checkout version cannot become the landing page's download.

Installing a release only switches the current link; nothing is stopped. Each long-running process checks once a minute whether current points to a different release and moves to it at an idle moment:

Process Moves when How
Stdio adapter (one process per app connection) no active turn, no pending request, 30 seconds quiet exits; the app starts a new adapter on the new release within seconds
SSH listener (remote Mac) a bootstrap on the installed release finds it running another release, or a newer release is installed and two idle checks in a row (about 1–2 minutes) pass with no active session; since 1.6.3, also when a connected app's proxy finds it on another release and it is idle (no request, turn or approval in flight, 30 s quiet), checked a minute after the connection opens and every 2 minutes after (each up to 30 s later, at random); since 1.6.4 a declined request is repeated after 35 s and again after 50 s, since a listener also declines for any traffic in the last 30 s on bootstrap replacement the old listener keeps serving its open sessions and the new one binds a fresh alias; on self-retire, or when the proxy asks it to retire (SIGUSR2, the same request gateway native install sends), the listener exits and the app's reconnect starts the new one
Inference server at its next check, busy or not the installed release's handler takes over the same socket in the same process: new requests use the new code at once, running requests finish on the old code; no connection is refused. A release whose serve runtime is incompatible falls back to an idle restart
Supervisor, execution host, relay fully idle: no app connected and no chat loaded; after 15 minutes (awake time) of waiting, quiet is enough: no turn running, no background terminal and no turn ended in the last 5 minutes, with apps still connected (services) idle or quiet restart; launchd starts it on the new release, and connected apps resume their chats on it

Every server response carries X-Gateway-Release with the release that served it. Unix sessions never use the stdio adapter's per-session upgrade exit. Reconnecting to the same old listener would otherwise repeat the disconnect without loading new code. The listener, not an individual session, owns its upgrade lifecycle. Until the supervisor is idle or quiet it keeps the previous release, so adapters and the host must stay protocol-compatible across one release. Each move is logged as self_upgrade, except an SSH listener replaced by a bootstrap, which is logged as ssh_listener_replaced. In that event from is unknown when the old listener recorded no release, which means it started from a checkout or from a release before 0.4.4, so the first 0.4.4 rollout logs from: "unknown" for every listener it replaces. Processes started from a checkout never move themselves, except that an SSH listener started from a checkout is replaced when a bootstrap on the installed release connects. Releases before 0.2.32 do not have this; moving from them needs one service restart. In-place server takeover starts with 0.2.35; a 0.2.32 to 0.2.34 server needs one restart of its inference process to reach it.

Run gateway versions --check on every Mac after an install. A process is lagging when it loaded an older release than the installed current target, which the command reads from mapped files with a launch-path fallback. For an app-owned Node whose JavaScript is not mapped, also check gateway health: a service that publishes runningRelease is compared against its live PID instead of the movable current link. Older services need one normal restart to publish that field. The X-Gateway-Release header alone is not proof of that: it names whichever release served one inference response, and says nothing about the other processes on the machine. Components that move only when idle, such as the service supervisor, the shared execution host and the engines it starts, the SSH listeners, the desktop adapters and the native relay, can stay on the previous release until they go idle. That is expected, so recheck after they have been idle rather than force-killing them; a kill skips the idle guard and cuts off whatever that process was holding. See diagnostics for what the command prints.

The installer keeps the current release, the previous one and the newest other release. Older releases are deleted after the checks below pass: each is first renamed to releases/.retired-NAME-PID and then removed, so an interrupted deletion never leaves a half-deleted release under its own name, and the next install finishes it. Published releases can always be downloaded again, so nothing goes to the Trash. Each managed CLI opens its immutable package/dist/cli.js and release marker read-only at startup; each loaded server runtime shares one marker descriptor until that runtime's own in-flight requests finish. This keeps an app-owned Node that was launched through the movable current link visible to lsof after later installs. Before pruning, the installer checks same-user lsof, ps, and managed LaunchAgent references. If any inspection fails, or an older process has no immutable bootstrap proof, pruning is skipped for that install. A later install cleans up once the holder exits.

The build refuses dirty source unless the preflight flag is explicit. It does not download credentials or publish automatically.

For a release containing the native connected-machine adapter, build and test the matching relay first, then use node scripts/build-release.mjs --with-native. The relay's source digest, package version, pinned SDK revision, archive checksum, linked Codex version and release profile must match the archive. The relay pins its own SDK (rust-v0.163.0-alpha.10.2 from 0.4.7) and the gateway SDK keeps its own, so reusing components checks each against its own pins. Run node scripts/e2e-native-relay.mjs --root /tmp/gwrl-$(date +%s) with the built relay before publishing. Verify the original machine registration and model picker before publishing. See native transport checks.

Gateway executors can use a separately compiled SDK with selected-model compaction. Build and test it from the checksum-pinned upstream archive:

node scripts/build-gateway-sdk.mjs --test
node scripts/build-gateway-sdk.mjs
node scripts/build-release.mjs --with-native --with-gateway-sdk

Disk space

Release scripts check free space before their heavy work. Gateway SDK tests require 40 GiB and SDK-only builds require 25 GiB; the native relay requires 40 GiB; the release archive requires 4 GiB; the macOS app release and its updater-fixture check each require 10 GiB; and the updater fixture requires 6 GiB on its --root volume before inspecting the candidate. Set GATEWAY_RELEASE_MIN_FREE_GIB to a positive number to replace the applicable default when a deliberately tighter host has been reviewed.

build-release.mjs stages packages under a per-checkout namespace in the operating system temporary directory, then retains only the newest successful archive and SHA256SUMS there. It removes its staging directory after success or ordinary failure. Signal cleanup is best effort between synchronous archive commands; a force-killed build can leave its exact build.lock, which must be removed manually only after checking that its recorded owner has stopped. Concurrent release builds for the same checkout refuse. Copy or upload the reported absolute paths before macOS clears temporary storage. To intentionally retain build outputs, pass --output-dir /absolute/release-assets; that directory is never broadly pruned, so its archives remain the operator's responsibility.

After a release is published and installed, run npm run release:clean to reclaim the cached Cargo targets while preserving release artifacts and source archives. Use npm run release:clean -- --dry-run to review the planned Cargo cleanup. It retains updater-fixture directories and prints their sizes with the exact manual Trash command. A failed updater fixture reports errorClass and, when space is exhausted or critically low, disk: "low" without exposing its error message.

For a development-profile test run, the SDK's large test futures need a larger thread stack: RUST_MIN_STACK=33554432 node scripts/build-gateway-sdk.mjs --test --dev.

The gateway SDK has its own executable, licenses, dependency inventory, and build receipt, and it now also carries codex-code-mode-host, the Code Mode companion built from the same pinned source; app signing gives that file its own identifier and the two JIT entitlements V8 needs, and the companion statically links V8 through Codex's code-mode runtime, whose license is listed with the Cargo packages in dependencies.json. The SDK build downloads V8's pointer-compression sandbox library from OpenAI's rusty-v8-v<version> release, as Codex's setup-rusty-v8 action does. It accepts the library only after its checksum manifest matches the digest pinned in the verified source (third_party/v8/rusty_v8_<version>_release_manifests.sha256), and records the version and archive digest as rustyV8 in build.json. After a build, run node scripts/e2e-code-mode-host.mjs --artifact .state/gateway-sdk-build/artifact --catalog ~/.codex/models_cache.json to prove a Code Mode cell runs. The release builder verifies its version, SDK revision, archive and patch digests, source digest, binary checksum, and capability. It refuses modified cached SDK source or unexpected artifact files. The native signed Codex executable and desktop bundle remain unchanged. Only a verified gateway executable can enable the selected-model compaction capability; older installations retain their existing fallback behavior. Run the real SDK mock cases and gateway continuation checks before publishing a release with this capability. The upstream release archive declares workspace version 0.162.0-alpha.17.2 while its lockfile retains 0.0.0. A guarded helper repairs exactly those 161 source-less workspace versions. All external package versions, sources, checksums, and dependency blocks remain byte-identical. The repaired lockfile and helper are hashed in the receipt; Cargo tests and builds still use --locked. The test gate runs three executor-identity unit tests in codex-model-provider-info and six compaction cases: the four for the gateway's ids (gatefox, subscription_gateway) and native chats, and two for openai inside a marked gateway executor. It also runs two queue integration tests covering both gateway identities, writes during a pause, cold resume, explicit continuation, explicit queue start, and the native failure behavior. Three cache-recovery accounting tests cover cumulative usage below, at and above the context window, absent usage and unchanged unmarked behavior. Also run scripts/e2e-cache-recovery.mjs against the built SDK and current Gateway code before publishing. It must prove that a third cold call and unchanged retry stop without a fake context-overflow signal or automatic compaction. Only a separate explicit client compaction may produce a smaller continuation in the same chat and model. See cache safety. The compaction cases use the upstream compact suite with its original test aliases and constructor. The runner temporarily narrows the suite aggregator to that module, avoiding unrelated upstream test compilation errors, then restores the exact original bytes. Its digest joins the receipt; the verified SDK source tree must match again before building the executable. The cases set the executor marker in the process environment, so the gate runs every case on one thread. Capability gateway-executor-v3 combines the executor identity patch with scripts/patches/gateway-queue.patch, which preserves queued messages after failed gateway turns. scripts/patches/gateway-dynamic-tools.patch also keeps declared-deferred client tools reachable when the selected model lacks search; Seven additional Rust cases preserve search, schemas, policy and namespace rules; the existing deferred-filtering regression also runs explicitly in the gate. The managed role context patch also carries paired context and compaction limits through the SDK's typed role projection, only inside marked Gateway executors for explicitly selected managed aliases and the named Qwen worker alias. Three Rust cases check its bounds, marker and alias isolation, and preservation of other runtime settings. All four patches contribute to the patch and source digests. A changed patch requires a new SDK build even when the capability name stays the same; it cannot reuse an SDK component with older source hashes. See client tool reachability for the mock E2E gate and the separate desktop scheduling boundary.

A JavaScript-only fix can explicitly reuse unchanged native components from a previous published installer instead of rebuilding Rust. Download the exact published asset and use its independently verified archive checksum:

node scripts/build-release.mjs --with-native --with-gateway-sdk \
  --reuse-components-from /absolute/path/to/previous-published-release.tar.gz \
  --components-sha256 VERIFIED_ARCHIVE_SHA256

Reuse verifies the whole archive checksum, safe tar entries, every payload file against its complete manifest, and the selected components' current source hashes, upstream pins, release profile, and capabilities before writing a release stage. Archive links and path overrides are refused. Validated macOS provenance metadata is ignored; only the six allowed files for each selected component are copied. Any changed component source requires a fresh build. Without both explicit reuse arguments, the usual matching-release artifact checks remain in force.

A gateway SDK fix can reuse the unchanged relay from its original published archive. Build the SDK for this version first, then use --with-native --with-gateway-sdk --reuse-native-from ORIGINAL_ARCHIVE --native-sha256 DIGEST. The relay retains its verified original receipt; the fresh SDK must match the current source, patch digests and release version.

A native relay fix can ship a freshly built relay while still reusing the gateway SDK from that bundle. Build the relay for this version first (node scripts/build-native-relay.mjs --test --dev, then node scripts/build-native-relay.mjs), then add --fresh-native:

node scripts/build-release.mjs --with-native --with-gateway-sdk \
  --reuse-components-from /absolute/path/to/previous-published-release.tar.gz \
  --components-sha256 VERIFIED_ARCHIVE_SHA256 --fresh-native

The flag requires both component flags and both reuse arguments. Only the gateway SDK is verified and copied from the bundle, so release.json lists only that component. The relay goes through the normal artifact checks: package version, release profile, pinned SDK revision, and the current native source digest. The bundle must be the one that built the gateway SDK; a bundle that itself reused the component is refused. Reuse requires a fresh release stage; move any earlier stage for that version to Trash before rebuilding it.

Reused build receipts keep their original version and bytes. The new package/release.json binds that earlier bundle's version and archive checksum to each reused file's size and checksum; runtime verification checks this binding. The CLI's package version and release commit still identify the new release. Run the installed SDK checks for the changed JavaScript path before publishing.

Upgrade and recovery

The installer verifies file checksums before changing the current release link. It saves the previous release target, and only when the current release is a different version, so reinstalling the same version keeps the last different release for rollback. Before switching, it runs the packaged gateway-codex --version once (60 s limit, failures only warn) so macOS scans the new large executable then and not during the service's first host start. Reinstalling identical content is safe; different content under the same version is rejected. Preserve the existing data directory. Service migration requests a safe idle stop before replacing a managed LaunchAgent. Busy inference or request uploads refuse the upgrade. The managed job is temporarily disabled during migration. The owning daemon unloads its job only after its idle guard accepts the stop, so KeepAlive cannot restart the old command. Older daemons need one safe restart to load the installed idle-upgrade protocol. Failure reenables the job and restores its earlier plist.

A 0.4.0 adapter that is still running, or a rollback to 0.4.0, reads the same state files, so this build saves a chat's tier or a new-chat default only when its row declares it; see rollback and mixed versions.

Use gateway service status, gateway doctor, gateway desktop status, and gateway versions --check after installation on each Mac. Registration prepares future app launches and does not interrupt an open app. One fresh launch is needed after first registration. The private desktop launch hook is specific to the tested Codex build, so verify it after app upgrades.

Preview job consolidation with gateway service consolidate. Apply it only after local and remote chats finish. See the service migration checks. An updated installer does not consolidate running jobs automatically. Test an isolated supervisor under real launchd with node scripts/e2e-service-supervisor.mjs /absolute/installed/bin/gateway. The fixture exercises HTTP streaming, child and supervisor crashes, login setup, and shutdown. Its relay child is a mock; it preserves the working native relay. For next-login cleanup changes, run node scripts/e2e-login-consolidation.mjs /absolute/installed/bin/gateway. It uses uniquely named launchd fixtures to verify live-PID preservation, refused manual promotion, and actual primary-job activation. It stops only those fixtures and uses no provider inference or native authentication.

To remove the installation, run gateway uninstall. On macOS it first reads the desktop state as a preflight and stops before removing anything, including when a launchctl getenv fails or a login promotion is staged for the next login. A unified service plan is not a refusal there, because the service step trashes that file with the service; off macOS the preflight is skipped and the service step gives its own platform answer. A later desktop error names what is already gone; resolve that cause, then rerun gateway uninstall. The installation and managed LaunchAgents move to Trash. Credentials and native Codex configuration remain. CLI registration cannot make an unconfigured SSH/cloud backend understand gateway models; test each execution backend used by a shared chat.