Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2581322
feat(packaging): add deb and rpm desktop targets
bigpod98 Jul 16, 2026
03b684c
docs: revert scripts reference changes
bigpod98 Jul 17, 2026
8b75392
Merge branch 'main' into main
juliusmarminge Jul 17, 2026
6f2eac5
Merge branch 'main' into main
bigpod98 Jul 19, 2026
ca035c7
Merge branch 'main' into main
bigpod98 Jul 19, 2026
bfd0201
feat(packaging): expose bundled headless server CLI
bigpod98 Jul 20, 2026
ab03901
fix(packaging): declare RPM runtime dependencies
bigpod98 Jul 20, 2026
a4f94d9
Merge branch 'main' into main
bigpod98 Jul 20, 2026
f5629f8
Merge branch 'main' into main
bigpod98 Jul 21, 2026
c88aa98
Merge branch 'main' into main
bigpod98 Jul 22, 2026
7153ea5
Merge branch 'main' into main
bigpod98 Jul 23, 2026
1373b12
Merge branch 'main' into main
bigpod98 Jul 24, 2026
3b649e9
Merge branch 'main' into main
bigpod98 Jul 25, 2026
4b56f00
Merge branch 'main' into main
bigpod98 Jul 29, 2026
1b6acb1
Merge remote-tracking branch 'upstream/main'
bigpod98 Jul 31, 2026
1984602
Merge branch 'main' into main
bigpod98 Aug 6, 2026
ab4f478
feat(release): publish the Linux .deb and .rpm from the release workflow
chukfinley Aug 6, 2026
306e95c
Merge pull request #1 from chukfinley/feat/linux-deb-rpm-release-wiring
bigpod98 Aug 6, 2026
4d2e2ff
refactor(packaging): split out Linux headless launcher
raulferrodrigues Aug 6, 2026
7ed2e88
Merge pull request #2 from raulferrodrigues/main
bigpod98 Aug 6, 2026
c882349
Merge branch 'main' into main
bigpod98 Aug 6, 2026
473d2d6
Add 'libgbm1' to desktop artifact dependencies
bigpod98 Aug 6, 2026
703f72c
Fix formatting of RPM dependencies in build script
bigpod98 Aug 6, 2026
b95d7cb
test(packaging): include libgbm in deb dependencies
bigpod98 Aug 6, 2026
c9fb83c
Merge branch 'pingdotgg:main' into main
bigpod98 Aug 7, 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
21 changes: 20 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ jobs:
- label: Linux x64
runner: blacksmith-32vcpu-ubuntu-2404
platform: linux
target: AppImage
target: AppImage,deb,rpm
arch: x64
rust_target: x86_64-unknown-linux-gnu
resource_key: linux-x64
Expand Down Expand Up @@ -515,6 +515,19 @@ jobs:
Select-Object -Unique
"PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV

# electron-builder shells out to rpmbuild for the rpm target. The Ubuntu
# image ships it in the `rpm` package, so this is a no-op there; installing
# it only when missing keeps the target working if the image ever drops it.
- name: Ensure rpmbuild is available
if: matrix.platform == 'linux' && contains(matrix.target, 'rpm')
shell: bash
run: |
if ! command -v rpmbuild >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y rpm
fi
rpmbuild --version

