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
13 changes: 13 additions & 0 deletions .github/scripts/merge-linux-update-manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# Order matters: the first present target supplies the legacy top-level
# path/sha512 that pre-6.x clients fall back to when `files` is absent.
TARGET_PRIORITY = ("deb", "rpm")
ARCH_FEED_SUFFIXES = ("-linux.yml", "-linux-arm64.yml")


def main(argv: list[str]) -> int:
Expand Down Expand Up @@ -50,6 +51,18 @@ def main(argv: list[str]) -> int:
print(f"No manifests found in {manifest_dir}", file=sys.stderr)
return 1

# A half-populated feed is worse than none: clients trust it and the
# missing format silently never sees an update.
for suffix in ARCH_FEED_SUFFIXES:
if not any(feed.endswith(suffix) for feed in groups):
print(f"No *{suffix} feed; an architecture failed to build", file=sys.stderr)
return 1
for feed, by_target in sorted(groups.items()):
absent = [t for t in TARGET_PRIORITY if t not in by_target]
if absent:
print(f"{feed}: missing {', '.join(absent)} manifest", file=sys.stderr)
return 1

for feed, by_target in sorted(groups.items()):
ordered = [t for t in TARGET_PRIORITY if t in by_target]
ordered += [t for t in sorted(by_target) if t not in TARGET_PRIORITY]
Expand Down
75 changes: 68 additions & 7 deletions .github/workflows/mirror-linux-build.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: Mirror Linux build

# Builds .deb and .rpm for x64 and arm64 from an upstream tag and publishes
# them as a release on this fork. Upstream's own release workflow ships a Linux
# x64 AppImage only, so this fills in the distro packages upstream never builds
# them as a release on this fork. Upstream's own release workflow ships Linux
# only as an AppImage, so this fills in the distro packages upstream never builds
# without touching any upstream file. The AppImage is left to upstream; the
# auto-update patch makes these .deb/.rpm installs updatable in place from this
# fork's releases.
#
# Every step here mirrors the "Linux x64" matrix entry of upstream's
# .github/workflows/release.yml. When that entry gains a dependency, this
# Every step here mirrors the Linux matrix entries of upstream's
# .github/workflows/release.yml. When those gain a dependency, this
# workflow needs the same one. See MIRROR.md.

on:
Expand Down Expand Up @@ -154,6 +154,12 @@ jobs:
# keeps that intentional rather than incidental, and guarantees a build
# run from a differently-named repo never points users at upstream.
T3CODE_DESKTOP_UPDATE_REPOSITORY: ${{ github.repository }}
# Upstream's public T3 Connect config, as baked into its own release
# builds. Without it cloud sign-in and T3 Connect are compiled out.
T3CODE_CLERK_PUBLISHABLE_KEY: pk_live_Y2xlcmsudDMuY29kZXMk
T3CODE_CLERK_JWT_TEMPLATE: t3-relay
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: hzxSgY2cH10sDU2r
T3CODE_RELAY_URL: https://relay.t3.codes
steps:
- name: Checkout upstream tag
uses: actions/checkout@v6
Expand Down Expand Up @@ -326,9 +332,65 @@ jobs:
path: release-publish/**
if-no-files-found: error

# Installs each package into a clean distro image, so a missing dependency,
# a package of the wrong format, or a build without the updater marker fails
# here instead of on a user's machine.
install:
name: Install ${{ matrix.target }} ${{ matrix.arch }}
needs: [build]
runs-on: ${{ matrix.runner }}
container: ${{ matrix.image }}
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- { target: deb, arch: x64, runner: ubuntu-26.04, image: "debian:13" }
- { target: deb, arch: arm64, runner: ubuntu-26.04-arm, image: "debian:13" }
- { target: rpm, arch: x64, runner: ubuntu-26.04, image: "fedora:43" }
- { target: rpm, arch: arm64, runner: ubuntu-26.04-arm, image: "fedora:43" }
steps:
- name: Download package
uses: actions/download-artifact@v8
with:
name: linux-${{ inputs.tag }}-${{ matrix.target }}-${{ matrix.arch }}
path: package

- name: Install and inspect
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
packages=(package/*."$TARGET")
if [[ "${#packages[@]}" -ne 1 ]]; then
echo "Expected one .$TARGET, found: ${packages[*]}" >&2
exit 1
fi
if [[ "$TARGET" == deb ]]; then
apt-get update
apt-get install -y "./${packages[0]}"
files=$(dpkg -L t3code)
else
dnf install -y "./${packages[0]}"
files=$(rpm -ql t3code)
fi
binary=$(grep '^/opt/.*/t3code$' <<<"$files")
dir=$(dirname "$binary")
if [[ "$(cat "$dir/resources/package-type")" != "$TARGET" ]]; then
echo "resources/package-type is not $TARGET; in-app updates would be disabled." >&2
exit 1
fi
if ldd "$binary" | grep 'not found'; then
echo "Package dependencies do not cover the libraries above." >&2
exit 1
fi

