| author | |
| committer | |
| log | c13f7d93fff0f578886985a3f673b4b21b7f6ac8 |
| tree | 4f53dec02735dd9bfae9c464477c3eb72d89511c |
| parent | 20f0ff1781fb02898a6fa5f97f76f36079cff0b8 |
| signature | Signed by PGP key B5690EEEBB952194 |
The Linux release build now also makes a .deb in the same electron-builder
run as the AppImage, and the release publishes it. electron-builder lists it
in latest-linux.yml and writes resources/package-type into it, so the app
updates a .deb install through electron-updater, which installs the new .deb
with dpkg. The desktop updater now allows that path.
The .deb uses xz in threaded mode (XZ_DEFAULTS=-T0): 113 MB in place of
141 MB with gzip. The Linux arm64 release job moves to a 16-vCPU Blacksmith
arm64 runner. The download page, README, install guide, and release docs
list the .deb.
Based on community work in #4071, #5139, #4900, and #4887.
Co-authored-by: chukfinley <chuk@chuk.dev>
Co-authored-by: Primož Ajdišek <bigpod@bigpod.si>
Co-authored-by: benthecarman <benthecarman@live.com>
Co-authored-by: NaveDanan <nave0712@gmail.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>11 files changed, 124 insertions(+), 13 deletions(-)
.github/workflows/release-desktop.yml+1| ... | ... | @@ -510,6 +510,7 @@ jobs: |
| 510 | 510 | "release/*.dmg" |
| 511 | 511 | "release/*.zip" |
| 512 | 512 | "release/*.AppImage" |
| 513 | "release/*.deb" | |
| 513 | 514 | "release/*.exe" |
| 514 | 515 | ) |
| 515 | 516 | # Preview builds have no publish config, so electron-builder writes |
.github/workflows/release.yml+2-1| ... | ... | @@ -625,7 +625,7 @@ jobs: |
| 625 | 625 | clerk_cli_oauth_client_id: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} |
| 626 | 626 | relay_url: ${{ needs.relay_public_config.outputs.relay_url }} |
| 627 | 627 | label: Linux arm64 |
| 628 | runner: ubuntu-24.04-arm | |
| 628 | runner: blacksmith-16vcpu-ubuntu-2404-arm | |
| 629 | 629 | platform: linux |
| 630 | 630 | target: AppImage |
| 631 | 631 | arch: arm64 |
| ... | ... | @@ -875,6 +875,7 @@ jobs: |
| 875 | 875 | echo 'release-assets/*.dmg' |
| 876 | 876 | echo 'release-assets/*.zip' |
| 877 | 877 | echo 'release-assets/*.AppImage' |
| 878 | echo 'release-assets/*.deb' | |
| 878 | 879 | echo 'release-assets/*.exe' |
| 879 | 880 | if [[ "${{ needs.preflight.outputs.release_channel }}" != "preview" ]]; then |
| 880 | 881 | echo 'release-assets/*.blockmap' |
README.md+8| ... | ... | @@ -54,6 +54,14 @@ winget install T3Tools.T3Code |
| 54 | 54 | brew install --cask t3-code |
| 55 | 55 | ``` |
| 56 | 56 | |
| 57 | #### Debian, Ubuntu (`.deb`) | |
| 58 | ||
| 59 | Download the `.deb` from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), then: | |
| 60 | ||
| 61 | ```bash | |
| 62 | sudo apt install ./T3-Code-*.deb | |
| 63 | ``` | |
| 64 | ||
| 57 | 65 | #### Arch Linux (AUR) |
| 58 | 66 | |
| 59 | 67 | Stable: |
apps/desktop/src/updates/DesktopUpdates.test.ts+19| ... | ... | @@ -87,6 +87,25 @@ describe("DesktopUpdates", () => { |
| 87 | 87 | }).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); |
| 88 | 88 | }); |
| 89 | 89 | |
| 90 | it.effect("updates Linux .deb installs and leaves other non-AppImage installs off", () => | |
| 91 | Effect.gen(function* () { | |
| 92 | const linuxState = (packageType: string | undefined) => | |
| 93 | Effect.scoped( | |
| 94 | Effect.gen(function* () { | |
| 95 | const updates = yield* DesktopUpdates.DesktopUpdates; | |
| 96 | yield* updates.configure; | |
| 97 | return yield* updates.getState; | |
| 98 | }), | |
| 99 | ).pipe(Effect.provide(makeHarness({ platform: "linux", packageType }).layer)); | |
| 100 | ||
| 101 | const deb = yield* linuxState("deb\n"); | |
| 102 | assert.equal(deb.status, "idle"); | |
| 103 | ||
| 104 | const unmarked = yield* linuxState(undefined); | |
| 105 | assert.equal(unmarked.status, "disabled"); | |
| 106 | }), | |
| 107 | ); | |
| 108 | ||
| 90 | 109 | it.effect("subscribe delivers the latest state plus subsequent changes", () => { |
| 91 | 110 | const harness = makeHarness(); |
| 92 | 111 |
apps/desktop/src/updates/DesktopUpdates.ts+16-2| ... | ... | @@ -250,6 +250,7 @@ function getAutoUpdateDisabledReason(args: { |
| 250 | 250 | isPackaged: boolean; |
| 251 | 251 | platform: NodeJS.Platform; |
| 252 | 252 | appImage?: string | undefined; |
| 253 | isDebPackage: boolean; | |
| 253 | 254 | disabledByEnv: boolean; |
| 254 | 255 | hasUpdateFeedConfig: boolean; |
| 255 | 256 | }): string | null { |
| ... | ... | @@ -262,8 +263,8 @@ function getAutoUpdateDisabledReason(args: { |
| 262 | 263 | if (args.disabledByEnv) { |
| 263 | 264 | return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; |
| 264 | 265 | } |
| 265 | if (args.platform === "linux" && !args.appImage) { | |
| 266 | return "Automatic updates on Linux require running the AppImage build."; | |
| 266 | if (args.platform === "linux" && !args.appImage && !args.isDebPackage) { | |
| 267 | return "Automatic updates on Linux require the AppImage or the .deb package."; | |
| 267 | 268 | } |
| 268 | 269 | return null; |
| 269 | 270 | } |
| ... | ... | @@ -332,6 +333,18 @@ export const make = Effect.gen(function* () { |
| 332 | 333 | ), |
| 333 | 334 | ); |
| 334 | 335 | |
| 336 | // The .deb carries electron-builder's resources/package-type marker. | |
| 337 | // electron-updater reads the same file and installs updates with dpkg. | |
| 338 | const isDebPackage = | |
| 339 | environment.platform === "linux" && environment.isPackaged | |
| 340 | ? yield* fileSystem | |
| 341 | .readFileString(environment.path.join(environment.resourcesPath, "package-type")) | |
| 342 | .pipe( | |
| 343 | Effect.map((packageType) => packageType.trim() === "deb"), | |
| 344 | Effect.orElseSucceed(() => false), | |
| 345 | ) | |
| 346 | : false; | |
| 347 | ||
| 335 | 348 | const hasUpdateFeedConfig = Ref.get(appUpdateYmlConfigRef).pipe( |
| 336 | 349 | Effect.map((appUpdateYmlConfig) => Option.isSome(appUpdateYmlConfig) || config.mockUpdates), |
| 337 | 350 | ); |
| ... | ... | @@ -344,6 +357,7 @@ export const make = Effect.gen(function* () { |
| 344 | 357 | isPackaged: environment.isPackaged, |
| 345 | 358 | platform: environment.platform, |
| 346 | 359 | appImage: Option.getOrUndefined(config.appImagePath), |
| 360 | isDebPackage, | |
| 347 | 361 | disabledByEnv: config.disableAutoUpdate, |
| 348 | 362 | hasUpdateFeedConfig: hasFeedConfig, |
| 349 | 363 | }), |
apps/desktop/src/updates/updatesTestHarness.ts+16-1| ... | ... | @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 2 | import type { DesktopUpdateState } from "@t3tools/contracts"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as PlatformError from "effect/PlatformError"; | |
| 5 | 6 | import * as Layer from "effect/Layer"; |
| 6 | 7 | import * as Option from "effect/Option"; |
| 7 | 8 | |
| ... | ... | @@ -33,6 +34,9 @@ export interface UpdatesHarnessOptions { |
| 33 | 34 | readonly stopBackend?: Effect.Effect<void>; |
| 34 | 35 | readonly startBackend?: Effect.Effect<void>; |
| 35 | 36 | readonly env?: Record<string, string | undefined>; |
| 37 | readonly platform?: NodeJS.Platform; | |
| 38 | /** Contents of the resources/package-type marker a Linux package ships. */ | |
| 39 | readonly packageType?: string | undefined; | |
| 36 | 40 | } |
| 37 | 41 | |
| 38 | 42 | export function makeHarness(options: UpdatesHarnessOptions = {}) { |
| ... | ... | @@ -146,7 +150,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { |
| 146 | 150 | const environmentLayer = DesktopEnvironment.layer({ |
| 147 | 151 | dirname: "/repo/apps/desktop/src", |
| 148 | 152 | homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`, |
| 149 | platform: "darwin", | |
| 153 | platform: options.platform ?? "darwin", | |
| 150 | 154 | processArch: "x64", |
| 151 | 155 | appVersion: "1.2.3", |
| 152 | 156 | appPath: "/repo", |
| ... | ... | @@ -208,6 +212,17 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { |
| 208 | 212 | // disk I/O that would outrun the tests' settle loops. |
| 209 | 213 | const updateRestartMarkers = new Set<string>(); |
| 210 | 214 | const fileSystemLayer = FileSystem.layerNoop({ |
| 215 | readFileString: (path) => | |
| 216 | path === "/missing/resources/package-type" && options.packageType !== undefined | |
| 217 | ? Effect.succeed(options.packageType) | |
| 218 | : Effect.fail( | |
| 219 | PlatformError.systemError({ | |
| 220 | module: "FileSystem", | |
| 221 | method: "readFileString", | |
| 222 | _tag: "NotFound", | |
| 223 | pathOrDescriptor: path, | |
| 224 | }), | |
| 225 | ), | |
| 211 | 226 | makeDirectory: () => Effect.void, |
| 212 | 227 | writeFileString: (path) => |
| 213 | 228 | Effect.sync(() => { |
apps/marketing/src/pages/download.astro+14| ... | ... | @@ -91,6 +91,16 @@ const imageProps = { |
| 91 | 91 | <h2 class="platform-name">Linux</h2> |
| 92 | 92 | </div> |
| 93 | 93 | <div class="download-cards"> |
| 94 | <a class="download-card" data-asset="amd64.deb" href={RELEASES_URL}> | |
| 95 | <span class="card-icon"> | |
| 96 | <Image class="card-icon--stable" src={iconStable} width={28} height={28} loading="lazy" {...imageProps} /> | |
| 97 | <Image class="card-icon--nightly" src={iconNightly} width={28} height={28} loading="lazy" {...imageProps} /> | |
| 98 | </span> | |
| 99 | <span class="card-text"> | |
| 100 | <span class="card-arch">Debian, Ubuntu</span> | |
| 101 | <span class="card-format">.deb</span> | |
| 102 | </span> | |
| 103 | </a> | |
| 94 | 104 | <a class="download-card" data-asset="x86_64.AppImage" href={RELEASES_URL}> |
| 95 | 105 | <span class="card-icon"> |
| 96 | 106 | <Image class="card-icon--stable" src={iconStable} width={28} height={28} loading="lazy" {...imageProps} /> |
| ... | ... | @@ -102,6 +112,10 @@ const imageProps = { |
| 102 | 112 | </span> |
| 103 | 113 | </a> |
| 104 | 114 | </div> |
| 115 | <p class="intel-note"> | |
| 116 | On ARM? Download the arm64 <a data-asset="arm64.deb" href={RELEASES_URL}>.deb</a> or | |
| 117 | <a data-asset="arm64.AppImage" href={RELEASES_URL}>AppImage</a>. | |
| 118 | </p> | |
| 105 | 119 | </section> |
| 106 | 120 | |
| 107 | 121 | <!-- Mobile. Stable only, there is no nightly mobile build. --> |
docs/operations/release.md+2-2| ... | ... | @@ -29,7 +29,7 @@ This document covers the unified release workflow for stable and nightly desktop |
| 29 | 29 | - 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 also wait for the same-arch Linux job, whose CLI archive they embed as the WSL runtime): |
| 30 | 30 | - macOS `arm64` DMG |
| 31 | 31 | - macOS `x64` DMG |
| 32 | - Linux `x64` and `arm64` AppImage | |
| 32 | - 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`. | |
| 33 | 33 | - Windows `x64` and `arm64` NSIS installer |
| 34 | 34 | - Publishes one GitHub Release with all produced files. |
| 35 | 35 | - Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases. |
| ... | ... | @@ -329,7 +329,7 @@ available. |
| 329 | 329 | - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. |
| 330 | 330 | - otherwise `GITHUB_REPOSITORY` from GitHub Actions. |
| 331 | 331 | - Required release assets for updater: |
| 332 | - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads) | |
| 332 | - platform installers (`.exe`, `.dmg`, `.AppImage`, `.deb`, plus macOS `.zip` for Squirrel.Mac update payloads) | |
| 333 | 333 | - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases |
| 334 | 334 | - `*.blockmap` files (used for differential downloads) |
| 335 | 335 | - macOS metadata note: |
docs/user/install.md+11-6| ... | ... | @@ -58,12 +58,17 @@ update it with `git pull` and a rebuild. |
| 58 | 58 | Download a release from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), |
| 59 | 59 | or use a package manager: |
| 60 | 60 | |
| 61 | | Platform | Install | | |
| 62 | | ------------------ | ------------------------------- | | |
| 63 | | Windows | `winget install T3Tools.T3Code` | | |
| 64 | | macOS | `brew install --cask t3-code` | | |
| 65 | | Arch Linux | `yay -S t3code-bin` | | |
| 66 | | Arch Linux nightly | `yay -S t3code-nightly-bin` | | |
| 61 | | Platform | Install | | |
| 62 | | ------------------ | ---------------------------------- | | |
| 63 | | Windows | `winget install T3Tools.T3Code` | | |
| 64 | | macOS | `brew install --cask t3-code` | | |
| 65 | | Debian, Ubuntu | `sudo apt install ./T3-Code-*.deb` | | |
| 66 | | Arch Linux | `yay -S t3code-bin` | | |
| 67 | | Arch Linux nightly | `yay -S t3code-nightly-bin` | | |
| 68 | ||
| 69 | The `.deb` updates itself like the other desktop builds. It asks for your | |
| 70 | password to install each update. If your desktop has no password prompt, the | |
| 71 | update fails. Download the new `.deb` and install it the same way. | |
| 67 | 72 | |
| 68 | 73 | ### Windows Subsystem for Linux |
| 69 | 74 |
scripts/build-desktop-artifact.test.ts+2| ... | ... | @@ -668,6 +668,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 668 | 668 | iconSize: 120, |
| 669 | 669 | iconTextSize: 12, |
| 670 | 670 | }); |
| 671 | // A Linux AppImage build also emits the .deb from the same run. | |
| 672 | assert.deepStrictEqual((linux.linux as Record<string, unknown>).target, ["AppImage", "deb"]); | |
| 671 | 673 | // Linux must register the renderer schemes so the generated .desktop |
| 672 | 674 | // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links. |
| 673 | 675 | assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [ |
scripts/build-desktop-artifact.ts+33-1| ... | ... | @@ -929,6 +929,7 @@ interface StagePackageJson { |
| 929 | 929 | readonly private: true; |
| 930 | 930 | readonly packageManager: string; |
| 931 | 931 | readonly description: string; |
| 932 | readonly homepage: string; | |
| 932 | 933 | readonly author: string; |
| 933 | 934 | readonly main: string; |
| 934 | 935 | readonly build: Record<string, unknown>; |
| ... | ... | @@ -2733,10 +2734,17 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2733 | 2734 | |
| 2734 | 2735 | if (platform === "linux") { |
| 2735 | 2736 | buildConfig.linux = { |
| 2736 | target: [target], | |
| 2737 | // The .deb is built from the same unpacked app after the AppImage. | |
| 2738 | // electron-builder lists both in latest-linux.yml and writes | |
| 2739 | // resources/package-type into the .deb only, so electron-updater updates | |
| 2740 | // each install in its own format. | |
| 2741 | target: target === "AppImage" ? [target, "deb"] : [target], | |
| 2737 | 2742 | executableName: "t3code", |
| 2738 | 2743 | icon: "icons", |
| 2739 | 2744 | category: "Development", |
| 2745 | synopsis: "Desktop GUI for coding agents", | |
| 2746 | // Required by the .deb control file. | |
| 2747 | maintainer: "T3 Tools <hello@t3.codes>", | |
| 2740 | 2748 | // electron-builder turns these into MimeType=x-scheme-handler/<scheme>; |
| 2741 | 2749 | // in the .desktop entry (Exec already gets %U), so browsers can hand |
| 2742 | 2750 | // t3code:// OAuth callbacks to the app. |
| ... | ... | @@ -2752,6 +2760,23 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2752 | 2760 | }, |
| 2753 | 2761 | }, |
| 2754 | 2762 | }; |
| 2763 | buildConfig.deb = { | |
| 2764 | // Electron's runtime libraries. Debian 13 and Ubuntu 24.04 renamed some | |
| 2765 | // for 64-bit time; the old name is the fallback for older releases. | |
| 2766 | depends: [ | |
| 2767 | "libasound2t64 | libasound2", | |
| 2768 | "libatspi2.0-0t64 | libatspi2.0-0", | |
| 2769 | "libgbm1", | |
| 2770 | "libgtk-3-0t64 | libgtk-3-0", | |
| 2771 | "libnotify4", | |
| 2772 | "libnss3", | |
| 2773 | "libsecret-1-0", | |
| 2774 | "libuuid1", | |
| 2775 | "libxss1", | |
| 2776 | "libxtst6", | |
| 2777 | "xdg-utils", | |
| 2778 | ], | |
| 2779 | }; | |
| 2755 | 2780 | } |
| 2756 | 2781 | |
| 2757 | 2782 | if (platform === "win") { |
| ... | ... | @@ -3642,6 +3667,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 3642 | 3667 | private: true, |
| 3643 | 3668 | packageManager: rootPackageJson.packageManager, |
| 3644 | 3669 | description: "T3 Code desktop build", |
| 3670 | // Required by the .deb control file. | |
| 3671 | homepage: "https://t3.codes", | |
| 3645 | 3672 | author: "T3 Tools", |
| 3646 | 3673 | main: "apps/desktop/dist-electron/main.cjs", |
| 3647 | 3674 | build: yield* createBuildConfig( |
| ... | ... | @@ -3738,6 +3765,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 3738 | 3765 | delete buildEnv[key]; |
| 3739 | 3766 | } |
| 3740 | 3767 | } |
| 3768 | if (options.platform === "linux") { | |
| 3769 | // fpm compresses the .deb with the system xz through tar. Threaded mode | |
| 3770 | // takes seconds on a many-core runner instead of about two minutes. | |
| 3771 | buildEnv.XZ_DEFAULTS = "-T0"; | |
| 3772 | } | |
| 3741 | 3773 | if (!options.signed) { |
| 3742 | 3774 | buildEnv.CSC_IDENTITY_AUTO_DISCOVERY = "false"; |
| 3743 | 3775 | delete buildEnv.CSC_LINK; |