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
1 change: 1 addition & 0 deletions .github/workflows/nix-pnpm-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- master
- canary

permissions:
contents: read
Expand Down
31 changes: 24 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- canary
workflow_dispatch:
inputs:
channel:
Expand All @@ -14,6 +15,7 @@ on:
options:
- stable
- nightly
- canary
version:
description: "Release version (for example 1.2.3 or v1.2.3)"
required: false
Expand All @@ -23,6 +25,10 @@ permissions:
contents: read
id-token: none

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/canary' }}

jobs:
preflight:
name: Preflight
Expand Down Expand Up @@ -140,16 +146,22 @@ jobs:
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "make_latest=true" >> "$GITHUB_OUTPUT"
else
nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"
prerelease_channel="nightly"
if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/canary" ]] ||
[[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "canary" ]]; then
prerelease_channel="canary"
fi
prerelease_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"

node scripts/resolve-nightly-release.ts \
--date "$nightly_date" \
--channel "$prerelease_channel" \
--date "$prerelease_date" \
--run-number "$NIGHTLY_RUN_NUMBER" \
--sha "$NIGHTLY_SHA" \
--github-output

echo "release_channel=nightly" >> "$GITHUB_OUTPUT"
echo "cli_dist_tag=nightly" >> "$GITHUB_OUTPUT"
echo "release_channel=$prerelease_channel" >> "$GITHUB_OUTPUT"
echo "cli_dist_tag=$prerelease_channel" >> "$GITHUB_OUTPUT"
Comment thread
tarik02 marked this conversation as resolved.
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
fi
Expand Down Expand Up @@ -665,7 +677,7 @@ jobs:

# Enable if Windows arm64 builds are enabled.
# Windows updater metadata is channel-specific (for example
# "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the
# "latest.yml", "nightly.yml", or "canary.yml"). Suffix each per-arch copy so the
# release job can merge matching arm64/x64 manifests back into one
# canonical manifest per channel.
# if [[ "${{ matrix.platform }}" == "win" ]]; then
Expand Down Expand Up @@ -713,7 +725,7 @@ jobs:
T3CODE_CLERK_JWT_TEMPLATE: ""
T3CODE_RELAY_URL: ""
VITE_HOSTED_STATIC_APP: "true"
VITE_HOSTED_APP_CHANNEL: ${{ needs.preflight.outputs.release_channel == 'stable' && 'latest' || 'nightly' }}
VITE_HOSTED_APP_CHANNEL: ${{ needs.preflight.outputs.release_channel == 'stable' && 'latest' || needs.preflight.outputs.release_channel }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -973,6 +985,7 @@ jobs:
T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }}
T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }}
T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }}
T3CODE_WEB_CANARY_DOMAIN: ${{ vars.T3CODE_WEB_CANARY_DOMAIN }}
VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }}
steps:
- name: Checkout
Expand Down Expand Up @@ -1013,16 +1026,20 @@ jobs:
router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}"
latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}"
nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}"
canary_domain="${T3CODE_WEB_CANARY_DOMAIN:-canary.app.t3.codes}"
router_domain="${router_url#http://}"
router_domain="${router_domain#https://}"
router_domain="${router_domain%%/*}"

if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then
channel_domain="$latest_domain"
channel_name="latest"
else
elif [[ "${{ needs.preflight.outputs.release_channel }}" == "nightly" ]]; then
channel_domain="$nightly_domain"
channel_name="nightly"
else
channel_domain="$canary_domain"
channel_name="canary"
fi

vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}"
Expand Down
13 changes: 8 additions & 5 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ This repository is a fork of `pingdotgg/t3code`. Keep this file focused on fork

### Release And CI