publish:
name: Publish release
needs: [build]
needs: [build, install]
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
Expand Down Expand Up @@ -427,12 +489,11 @@ jobs:
fi
echo "---"
echo
echo "These are **not** official T3 Code builds. They are produced by this fork from unmodified upstream source at [\`$TAG\`](https://github.com/pingdotgg/t3code/releases/tag/$TAG), because upstream CI ships a Linux x64 AppImage only. This tag is $channel_note."
echo "These are **not** official T3 Code builds. They are produced by this fork from unmodified upstream source at [\`$TAG\`](https://github.com/pingdotgg/t3code/releases/tag/$TAG), because upstream CI ships Linux only as an AppImage. This tag is $channel_note."
echo
echo "- Formats: \`.deb\` and \`.rpm\`, x64 and arm64"
echo "- **Auto-update works**, from this fork's releases. Installs prompt once for your password (via \`pkexec\`) because the update is applied with \`dpkg\`/\`rpm\`, the same way Windows prompts through UAC. A desktop without \`pkexec\` cannot auto-update; reinstall the newer package by hand."
echo "- For the AppImage, use the official [pingdotgg/t3code releases](https://github.com/pingdotgg/t3code/releases) - this fork does not rebuild it."
echo "- Cloud sign-in and T3 Connect are disabled in these builds. See MIRROR.md."
echo
echo "For official builds, use [pingdotgg/t3code releases](https://github.com/pingdotgg/t3code/releases)."
} > release-notes.md
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/mirror-patch-apply-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,18 @@ jobs:
- name: Checkout upstream tag
uses: actions/checkout@v6
with:
repository: pingdotgg/t3code
ref: ${{ matrix.tag }}

- name: Apply mirror patches
shell: bash
env:
MIRROR_REPO: ${{ github.server_url }}/${{ github.repository }}
MIRROR_REF: ${{ github.ref }}
run: |
set -euo pipefail

git fetch --no-tags --depth=1 origin "$MIRROR_REF"
git fetch --no-tags --depth=1 "$MIRROR_REPO" "$MIRROR_REF"
git checkout FETCH_HEAD -- \
mirror-patches \
.github/scripts/apply-mirror-patches.sh
Expand Down
19 changes: 9 additions & 10 deletions MIRROR.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

This fork exists to build T3 Code Linux packages that upstream CI does not produce.

Upstream `pingdotgg/t3code` ships a Linux **x64 AppImage** and nothing else for Linux. This fork adds the distro packages upstream never builds:
Upstream `pingdotgg/t3code` ships Linux only as an **AppImage** (x64 and arm64), plus the AUR packages that repackage it. This fork adds the distro packages upstream never builds:

- `.deb` and `.rpm`
- x64 and arm64

The AppImage is left to upstream; use their releases for it. Nothing else differs, apart from a three-line patch that lets `.deb` and `.rpm` installs use the in-app updater (see below). Everything else is upstream's source at an upstream tag.
The AppImage is left to upstream; use their releases for it. The source is upstream's at an upstream tag, plus the small patches in `mirror-patches/` that make `.deb` and `.rpm` installs updatable and properly described (see below).

## What these builds are not

They are not official. Nobody at T3 Tools signs off on them, and bugs you hit here should be reproduced against an official build before being reported upstream.

Two behavioural differences worth knowing before you install:
Two things worth knowing before you install:

**Auto-update works, and it updates from this fork.** The in-app "Update available" flow works on both formats. `.deb` and `.rpm` prompt once for your password through `pkexec`, because applying the update means running `dpkg -i` or `rpm -U` as root - the same trade Windows makes with its UAC prompt. No apt or dnf repository to add, and no waiting for a scheduled `apt upgrade`.

