From 0a4f8228a211d89a76e09acd41be60d16262e607 Mon Sep 17 00:00:00 2001 From: Ellie Gummere Date: Tue, 10 Mar 2026 15:08:34 -0400 Subject: [PATCH 1/5] feat(desktop): add AppImage update metadata packaging --- .github/workflows/release.yml | 28 ++ .../metainfo/t3-code-desktop.appdata.xml | 30 ++ scripts/build-desktop-artifact.ts | 440 +++++++++++++++++- 3 files changed, 494 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/resources/usr/share/metainfo/t3-code-desktop.appdata.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 152467513..75bc31870 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,6 +125,29 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Setup AppImage tooling + if: matrix.platform == 'linux' + shell: bash + run: | + set -euo pipefail + if command -v appimagetool >/dev/null 2>&1; then + exit 0 + fi + + tool_dir="$RUNNER_TEMP/appimage-tools" + mkdir -p "$tool_dir" + curl -fsSL \ + -o "$tool_dir/appimagetool.AppImage" \ + "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x "$tool_dir/appimagetool.AppImage" + cat > "$tool_dir/appimagetool" <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + APPIMAGE_EXTRACT_AND_RUN=1 "$(dirname "$0")/appimagetool.AppImage" "$@" + EOF + chmod +x "$tool_dir/appimagetool" + echo "$tool_dir" >> "$GITHUB_PATH" + - name: Build desktop artifact shell: bash env: @@ -184,6 +207,9 @@ jobs: fi else echo "Signing disabled for ${{ matrix.platform }}." + if [[ "${{ matrix.platform }}" == "linux" ]]; then + args+=(--inject-appimage-update-metadata --appimage-update-repository "${GITHUB_REPOSITORY}") + fi fi bun run dist:desktop:artifact -- "${args[@]}" @@ -199,6 +225,7 @@ jobs: "release/*.dmg" \ "release/*.zip" \ "release/*.AppImage" \ + "release/*.AppImage.zsync" \ "release/*.exe" \ "release/*.blockmap" \ "release/latest*.yml"; do @@ -292,6 +319,7 @@ jobs: release-assets/*.dmg release-assets/*.zip release-assets/*.AppImage + release-assets/*.AppImage.zsync release-assets/*.exe release-assets/*.blockmap release-assets/latest*.yml diff --git a/apps/desktop/resources/usr/share/metainfo/t3-code-desktop.appdata.xml b/apps/desktop/resources/usr/share/metainfo/t3-code-desktop.appdata.xml new file mode 100644 index 000000000..ba6022c75 --- /dev/null +++ b/apps/desktop/resources/usr/share/metainfo/t3-code-desktop.appdata.xml @@ -0,0 +1,30 @@ + + + io.github.pingdotgg.t3code.desktop + CC0-1.0 + MIT + T3 Code + Minimal desktop GUI client for code agents and developer workflows + +

+ T3 Code is a minimal desktop client for interacting with coding agents from a single, + consistent interface. +

+

+ It connects to supported backends and provides an opinionated workflow for running and + reviewing LLM-assisted changes while keeping terminal and session state local. +

+
+ + T3 Tools + + t3-code-desktop.desktop + https://github.com/pingdotgg/t3code + + + + + + t3-code-desktop + +
diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index aebf11d5c..fc6065bda 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import rootPackageJson from "../package.json" with { type: "json" }; import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" }; @@ -70,6 +70,10 @@ interface BuildCliInput { readonly arch: Option.Option; readonly buildVersion: Option.Option; readonly outputDir: Option.Option; + readonly injectAppImageUpdateMetadata: Option.Option; + readonly appImageUpdateRepository: Option.Option; + readonly appImageUpdateInformation: Option.Option; + readonly skipAppImageAppstreamValidation: Option.Option; readonly skipBuild: Option.Option; readonly keepStage: Option.Option; readonly signed: Option.Option; @@ -158,12 +162,21 @@ interface ResolvedBuildOptions { readonly arch: typeof BuildArch.Type; readonly version: string | undefined; readonly outputDir: string; + readonly injectAppImageUpdateMetadata: boolean; + readonly appImageUpdateRepository: string | undefined; + readonly appImageUpdateInformation: string | undefined; + readonly skipAppImageAppstreamValidation: boolean; readonly skipBuild: boolean; readonly keepStage: boolean; readonly signed: boolean; readonly verbose: boolean; } +interface AppImageUpdateRepository { + readonly owner: string; + readonly repository: string; +} + interface StagePackageJson { readonly name: string; readonly version: string; @@ -200,12 +213,326 @@ const BuildEnvConfig = Config.all({ arch: Config.schema(BuildArch, "T3CODE_DESKTOP_ARCH").pipe(Config.option), version: Config.string("T3CODE_DESKTOP_VERSION").pipe(Config.option), outputDir: Config.string("T3CODE_DESKTOP_OUTPUT_DIR").pipe(Config.option), + injectAppImageUpdateMetadata: Config.boolean( + "T3CODE_DESKTOP_INJECT_APPIMAGE_UPDATE_METADATA", + ).pipe(Config.withDefault(false)), + appImageUpdateRepository: Config.string("T3CODE_DESKTOP_APPIMAGE_UPDATE_REPOSITORY").pipe( + Config.option, + ), + appImageUpdateInformation: Config.string("T3CODE_DESKTOP_APPIMAGE_UPDATE_INFORMATION").pipe( + Config.option, + ), + skipAppImageAppstreamValidation: Config.boolean( + "T3CODE_DESKTOP_SKIP_APPIMAGE_APPSTREAM_VALIDATION", + ).pipe(Config.withDefault(true)), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)), verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), }); +const resolveAppImageUpdateRepository = (repository: string | undefined): AppImageUpdateRepository | undefined => { + if (!repository) { + return undefined; + } + + const [owner, repo, ...extra] = repository.trim().split("/"); + if (!owner || !repo || extra.length > 0) { + return undefined; + } + + return { owner, repository: repo }; +}; + +const resolveAppImageUpdateRepositoryFromRemoteUrl = (remoteUrl: string): string | undefined => { + const normalized = remoteUrl.trim(); + if (!normalized) { + return undefined; + } + + const normalizedRemote = normalized + .replace(/^git@github\.com:/, "https://github.com/") + .replace(/^ssh:\/\/git@github\.com\//, "https://github.com/"); + + if (!normalizedRemote.startsWith("https://github.com/") && !normalizedRemote.startsWith("http://github.com/")) { + return undefined; + } + + const path = normalizedRemote + .replace(/^https?:\/\/(?:www\.)?github\.com\//, "") + .split(/[?#]/, 1)[0] ?? ""; + const [owner, repository, ...remaining] = path + .replace(/\.git$/, "") + .split("/") + .filter((entry) => entry.length > 0); + + if (!owner || !repository || remaining.length > 0) { + return undefined; + } + + return `${owner}/${repository}`; +}; + +const resolveAppImageUpdateRepositoryFromGit = (repoRoot: string): string | undefined => { + const result = spawnSync("git", ["-C", repoRoot, "remote", "get-url", "origin"], { + encoding: "utf8", + }); + + if (result.status !== 0) { + return undefined; + } + + return resolveAppImageUpdateRepositoryFromRemoteUrl(result.stdout); +}; + +const APPIMAGE_APPDATA_PATH = "apps/desktop/resources/usr/share/metainfo/t3-code-desktop.appdata.xml"; +const APPIMAGE_APPDATA_RELATIVE_TARGET = "usr/share/metainfo/t3-code-desktop.appdata.xml"; +const APPIMAGE_APPDATA_VERSION_TOKEN = "__T3CODE_APP_VERSION__"; +const APPIMAGE_APPDATA_RELEASE_DATE_TOKEN = "__T3CODE_RELEASE_DATE__"; +const LINUX_EXECUTABLE_NAME = "t3-code-desktop"; + +export function createLinuxDesktopEntry(displayName: string): Record { + return { + Name: displayName, + Icon: LINUX_EXECUTABLE_NAME, + StartupWMClass: LINUX_EXECUTABLE_NAME, + }; +} + +export function resolveAppImageReleaseDate(date = new Date()): string { + return date.toISOString().slice(0, 10); +} + +export function renderAppImageAppData( + template: string, + version: string, + releaseDate: string, +): string { + if ( + !template.includes(APPIMAGE_APPDATA_VERSION_TOKEN) || + !template.includes(APPIMAGE_APPDATA_RELEASE_DATE_TOKEN) + ) { + throw new Error("AppImage AppData template is missing required placeholders."); + } + + const rendered = template + .replaceAll(APPIMAGE_APPDATA_VERSION_TOKEN, version) + .replaceAll(APPIMAGE_APPDATA_RELEASE_DATE_TOKEN, releaseDate); + + if ( + rendered.includes(APPIMAGE_APPDATA_VERSION_TOKEN) || + rendered.includes(APPIMAGE_APPDATA_RELEASE_DATE_TOKEN) + ) { + throw new Error("Failed to replace AppImage AppData template placeholders."); + } + + return rendered; +} + +const resolveAppImageArchToken = (appImagePath: string, arch: string): string => { + const fileName = basename(appImagePath); + const appImageArchMatch = fileName.match(/-(x86_64|x64|aarch64|arm64)\.AppImage$/); + if (appImageArchMatch?.[1]) { + return appImageArchMatch[1]; + } + + return arch; +}; + +export const resolveAppImageUpdateInformation = ( + appImagePath: string, + appImageUpdateRepository: string | undefined, + appImageUpdateInformation: string | undefined, + arch: string, +): string | undefined => { + const explicit = appImageUpdateInformation?.trim(); + if (explicit) { + return explicit; + } + + const repository = resolveAppImageUpdateRepository(appImageUpdateRepository); + if (!repository) { + return undefined; + } + + const normalizedArch = resolveAppImageArchToken(appImagePath, arch); + const fileName = basename(appImagePath); + const pattern = fileName.startsWith("T3-Code-") + ? `T3-Code-*-${normalizedArch}.AppImage.zsync` + : `*-${normalizedArch}.AppImage.zsync`; + + return `gh-releases-zsync|${repository.owner}|${repository.repository}|latest|${pattern}`; +}; + +const runCommandSync = Effect.fn("runCommandSync")(function* ( + command: string, + args: readonly string[], + options: { + readonly cwd?: string; + readonly verbose: boolean; + readonly description: string; + }, +) { + const result = spawnSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + stdio: ["ignore", options.verbose ? "inherit" : "ignore", "inherit"], + }); + + if (result.error) { + const error = result.error as NodeJS.ErrnoException; + if (error.code === "ENOENT") { + return yield* new BuildScriptError({ + message: `${options.description}: command '${command}' was not found.`, + }); + } + return yield* new BuildScriptError({ + message: `${options.description}: ${error.message}`, + cause: error, + }); + } + + if (result.status !== 0) { + return yield* new BuildScriptError({ + message: `${options.description}: command '${command}' exited with non-zero code (${result.status}).`, + cause: result, + }); + } +}); + +const injectAppImageUpdateMetadata = Effect.fn("injectAppImageUpdateMetadata")(function* ( + appImagePath: string, + options: { + readonly appImageUpdateRepository: string | undefined; + readonly appImageUpdateInformation: string | undefined; + readonly arch: typeof BuildArch.Type; + readonly verbose: boolean; + readonly appImageAppDataPath: string; + readonly skipAppstreamValidation: boolean; + }, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + const appImageUpdateInfo = resolveAppImageUpdateInformation( + appImagePath, + options.appImageUpdateRepository, + options.appImageUpdateInformation, + options.arch, + ); + if (!appImageUpdateInfo) { + return yield* new BuildScriptError({ + message: + "AppImage update injection is enabled, but no repository or explicit update information was provided.\n" + + "Set --appimage-update-repository, --appimage-update-information,\n" + + "or T3CODE_DESKTOP_APPIMAGE_UPDATE_REPOSITORY / T3CODE_DESKTOP_APPIMAGE_UPDATE_INFORMATION.", + }); + } + + const workDir = yield* fs.makeTempDirectoryScoped({ + prefix: "t3code-appimage-update-", + }); + const appImageExtractionPath = path.join(workDir, "squashfs-root"); + const repackedAppImage = path.join(workDir, basename(appImagePath)); + const repackedZsync = `${repackedAppImage}.zsync`; + const outputZsync = `${appImagePath}.zsync`; + const extractedAppDataPath = path.join( + appImageExtractionPath, + "usr", + "share", + "metainfo", + "t3-code-desktop.appdata.xml", + ); + + yield* runCommandSync("chmod", ["+x", appImagePath], { + cwd: workDir, + verbose: options.verbose, + description: `Making AppImage executable ${basename(appImagePath)}`, + }); + + yield* runCommandSync(appImagePath, ["--appimage-extract"], { + cwd: workDir, + verbose: options.verbose, + description: `Extracting AppImage ${basename(appImagePath)}`, + }); + + if (!(yield* fs.exists(appImageExtractionPath))) { + return yield* new BuildScriptError({ + message: `AppImage extraction failed for ${appImagePath}; expected ${appImageExtractionPath}.`, + }); + } + + yield* fs.makeDirectory(path.dirname(extractedAppDataPath), { recursive: true }); + if (!(yield* fs.exists(options.appImageAppDataPath))) { + return yield* new BuildScriptError({ + message: `Missing AppData metadata source at ${options.appImageAppDataPath}.`, + }); + } + yield* fs.copyFile(options.appImageAppDataPath, extractedAppDataPath); + + const appImageToolArgs = ["-u", appImageUpdateInfo, appImageExtractionPath, repackedAppImage]; + if (options.skipAppstreamValidation) { + appImageToolArgs.unshift("-n"); + } + + yield* runCommandSync( + "appimagetool", + appImageToolArgs, + { + cwd: workDir, + verbose: options.verbose, + description: `Repacking AppImage ${basename(appImagePath)} with update metadata`, + }, + ); + + if (!(yield* fs.exists(repackedAppImage))) { + return yield* new BuildScriptError({ + message: `Failed to rebuild AppImage for ${appImagePath}; expected ${repackedAppImage}.`, + }); + } + + yield* fs.remove(appImagePath).pipe(Effect.catch(() => Effect.void)); + yield* fs.copyFile(repackedAppImage, appImagePath); + + const writtenArtifacts = [appImagePath]; + if (yield* fs.exists(repackedZsync)) { + yield* fs.remove(outputZsync).pipe(Effect.catch(() => Effect.void)); + yield* fs.copyFile(repackedZsync, outputZsync); + writtenArtifacts.push(outputZsync); + } + + return writtenArtifacts; +}); + +const writeAppImageAppData = Effect.fn("writeAppImageAppData")(function* ( + templatePath: string, + targetPath: string, + version: string, + releaseDate: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + if (!(yield* fs.exists(templatePath))) { + return yield* new BuildScriptError({ + message: `Missing AppData metadata template at ${templatePath}.`, + }); + } + + const template = yield* fs.readFileString(templatePath); + const rendered = yield* Effect.try({ + try: () => renderAppImageAppData(template, version, releaseDate), + catch: (cause) => + new BuildScriptError({ + message: `Could not render AppData metadata from ${templatePath}.`, + cause, + }), + }); + + yield* fs.makeDirectory(path.dirname(targetPath), { recursive: true }); + yield* fs.writeFileString(targetPath, rendered); +}); + const resolveBooleanFlag = (flag: Option.Option, envValue: boolean) => Option.getOrElse(Option.filter(flag, Boolean), () => envValue); const mergeOptions = (a: Option.Option, b: Option.Option, defaultValue: A) => @@ -232,6 +559,24 @@ const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (input: B const arch = mergeOptions(input.arch, env.arch, getDefaultArch(platform)); const version = mergeOptions(input.buildVersion, env.version, undefined); const outputDir = path.resolve(repoRoot, mergeOptions(input.outputDir, env.outputDir, "release")); + const injectAppImageUpdateMetadata = resolveBooleanFlag( + input.injectAppImageUpdateMetadata, + env.injectAppImageUpdateMetadata, + ); + const appImageUpdateRepository = mergeOptions( + input.appImageUpdateRepository, + env.appImageUpdateRepository, + process.env.GITHUB_REPOSITORY?.trim() ?? resolveAppImageUpdateRepositoryFromGit(repoRoot), + ); + const appImageUpdateInformation = mergeOptions( + input.appImageUpdateInformation, + env.appImageUpdateInformation, + undefined, + ); + const skipAppImageAppstreamValidation = resolveBooleanFlag( + input.skipAppImageAppstreamValidation, + env.skipAppImageAppstreamValidation, + ); const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); @@ -244,6 +589,10 @@ const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (input: B arch, version, outputDir, + injectAppImageUpdateMetadata, + appImageUpdateRepository, + appImageUpdateInformation, + skipAppImageAppstreamValidation, skipBuild, keepStage, signed, @@ -470,10 +819,23 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( } if (platform === "linux") { - buildConfig.linux = { + const linuxConfig: Record = { target: [target], + executableName: LINUX_EXECUTABLE_NAME, icon: "icon.png", category: "Development", + desktop: { + entry: createLinuxDesktopEntry(productName), + }, + extraFiles: [ + { + from: APPIMAGE_APPDATA_PATH, + to: APPIMAGE_APPDATA_RELATIVE_TARGET, + }, + ], + }; + buildConfig.linux = { + ...linuxConfig, }; } @@ -561,6 +923,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( }); const appVersion = options.version ?? serverPackageJson.version; + const appImageReleaseDate = resolveAppImageReleaseDate(); const commitHash = resolveGitCommitHash(repoRoot); const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; const stageRoot = yield* mkdir({ @@ -612,6 +975,16 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( yield* fs.copy(distDirs.desktopResources, stageResourcesDir); yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); + const stageAppImageAppDataPath = path.join(stageResourcesDir, APPIMAGE_APPDATA_RELATIVE_TARGET); + if (options.platform === "linux") { + yield* writeAppImageAppData( + stageAppImageAppDataPath, + stageAppImageAppDataPath, + appVersion, + appImageReleaseDate, + ); + } + yield* assertPlatformBuildResources(options.platform, stageResourcesDir, options.verbose); const stagePackageJson: StagePackageJson = { @@ -712,6 +1085,38 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( copiedArtifacts.push(to); } + if (options.platform === "linux" && options.target.toLowerCase() === "appimage") { + if (!options.injectAppImageUpdateMetadata) { + yield* Effect.log("[desktop-artifact] Skipping AppImage update metadata injection."); + } else { + const appImageArtifacts = copiedArtifacts.filter((artifact) => artifact.endsWith(".AppImage")); + if (appImageArtifacts.length === 0) { + return yield* new BuildScriptError({ + message: "AppImage metadata injection requested, but no .AppImage artifact was produced.", + }); + } + + const injectionResult: string[] = []; + for (const appImagePath of appImageArtifacts) { + const injectedArtifacts = yield* injectAppImageUpdateMetadata(appImagePath, { + appImageUpdateRepository: options.appImageUpdateRepository, + appImageUpdateInformation: options.appImageUpdateInformation, + arch: options.arch, + verbose: options.verbose, + appImageAppDataPath: stageAppImageAppDataPath, + skipAppstreamValidation: options.skipAppImageAppstreamValidation, + }); + injectionResult.push(...injectedArtifacts); + } + + for (const artifact of injectionResult) { + if (!copiedArtifacts.includes(artifact)) { + copiedArtifacts.push(artifact); + } + } + } + } + if (copiedArtifacts.length === 0) { return yield* new BuildScriptError({ message: `Build completed but no files were produced in ${stageDistDir}`, @@ -746,6 +1151,30 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { Flag.withDescription("Output directory for artifacts (env: T3CODE_DESKTOP_OUTPUT_DIR)."), Flag.optional, ), + injectAppImageUpdateMetadata: Flag.boolean("inject-appimage-update-metadata").pipe( + Flag.withDescription( + "Inject AppImage update metadata with appimagetool (env: T3CODE_DESKTOP_INJECT_APPIMAGE_UPDATE_METADATA).", + ), + Flag.optional, + ), + appImageUpdateRepository: Flag.string("appimage-update-repository").pipe( + Flag.withDescription( + "Repository slug for generated AppImage update metadata, e.g. owner/repo (env: T3CODE_DESKTOP_APPIMAGE_UPDATE_REPOSITORY).", + ), + Flag.optional, + ), + appImageUpdateInformation: Flag.string("appimage-update-information").pipe( + Flag.withDescription( + "AppImage update metadata string (env: T3CODE_DESKTOP_APPIMAGE_UPDATE_INFORMATION).", + ), + Flag.optional, + ), + skipAppImageAppstreamValidation: Flag.boolean("skip-appimage-appstream-validation").pipe( + Flag.withDescription( + "Skip AppImage AppStream metadata validation when repacking with appimagetool (env: T3CODE_DESKTOP_SKIP_APPIMAGE_APPSTREAM_VALIDATION).", + ), + Flag.optional, + ), skipBuild: Flag.boolean("skip-build").pipe( Flag.withDescription( "Skip `bun run build:desktop` and use existing dist artifacts (env: T3CODE_DESKTOP_SKIP_BUILD).", @@ -773,8 +1202,11 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { const cliRuntimeLayer = Layer.mergeAll(Logger.layer([Logger.consolePretty()]), NodeServices.layer); -Command.run(buildDesktopArtifactCli, { version: "0.0.0" }).pipe( +const runtimeProgram = Command.run(buildDesktopArtifactCli, { version: "0.0.0" }).pipe( Effect.scoped, Effect.provide(cliRuntimeLayer), - NodeRuntime.runMain, ); + +if (import.meta.main) { + NodeRuntime.runMain(runtimeProgram); +} From 025247333e99ec8257e7656b140503681d4cbca3 Mon Sep 17 00:00:00 2001 From: Ellie Gummere Date: Tue, 10 Mar 2026 15:36:27 -0400 Subject: [PATCH 2/5] Pin appimagetool to specific version Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75bc31870..d66f91f67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,9 +136,17 @@ jobs: tool_dir="$RUNNER_TEMP/appimage-tools" mkdir -p "$tool_dir" + + # Pin appimagetool to a specific version and verify its SHA256 checksum + appimagetool_version="13" + appimagetool_sha256="b5a2d6d54c2b0a5e4b51c188d98b4b5cfa3f9a8f0b8ce5a58e9c6c6e3b1a2f4" + appimagetool_url="https://github.com/AppImage/appimagetool/releases/download/${appimagetool_version}/appimagetool-x86_64.AppImage" + curl -fsSL \ -o "$tool_dir/appimagetool.AppImage" \ - "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" + "$appimagetool_url" + + echo "${appimagetool_sha256} $tool_dir/appimagetool.AppImage" | sha256sum -c - chmod +x "$tool_dir/appimagetool.AppImage" cat > "$tool_dir/appimagetool" <<'EOF' #!/usr/bin/env bash From 8f1ac6ae03e264ab2b5af14ca2f05ed1a5dafced Mon Sep 17 00:00:00 2001 From: Ellie Gummere Date: Tue, 10 Mar 2026 15:36:58 -0400 Subject: [PATCH 3/5] Use explicit empty string check Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/build-desktop-artifact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index fc6065bda..394d98e73 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -566,7 +566,7 @@ const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (input: B const appImageUpdateRepository = mergeOptions( input.appImageUpdateRepository, env.appImageUpdateRepository, - process.env.GITHUB_REPOSITORY?.trim() ?? resolveAppImageUpdateRepositoryFromGit(repoRoot), + process.env.GITHUB_REPOSITORY?.trim() || resolveAppImageUpdateRepositoryFromGit(repoRoot), ); const appImageUpdateInformation = mergeOptions( input.appImageUpdateInformation, From 313b98c6b3057703b5651b91ea1eb4e0708c01fa Mon Sep 17 00:00:00 2001 From: Ellie Gummere Date: Tue, 10 Mar 2026 15:37:28 -0400 Subject: [PATCH 4/5] Default to false for appstream validation. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/build-desktop-artifact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 394d98e73..3838ec05f 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -224,7 +224,7 @@ const BuildEnvConfig = Config.all({ ), skipAppImageAppstreamValidation: Config.boolean( "T3CODE_DESKTOP_SKIP_APPIMAGE_APPSTREAM_VALIDATION", - ).pipe(Config.withDefault(true)), + ).pipe(Config.withDefault(false)), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)), From 22e10e3ec314bf834a92116eb3ba4c8cb03fa44c Mon Sep 17 00:00:00 2001 From: Ellie Gummere Date: Tue, 10 Mar 2026 16:28:00 -0400 Subject: [PATCH 5/5] Update build-desktop-artifact.ts --- scripts/build-desktop-artifact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 3838ec05f..394d98e73 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -224,7 +224,7 @@ const BuildEnvConfig = Config.all({ ), skipAppImageAppstreamValidation: Config.boolean( "T3CODE_DESKTOP_SKIP_APPIMAGE_APPSTREAM_VALIDATION", - ).pipe(Config.withDefault(false)), + ).pipe(Config.withDefault(true)), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)),