- Fork workflows create/update a daily stable release PR while main-branch pushes produce nightly releases.
- Fork workflows create/update a daily stable release PR, main-branch pushes produce nightly releases, and canary-branch pushes produce canary releases.
- Canary releases use immutable `X.Y.Z-canary.YYYYMMDD.RUN` versions and GitHub prereleases without changing stable package versions.
- The Nix dependency hash workflow checks both `main` and `canary`, and targets repair PRs back to the branch that drifted.
- Stable release PRs list every commit since the previous stable tag, including commits brought in by upstream merges.
- Main-branch pushes update the stable release PR immediately when the committed package version is already tagged.
- Stable-version pushes wait for the matching release to finish so tag-based version resolution advances past the published version.
Expand All @@ -57,13 +59,14 @@ This repository is a fork of `pingdotgg/t3code`. Keep this file focused on fork
### Nix Package

- The fork exposes the server and web bundle as an `x86_64-linux` flake package.
- The package version follows the server manifest by default and supports the generated nightly version override.
- Main-branch pushes verify the pnpm dependency hash, open or update a repair PR when it drifts, and fail the source workflow run.
- The package version follows the server manifest by default and supports generated nightly and canary version overrides.
- Main- and canary-branch pushes verify the pnpm dependency hash, open or update a repair PR against the affected branch when it drifts, and fail the source workflow run.

### Desktop Updater Channels

- Stable builds use `latest`; nightly builds use `nightly`.
- Nightly detection accepts fork release metadata while preserving the upstream channel split.
- Stable builds use `latest`, nightly builds use `nightly`, and canary builds use `canary`.
- Nightly and canary detection accepts fork release metadata while preserving separate updater feeds.
- Canary desktop builds isolate server and client state under `~/.t3/canary` and Electron data under `t3code-canary`. Desktop settings stay shared so update-track changes survive channel switches.