These builds update from **this fork's** releases, not upstream's, since that is where the arm64 and distro packages live. Moving to an official build later means downloading it from upstream once.
These builds update from **this fork's** releases, not upstream's, since that is where the distro packages live. Moving to an official build later means downloading it from upstream once.

**No cloud sign-in or T3 Connect.** Those need `T3CODE_CLERK_PUBLISHABLE_KEY` and `T3CODE_RELAY_URL`, which upstream injects from its own production environment. This fork does not have them, so `apps/server/vite.config.ts` bakes in empty strings and the features stay off. Local and LAN use is unaffected. Setting these would point users at the maintainers' relay infrastructure, so don't, without asking them first.
**Cloud sign-in and T3 Connect work, against upstream's service.** The build bakes in the same public Clerk and relay config upstream's own release builds carry (`T3CODE_CLERK_*`, `T3CODE_RELAY_URL` in `mirror-linux-build.yml`), so these builds use the maintainers' relay infrastructure exactly like an official install. If upstream rotates any of those values, update them there.

## How it works

Expand All @@ -35,7 +35,7 @@ You can also build a specific tag by hand: Actions → Mirror Linux build → Ru

## Keeping up with upstream

The build steps are copied from the "Linux x64" matrix entry in upstream's `.github/workflows/release.yml`. When upstream adds a build dependency, this fork needs it too.
The build steps are copied from the "Linux x64" and "Linux arm64" matrix entries in upstream's `.github/workflows/release.yml`. When upstream adds a build dependency, this fork needs it too.

There is no drift-detection job, deliberately. Upstream's `preflightLinuxDesktopBuild` in `scripts/build-desktop-artifact.ts` already checks `LINUX_DESKTOP_BUILD_PREREQUISITES` and fails with the missing package names, so a new dependency shows up as a legible build error rather than something subtle. Read that error before assuming the workflow is at fault.

Expand All @@ -62,6 +62,8 @@ The change lives in `mirror-patches/`, not as a commit on `main`. `main` therefo

A `preflight` job checks the patches before the four build jobs start, so a stale patch costs two minutes instead of four long builds.

After the builds, an `install` job installs each package into a clean `debian:13` or `fedora:43` container and fails the release if the package's dependencies leave any of the app's libraries unresolved, or if the updater marker (`resources/package-type`) is missing.

## When a patch goes stale

Preflight files an issue labelled `mirror-patch` naming the patch and linking the failed run, then assigns GitHub Copilot to it. Copilot opens a pull request against `main` with the patch file rewritten against the new upstream code.
Expand All @@ -72,9 +74,6 @@ Assignment needs a `MIRROR_COPILOT_TOKEN` repository secret holding a PAT with i

## Upstreaming

Two things here are worth offering upstream, and both are small:

- The arm64 gap is about eight lines in their release matrix, on a free runner.
- The auto-update patch is three lines and benefits them the moment they ship any distro package. It is also arguably a bug fix: the current check asks "is this an AppImage" when what it means is "can this install be updated", and electron-updater already answers that question more precisely.
The auto-update patch is worth offering upstream. It is three lines and benefits them the moment they ship any distro package. It is also arguably a bug fix: the current check asks "is this an AppImage" when what it means is "can this install be updated", and electron-updater already answers that question more precisely.

