For maintainers. Using T3 Code? See docs/user.
This document covers the unified release workflow for stable and nightly desktop releases.
.github/workflows/release.ymlworkflow_dispatch with channel=stable, the normal way to ship stablev*.*.* for a stable release of an explicit commitworkflow_dispatch with channel=nightlymain HEAD.
Nightly is the release candidate: verify the nightly, then promote it. Merges to main keep
landing while you verify and never leak into the stable build.0.0.39-nightly.* ships as 0.0.39).
Pass the version input to override it, for example for a minor bump.vX.Y.Z tag by hand still works and builds exactly the tagged commit. Use it when
the commit to ship is not the latest nightly, such as a cherry-picked fix on a release branch.arm64 DMGx64 DMGx64 AppImagex64 NSIS installerX.Y.Z (for example 1.2.3-alpha.1) are published as GitHub prereleases.X.Y.Z releases are marked as the repository's latest release.latest*.yml, nightly*.yml, and *.blockmap) in release assets.apps/server, npm package t3) with OIDC trusted publishing from the same workflow file:latestnightlylatest hosted app channelnightly hosted app channelStable releases require these GitHub Actions secrets in addition to the platform and deployment credentials documented below:
RELEASE_APP_IDRELEASE_APP_PRIVATE_KEYThe finalize job uses them to commit and push aligned package versions to main as the Release App.
GitHub Release publication uses the repository-scoped workflow token so it has a rate-limit quota
independent from the shared Release App installation.
The relay is a shared control plane versioned separately from client releases. Stable and nightly client builds must point at the same relay so users see the same linked environments when switching release channels.
.github/workflows/deploy-relay.yml deploys Alchemy stage prod on every push to main. The
release workflow reads the relay URL and Clerk client configuration from the existing production
GitHub Actions environment before building desktop, CLI, or hosted web artifacts.
Required repository variables shared by relay deployments:
CLOUDFLARE_ACCOUNT_IDPLANETSCALE_ORGANIZATIONAXIOM_ORG_IDRequired repository secrets shared by relay deployments:
CLOUDFLARE_API_TOKENPLANETSCALE_API_TOKEN_IDPLANETSCALE_API_TOKENAXIOM_TOKENRequired production environment variables:
RELAY_API_ZONE_NAMERELAY_TUNNEL_ZONE_NAMECLERK_PUBLISHABLE_KEYCLERK_JWT_AUDIENCECLERK_JWT_TEMPLATECLERK_CLI_OAUTH_CLIENT_IDAPNS_ENVIRONMENTAPNS_TEAM_IDAPNS_KEY_IDAPNS_BUNDLE_IDOptional production environment variables:
RELAY_DOMAIN when overriding the derived relay.<RELAY_API_ZONE_NAME> domainRequired production environment secrets:
CLERK_SECRET_KEYAPNS_PRIVATE_KEYThe account-scoped repository credentials are consumed by Alchemy while provisioning relay stages; they
are not bound into the relay Worker. The production deployment uses an Axiom personal access token,
so AXIOM_ORG_ID must accompany AXIOM_TOKEN. The prod stage owns the retained PlanetScale
database. Local personal stages provision isolated branches from it and are never deployed by CI.
Production adopts the configured relay API and tunnel DNS zones as retained Cloudflare resources.
Personal stages reference the production-owned zones.
Developers deploy personal stages locally rather than through pull-request automation:
| 1 | vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local |
After a nightly release is published, the release workflow deploys the same commit to the marketing site's Vercel production project. Stable releases do not deploy the marketing site because they can promote an older nightly commit.
The job looks up the t3code-marketing project using the existing VERCEL_TOKEN
and VERCEL_ORG_ID secrets. It also respects the optional VERCEL_TEAM_SLUG
variable. The Vercel project's root directory must be apps/marketing.
Git deployments remain disabled in apps/marketing/vercel.ts.
The hosted app is intentionally not deployed by Vercel's Git integration. The
web project disables automatic Git deployments in apps/web/vercel.ts via
git.deploymentEnabled: false, and .github/workflows/release.yml deploys the
web app with Vercel CLI after the GitHub Release succeeds.
Required GitHub Actions secrets:
VERCEL_TOKENVERCEL_ORG_IDVERCEL_PROJECT_IDOptional GitHub Actions variables:
VERCEL_TEAM_SLUG: overrides the Vercel CLI scope when the team slug is preferred over the VERCEL_ORG_ID secret.T3CODE_WEB_ROUTER_URL: defaults to https://app.t3.codes.T3CODE_WEB_LATEST_DOMAIN: defaults to latest.app.t3.codes.T3CODE_WEB_NIGHTLY_DOMAIN: defaults to nightly.app.t3.codes.Required Vercel domains:
app.t3.codes: the router domain users open, updated by stable releases.latest.app.t3.codes: channel alias updated by stable releases.nightly.app.t3.codes: channel alias updated by nightly releases.The router domain uses apps/web/vercel.ts routes. Users opt into a channel by
visiting /__t3code/channel?channel=latest or
/__t3code/channel?channel=nightly; the router stores the
t3code_web_channel cookie and rewrites future requests on app.t3.codes to
the matching channel alias.
The release deploy job rewrites release package versions before upload so the
hosted app's About panel renders the release version. Stable deploys alias the
same deployment to both the latest channel and the router domain so the router
rules stay current. Nightly deploys only alias the nightly channel. The job
also passes VITE_HOSTED_APP_CHANNEL=latest|nightly, which renders the hosted
update track selector in the About panel. Changing the selector navigates
through /__t3code/channel on the router domain so the user's channel cookie is
updated before redirecting to the hosted app root.
One-time Vercel dashboard setup:
apps/web.vercel.ts setting is the source-of-truth, but disconnecting Git in the
dashboard is also safe.app.t3.codes points at a deployment containing the router
rules in apps/web/vercel.ts. Future stable releases keep this alias current..github/workflows/release.ymlworkflow_dispatch with channel=nightlyvX.Y.Z-nightly.YYYYMMDD.<run_number>nightly-v... is accepted only as a legacy previous-nightly tagmake_latest is always false0.0.17 produces nightlies on 0.0.18-nightly.*.nightly updater channel, so desktop users can opt into that track independently from stable.apps/server, npm package t3) to the nightly npm dist-tag using the same nightly version.main.Connected servers update to the client's exact version, not to an npm dist-tag. Every released
desktop or hosted client version must therefore have a matching t3@<version> package available on
npm before users can receive that client.
The workflow enforces this ordering:
publish_cli publishes the exact stable or nightly version to npm.release depends on publish_cli before exposing desktop artifacts in GitHub Releases.deploy_web depends on release before moving the hosted channel to the new client.Preserve these dependencies when changing the release graph. Publishing a client first would leave the Update server action targeting a package version that does not exist yet.
For a release smoke test, confirm npm view t3@<version> version returns the expected version, then
connect the new client to a server on the previous version and verify that the update action
reconnects to the matching server. When the release adds database migrations, verify that the
remote update applies them and reconnects. A failed trial must restore the database snapshot and
restart the previous server. If the installed launcher does not support the target protocol,
verify that the update stops before restart and run npx t3@<version> service update once on the
server machine. Also test the manual or desktop-managed guidance when those environments are
available.
apps/desktop/src/updates/DesktopUpdates.ts.electron-updater adapter: apps/desktop/src/electron/ElectronUpdater.ts.apps/desktop/src/main.ts only wires the updater layers into the desktop runtime.provider: github) configured at build time.T3CODE_DESKTOP_UPDATE_REPOSITORY (format owner/repo), if set.GITHUB_REPOSITORY from GitHub Actions..exe, .dmg, .AppImage, plus macOS .zip for Squirrel.Mac update payloads)latest*.yml for stable releases, nightly*.yml for nightly releases*.blockmap files (used for differential downloads)electron-updater reads latest-mac.yml on stable and nightly-mac.yml on nightly, for both Intel and Apple Silicon.Windows packages the bundled server and only its runtime-external/native
dependency closure in resources/server.asar. Native modules and helper
executables declared as unpacked by that archive must be present at the matching
paths below resources/server.asar.unpacked. The Windows-native backend reads
the archive in place through Electron. Packaged Windows builds also ship a
Linux-only resources/wsl-runtime.tar.gz plus its SHA-256 sidecar. WSL verifies
and extracts that archive into ~/.t3/wsl-runtime/sha256-<archive-digest> inside
the selected distro, then reuses it for later launches of the same update. The
Windows-side wsl-server-tree/<version> extraction remains a fallback and is
removed after the distro-local runtime passes preflight.
Windows keeps JavaScript and package metadata inside app.asar and unpacks only
native libraries and helper executables. Avoid enabling whole-package smart
unpacking: each loose file adds work to NSIS installation and counts against
the payload limit.
The artifact builder rejects a Windows package when any of these invariants break:
resources/server.asar is absent or does not contain the server entry.resources/server.asar.unpacked.server.asar through its .unpacked sibling.Cross-architecture Windows builds retain every structural and extracted-sidecar check, but skip executing the target Electron binary. A same-architecture build for each release target must exercise the primary native-load probe.
NSIS differential packaging remains enabled. A sidecar layout transition can produce a larger one-time download; subsequent small releases retain their blockmaps, with a 60 MB maximum for a representative sidecar-to-sidecar update.
The workflow invokes node apps/server/scripts/cli.ts publish after aligning package versions. That
script temporarily prepares the t3 package, then runs vp pm publish --filter t3 ... from the
repository root so workspace publish configuration is applied correctly.
Checklist:
t3 (or rename package first if needed)..github/workflows/release.ymlvX.Y.Z and push; workflow will:X.Y.Zlatestnightly.There is no dry-run tag path. Pushing any accepted non-nightly tag, including
v0.0.0-test.1, classifies the run as the stable channel. It publishes t3 with npm dist-tag
latest, creates a real GitHub Release, aliases the hosted app to latest.app.t3.codes and
app.t3.codes, and can commit a version bump to main in the finalize job. Do not push a test tag
to validate the workflow.
The workflow has no non-publishing workflow_dispatch mode. Use normal CI or local quality gates to
validate checks and builds without shipping. To exercise the complete release graph at lower stable
risk, manually dispatch channel=nightly; this still publishes a real nightly npm package, GitHub
prerelease, desktop updater release, hosted nightly alias, and marketing site, but it does not update stable app aliases or
commit a version bump to main. Only run it when a real nightly release is acceptable.
Manual channel=stable is also a real stable-channel release. Omitting signing secrets only makes
platform artifacts unsigned; it does not prevent publication.
Required secrets used by the workflow:
CSC_LINKCSC_KEY_PASSWORDAPPLE_API_KEYAPPLE_API_KEY_IDAPPLE_API_ISSUERMACOS_PROVISIONING_PROFILE (base64-encoded provisioning profile with Associated Domains)Required repository variables:
APPLE_TEAM_IDOptional repository variables:
CLERK_PASSKEY_RP_DOMAINS: comma-separated RP-domain override. By default, the build derives the
domain from the production Clerk publishable key.Checklist:
com.t3tools.t3code and enable Associated Domains.Developer ID Application certificate and a compatible provisioning profile for that
App ID with Associated Domains enabled..p12 from Keychain..p12 and store as CSC_LINK.MACOS_PROVISIONING_PROFILE..p12 export password as CSC_KEY_PASSWORD, and set APPLE_TEAM_ID to the
10-character Apple Developer Team ID.APPLE_API_KEY: contents of the downloaded .p8APPLE_API_KEY_ID: Key IDAPPLE_API_ISSUER: Issuer IDcom.apple.developer.associated-domains entitlement.Notes:
APPLE_API_KEY is stored as raw key text in secrets.AuthKey_<id>.p8 file at runtime.MACOS_PROVISIONING_PROFILE, validates it with security cms, and passes it
to the desktop packager.Required secrets used by the workflow:
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRETAZURE_TRUSTED_SIGNING_ENDPOINTAZURE_TRUSTED_SIGNING_ACCOUNT_NAMEAZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAMEAZURE_TRUSTED_SIGNING_PUBLISHER_NAMEChecklist:
channel=stable. Leave version empty unless the version
should differ from the one the nightly previewed.Resolve release commit notice names the nightly tag and commit you verified. If a
newer nightly published in between, the run builds that one instead.publish_cli publishes the exact release version before the release jobAPPLE_TEAM_ID are populated and non-empty.APPLE_TEAM_ID.com.t3tools.t3code and includes
Associated Domains.