### Fork Persistence

Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/app/DesktopEarlyElectronStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("reads the persisted linux password-store preference before Electron is ready", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: { T3CODE_HOME: "/home/user/.t3-test" },
homeDirectory: "/home/user",
joinPath,
Expand All @@ -26,6 +27,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("accepts JSONC in the early desktop settings file", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: { T3CODE_HOME: "/home/user/.t3-test" },
homeDirectory: "/home/user",
joinPath,
Expand All @@ -40,6 +42,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("falls back to auto when the early settings document is missing or invalid", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: {},
homeDirectory: "/home/user",
joinPath,
Expand All @@ -53,6 +56,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("preserves absolute root paths when resolving early settings", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: { T3CODE_HOME: "/" },
homeDirectory: "/home/user",
joinPath,
Expand All @@ -67,6 +71,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("resolves the early linux Electron switches", () => {
const options = resolveEarlyLinuxElectronOptions({
appVersion: "1.2.3",
env: {
T3CODE_HOME: "/home/user/.t3-test",
XDG_CURRENT_DESKTOP: "niri",
Expand All @@ -88,6 +93,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("keeps implicit development state under ~/.t3/dev when T3CODE_HOME is unset", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: {
VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
},
Expand All @@ -104,6 +110,7 @@ describe("DesktopEarlyElectronStartup", () => {

it("treats whitespace-only T3CODE_HOME as unconfigured in development", () => {
const preference = resolveEarlyLinuxPasswordStorePreference({
appVersion: "1.2.3",
env: {
T3CODE_HOME: " ",
VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
Expand Down
18 changes: 11 additions & 7 deletions apps/desktop/src/app/DesktopEarlyElectronStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
type LinuxPasswordStoreSwitch,
type LinuxPasswordStorePreference,
} from "../linuxSecretStorage.ts";
import { isCanaryDesktopVersion } from "../updates/updateChannels.ts";
import {
resolveDesktopBaseDir,
resolveDesktopStateDir,
type JoinPath,
} from "./DesktopStatePaths.ts";

interface EarlyDesktopSettingsInput {
readonly appVersion: string;
readonly env: NodeJS.ProcessEnv;
readonly homeDirectory: string;
readonly joinPath: JoinPath;
Expand Down Expand Up @@ -44,20 +46,17 @@ const decodeEarlyDesktopSettingsJson = Schema.decodeSync(EarlyDesktopSettingsJso
const isDevelopmentEnvironment = (env: NodeJS.ProcessEnv): boolean =>
trimNonEmpty(env.VITE_DEV_SERVER_URL) !== null;

function resolveEarlyDesktopSettingsPath(input: {
readonly env: NodeJS.ProcessEnv;
readonly homeDirectory: string;
readonly joinPath: JoinPath;
}): string {
function resolveEarlyDesktopSettingsPath(input: EarlyDesktopSettingsInput): string {
const t3Home = Option.fromUndefinedOr(input.env.T3CODE_HOME);
const baseDir = resolveDesktopBaseDir({
homeDirectory: input.homeDirectory,
joinPath: input.joinPath,
t3Home,
});
const isDevelopment = isDevelopmentEnvironment(input.env);
const stateDir = resolveDesktopStateDir({
baseDir,
isDevelopment: isDevelopmentEnvironment(input.env),
isDevelopment,
joinPath: input.joinPath,
t3Home,
});
Expand All @@ -80,8 +79,13 @@ export function resolveEarlyLinuxElectronOptions(
input: EarlyLinuxElectronOptionsInput,
): EarlyLinuxElectronOptions {
const preference = resolveEarlyLinuxPasswordStorePreference(input);
const isDevelopment = isDevelopmentEnvironment(input.env);
return {
linuxWmClass: isDevelopmentEnvironment(input.env) ? "t3code-dev" : "t3code",
linuxWmClass: isDevelopment
? "t3code-dev"
: isCanaryDesktopVersion(input.appVersion)
? "t3code-canary"
Comment thread
tarik02 marked this conversation as resolved.
: "t3code",
passwordStore: resolveLinuxPasswordStoreSwitch({
preference,
env: input.env,
Expand Down
30 changes: 20 additions & 10 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as Path from "effect/Path";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopConfig from "./DesktopConfig.ts";
import { resolveDesktopBaseDir, resolveDesktopStateDir } from "./DesktopStatePaths.ts";
import { isNightlyDesktopVersion } from "../updates/updateChannels.ts";
import { isCanaryDesktopVersion, isNightlyDesktopVersion } from "../updates/updateChannels.ts";

export interface MakeDesktopEnvironmentInput {
readonly dirname: string;
Expand Down Expand Up @@ -90,6 +90,9 @@ function resolveDesktopAppStageLabel(input: {
return "Dev";
}

if (isCanaryDesktopVersion(input.appVersion)) {
return "Canary";
}
return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";
}

Expand Down Expand Up @@ -143,6 +146,7 @@ const make = Effect.fn("desktop.environment.make")(function* (
const homeDirectory = input.homeDirectory;
const devServerUrl = config.devServerUrl;
const isDevelopment = Option.isSome(devServerUrl);
const isCanary = !isDevelopment && isCanaryDesktopVersion(input.appVersion);
const appDataDirectory =
input.platform === "win32"
? Option.getOrElse(config.appDataDirectory, () =>
Expand All @@ -163,14 +167,22 @@ const make = Effect.fn("desktop.environment.make")(function* (
appVersion: input.appVersion,
});
const displayName = branding.displayName;
const stateDir = resolveDesktopStateDir({
const desktopSettingsDir = resolveDesktopStateDir({
baseDir,
isDevelopment,
joinPath: path.join,
t3Home: config.t3Home,
});
const userDataDirName = isDevelopment ? "t3code-dev" : "t3code";
const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const stateDir = isCanary ? path.join(baseDir, "canary") : desktopSettingsDir;
const appIdentitySuffix = isDevelopment ? "dev" : isCanary ? "canary" : null;
const userDataDirName = appIdentitySuffix !== null ? `t3code-${appIdentitySuffix}` : "t3code";
const legacyUserDataDirName = isDevelopment
? "T3 Code (Dev)"
: isCanary
? "T3 Code (Canary)"
: "T3 Code (Alpha)";
const appUserModelId =
appIdentitySuffix !== null ? `com.t3tools.t3code.${appIdentitySuffix}` : "com.t3tools.t3code";
const linuxApplicationsDir = path.join(
Option.getOrElse(config.xdgDataHome, () => path.join(homeDirectory, ".local", "share")),
"applications",
Expand All @@ -191,7 +203,7 @@ const make = Effect.fn("desktop.environment.make")(function* (
appDataDirectory,
baseDir,
stateDir,
desktopSettingsPath: path.join(stateDir, "desktop-settings.json"),
desktopSettingsPath: path.join(desktopSettingsDir, "desktop-settings.json"),
clientSettingsPath: path.join(stateDir, "client-settings.json"),
savedEnvironmentRegistryPath: path.join(stateDir, "saved-environments.json"),
serverSettingsPath: path.join(stateDir, "settings.json"),
Expand All @@ -213,11 +225,9 @@ const make = Effect.fn("desktop.environment.make")(function* (
otlpExportIntervalMs: config.otlpExportIntervalMs,
branding,
displayName,
appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () =>
isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code",
),
linuxDesktopEntryName: isDevelopment ? "t3code-dev.desktop" : "t3code.desktop",
linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",
appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () => appUserModelId),
linuxDesktopEntryName: `${userDataDirName}.desktop`,
linuxWmClass: userDataDirName,
linuxApplicationsDir,
appImagePath: config.appImagePath,
userDataDirName,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopPreReadyPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function readCommandLineSwitchValue(
export const resolveEarlyLinuxElectronOptionsFromProcess =
(): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
appVersion: Electron.app.getVersion(),
env: process.env,
homeDirectory: NodeOS.homedir(),
joinPath: NodePath.posix.join,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const DesktopSettingsPatch = Schema.Struct({
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
tailscaleServePort: Schema.optionalKey(Schema.Number),
updateChannel: Schema.optionalKey(Schema.Literals(["latest", "nightly"])),
updateChannel: Schema.optionalKey(Schema.Literals(["latest", "nightly", "canary"])),
updateChannelConfiguredByUser: Schema.optionalKey(Schema.Boolean),
wslBackendEnabled: Schema.optionalKey(Schema.Boolean),
wslMode: Schema.optionalKey(Schema.Literals(["local", "wsl"])),
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export const make = Effect.gen(function* () {
channel: DesktopUpdateChannel,
) {
yield* Effect.annotateCurrentSpan({ channel });
const allowsPrerelease = channel === "nightly";
const allowsPrerelease = channel !== "latest";
yield* electronUpdater.setChannel(channel);
yield* electronUpdater.setAllowPrerelease(allowsPrerelease);
yield* electronUpdater.setAllowDowngrade(allowsPrerelease);
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/updates/updateChannels.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import type { DesktopUpdateChannel } from "@t3tools/contracts";

const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}(?:\.\d+)?(?:\+[0-9A-Za-z.-]+)?$/;
const CANARY_VERSION_PATTERN = /-canary\.\d{8}(?:\.\d+)?(?:\+[0-9A-Za-z.-]+)?$/;

export function isNightlyDesktopVersion(version: string): boolean {
return NIGHTLY_VERSION_PATTERN.test(version);
}

export function isCanaryDesktopVersion(version: string): boolean {
return CANARY_VERSION_PATTERN.test(version);
}

export function resolveDefaultDesktopUpdateChannel(appVersion: string): DesktopUpdateChannel {
return isNightlyDesktopVersion(appVersion) ? "nightly" : "latest";
if (isCanaryDesktopVersion(appVersion)) {
return "canary";
}
if (isNightlyDesktopVersion(appVersion)) {
return "nightly";
}
return "latest";
}
Loading
Loading