Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,12 @@ export const make = Effect.gen(function* () {
const allowsPrerelease = channel === "nightly";
// T3-CUSTOM(expbkt3): BEGIN - the fork ships two apps from one release
// repository, so the string handed to electron-updater is the brand's
// ("staging-nightly" / "production-nightly"), not the contract-level
// channel. GitHubProvider matches it against each release tag's first
// prerelease identifier, which is what keeps a staging build invisible to a
// production app. The contract channel is unchanged, so the settings UI and
// the persisted setting keep their existing "latest" | "nightly" values.
// ("stage-nightly" / "production-nightly"), not the contract-level
// channel. It names the manifest the app reads (`<channel>-mac.yml`), and
// apps built before the fixed update feed match it against each release
// tag's first prerelease identifier. The contract channel is unchanged, so
// the settings UI and the persisted setting keep their existing
// "latest" | "nightly" values.
const brandChannel = resolveRuntimeBrand()?.updateChannel;
const updaterChannel = allowsPrerelease ? (brandChannel ?? channel) : channel;
yield* electronUpdater.setChannel(updaterChannel);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const DESKTOP_RELEASE_TAG_URL = `${DESKTOP_RELEASE_HISTORY_URL}/tag`;

// T3-CUSTOM(expbkt3): BEGIN - a fork build updates from the fork's releases, so
// pointing "Read more" at upstream's tags 404s on every fork version.
const BK_DESKTOP_RELEASE_TAG_URL = "https://github.com/beknown-work/bkt3code/releases/tag";
const BK_DESKTOP_RELEASE_TAG_URL = "https://github.com/iamtushar324/bkt3code/releases/tag";

function resolveDesktopReleaseTagBaseUrl(): string {
return isBkManagedPrimary() ? BK_DESKTOP_RELEASE_TAG_URL : DESKTOP_RELEASE_TAG_URL;
Expand Down
24 changes: 13 additions & 11 deletions docs/operations/bk-desktop-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The **updater cache is a third isolation axis**, and an easy one to miss. It liv

The encrypted desktop connection catalog follows the same isolation boundary. BK builds store it inside their own Application Support directory (`bkt3code` or `bkt3code-staging`) instead of the shared T3 home, because Electron safe-storage ciphertext from one app identity cannot be decrypted by the other. The older shared catalog is left untouched.

Both are Apple Silicon (`arm64`) only, code signed, **keyless**, and published as prereleases on [`beknown-work/bkt3code`](https://github.com/beknown-work/bkt3code/releases). The identity in use during the current trial is an Apple Development certificate, not the self-signed root this document otherwise describes — see [Current identity is a test certificate](#current-identity-is-a-test-certificate).
Both are Apple Silicon (`arm64`) only, code signed, **keyless**, and published as prereleases on [`iamtushar324/bkt3code`](https://github.com/iamtushar324/bkt3code/releases) (formerly `beknown-work/bkt3code`, which GitHub redirects). The identity in use during the current trial is an Apple Development certificate, not the self-signed root this document otherwise describes — see [Current identity is a test certificate](#current-identity-is-a-test-certificate).

## How it runs

Expand Down Expand Up @@ -85,14 +85,16 @@ Staging therefore pairs by pasting the credential into the pairing screen, which

## What keeps the two apps apart

One repository holds both apps' releases, and the **updater channel** is the only thing separating them. electron-updater's `GitHubProvider` walks the releases feed and takes the first release whose `semver.prerelease(tag)[0]` equals the running app's channel, then reads `<channel>-mac.yml` from it. So the channel has to be the version's first prerelease identifier:
One repository holds both apps' releases, plus the mobile builds. Each app reads its update from its **own fixed release** through electron-updater's generic provider, never from the releases feed:

| App | Version | Channel | Manifest |
| ---------- | ------------------------------------- | -------------------- | ---------------------------- |
| Staging | `X.Y.Z-stage-nightly.YYYYMMDD.N` | `stage-nightly` | `stage-nightly-mac.yml` |
| Production | `X.Y.Z-production-nightly.YYYYMMDD.N` | `production-nightly` | `production-nightly-mac.yml` |
| App | Version | Channel | Feed release | Manifest |
| ---------- | ------------------------------------- | -------------------- | ----------------------- | ---------------------------- |
| Staging | `X.Y.Z-stage-nightly.YYYYMMDD.N` | `stage-nightly` | `bk-desktop-staging` | `stage-nightly-mac.yml` |
| Production | `X.Y.Z-production-nightly.YYYYMMDD.N` | `production-nightly` | `bk-desktop-production` | `production-nightly-mac.yml` |

A staging release is therefore invisible to a production app, and vice versa.
The feed release holds only the manifest. After each dated release is public, the publisher uploads that build's manifest to the feed release with every payload URL made absolute, pointing into the dated release. A staging build never touches the production feed, and vice versa.

**Why not the releases feed.** `GitHubProvider` reads `releases.atom`, which lists only the ten newest releases, and walks it for a tag whose `semver.prerelease(tag)[0]` equals the app's channel. Staging and mobile builds publish many times a day, so they pushed every production build out of those ten, and production apps silently stopped finding updates (October 2026). Apps built before the fixed feed still use `GitHubProvider`. They pick up the first fixed-feed build only while it is among the ten newest releases; an app that misses that window needs one manual install.
The old `staging-nightly` releases from `expbkmain` are also invisible to the
new `stage-nightly` app. Install the first `stage-nightly` DMG manually over the
existing Stage BK T3 Code installation; its bundle identity and profile path
Expand Down Expand Up @@ -272,19 +274,19 @@ If a publish fails partway, it leaves a **draft** holding that version's tag. Th

Release notes record the signing identity as a **fingerprint of the designated requirement**, never the raw string. This repository is public and an Apple-issued certificate embeds the developer's email and Team ID in its common name; publishing the raw requirement would put that on every release. The fingerprint answers "did the signing identity change between builds?", which is the operational question. For the raw value, run `codesign -dr -` against the app.

Once a version is published, **never replace its ZIP or manifest.** Clients cache by version; a mutated asset is undetectable and unfixable from their side. Publish a higher version instead.
Once a version is published, **never replace its ZIP or manifest.** Clients cache by version; a mutated asset is undetectable and unfixable from their side. Publish a higher version instead. The feed release's manifest is the one exception: it is a pointer, and the publisher replaces it on every build. Never delete the feed release, or every installed app stops updating. If the feed step fails after the dated release is public, use **Re-run failed jobs**: the publisher finds the dated release published from the same commit and only repoints the feed. It refuses to point the feed at a version older than the one it serves, because the apps allow downgrades.

After publishing, confirm the upstream pipeline did not fire and that npm is untouched:

```sh
gh run list --repo beknown-work/bkt3code --workflow release.yml -L 3
gh run list --repo iamtushar324/bkt3code --workflow release.yml -L 3
npm view t3 dist-tags
```

## Installing (send this to teammates)

1. Install the `BK Code Signing` certificate and set it to **Always Trust** (one-time).
2. Download the `.dmg` from the [latest prerelease](https://github.com/beknown-work/bkt3code/releases) — `BK T3 Code` for day-to-day work, `Stage BK T3 Code` to try `stage`.
2. Download the `.dmg` from the [latest prerelease](https://github.com/iamtushar324/bkt3code/releases) — `BK T3 Code` for day-to-day work, `Stage BK T3 Code` to try `stage`.
3. Drag it to Applications.
4. Builds are not notarised, so macOS quarantines them. Clear that once per app:
```sh
Expand Down Expand Up @@ -343,7 +345,7 @@ Before advertising automatic updates on a channel, prove all of these. Nothing h

Check, in order:

1. The release exists and is a **prerelease** with the right manifest asset for that channel.
1. The feed release (`bk-desktop-production` or `bk-desktop-staging`) holds the channel manifest, and that manifest names the newest version. Its URLs must point into a dated release that exists.
2. The version is strictly newer than the installed one _within the same channel_.
3. The installed app and the update are signed with the **same** certificate. Compare `codesign -dr - "/Applications/BK T3 Code.app"` against the built app and against the release notes; the designated requirements must be identical.

Expand Down
25 changes: 13 additions & 12 deletions scripts/bk-desktop-brand-overrides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const originalChannel = process.env[BK_MANAGED_CHANNEL_ENV_VAR];
// Set once, before any Config read. Effect's default ConfigProvider snapshots
// process.env on first use, so a per-test assignment would be silently ignored
// by every test after the first.
process.env.T3CODE_DESKTOP_UPDATE_REPOSITORY = "beknown-work/bkt3code";
process.env.T3CODE_DESKTOP_UPDATE_REPOSITORY = "pingdotgg/t3code";

function withBkBrand(variant?: "staging" | "production"): void {
process.env[DESKTOP_BRAND_ENV_VAR] = "bk";
Expand Down Expand Up @@ -130,24 +130,25 @@ describe("updater channel in the publish config", () => {
}),
);

it.effect("gives each fork app its own channel, which separates their updates", () =>
it.effect("points each fork app at its own fixed update-feed release", () =>
Effect.gen(function* () {
// Both apps publish into one repository. electron-updater picks a release
// by matching semver.prerelease(tag)[0] against the running app's channel,
// so these strings are the entire isolation mechanism — and they must
// equal the version's first prerelease identifier.
// The releases feed lists only the ten newest releases, and staging and
// mobile builds pushed every production build out of it. A fixed URL per
// app cannot be crowded out, and the channel names the manifest file.
withBkBrand("staging");
expect(yield* publishConfig("0.0.17-stage-nightly.20260413.42")).toMatchObject({
owner: "beknown-work",
repo: "bkt3code",
releaseType: "prerelease",
expect(yield* publishConfig("0.0.17-stage-nightly.20260413.42")).toEqual({
provider: "generic",
url: "https://github.com/iamtushar324/bkt3code/releases/download/bk-desktop-staging",
channel: "stage-nightly",
useMultipleRangeRequest: false,
});

withBkBrand("production");
expect(yield* publishConfig("0.0.17-production-nightly.20260413.42")).toMatchObject({
releaseType: "prerelease",
expect(yield* publishConfig("0.0.17-production-nightly.20260413.42")).toEqual({
provider: "generic",
url: "https://github.com/iamtushar324/bkt3code/releases/download/bk-desktop-production",
channel: "production-nightly",
useMultipleRangeRequest: false,
});
}),
);
Expand Down
3 changes: 0 additions & 3 deletions scripts/build-bk-desktop-dmg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import {
BK_DESKTOP_BRAND_ID,
BK_DESKTOP_BRANDS,
BK_DESKTOP_UPDATE_REPOSITORY,
DESKTOP_BRAND_ENV_VAR,
} from "./lib/bk-desktop-brand.ts";
import { BK_SIGNING_IDENTITY_ENV_VAR, resolveBkSigningIdentity } from "./lib/bk-desktop-signing.ts";
Expand Down Expand Up @@ -347,8 +346,6 @@ const runBuild = Effect.fn("runBuild")(function* (options: {
// and — via resolveBkDesktopVariant — selects which of the two fork apps
// this is: bundle id, product name, user-data directory and updater channel.
[BK_MANAGED_CHANNEL_ENV_VAR]: options.managedChannel,
// Points electron-updater at the fork's releases instead of upstream's.
T3CODE_DESKTOP_UPDATE_REPOSITORY: BK_DESKTOP_UPDATE_REPOSITORY,
VITE_T3_EXPERIMENTAL_CONTROL_CENTER: "true",
// Scrubbed, not merely omitted: `...process.env` above would otherwise carry
// an inherited key straight into the bundle, which is exactly the accident
Expand Down
33 changes: 21 additions & 12 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
type WebAssetBrand,
} from "./lib/brand-assets.ts";
// T3-CUSTOM(expbkt3): BEGIN - fork desktop brand (bundle id, name, icon, artifact).
import { resolveBkDesktopBrand } from "./lib/bk-desktop-brand.ts";
import { bkDesktopUpdateFeedUrl, resolveBkDesktopBrand } from "./lib/bk-desktop-brand.ts";
import { resolveBkSigningIdentity } from "./lib/bk-desktop-signing.ts";
// T3-CUSTOM(expbkt3): END
import { getDefaultBuildArch } from "./lib/build-target-arch.ts";
Expand Down Expand Up @@ -2572,6 +2572,25 @@ export function resolveDesktopRuntimeDependencies(
export const resolveGitHubPublishConfig = Effect.fn("resolveGitHubPublishConfig")(function* (
updateChannel: "latest" | "nightly",
) {
// T3-CUSTOM(expbkt3): BEGIN - a fork app reads its manifest from one fixed
// release, not from the releases feed. The feed lists only the ten newest
// releases, and staging and mobile builds in the same repository push
// production builds out of it. The generic provider reads
// `<channel>-mac.yml` under `url`. See scripts/lib/bk-desktop-brand.ts.
// GitHub answers a multi-range request with 501, which GitHubProvider knows
// and the generic provider does not, so without the flag every differential
// download falls back to the full ZIP.
const brand = resolveDesktopBrandOverride();
if (brand && updateChannel === "nightly") {
return {
provider: "generic",
url: bkDesktopUpdateFeedUrl(brand),
channel: brand.updateChannel,
useMultipleRangeRequest: false,
};
}
// T3-CUSTOM(expbkt3): END

const env = yield* Config.all({
updateRepository: Config.String("T3CODE_DESKTOP_UPDATE_REPOSITORY").pipe(Config.option),
githubRepository: Config.String("GITHUB_REPOSITORY").pipe(Config.option),
Expand All @@ -2586,22 +2605,12 @@ export const resolveGitHubPublishConfig = Effect.fn("resolveGitHubPublishConfig"
const [owner, repo, ...rest] = rawRepo.split("/");
if (!owner || !repo || rest.length > 0) return undefined;

// T3-CUSTOM(expbkt3): BEGIN - the fork ships two apps out of this one
// repository, so the channel is the brand's, not the literal "nightly".
// That name becomes the manifest asset (`<channel>-mac.yml`) and must equal
// the first prerelease identifier of the version, which is what
// electron-updater's GitHubProvider matches on. See
// scripts/lib/bk-desktop-brand.ts.
const brandChannel = resolveDesktopBrandOverride()?.updateChannel;
// T3-CUSTOM(expbkt3): END

return {
provider: "github",
owner,
repo,
releaseType: updateChannel === "nightly" ? "prerelease" : "release",
// T3-CUSTOM(expbkt3): brandChannel when a fork brand is active, else upstream's.
...(updateChannel === "nightly" ? { channel: brandChannel ?? ("nightly" as const) } : {}),
...(updateChannel === "nightly" ? { channel: "nightly" as const } : {}),
};
});

Expand Down
28 changes: 21 additions & 7 deletions scripts/lib/bk-desktop-brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export const BK_DESKTOP_BRAND_ID = "bk";

export type DesktopBrandId = "upstream" | "bk";

/** Repository whose GitHub releases the fork's desktop updater reads. */
export const BK_DESKTOP_UPDATE_REPOSITORY = "beknown-work/bkt3code";
/** Repository whose GitHub releases hold the fork's desktop builds. */
export const BK_DESKTOP_UPDATE_REPOSITORY = "iamtushar324/bkt3code";

/**
* The fork ships one app per managed environment, so `stage` and `bkmain`
Expand Down Expand Up @@ -88,13 +88,20 @@ export interface DesktopBrand {
* electron-updater channel, and therefore the manifest asset name
* (`<updateChannel>-mac.yml`) and the version's first prerelease identifier.
*
* This is what keeps the two apps apart inside one release repository.
* `GitHubProvider.getLatestVersion` walks the releases feed and takes the
* first release whose `semver.prerelease(tag)[0]` equals the running app's
* channel, so a staging build is invisible to a production app and vice
* versa. See `scripts/lib/bk-desktop-release.ts` for the version format.
* The app reads `<updateChannel>-mac.yml` from {@link updateFeedTag}'s
* release. See `scripts/lib/bk-desktop-release.ts` for the version format.
*/
readonly updateChannel: string;
/**
* Tag of the fixed GitHub release this app's updater reads. The publish
* script replaces its manifest after every build.
*
* The app does not search the releases feed. That feed lists only the ten
* newest releases, and the staging and mobile builds that share this
* repository pushed every production build out of it, so production apps
* stopped finding updates.
*/
readonly updateFeedTag: string;
/**
* OS-level URL scheme this app registers as a handler for, or `null` to
* register none.
Expand Down Expand Up @@ -147,6 +154,7 @@ export const BK_DESKTOP_BRANDS: Readonly<Record<BkDesktopVariant, DesktopBrand>>
linuxIconPng: BK_BRAND_ASSET_PATHS.universalIconPng,
windowsIconIco: BK_BRAND_ASSET_PATHS.windowsIconIco,
updateChannel: "production-nightly",
updateFeedTag: "bk-desktop-production",
deepLinkScheme: "t3code",
},
staging: {
Expand All @@ -169,6 +177,7 @@ export const BK_DESKTOP_BRANDS: Readonly<Record<BkDesktopVariant, DesktopBrand>>
linuxIconPng: BK_BRAND_ASSET_PATHS.universalIconPng,
windowsIconIco: BK_BRAND_ASSET_PATHS.windowsIconIco,
updateChannel: "stage-nightly",
updateFeedTag: "bk-desktop-staging",
deepLinkScheme: null,
},
};
Expand All @@ -185,6 +194,11 @@ export function isBkDesktopBrandId(value: string | undefined): boolean {
return value?.trim().toLowerCase() === BK_DESKTOP_BRAND_ID;
}

/** Base URL electron-updater's generic provider reads a brand's manifest from. */
export function bkDesktopUpdateFeedUrl(brand: DesktopBrand): string {
return `https://github.com/${BK_DESKTOP_UPDATE_REPOSITORY}/releases/download/${brand.updateFeedTag}`;
}

/** Resolves the brand from a build environment, defaulting to upstream. */
export function resolveDesktopBrandId(
env: Readonly<Record<string, string | undefined>>,
Expand Down
Loading
Loading