From 21bb5c9c6b3b8ecc76088d2cd62add170150ed45 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Wed, 13 May 2026 15:05:10 -0400 Subject: [PATCH 1/3] feat(code): add Linux AppImage build target Adds @reforged/maker-appimage so Forge produces x64 and arm64 AppImages alongside the existing macOS/Windows artifacts. The release workflow gains a publish-linux matrix job (ubuntu-latest + ubuntu-24.04-arm) which installs the AppImage tooling (squashfs-tools, zsync, libfuse2t64) before running electron-forge publish, and finalize-release now depends on it. Also includes a make:linux script that builds the AppImage from macOS via Docker for local verification, since electron-forge can't natively cross-build AppImages from darwin. Drive-by fix: remove a duplicate `const { isOnline } = useConnectivity()` in SessionView.tsx that came in via a merge collision between #1971 and #2121 and was failing typecheck on main. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/code-release.yml | 91 ++++++++++++++++++- .gitignore | 1 + apps/code/forge.config.ts | 14 +++ apps/code/package.json | 2 + apps/code/scripts/build-linux-docker.sh | 59 ++++++++++++ .../sessions/components/SessionView.tsx | 1 - pnpm-lock.yaml | 27 ++++++ 7 files changed, 193 insertions(+), 2 deletions(-) create mode 100755 apps/code/scripts/build-linux-docker.sh diff --git a/.github/workflows/code-release.yml b/.github/workflows/code-release.yml index d3f6b93e8..36ce5c235 100644 --- a/.github/workflows/code-release.yml +++ b/.github/workflows/code-release.yml @@ -211,8 +211,97 @@ jobs: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: pnpm --filter code run publish + publish-linux: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + arch: x64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + permissions: + id-token: write + contents: write + env: + NODE_OPTIONS: "--max-old-space-size=8192" + NODE_ENV: production + VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }} + VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }} + steps: + - name: Get app token + id: app-token + uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc # v3 + with: + app_id: ${{ secrets.GH_APP_ARRAY_RELEASER_APP_ID }} + private_key: ${{ secrets.GH_APP_ARRAY_RELEASER_PRIVATE_KEY }} + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false + + - name: Install AppImage build tooling + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + squashfs-tools zsync libfuse2t64 + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Extract version from tag + id: version + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/v}" + echo "Version: $TAG_VERSION" + echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT" + + - name: Set version in package.json + env: + APP_VERSION: ${{ steps.version.outputs.version }} + run: | + jq --arg v "$APP_VERSION" '.version = $v' apps/code/package.json > tmp.json && mv tmp.json apps/code/package.json + echo "Set apps/code/package.json version to $APP_VERSION" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build electron-trpc package + run: pnpm --filter @posthog/electron-trpc run build + + - name: Build platform package + run: pnpm --filter @posthog/platform run build + + - name: Build shared package + run: pnpm --filter @posthog/shared run build + + - name: Build git package + run: pnpm --filter @posthog/git run build + + - name: Build enricher package + run: pnpm --filter @posthog/enricher run build + + - name: Build agent package + run: pnpm --filter @posthog/agent run build + + - name: Publish with Electron Forge + env: + APP_VERSION: ${{ steps.version.outputs.version }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: pnpm --filter code run publish + finalize-release: - needs: [publish-macos, publish-windows] + needs: [publish-macos, publish-windows, publish-linux] runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index d603ab8a5..479cee9c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies node_modules/ +.pnpm-store/ # Build outputs dist/ diff --git a/apps/code/forge.config.ts b/apps/code/forge.config.ts index f52736bf2..676402598 100644 --- a/apps/code/forge.config.ts +++ b/apps/code/forge.config.ts @@ -8,6 +8,7 @@ import { MakerZIP } from "@electron-forge/maker-zip"; import { VitePlugin } from "@electron-forge/plugin-vite"; import { PublisherGithub } from "@electron-forge/publisher-github"; import type { ForgeConfig } from "@electron-forge/shared-types"; +import { MakerAppImage } from "@reforged/maker-appimage"; const appleCodesignIdentity = process.env.APPLE_CODESIGN_IDENTITY; const appleTeamId = process.env.APPLE_TEAM_ID; @@ -193,6 +194,13 @@ const config: ForgeConfig = { name: "PostHogCode", setupIcon: "./build/app-icon.ico", }), + new MakerAppImage({ + options: { + icon: "./build/app-icon.png", + categories: ["Development"], + bin: "PostHog Code", + }, + }), new MakerZIP({}, ["darwin", "linux"]), ], hooks: { @@ -248,6 +256,12 @@ const config: ForgeConfig = { ? "@parcel/watcher-win32-arm64" : "@parcel/watcher-win32-x64"; copyNativeDependency(watcherPkg, buildPath); + } else if (process.platform === "linux") { + const watcherPkg = + process.arch === "arm64" + ? "@parcel/watcher-linux-arm64-glibc" + : "@parcel/watcher-linux-x64-glibc"; + copyNativeDependency(watcherPkg, buildPath); } // Copy @parcel/watcher's hoisted dependencies diff --git a/apps/code/package.json b/apps/code/package.json index 1617808a2..bdccbff43 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -16,6 +16,7 @@ "package": "electron-forge package", "package:dev": "FORCE_DEV_MODE=1 SKIP_NOTARIZE=1 electron-forge package", "make": "electron-forge make", + "make:linux": "bash scripts/build-linux-docker.sh", "publish": "electron-forge publish", "build": "pnpm package", "build-native": "bash scripts/build-native-modules.sh", @@ -50,6 +51,7 @@ "@electron-forge/plugin-vite": "^7.11.1", "@electron-forge/publisher-github": "^7.11.1", "@electron-forge/shared-types": "^7.11.1", + "@reforged/maker-appimage": "^5.2.0", "@electron/rebuild": "^4.0.3", "@playwright/test": "^1.42.0", "@posthog/rollup-plugin": "^1.4.0", diff --git a/apps/code/scripts/build-linux-docker.sh b/apps/code/scripts/build-linux-docker.sh new file mode 100755 index 000000000..12e24886a --- /dev/null +++ b/apps/code/scripts/build-linux-docker.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH="${ARCH:-x64}" +case "$ARCH" in + x64) DOCKER_PLATFORM="linux/amd64" ;; + arm64) DOCKER_PLATFORM="linux/arm64" ;; + *) echo "Unsupported ARCH=$ARCH (expected x64 or arm64)" >&2; exit 1 ;; +esac + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +OUT_DIR="$REPO_ROOT/apps/code/out" +mkdir -p "$OUT_DIR" + +# Stream the repo source (no node_modules / build artifacts) into the container +# so node_modules lives on the container's overlayfs, not a slow FUSE bind mount. +# Only the output dir is bind-mounted so artifacts come back to the host. +cd "$REPO_ROOT" +# COPYFILE_DISABLE stops bsdtar from embedding macOS extended attrs as ._ files. +COPYFILE_DISABLE=1 tar -cf - \ + --exclude='./.git' \ + --exclude='./.pnpm-store' \ + --exclude='node_modules' \ + --exclude='.turbo' \ + --exclude='.vite' \ + --exclude='dist' \ + --exclude='out' \ + --exclude='playwright-results' \ + --exclude='._*' \ + --exclude='.DS_Store' \ + . | exec docker run --rm -i \ + --platform "$DOCKER_PLATFORM" \ + -e CI=true \ + -e NODE_OPTIONS="--max-old-space-size=8192" \ + -e NODE_ENV=production \ + -e ARCH="$ARCH" \ + -v "$OUT_DIR":/out \ + node:22-bookworm bash -lc ' + set -euo pipefail + trap "rc=\$?; echo >&2; echo \"[build-linux-docker] FAILED (exit \$rc) at line \$LINENO: \$BASH_COMMAND\" >&2; exit \$rc" ERR + mkdir -p /work && cd /work && tar -xf - + corepack enable && corepack prepare pnpm@latest --activate + apt-get update && apt-get install -y --no-install-recommends \ + libsecret-1-dev fuse libfuse2 ca-certificates git squashfs-tools zsync zip + # Tarball arrived owned by the host uid; tell git not to refuse on uid mismatch. + git config --global --add safe.directory /work + # Postinstall scripts call `git rev-parse` — give them a repo to find. + git init -q && git add -A && git -c user.email=x@x -c user.name=x commit -q -m init + pnpm install --frozen-lockfile + pnpm --filter @posthog/electron-trpc build + pnpm --filter @posthog/platform build + pnpm --filter @posthog/shared build + pnpm --filter @posthog/git build + pnpm --filter @posthog/enricher build + pnpm --filter @posthog/agent build + pnpm --filter code make --platform=linux --arch="$ARCH" + mkdir -p /out + cp -r apps/code/out/make /out/ + ' diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index 05e4de94e..b95675e7d 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -250,7 +250,6 @@ export function SessionView({ [onSendPrompt], ); - const { isOnline } = useConnectivity(); const handleBeforeSubmit = useCallback( (text: string, clearEditor: () => void): boolean => { if (!isOnline) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18830327f..023a8a0db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -419,6 +419,9 @@ importers: '@posthog/rollup-plugin': specifier: ^1.4.0 version: 1.4.0(rollup@4.57.1) + '@reforged/maker-appimage': + specifier: ^5.2.0 + version: 5.2.0 '@storybook/addon-a11y': specifier: 10.2.0 version: 10.2.0(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) @@ -4554,6 +4557,13 @@ packages: '@react-navigation/routers@7.5.3': resolution: {integrity: sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==} + '@reforged/maker-appimage@5.2.0': + resolution: {integrity: sha512-5u7spsDMyMfwqAnTRsSipVgTIy+DW+wlfhceaRghCuTvyY8Sti8/tFhVKj4vb+dYTqPvS7m30Gl0InGv22J8RQ==} + engines: {node: '>=19.0.0 || ^18.11.0'} + + '@reforged/maker-types@2.1.0': + resolution: {integrity: sha512-gNMAFO6mxqGwuUov0CzXGTHUMfAawlM6v/uYrqVnKeMwmceaLBt3HtfPcuNapDSH4br6D4EZ14WuWbgefmDfOQ==} + '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -4757,6 +4767,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@spacingbat3/lss@1.2.0': + resolution: {integrity: sha512-aywhxHNb6l7COooF3m439eT/6QN8E/RSl5IVboSKthMHcp0GlZYMSoS7546rqDLmFRxTD8f1tu/NIS9vtDwYAg==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -16324,6 +16337,18 @@ snapshots: dependencies: nanoid: 3.3.11 + '@reforged/maker-appimage@5.2.0': + dependencies: + '@electron-forge/maker-base': 7.11.1 + '@reforged/maker-types': 2.1.0 + '@spacingbat3/lss': 1.2.0 + semver: 7.7.3 + transitivePeerDependencies: + - bluebird + - supports-color + + '@reforged/maker-types@2.1.0': {} + '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -16473,6 +16498,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@spacingbat3/lss@1.2.0': {} + '@standard-schema/spec@1.1.0': {} '@storybook/addon-a11y@10.2.0(storybook@10.2.0(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': From 08e398e4d64f0b6483ea215f39e7bc28c761b983 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 15 May 2026 14:22:41 -0400 Subject: [PATCH 2/3] maybe fix titlebar --- apps/code/src/main/window.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 667b62e81..31d7b27b4 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -168,14 +168,16 @@ export function createWindow(): void { titleBarStyle: "hiddenInset" as const, trafficLightPosition: { x: 12, y: 9 }, } - : { - titleBarStyle: "hidden" as const, - titleBarOverlay: { - color: "#0a0a0a", - symbolColor: "#ffffff", - height: 36, - }, - }; + : process.platform === "win32" + ? { + titleBarStyle: "hidden" as const, + titleBarOverlay: { + color: "#0a0a0a", + symbolColor: "#ffffff", + height: 36, + }, + } + : {}; mainWindow = new BrowserWindow({ ...(savedState.x !== undefined && { x: savedState.x }), From 8239300917d1dc6a1f359ac63b013e06195634d0 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 15 May 2026 19:32:29 -0400 Subject: [PATCH 3/3] fix(code): set window icon on Linux/Windows macOS uses the .app bundle icon; Linux/Windows need an explicit BrowserWindow icon for the window decoration. Ship build/app-icon.png via extraResource and resolve it via process.resourcesPath when packaged, app.getAppPath() in dev. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/code/forge.config.ts | 10 +++++++--- apps/code/scripts/build-linux-docker.sh | 1 + apps/code/src/main/window.ts | 9 +++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/code/forge.config.ts b/apps/code/forge.config.ts index 676402598..2441b4852 100644 --- a/apps/code/forge.config.ts +++ b/apps/code/forge.config.ts @@ -82,7 +82,7 @@ const osxSignConfig = shouldSignMacApp && appleCodesignIdentity ? ({ identity: appleCodesignIdentity, - optionsForFile: (_filePath) => { + optionsForFile: () => { // Entitlements for all binaries/frameworks return { hardenedRuntime: true, @@ -140,6 +140,8 @@ function copySync(dependency: string, destinationRoot: string, source: string) { ); } +const hasAssetsCar = existsSync("build/Assets.car"); + const config: ForgeConfig = { packagerConfig: { asar: { @@ -152,8 +154,10 @@ const config: ForgeConfig = { icon: "./build/app-icon", // Forge adds .icns/.ico/.png based on platform appBundleId: "com.posthog.array", appCategoryType: "public.app-category.productivity", - extraResource: existsSync("build/Assets.car") ? ["build/Assets.car"] : [], - extendInfo: existsSync("build/Assets.car") + extraResource: hasAssetsCar + ? ["build/Assets.car", "build/app-icon.png"] + : ["build/app-icon.png"], + extendInfo: hasAssetsCar ? { CFBundleIconName: "Icon", } diff --git a/apps/code/scripts/build-linux-docker.sh b/apps/code/scripts/build-linux-docker.sh index 12e24886a..399ebba82 100755 --- a/apps/code/scripts/build-linux-docker.sh +++ b/apps/code/scripts/build-linux-docker.sh @@ -30,6 +30,7 @@ COPYFILE_DISABLE=1 tar -cf - \ --exclude='.DS_Store' \ . | exec docker run --rm -i \ --platform "$DOCKER_PLATFORM" \ + --name build-linux \ -e CI=true \ -e NODE_OPTIONS="--max-old-space-size=8192" \ -e NODE_ENV=production \ diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 31d7b27b4..e1a640f86 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -179,6 +179,14 @@ export function createWindow(): void { } : {}; + // macOS uses the .app bundle icon, but Linux/Windows need an explicit icon + const windowIcon = + process.platform !== "darwin" + ? app.isPackaged + ? path.join(process.resourcesPath, "app-icon.png") + : path.join(app.getAppPath(), "build/app-icon.png") + : undefined; + mainWindow = new BrowserWindow({ ...(savedState.x !== undefined && { x: savedState.x }), ...(savedState.y !== undefined && { y: savedState.y }), @@ -187,6 +195,7 @@ export function createWindow(): void { minWidth: 1200, minHeight: 600, backgroundColor: "#0a0a0a", + ...(windowIcon ? { icon: windowIcon } : {}), ...platformWindowConfig, show: false, webPreferences: {