mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 03:41:17 +02:00
609 lines
38 KiB
Markdown
609 lines
38 KiB
Markdown
# Release Checklist
|
|
|
|
> For maintainers. Using T3 Code? See [docs/user](../user/).
|
|
|
|
This document covers the unified release workflow for stable and nightly desktop releases.
|
|
|
|
## What the workflow does
|
|
|
|
- Workflow: `.github/workflows/release.yml`
|
|
- Triggers:
|
|
- manual `workflow_dispatch` with `channel=stable`, the normal way to ship stable. Stable
|
|
and nightly dispatches must select `main`; preview may select any branch. The channel defaults
|
|
to preview so an omitted selection cannot publish a stable release.
|
|
- push tag matching `v*.*.*` for a stable release of an explicit commit
|
|
- scheduled nightly check every 30 minutes
|
|
- manual `workflow_dispatch` with `channel=nightly`
|
|
- manual `workflow_dispatch` with `channel=preview`, the maintainers' test train. It exercises the whole release flow (build, sign, notarize, smoke, publish) for a commit that end users must never receive, which is how an unmerged branch or a risky change gets a real release run before it lands. It builds the triggering commit with nightly's versioning under the `preview` prerelease identifier (`0.0.41-preview.<date>.<run>`) and publishes a GitHub prerelease plus the npm packages under the `preview` dist-tag. Preview is not on the schedule, no default npm dist-tag points at it, its desktop builds carry no update feed, and no updater manifest (`latest*.yml`, `nightly*.yml`, blockmaps) is attached, so a stable or nightly install cannot be offered one. The only ways onto it are downloading the release by hand, `npx t3@preview`, `T3CODE_CHANNEL=preview` for the install scripts, or `t3 update --channel preview` from a terminal; each prints a warning, and the CLI asks for confirmation when the running build is not itself a preview. The release itself is named as a maintainer test build and its body is a warning rather than generated notes: a changelog of unmerged branch history is not a changelog, and nightly and stable notes are unaffected because each series resolves its previous tag within its own channel. The hosted web app, AUR, and Discord announcements are skipped. Keep it; it costs nothing when idle.
|
|
- A manual stable release builds the commit of the latest published nightly, not `main` 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.
|
|
- The version defaults to the one the nightly previewed (`0.0.39-nightly.*` ships as `0.0.39`).
|
|
Pass the `version` input to override it, for example for a minor bump.
|
|
- The stable tag is created on the nightly's commit when the GitHub Release is published.
|
|
- Pushing a `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.
|
|
- Runs lint, typecheck, and tests alongside artifact builds. Publishing waits for every check.
|
|
- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients.
|
|
- Builds the platform-independent JS (server bundle, web client, Electron main) once in the `build_bundle` job and hands it to every platform job as the `js-bundle` artifact; the platform jobs only package it, so no runner rebuilds it.
|
|
- Builds six desktop artifacts in parallel for both channels, each as its own job (`desktop_<platform>_<arch>`, one call of `release-desktop.yml`) on hardware of its own architecture, gated only on the bundle. The Windows jobs embed the same-arch Linux CLI archive as the WSL runtime and wait for that artifact partway through, not for the whole Linux job:
|
|
- macOS `arm64` DMG
|
|
- macOS `x64` DMG
|
|
- Linux `x64` and `arm64` AppImage and `.deb`, from one electron-builder run. The `.deb` updates in the app through electron-updater, which installs it with `dpkg`.
|
|
- Windows `x64` and `arm64` NSIS installer
|
|
- Publishes one GitHub Release with all produced files.
|
|
- Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases.
|
|
- Only plain stable `X.Y.Z` releases are marked as the repository's latest release.
|
|
- Nightly runs are always GitHub prereleases and never marked latest.
|
|
- Automatically generated release notes are pinned to the previous tag in the same channel, so stable compares to the previous stable tag and nightly compares to the previous nightly tag.
|
|
- 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 built, signed, and smoke-tested on hardware of its own architecture. 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 npm packages exist for people who run `npx t3` or `npm install -g t3` themselves and carry the same archive contents; 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`.
|
|
- 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 on its build runner (`scripts/smoke-cli-archive.ts`) before it is uploaded.
|
|
- Publishes the CLI to npm with OIDC trusted publishing from the same workflow file, as the same bytes the GitHub Release carries: `scripts/build-npm-platform-packages.ts` unpacks the five CLI archives into `@t3code/t3-<platform>-<arch>` packages (each with `os`/`cpu` set so npm installs only the matching one) and generates the `t3` launcher, whose `bin/t3.js` lists them as `optionalDependencies` and execs the installed executable. `npx t3` therefore needs Node only to run the launcher, never to run the server. `node apps/server/scripts/cli.ts publish` publishes the platform packages first and the launcher last, after a `--dry-run` pass over all of them so an auth or scope error fails before anything is live.
|
|
- stable releases publish npm dist-tag `latest`
|
|
- nightly releases publish npm dist-tag `nightly`
|
|
- preview releases publish npm dist-tag `preview`, which nothing resolves unless asked for by name
|
|
- one-time setup: the `@t3code` npm scope (org) must exist, and `t3` and each `@t3code/t3-<platform>-<arch>` package needs a trusted publisher registered for this workflow file (see below).
|
|
- Builds the hosted web app on Vercel while the desktop jobs run, and makes it live only after a release is published:
|
|
- stable releases are aliased to the `latest` hosted app channel
|
|
- nightly releases are aliased to the `nightly` hosted app channel
|
|
- Signing is optional and auto-detected per platform from secrets.
|
|
|
|
## Pull request macOS previews
|
|
|
|
Labeling a PR `preview:mac` publishes a signed, notarized Apple Silicon DMG with T3 Connect enabled
|
|
to the rolling `desktop-preview` prerelease, and works for fork PRs. The label is a one-shot request
|
|
for the commit it is applied to: the trusted workflow removes it once the build is in hand, and later
|
|
pushes do not build until a maintainer applies it again. Every signed preview is therefore a
|
|
per-commit maintainer decision, which matters because the result carries the Developer ID signature.
|
|
Vouching a contributor lets their labeled commits be signed; it is not a standing grant. The build is
|
|
split so the Developer ID certificate never shares a job with PR code:
|
|
|
|
- `.github/workflows/desktop-macos-preview.yml` runs on `pull_request` with no secrets and builds
|
|
only the JS bundle from the PR (the same `js-bundle` artifact `release.yml` produces).
|
|
- `.github/workflows/desktop-macos-preview-publish.yml` runs on `workflow_run` from `main`. It
|
|
refuses unless the PR is open, still labeled, its head is the built commit, and the author is a
|
|
bot, a collaborator, or listed in `.github/VOUCHED.td` (read from the default branch, so a PR cannot vouch
|
|
for itself). It then packages and signs the bundle through `release-desktop.yml` checked out at
|
|
`main`, so packaging, native helpers, and the Electron/desktop dependencies come from `main`, not
|
|
the PR. Only the version and the public T3 Connect identifiers in `.env.example` are read from the
|
|
PR commit, as data, so the signed app's passkey entitlement matches the bundle. A PR that changes
|
|
packaging must use the `channel=preview` release train above instead.
|
|
|
|
Before handing the bundle to the signing runner, the trusted workflow validates its ZIP entries
|
|
and accepts only regular files under `server/dist` and `desktop/dist-electron`, plus the directory
|
|
entries that lead to those roots. The artifact cannot
|
|
overwrite packaging code or installed dependencies. The bundle is copied into the app, never executed,
|
|
on the signing runner. The
|
|
`pull_request_target` cleanup job in the publish workflow removes the download when the PR closes, or
|
|
when the label is removed by hand before a build consumed it, and never checks out PR code.
|
|
|
|
## Required release credentials
|
|
|
|
Stable releases require these GitHub Actions secrets in addition to the platform and deployment
|
|
credentials documented below:
|
|
|
|
- `RELEASE_APP_ID`
|
|
- `RELEASE_APP_PRIVATE_KEY`
|
|
|
|
The 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.
|
|
|
|
## T3 Connect relay deployment
|
|
|
|
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_ID`
|
|
- `PLANETSCALE_ORGANIZATION`
|
|
- `AXIOM_ORG_ID`
|
|
|
|
Required repository secrets shared by relay deployments:
|
|
|
|
- `CLOUDFLARE_API_TOKEN`
|
|
- `PLANETSCALE_API_TOKEN_ID`
|
|
- `PLANETSCALE_API_TOKEN`
|
|
- `AXIOM_TOKEN`
|
|
|
|
Required `production` environment variables:
|
|
|
|
- `RELAY_API_ZONE_NAME`
|
|
- `RELAY_TUNNEL_ZONE_NAME`
|
|
- `CLERK_PUBLISHABLE_KEY`
|
|
- `CLERK_JWT_AUDIENCE`
|
|
- `CLERK_JWT_TEMPLATE`
|
|
- `CLERK_CLI_OAUTH_CLIENT_ID`
|
|
- `APNS_ENVIRONMENT`
|
|
- `APNS_TEAM_ID`
|
|
- `APNS_KEY_ID`
|
|
- `APNS_BUNDLE_ID`
|
|
|
|
Optional `production` environment variables:
|
|
|
|
- `RELAY_DOMAIN` when overriding the derived `relay.<RELAY_API_ZONE_NAME>` domain
|
|
- `RELAY_TUNNEL_CLEANUP_MODE` with `off`, `dry-run`, or `enabled`. Missing and blank values use
|
|
`off`.
|
|
- `RELAY_LEGACY_TUNNEL_CLEANUP_MODE` with the same values, for tunnels whose host never registered
|
|
recovery. Missing and blank values use `off`.
|
|
|
|
Required `production` environment secrets:
|
|
|
|
- `CLERK_SECRET_KEY`
|
|
- `APNS_PRIVATE_KEY`
|
|
|
|
After changing a variable or secret, run the **Deploy T3 Connect relay** workflow manually from
|
|
`main` with **force** unchecked. Alchemy compares the values the Worker reads and redeploys it when
|
|
one changed. Check **force** only to redeploy resources with no detected change: a forced run also
|
|
replaces the Postgres runtime role and its password
|
|
([alchemy-run/alchemy#1832](https://github.com/alchemy-run/alchemy/issues/1832)).
|
|
|
|
The 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:
|
|
|
|
```sh
|
|
vp run --filter t3code-relay deploy -- --stage "$USER" --env-file .env.local
|
|
```
|
|
|
|
### Managed tunnel cleanup rollout
|
|
|
|
Keep `RELAY_TUNNEL_CLEANUP_MODE=off` for the first production deploy. That deploy applies the
|
|
nullable allocation migration and adds the recovery endpoints. Web and mobile clients need no
|
|
coordinated release. CLI and desktop server builds must reach users before cleanup is enabled,
|
|
because those builds register recovery and replace a deleted tunnel after wake.
|
|
|
|
1. Deploy the relay and migration with cleanup `off`.
|
|
2. Release the server build and confirm current hosts register recovery. Older hosts stay marked
|
|
legacy and are only candidates under the legacy switch below.
|
|
3. Set `dry-run`, run a relay deploy, and read the sweep counters (`scanned`, `wouldDelete`,
|
|
`skippedLegacy`, `skippedOrphan`, `failed`, `truncated`) across several sweeps. Each sweep records
|
|
them, and the active `mode`, as `relay.managed_endpoint_reaper.*` attributes on its
|
|
`relay.managed_endpoint_reaper.sweep` span in Axiom.
|
|
4. Run the disposable-host canary below.
|
|
5. Set `enabled` only after the canary recovers without a server restart.
|
|
|
|
The job runs every five minutes with a five-minute grace period for tunnels that lost their
|
|
connector, so a candidate is usually removed five to ten minutes after it goes down. Tunnels that
|
|
never connected wait an hour. One sweep attempts at most 100 deletions, so a backlog takes longer.
|
|
Changing `RELAY_TUNNEL_CLEANUP_MODE`, including turning cleanup off during an incident, needs a relay
|
|
deploy without force. Confirm the new `mode` on the next sweep span.
|
|
|
|
To roll back, set cleanup to `off` and run a relay deploy before downgrading any host. Keep the
|
|
recovery endpoints deployed while current server builds are in use. The nullable columns can stay.
|
|
|
|
### Legacy tunnel cleanup
|
|
|
|
A legacy tunnel belongs to a host that never registered recovery, usually one that went offline
|
|
before the recovery build shipped. `RELAY_LEGACY_TUNNEL_CLEANUP_MODE` deletes these once Cloudflare
|
|
reports them down, or never connected, for more than 7 days. It is independent of
|
|
`RELAY_TUNNEL_CLEANUP_MODE`, and every other check still applies.
|
|
|
|
A deleted legacy tunnel keeps its allocation, so its hostname is kept. When the host comes back:
|
|
|
|
- On a build with recovery, the connector is rejected and the host requests a replacement tunnel at
|
|
the same hostname.
|
|
- On an older build with a CLI link, startup provisions a new tunnel.
|
|
- On an older build linked from web or mobile, the host stays offline until T3 Code on that computer
|
|
is updated.
|
|
|
|
Ship the web and mobile builds that show the offline reason before enabling legacy cleanup, so a
|
|
user whose host is affected sees what to do. The relay adds the `tunnel_released_at` allocation
|
|
column in its first deploy with this change; the legacy switch stays `off` until you set it.
|
|
|
|
1. Run `vp run --filter t3code-relay tunnels:census` with a read-only Cloudflare token. It counts
|
|
tunnels in every relay stage. The reaper only sees its own stage's tunnels, so clean up the rest
|
|
by hand.
|
|
2. Set the legacy mode to `dry-run`, deploy, and read `wouldDeleteLegacy`, `legacyOver30Days`,
|
|
`totalDown`, and `totalInactive` on the sweep spans for a day. `wouldDeleteLegacy` counts only the
|
|
tunnels a sweep inspected, at most 500 per status. `totalDown` and `totalInactive` are Cloudflare's
|
|
counts of this stage's tunnels down for over five minutes and never connected for over an hour.
|
|
They include ones the reaper skips, so they are an upper bound on the backlog. The share of `wouldDeleteLegacy` in each sweep's `scanned` estimates how
|
|
much of that total is eligible.
|
|
3. Run the legacy steps of the disposable-host canary below.
|
|
4. Before enabling, confirm the web and mobile builds that show the "update T3 Code on that computer"
|
|
message are live. Without them, a user whose older host lost its tunnel only sees it as offline.
|
|
5. Set the legacy mode to `enabled`. One sweep deletes at most 100 tunnels, four at a time, and
|
|
stops starting new deletions after 90 seconds. A backlog of 20,000 takes about 17 hours if each
|
|
sweep finishes its 100. Watch `deletedLegacy`, `attempted`, `failed`, and `truncated`. An
|
|
`attempted` well under 100 with `truncated` set means the sweep stopped early: either the time
|
|
budget ran out or Cloudflare rate-limited a deletion. The counters don't say which; the relay
|
|
logs a warning with the Cloudflare error for each failed deletion.
|
|
|
|
In Axiom, filter the relay traces dataset on `name == "relay.managed_endpoint_reaper.sweep"` and
|
|
chart the `attributes.custom.relay.managed_endpoint_reaper.*` fields over time.
|
|
|
|
Set the legacy mode back to `off` and deploy if any of these happen:
|
|
|
|
- `failed` stays above a few per sweep. Read the warning log for the Cloudflare error.
|
|
- Users report an environment that is offline with the update message after they have updated T3
|
|
Code on that computer and restarted it.
|
|
- Relay request errors rise while sweeps run. Deletions share the Postgres connection pool with
|
|
request handlers.
|
|
|
|
Turning the legacy mode off stops new legacy deletions; `RELAY_TUNNEL_CLEANUP_MODE` keeps deleting
|
|
tunnels of hosts with recovery while it is `enabled`. Deleted tunnels stay deleted; their hosts
|
|
recover as described above.
|
|
|
|
### Disposable-host canary
|
|
|
|
This test has not been run against a real Cloudflare account. Run it against a disposable relay
|
|
stage, test Cloudflare account, disposable host, and disposable T3 home. Keep production cleanup at
|
|
`off` or `dry-run` until it passes. Do not stop a daily-use T3 server.
|
|
|
|
1. Deploy the disposable stage with cleanup `dry-run`. Link a first disposable environment through
|
|
web or mobile settings and confirm its tunnel is healthy and recovery is registered.
|
|
2. Stop that host and restart the same T3 home on a different local port. Confirm the public
|
|
hostname reaches the new port and sends nothing to the old one.
|
|
3. Link a second disposable environment with a server build that predates recovery registration.
|
|
Capture its managed `cloudflared` child PID, confirm it belongs to that host, and pause only that
|
|
child with `kill -STOP <legacy-pid>`. Wait until Cloudflare reports it down for over five minutes.
|
|
4. Capture the first environment's `cloudflared` child PID from its server logs, confirm ownership,
|
|
and pause it with `kill -STOP <first-pid>`. Wait until Cloudflare reports it down for over five
|
|
minutes.
|
|
5. Confirm dry-run counts the first tunnel in `wouldDelete` and the second in `skippedLegacy`.
|
|
6. Set cleanup `enabled` on the disposable stage and deploy. Confirm in the test Cloudflare account
|
|
that the first tunnel is deleted and the legacy tunnel still exists.
|
|
7. Resume the first child with `kill -CONT <first-pid>`. Confirm the running server detects the
|
|
repeated rejection, requests recovery, and becomes reachable at the same hostname without a
|
|
restart.
|
|
8. Resume the legacy child with `kill -CONT <legacy-pid>` and confirm its tunnel reconnects.
|
|
9. Repeat with a physical sleep and wake cycle on a disposable laptop before broad rollout.
|
|
|
|
Legacy cleanup, on the same disposable stage:
|
|
|
|
10. Set `RELAY_LEGACY_TUNNEL_GRACE_MINUTES=10` and the legacy mode to `dry-run`, then deploy. The
|
|
override shortens the 7-day grace period and is ignored on `prod`. Pause the legacy child again
|
|
and wait until Cloudflare reports it down for over ten minutes.
|
|
11. Confirm the sweep counts it in `wouldDeleteLegacy`, then set the legacy mode to `enabled` and
|
|
deploy. Confirm the legacy tunnel is deleted and its allocation row remains.
|
|
12. With the legacy host still on its old build, resume the child. A CLI-linked host provisions a
|
|
new tunnel on its next restart; a web- or mobile-linked host stays offline.
|
|
13. Update that host to the current build and start it. Confirm it requests recovery and is
|
|
reachable at the same hostname.
|
|
14. Remove `RELAY_LEGACY_TUNNEL_GRACE_MINUTES` from the disposable stage.
|
|
|
|
## Marketing site deployment
|
|
|
|
On nightly releases, the release workflow builds the same commit as a staged
|
|
production deployment of the marketing site's Vercel project while the desktop
|
|
jobs run, and promotes it with `vercel promote` after the release is published.
|
|
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`.
|
|
|
|
## Hosted web app release deployment
|
|
|
|
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`. `.github/workflows/release.yml` builds the web
|
|
app with Vercel CLI as a staged production deployment (`--skip-domain`) while
|
|
the desktop jobs run, and aliases the channel domains to it after the GitHub
|
|
Release succeeds.
|
|
|
|
Required GitHub Actions secrets:
|
|
|
|
- `VERCEL_TOKEN`
|
|
- `VERCEL_ORG_ID`
|
|
- `VERCEL_PROJECT_ID`
|
|
|
|
Optional 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:
|
|
|
|
1. Confirm the web project root directory remains `apps/web`.
|
|
2. Add the three domains above to the web project.
|
|
3. Disable automatic Git deployments in the dashboard if desired; the committed
|
|
`vercel.ts` setting is the source-of-truth, but disconnecting Git in the
|
|
dashboard is also safe.
|
|
4. Run one stable release deployment, or manually alias the current stable
|
|
deployment, so `app.t3.codes` points at a deployment containing the router
|
|
rules in `apps/web/vercel.ts`. Future stable releases keep this alias current.
|
|
|
|
## Nightly builds
|
|
|
|
- Workflow: `.github/workflows/release.yml`
|
|
- Triggers:
|
|
- scheduled check every 30 minutes
|
|
- manual `workflow_dispatch` with `channel=nightly`
|
|
- Automatic nightlies require new commits and at least six hours since the last nightly was published, including manual nightlies.
|
|
- Manual nightlies bypass the time and change checks. Nightly runs remain serialized. Scheduled runs wait for an active nightly to finish, then check the publication gap before building.
|
|
- Runs the same desktop quality gates and artifact matrix as the tagged release flow.
|
|
- Publishes a GitHub prerelease only:
|
|
- current tag format: `vX.Y.Z-nightly.YYYYMMDD.<run_number>`
|
|
- `nightly-v...` is accepted only as a legacy previous-nightly tag
|
|
- release name includes the short commit SHA
|
|
- `make_latest` is always `false`
|
|
- Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`.
|
|
- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.
|
|
- Publishes the CLI npm packages (`t3` and `@t3code/t3-<platform>-<arch>`) to the `nightly` npm dist-tag using the same nightly version.
|
|
- Does not commit version bumps back to `main`.
|
|
|
|
## Server self-update release invariant
|
|
|
|
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:
|
|
|
|
1. `publish_cli` publishes the exact release version to npm, on every channel.
|
|
2. `release` depends on `publish_cli` before exposing desktop artifacts in GitHub Releases.
|
|
3. `deploy_web` depends on `release` before moving the hosted channel to the new client.
|
|
`build_web` builds that client earlier with `vercel deploy --prod --skip-domain`, which
|
|
leaves the custom domains alone but moves the project's own `*.vercel.app` production
|
|
hostname. That hostname is behind Vercel SSO, so users only get the client through the
|
|
custom domains.
|
|
|
|
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.
|
|
|
|
## Desktop auto-update notes
|
|
|
|
- Updater runtime: `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.
|
|
- Update UX:
|
|
- Background checks run on startup delay + interval.
|
|
- No automatic download or install.
|
|
- The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install.
|
|
- Provider: GitHub Releases (`provider: github`) configured at build time.
|
|
- Repository slug source:
|
|
- `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set.
|
|
- otherwise `GITHUB_REPOSITORY` from GitHub Actions.
|
|
- Required release assets for updater:
|
|
- platform installers (`.exe`, `.dmg`, `.AppImage`, `.deb`, plus macOS `.zip` for Squirrel.Mac update payloads)
|
|
- channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases
|
|
- `*.blockmap` files (used for differential downloads)
|
|
- macOS metadata note:
|
|
- `electron-updater` reads `latest-mac.yml` on stable and `nightly-mac.yml` on nightly, for both Intel and Apple Silicon.
|
|
- The workflow merges the per-arch mac manifests into one channel-specific mac manifest before publishing the GitHub Release.
|
|
|
|
### Windows payload topology and update validation
|
|
|
|
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
|
|
`resources/wsl-runtime.tar.gz` plus its SHA-256 sidecar: the Linux CLI archive
|
|
(`t3-<version>-linux-<arch>.tar.gz`, the same arch as the Windows host) built
|
|
by the Linux desktop job and handed to the Windows desktop build as
|
|
`--wsl-runtime`, copied in verbatim so WSL runs the exact bytes a Linux user
|
|
downloads. 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.
|
|
|
|
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.
|
|
- Any file marked unpacked in the ASAR header is absent from
|
|
`resources/server.asar.unpacked`.
|
|
- On same-architecture Windows builds, the packaged primary cannot load the fff
|
|
native library from inside `server.asar` through its `.unpacked` sibling.
|
|
- The isolated, extracted sidecar cannot load the server entry with plain Node.
|
|
- A Windows build given `--wsl-runtime` omits the WSL archive or SHA-256
|
|
sidecar, or the sidecar digest does not match the emitted archive.
|
|
- The emitted WSL archive is not a Linux CLI release archive: it must unpack to
|
|
a single `t3-<version>-linux-<arch>` directory holding `t3`, `client/`, and
|
|
`node_modules/` with the Linux node-pty binary, and must not carry a loose
|
|
server bundle (`bin.mjs`).
|
|
- The external Windows resource monitor is absent.
|
|
- The unpacked Windows application contains more than 80 files.
|
|
|
|
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.
|
|
|
|
## 0) npm OIDC trusted publishing setup (CLI)
|
|
|
|
The workflow runs `node scripts/build-npm-platform-packages.ts` on the downloaded CLI archives, then
|
|
`node apps/server/scripts/cli.ts publish --packages-dir npm-packages`, which runs `npm publish` on
|
|
each `@t3code/t3-<platform>-<arch>.tgz` and finally on `t3.tgz`, the launcher. The script publishes
|
|
tarballs it built itself rather than directories: `npm publish <dir>` strips `node_modules/` from the
|
|
tarball no matter what `files` says, and the executable loads its native addons from there. Seven
|
|
packages are published per release: `t3`, `@t3code/t3-darwin-arm64`, `@t3code/t3-darwin-x64`,
|
|
`@t3code/t3-linux-arm64`, `@t3code/t3-linux-x64`, `@t3code/t3-win32-arm64`,
|
|
`@t3code/t3-win32-x64`.
|
|
|
|
Checklist:
|
|
|
|
1. Confirm the npm org owns package `t3` and the `@t3code` scope exists on npm (create the org if
|
|
it does not).
|
|
2. For `t3` and each `@t3code/t3-<platform>-<arch>` package, configure a Trusted Publisher in the
|
|
npm package settings (a package that has never been published needs a first publish or a
|
|
placeholder before the setting exists; the `--dry-run` step in `publish_cli` reports which
|
|
names are still rejected):
|
|
- Provider: GitHub Actions
|
|
- Repository: this repo
|
|
- Workflow file: `.github/workflows/release.yml`
|
|
- Environment (if used): match your npm trusted publishing config
|
|
3. Ensure npm account and org policies allow trusted publishing for every package.
|
|
4. Create release tag `vX.Y.Z` and push; workflow will:
|
|
- build and smoke-test the five CLI archives
|
|
- build the npm packages from those archives
|
|
- publish them with npm dist-tag `latest`
|
|
5. Nightly runs publish with npm dist-tag `nightly`; preview runs with `preview`.
|
|
|
|
## 1) Release validation and unsigned builds
|
|
|
|
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.
|
|
|
|
## 2) Apple signing + notarization setup (macOS)
|
|
|
|
Required secrets used by the workflow:
|
|
|
|
- `CSC_LINK`
|
|
- `CSC_KEY_PASSWORD`
|
|
- `APPLE_API_KEY`
|
|
- `APPLE_API_KEY_ID`
|
|
- `APPLE_API_ISSUER`
|
|
- `MACOS_PROVISIONING_PROFILE` (base64-encoded provisioning profile with Associated Domains)
|
|
|
|
Required repository variables:
|
|
|
|
- `APPLE_TEAM_ID`
|
|
|
|
Optional 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:
|
|
|
|
1. Apple Developer account access:
|
|
- Team has rights to create Developer ID certificates.
|
|
2. Create an explicit App ID for `com.t3tools.t3code` and enable Associated Domains.
|
|
3. Create a `Developer ID Application` certificate and a compatible provisioning profile for that
|
|
App ID with Associated Domains enabled.
|
|
4. Export the certificate + private key as `.p12` from Keychain.
|
|
5. Base64-encode the `.p12` and store as `CSC_LINK`.
|
|
6. Base64-encode the provisioning profile and store it as `MACOS_PROVISIONING_PROFILE`.
|
|
7. Store the `.p12` export password as `CSC_KEY_PASSWORD`, and set `APPLE_TEAM_ID` to the
|
|
10-character Apple Developer Team ID.
|
|
8. In App Store Connect, create an API key (Team key).
|
|
9. Add API key values:
|
|
- `APPLE_API_KEY`: contents of the downloaded `.p8`
|
|
- `APPLE_API_KEY_ID`: Key ID
|
|
- `APPLE_API_ISSUER`: Issuer ID
|
|
10. Complete the Clerk Native API and AASA setup in [T3 Connect setup](./connect-setup.md#desktop-passkeys).
|
|
11. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected
|
|
`com.apple.developer.associated-domains` entitlement.
|
|
|
|
Notes:
|
|
|
|
- `APPLE_API_KEY` is stored as raw key text in secrets.
|
|
- The workflow writes it to a temporary `AuthKey_<id>.p8` file at runtime.
|
|
- The workflow decodes `MACOS_PROVISIONING_PROFILE`, validates it with `security cms`, and passes it
|
|
to the desktop packager.
|
|
|
|
## 3) Azure Trusted Signing setup (Windows)
|
|
|
|
Required secrets used by the workflow:
|
|
|
|
- `AZURE_TENANT_ID`
|
|
- `AZURE_CLIENT_ID`
|
|
- `AZURE_CLIENT_SECRET`
|
|
- `AZURE_TRUSTED_SIGNING_ENDPOINT`
|
|
- `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`
|
|
- `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`
|
|
- `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`
|
|
|
|
Checklist:
|
|
|
|
1. Create Azure Trusted Signing account and certificate profile.
|
|
2. Record ATS values:
|
|
- Endpoint
|
|
- Account name
|
|
- Certificate profile name
|
|
- Publisher name
|
|
3. Create/choose an Entra app registration (service principal).
|
|
4. Grant service principal permissions required by Trusted Signing.
|
|
5. Create a client secret for the service principal.
|
|
6. Add Azure secrets listed above in GitHub Actions secrets.
|
|
7. Re-run a tag release and confirm Windows installer is signed.
|
|
|
|
## 4) Ongoing release checklist
|
|
|
|
1. Pick the latest nightly and verify it: run the smoke test above against its artifacts and
|
|
check the nightly channel for regressions.
|
|
2. Dispatch the Release workflow with `channel=stable`. Leave `version` empty unless the version
|
|
should differ from the one the nightly previewed.
|
|
3. Confirm the `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.
|
|
4. Verify workflow steps:
|
|
- preflight passes
|
|
- release quality checks pass
|
|
- `build_bundle` and all platform builds pass
|
|
- `publish_cli` publishes the exact release version before the release job
|
|
- release job uploads expected files
|
|
5. Smoke test downloaded artifacts.
|
|
|
|
## 5) Troubleshooting
|
|
|
|
- macOS build unsigned when expected signed:
|
|
- Check all Apple secrets plus `APPLE_TEAM_ID` are populated and non-empty.
|
|
- Confirm the provisioning profile belongs to `APPLE_TEAM_ID.com.t3tools.t3code` and includes
|
|
Associated Domains.
|
|
- Windows build unsigned when expected signed:
|
|
- Check all Azure ATS and auth secrets are populated and non-empty.
|
|
- Build fails with signing error:
|
|
- Retry with secrets removed to confirm unsigned path still works.
|
|
- Re-check certificate/profile names and tenant/client credentials.
|