- name: Build desktop artifact
shell: bash
env:
Expand Down Expand Up @@ -608,6 +621,8 @@ jobs:
"release/*.dmg" \
"release/*.zip" \
"release/*.AppImage" \
"release/*.deb" \
"release/*.rpm" \
"release/*.exe" \
"release/*.blockmap" \
"release/*.yml"; do
Expand Down Expand Up @@ -830,6 +845,8 @@ jobs:
release-assets/*.dmg
release-assets/*.zip
release-assets/*.AppImage
release-assets/*.deb
release-assets/*.rpm
release-assets/*.exe
release-assets/*.blockmap
release-assets/*.yml
Expand All @@ -850,6 +867,8 @@ jobs:
release-assets/*.dmg
release-assets/*.zip
release-assets/*.AppImage
release-assets/*.deb
release-assets/*.rpm
release-assets/*.exe
release-assets/*.blockmap
release-assets/*.yml
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ build/
.logs/
release/
release-mock/
packaging-output/
.t3
.idea/
apps/web/.playwright
Expand Down
8 changes: 8 additions & 0 deletions apps/marketing/src/pages/download.astro
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site";
<span class="card-arch">x86_64</span>
<span class="card-format">AppImage</span>
</a>
<a class="download-card" data-asset="amd64.deb" href={RELEASES_URL}>
<span class="card-arch">Debian, Ubuntu (x86_64)</span>
<span class="card-format">.deb</span>
</a>
<a class="download-card" data-asset="x86_64.rpm" href={RELEASES_URL}>
<span class="card-arch">Fedora, RHEL (x86_64)</span>
<span class="card-format">.rpm</span>
</a>
</div>
</section>

Expand Down
7 changes: 7 additions & 0 deletions docs/internals/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,16 @@ authenticated.
to the host, so this produces an arm64 DMG on Apple Silicon. Use `dist:desktop:dmg:arm64` or
`dist:desktop:dmg:x64`, or pass `--arch <arm64|x64|universal>`, to force one.
- `vp run dist:desktop:linux`: Builds a Linux AppImage into `./release`.
- `vp run dist:desktop:deb`: Builds a Debian/Ubuntu `.deb` into `./packaging-output`. Architecture
defaults to the host; `:arm64` and `:x64` variants exist.
- `vp run dist:desktop:rpm`: Builds a Fedora/RHEL `.rpm` into `./packaging-output`. `:arm64` and
`:x64` variants exist. Needs `rpmbuild` on the host (`apt install rpm` on Debian/Ubuntu).
- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64`
variants exist.

`--target` accepts a comma-separated list for Linux, so `--target AppImage,deb,rpm` emits all three
packages from one electron-builder run. That is what the release workflow uses.

### Desktop `.dmg` packaging notes

- Default build is unsigned/not notarized for local sharing.
Expand Down
4 changes: 3 additions & 1 deletion docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This document covers the unified release workflow for stable and nightly desktop
- Builds four artifacts in parallel for both channels:
- macOS `arm64` DMG
- macOS `x64` DMG
- Linux `x64` AppImage
- Linux `x64` AppImage plus `.deb` and `.rpm` emitted by the same electron-builder run
- Windows `x64` NSIS installer
- Publishes one GitHub Release with all produced files.
- Stable tags with a suffix after `X.Y.Z` (for example `1.2.3-alpha.1`) are published as GitHub prereleases.
Expand Down Expand Up @@ -208,6 +208,8 @@ desktop-managed guidance when those environments are available.
- otherwise `GITHUB_REPOSITORY` from GitHub Actions.
- Required release assets for updater:
- platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads)
- the Linux `.deb` and `.rpm` ship in the same release but are not updater payloads: in-app updates
on Linux stay AppImage-only, so those installs upgrade through `dpkg`/`apt` and `rpm`/`dnf`
- channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases
- `*.blockmap` files (used for differential downloads)
- macOS metadata note:
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
"dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64",
"dist:desktop:dmg:x64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch x64",
"dist:desktop:linux": "node scripts/build-desktop-artifact.ts --platform linux --target AppImage --arch x64",
"dist:desktop:deb": "node scripts/build-desktop-artifact.ts --platform linux --target deb --output-dir packaging-output",
"dist:desktop:deb:arm64": "node scripts/build-desktop-artifact.ts --platform linux --target deb --arch arm64 --output-dir packaging-output",
"dist:desktop:deb:x64": "node scripts/build-desktop-artifact.ts --platform linux --target deb --arch x64 --output-dir packaging-output",
"dist:desktop:rpm": "node scripts/build-desktop-artifact.ts --platform linux --target rpm --output-dir packaging-output",
"dist:desktop:rpm:arm64": "node scripts/build-desktop-artifact.ts --platform linux --target rpm --arch arm64 --output-dir packaging-output",
"dist:desktop:rpm:x64": "node scripts/build-desktop-artifact.ts --platform linux --target rpm --arch x64 --output-dir packaging-output",
"dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis",
"dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64",
"dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
Expand Down
110 changes: 110 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
resolveDesktopProductName,
resolveDesktopUpdateChannel,
resolveDesktopWebAssetBrand,
resolveLinuxTargets,
resolveResourceMonitorRustTargets,
resourceMonitorExecutableName,
resolveGitHubPublishConfig,
Expand Down Expand Up @@ -552,6 +553,115 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it("splits a comma-separated Linux target into one target per package", () => {
assert.deepStrictEqual(resolveLinuxTargets("AppImage"), ["AppImage"]);
assert.deepStrictEqual(resolveLinuxTargets("AppImage,deb,rpm"), ["AppImage", "deb", "rpm"]);
assert.deepStrictEqual(resolveLinuxTargets(" AppImage , deb "), ["AppImage", "deb"]);
});

it.effect("emits every requested Linux package from a single build", () =>
Effect.gen(function* () {
const all = yield* createBuildConfig(
"linux",
"AppImage,deb,rpm",
"1.2.3",
false,
false,
undefined,
undefined,
);
const appImageOnly = yield* createBuildConfig(
"linux",
"AppImage",
"1.2.3",
false,
false,
undefined,
undefined,
);

assert.deepStrictEqual((all.linux as Record<string, unknown>).target, [
"AppImage",
"deb",
"rpm",
]);
// Both fpm-backed formats are configured from the one run.
assert.property(all, "deb");
assert.property(all, "rpm");

// An AppImage-only build stays free of package-format sections.
assert.notProperty(appImageOnly, "deb");
assert.notProperty(appImageOnly, "rpm");
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it.effect("includes distribution metadata in Linux package builds", () =>
Effect.gen(function* () {
const config = yield* createBuildConfig(
"linux",
"deb",
"1.2.3",
false,
false,
undefined,
undefined,
);

const linux = config.linux as Record<string, unknown>;
assert.deepStrictEqual(linux.target, ["deb"]);
assert.equal(linux.executableName, "t3code");
assert.equal(linux.category, "Development");
assert.equal(linux.maintainer, "T3 Tools <support@t3.gg>");
assert.equal(linux.vendor, "T3 Tools");
assert.equal(linux.synopsis, "A desktop GUI for AI coding agents");
assert.equal(linux.syncDesktopName, true);

const deb = config.deb as Record<string, unknown>;
assert.deepStrictEqual(deb.depends, [
"libgtk-3-0",
"libnotify4",
"libnss3",
"libxss1",
"libxtst6",
"xdg-utils",
"libatspi2.0-0",
"libuuid1",
"libsecret-1-0",
"libasound2t64 | libasound2",
"libgbm1",
]);
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it.effect("includes runtime dependencies in RPM packages", () =>
Effect.gen(function* () {
const config = yield* createBuildConfig(
"linux",
"rpm",
"1.2.3",
false,
false,
undefined,
undefined,
);

const rpm = config.rpm as Record<string, unknown>;
assert.deepStrictEqual(rpm.depends, [
"gtk3",
"libnotify",
"nss",
"libXScrnSaver",
"(libXtst or libXtst6)",
"xdg-utils",
"at-spi2-core",
"(libuuid or libuuid1)",
"alsa-lib",
"libsecret",
"mesa-libgbm",
]);
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it("stages the resource monitor as an external executable resource", () => {
assert.deepStrictEqual(DESKTOP_EXTRA_RESOURCES, [
{
Expand Down
74 changes: 71 additions & 3 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = {
},
};

const DEB_DEPENDENCIES = [
"libgtk-3-0",
"libnotify4",
"libnss3",
"libxss1",
"libxtst6",
"xdg-utils",
"libatspi2.0-0",
"libuuid1",
"libsecret-1-0",
"libasound2t64 | libasound2",
"libgbm1",
Comment thread
bigpod98 marked this conversation as resolved.
] as const;
Comment thread
cursor[bot] marked this conversation as resolved.

const RPM_DEPENDENCIES = [
"gtk3",
"libnotify",
"nss",
"libXScrnSaver",
"(libXtst or libXtst6)",
"xdg-utils",
"at-spi2-core",
"(libuuid or libuuid1)",
"alsa-lib",
"libsecret",
"mesa-libgbm",
] as const;

interface BuildCliInput {
readonly platform: Option.Option<typeof BuildPlatform.Type>;
readonly target: Option.Option<string>;
Expand Down Expand Up @@ -617,6 +645,9 @@ interface StagePackageJson {
readonly packageManager: string;
readonly description: string;
readonly author: string;
readonly homepage: string;
readonly license: string;
readonly desktopName: string;
readonly main: string;
readonly build: Record<string, unknown>;
readonly dependencies: Record<string, unknown>;
Expand Down Expand Up @@ -1521,6 +1552,23 @@ export function resolveDesktopProductName(version: string): string {
: (desktopPackageJson.productName ?? "T3 Code");
}

/**
* Splits a comma-separated Linux target string, for example "AppImage,deb,rpm",
* so one electron-builder run emits every package from the same staged app.
*
* The alternative — one release-matrix entry per format — collides on the
* upload artifact name (`desktop-${platform}-${arch}` is identical for two
* entries that differ only by target), which is what sank #1655. artifactName's
* ${ext} already keeps the output files apart, so a single run needs no extra
* runner and no extra release wall-clock.
*/
export function resolveLinuxTargets(target: string): ReadonlyArray<string> {
return target
.split(",")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
}