This fork is worth keeping for `.deb` and `.rpm` either way, since those carry real packaging support burden upstream has not signed up for.
9 changes: 8 additions & 1 deletion mirror-patches/0002-linux-deb-rpm-package-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ there. `FpmTarget` reads `license` straight off that staged metadata for the rpm
build-internal string `"T3 Code desktop build"`, and
`LinuxTargetHelper.getDescription` feeds it to the deb `Description:` field,
the rpm `%description`, and the `.desktop` `Comment` — so it surfaced verbatim
in `apt show` and as both title and subtitle in GNOME Software.
in `apt show` and as both title and subtitle in GNOME Software. It ends by
saying the build is unofficial, as does the AppStream description in 0003,
since both are what users see before installing.
- `desktop.entry` additions (`GenericName`, `Keywords`). `Keywords` is a
desktop-entry string list, so it keeps its trailing `;` — dropping it makes
the whole value invalid. `Comment` is deliberately not set there:
`LinuxTargetHelper.writeDesktopEntry` merges `desktop.entry` first and then
overwrites `Comment` from `description`, so an entry value would be silently
dropped.
- `deb.depends` and `rpm.depends`: electron-builder's defaults plus the ALSA
and GBM libraries, which Electron links against but the defaults omit, so a
minimal install could not start the app. `depends` replaces the defaults, so
they are copied; the install job in `mirror-linux-build.yml` catches it with
`ldd` if the list falls behind again.
- `deb.recommends` and `deb.packageCategory`. `recommends` includes
`policykit-1 | pkexec` because electron-updater installs a downloaded `.deb`
through `pkexec`. It also repeats electron-builder's own default
Expand Down
39 changes: 33 additions & 6 deletions mirror-patches/0002-linux-deb-rpm-package-metadata.patch
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts
index 44d5f0ab1..ae8818c84 100644
index cb47e40b8..17e513520 100644
--- a/scripts/build-desktop-artifact.ts
+++ b/scripts/build-desktop-artifact.ts
@@ -944,6 +944,8 @@ interface StagePackageJson {
@@ -930,6 +930,8 @@ interface StagePackageJson {
readonly packageManager: string;
readonly description: string;
readonly author: string;
Expand All @@ -11,18 +11,18 @@ index 44d5f0ab1..ae8818c84 100644
readonly main: string;
readonly build: Record<string, unknown>;
readonly dependencies: Record<string, unknown>;
@@ -2761,6 +2763,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
@@ -2737,6 +2739,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
executableName: "t3code",
icon: "icons",
category: "Development",
+ maintainer: "T3 Tools <97451137+TonybynMp4@users.noreply.github.com>",
+ synopsis: "The open-source control plane for coding agents",
+ description:
+ "Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions.",
+ "Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions. Unofficial community build of T3 Code.",
// electron-builder turns these into MimeType=x-scheme-handler/<scheme>;
// in the .desktop entry (Exec already gets %U), so browsers can hand
// t3code:// OAuth callbacks to the app.
@@ -2773,9 +2779,15 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
@@ -2749,9 +2755,42 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
desktop: {
entry: {
StartupWMClass: "t3code",
Expand All @@ -32,13 +32,40 @@ index 44d5f0ab1..ae8818c84 100644
},
};
+ buildConfig.deb = {
+ depends: [
+ "libgtk-3-0",
+ "libnotify4",
+ "libnss3",
+ "libxss1",
+ "libxtst6",
+ "xdg-utils",
+ "libatspi2.0-0",
+ "libuuid1",
+ "libsecret-1-0",
+ "libasound2",
+ "libgbm1",
+ ],
+ recommends: ["libappindicator3-1", "policykit-1 | pkexec"],
+ packageCategory: "devel",
+ };
+ buildConfig.rpm = {
+ depends: [
+ "gtk3",
+ "libnotify",
+ "nss",
+ "libXScrnSaver",
+ "(libXtst or libXtst6)",
+ "xdg-utils",
+ "at-spi2-core",
+ "(libuuid or libuuid1)",
+ "(alsa-lib or libasound2)",
+ "(mesa-libgbm or libgbm1)",
+ ],
+ };
}

if (platform === "win") {
@@ -3774,7 +3786,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
@@ -3651,7 +3690,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
private: true,
packageManager: rootPackageJson.packageManager,
description: "T3 Code desktop build",
Expand Down
2 changes: 1 addition & 1 deletion mirror-patches/0003-linux-appstream-metainfo.patch
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ index 848c5806b..95e3f8deb 100644
+ <summary>The open-source control plane for coding agents</summary>
+ <description>
+ <p>
+ Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions.
+ Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions. This is an unofficial community build of T3 Code.
+ </p>
+ </description>
+ <developer id="io.github.tonybynmp4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ index 44d5f0ab1..7dc738f85 100644
+ <summary>The open-source control plane for coding agents</summary>
+ <description>
+ <p>
+ Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions.
+ Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface. Bring your own subscriptions. This is an unofficial community build of T3 Code.
+ </p>
+ </description>
+ <developer id="io.github.tonybynmp4">
Expand Down
Loading