export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
platform: typeof BuildPlatform.Type,
target: string,
Expand Down Expand Up @@ -1584,11 +1632,16 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
}

if (platform === "linux") {
const linuxTargets = resolveLinuxTargets(target);
buildConfig.linux = {
target: [target],
target: linuxTargets,
executableName: "t3code",
icon: "icons",
category: "Development",
maintainer: "T3 Tools <support@t3.gg>",
vendor: "T3 Tools",
synopsis: "A desktop GUI for AI coding agents",
syncDesktopName: true,
// 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.
Expand All @@ -1604,6 +1657,18 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
},
},
};

if (linuxTargets.includes("deb")) {
buildConfig.deb = {
depends: [...DEB_DEPENDENCIES],
};
}

if (linuxTargets.includes("rpm")) {
buildConfig.rpm = {
depends: [...RPM_DEPENDENCIES],
};
}
}

if (platform === "win") {
Expand Down Expand Up @@ -1918,8 +1983,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
t3codeCommitHash: commitHash,
private: true,
packageManager: rootPackageJson.packageManager,
description: "T3 Code desktop build",
author: "T3 Tools",
description: "A desktop GUI for AI coding agents such as Codex, Claude, Cursor, and OpenCode.",
author: "T3 Tools <support@t3.gg>",
homepage: "https://github.com/pingdotgg/t3code",
license: "MIT",
desktopName: "t3code.desktop",
main: "apps/desktop/dist-electron/main.cjs",
build: yield* createBuildConfig(
options.platform,
Expand Down
Loading