feat(client): linux flatpak via tauri shell (#188) #222

Merged
james merged 4 commits from 188-linux-flatpak into main 2026-06-21 18:22:28 +00:00
Owner

Summary

Wraps the Expo Web bundle in a Tauri v2 shell and ships it as a Linux Flatpak, attached tag-driven to the same release page the API container goes to. Closes #188 (well — closes once verified on the runner).

Shell decision: Tauri 2 (kept)

ADR-0027 §2 already calls Tauri out as the recommended shell; the ticket agrees; I agree after running the build out. Why I didn't switch:

  • Tauri 2 ships a single ~10 MB self-contained binary that loads the webkit2gtk-4.1 webview already in GNOME Platform 48. Cargo.lock is committed, so a release build is reproducible against a specific 2.x line (currently tauri = 2.11.3, tauri-build = 2.6.3).
  • Electron would mean bundling Chromium per app (≥ 80 MB) and a much heavier Flatpak; the per-app payload only makes sense if the webview compatibility surface needed it, and Carol's web bundle does not.
  • A bare gtk-webkit2 Rust binding wrapper would be smaller still but every desktop feature (window controls, drag regions, future system-tray) becomes hand-glue. Tauri 2 is the right next step.

No fatal Tauri blocker showed up. If one ever does, the manifest + shell layout in this PR ports cleanly to a smaller wrapper.

Runtime URL plumbing

Tauri's webview loads the bundle from tauri://localhost, so Platform.OS === "web" matches but the API is not at same-origin. New helper apps/client/lib/runtimeShell.ts detects the Tauri webview via the standard window.__TAURI_INTERNALS__ (v2) / window.__TAURI__ (v1) markers. apiClient.ts, serverUrl.ts, and the ServerUrlGate in _layout.tsx all collapse the previous "is it native?" check into a new "is it off-origin?" check that returns true for both Android and the Tauri shell. serverUrl.ts swaps its backing store between expo-secure-store (Android) and localStorage (Tauri webview) based on the same signal.

PWA behaviour is unchanged.

What's in the PR

  • apps/client/lib/runtimeShell.ts + the apiClient/serverUrl/_layout cascade.
  • apps/client/src-tauri/ — Tauri 2 project (Cargo.toml, Cargo.lock, tauri.conf.json, capabilities/default.json, placeholder icons, minimal main.rs + lib.rs).
  • apps/client/scripts/build-flatpak.sh + pnpm -F @carol/client build:flatpak script.
  • flatpak/tech.wynning.carol.{yml,desktop,metainfo.xml} at the repo root (flatpak-builder resolves source paths relative to the manifest dir, and several sources live outside apps/client/).
  • .forgejo/workflows/release-flatpak.yml — tag-triggered, polls the API release page from release.yml, attaches a carol-vX.Y.Z.flatpak asset. Mirrors the shape of release-android.yml.
  • apps/client/README.md — "Linux Flatpak build" section + the off-origin runtime URL notes.
  • apps/client/tests/runtimeShell.test.ts — pure unit coverage for the Tauri detector.

Flatpak runtime / SDK pin

org.gnome.Platform//48 + org.gnome.Sdk//48. GNOME 48 ships webkit2gtk-4.1 (Tauri 2's webview), gtk3, glib, and librsvg out of the box. We do not use org.freedesktop.Platform because it lacks webkit2gtk; switching would force building webkit from source inside the manifest (multi-hour). The pin is manual (Renovate doesn't track Flatpak runtimes); bumping requires editing the manifest + workflow env in the same PR.

Forgejo runner image — system deps

The current self-hosted runner uses ghcr.io/catthehacker/ubuntu:js-24.04, which does not ship rustup, webkit2gtk-4.1-dev, or flatpak. The workflow installs them on each run (apt-get install + rustup-init). Single Rust build + GNOME runtime download dominates runtime — expect roughly 5-10 min added per release vs. the API container build. Worth filing a follow-up to bake those deps into a custom runner image (see follow-ups below) but functionally fine today.

Verified locally

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck
  • pnpm -F @carol/api-client typecheck
  • pnpm -F @carol/client typecheck
  • pnpm -F @carol/client lint
  • pnpm -F @carol/client test — all 29 tests pass (4 new Tauri-detector cases)
  • pnpm -F @carol/client export:web — bundle still produces cleanly
  • actionlint .forgejo/workflows/*.yml — no findings
  • cargo metadata inside src-tauri/ parses (Cargo.toml syntax + dep resolution OK)
  • cargo generate-lockfile resolves tauri = "2" to v2.11.3 — committed
  • flatpak-builder --show-manifest flatpak/tech.wynning.carol.yml — manifest parses cleanly
  • desktop-file-validate flatpak/tech.wynning.carol.desktop — passes
  • appstreamcli validate flatpak/tech.wynning.carol.metainfo.xml — passes

Waits on CI

  • cargo build --release inside src-tauri/ — needs webkit2gtk-4.1-dev system deps; my local box doesn't have them.
  • flatpak-builder + flatpak build-bundle against GNOME Platform 48 — needs the runtime installed.
  • End-to-end smoke test of the resulting .flatpak (flatpak install, flatpak run, server-setup screen).

Per the ticket's guidance, the actual Flatpak production runs on the Forgejo runner. The first tag push after this lands is the first true end-to-end test.

Follow-ups worth filing

  • Custom self-hosted runner image with Rust toolchain + webkit2gtk-4.1-dev + flatpak-builder + GNOME 48 runtime pre-installed. Cuts release-flatpak job time roughly in half.
  • Tauri's auto-updater hook — separate ticket if desired; needs a release feed + signing key.
  • Flathub submission — explicitly out of scope per #188; needs a proper Carol-designed icon set and a sustained release cadence first.
  • A real icon. The committed PNGs are a placeholder blue square with "C" in the centre, generated locally by ImageMagick. A designer should produce the master at some point.

Test plan

  • Push a v0.0.0-rc.1 tag to a fork or a maintenance branch; watch the workflow run end-to-end and attach the .flatpak asset.
  • flatpak install --user carol-v0.0.0-rc.1.flatpak on a Fedora Workstation box.
  • First launch shows the server-setup screen; entering a Carol URL persists across restart.
  • Account screen "Change server" routes back to setup.
  • Verify the PWA (apps/api serving apps/client/dist/) still works unchanged (no regression in same-origin path).

Generated with Claude Code

## Summary Wraps the Expo Web bundle in a Tauri v2 shell and ships it as a Linux Flatpak, attached tag-driven to the same release page the API container goes to. Closes #188 (well — closes once verified on the runner). ## Shell decision: Tauri 2 (kept) ADR-0027 §2 already calls Tauri out as the recommended shell; the ticket agrees; I agree after running the build out. Why I didn't switch: - Tauri 2 ships a single ~10 MB self-contained binary that loads the webkit2gtk-4.1 webview already in GNOME Platform 48. Cargo.lock is committed, so a release build is reproducible against a specific 2.x line (currently `tauri = 2.11.3`, `tauri-build = 2.6.3`). - Electron would mean bundling Chromium per app (≥ 80 MB) and a much heavier Flatpak; the per-app payload only makes sense if the webview compatibility surface needed it, and Carol's web bundle does not. - A bare `gtk-webkit2` Rust binding wrapper would be smaller still but every desktop feature (window controls, drag regions, future system-tray) becomes hand-glue. Tauri 2 is the right next step. No fatal Tauri blocker showed up. If one ever does, the manifest + shell layout in this PR ports cleanly to a smaller wrapper. ## Runtime URL plumbing Tauri's webview loads the bundle from `tauri://localhost`, so `Platform.OS === "web"` matches but the API is not at same-origin. New helper [`apps/client/lib/runtimeShell.ts`](https://forge.wynning.tech/james/carol/src/branch/188-linux-flatpak/apps/client/lib/runtimeShell.ts) detects the Tauri webview via the standard `window.__TAURI_INTERNALS__` (v2) / `window.__TAURI__` (v1) markers. `apiClient.ts`, `serverUrl.ts`, and the `ServerUrlGate` in `_layout.tsx` all collapse the previous "is it native?" check into a new "is it off-origin?" check that returns true for both Android and the Tauri shell. `serverUrl.ts` swaps its backing store between `expo-secure-store` (Android) and `localStorage` (Tauri webview) based on the same signal. PWA behaviour is unchanged. ## What's in the PR - `apps/client/lib/runtimeShell.ts` + the apiClient/serverUrl/_layout cascade. - `apps/client/src-tauri/` — Tauri 2 project (`Cargo.toml`, `Cargo.lock`, `tauri.conf.json`, `capabilities/default.json`, placeholder icons, minimal `main.rs` + `lib.rs`). - `apps/client/scripts/build-flatpak.sh` + `pnpm -F @carol/client build:flatpak` script. - `flatpak/tech.wynning.carol.{yml,desktop,metainfo.xml}` at the repo root (flatpak-builder resolves source paths relative to the manifest dir, and several sources live outside `apps/client/`). - `.forgejo/workflows/release-flatpak.yml` — tag-triggered, polls the API release page from `release.yml`, attaches a `carol-vX.Y.Z.flatpak` asset. Mirrors the shape of `release-android.yml`. - `apps/client/README.md` — "Linux Flatpak build" section + the off-origin runtime URL notes. - `apps/client/tests/runtimeShell.test.ts` — pure unit coverage for the Tauri detector. ## Flatpak runtime / SDK pin `org.gnome.Platform//48` + `org.gnome.Sdk//48`. GNOME 48 ships webkit2gtk-4.1 (Tauri 2's webview), gtk3, glib, and librsvg out of the box. We do _not_ use `org.freedesktop.Platform` because it lacks webkit2gtk; switching would force building webkit from source inside the manifest (multi-hour). The pin is manual (Renovate doesn't track Flatpak runtimes); bumping requires editing the manifest + workflow env in the same PR. ## Forgejo runner image — system deps The current self-hosted runner uses `ghcr.io/catthehacker/ubuntu:js-24.04`, which does **not** ship rustup, webkit2gtk-4.1-dev, or flatpak. The workflow installs them on each run (`apt-get install` + rustup-init). Single Rust build + GNOME runtime download dominates runtime — expect roughly 5-10 min added per release vs. the API container build. Worth filing a follow-up to bake those deps into a custom runner image (see follow-ups below) but functionally fine today. ## Verified locally - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` - [x] `pnpm -F @carol/api-client typecheck` - [x] `pnpm -F @carol/client typecheck` - [x] `pnpm -F @carol/client lint` - [x] `pnpm -F @carol/client test` — all 29 tests pass (4 new Tauri-detector cases) - [x] `pnpm -F @carol/client export:web` — bundle still produces cleanly - [x] `actionlint .forgejo/workflows/*.yml` — no findings - [x] `cargo metadata` inside `src-tauri/` parses (Cargo.toml syntax + dep resolution OK) - [x] `cargo generate-lockfile` resolves `tauri = "2"` to v2.11.3 — committed - [x] `flatpak-builder --show-manifest flatpak/tech.wynning.carol.yml` — manifest parses cleanly - [x] `desktop-file-validate flatpak/tech.wynning.carol.desktop` — passes - [x] `appstreamcli validate flatpak/tech.wynning.carol.metainfo.xml` — passes ## Waits on CI - [ ] `cargo build --release` inside `src-tauri/` — needs webkit2gtk-4.1-dev system deps; my local box doesn't have them. - [ ] `flatpak-builder` + `flatpak build-bundle` against GNOME Platform 48 — needs the runtime installed. - [ ] End-to-end smoke test of the resulting `.flatpak` (`flatpak install`, `flatpak run`, server-setup screen). Per the ticket's guidance, the actual Flatpak production runs on the Forgejo runner. The first tag push after this lands is the first true end-to-end test. ## Follow-ups worth filing - Custom self-hosted runner image with Rust toolchain + webkit2gtk-4.1-dev + flatpak-builder + GNOME 48 runtime pre-installed. Cuts release-flatpak job time roughly in half. - Tauri's auto-updater hook — separate ticket if desired; needs a release feed + signing key. - Flathub submission — explicitly out of scope per #188; needs a proper Carol-designed icon set and a sustained release cadence first. - A real icon. The committed PNGs are a placeholder blue square with "C" in the centre, generated locally by ImageMagick. A designer should produce the master at some point. ## Test plan - [ ] Push a `v0.0.0-rc.1` tag to a fork or a maintenance branch; watch the workflow run end-to-end and attach the `.flatpak` asset. - [ ] `flatpak install --user carol-v0.0.0-rc.1.flatpak` on a Fedora Workstation box. - [ ] First launch shows the server-setup screen; entering a Carol URL persists across restart. - [ ] Account screen "Change server" routes back to setup. - [ ] Verify the PWA (`apps/api` serving `apps/client/dist/`) still works unchanged (no regression in same-origin path). Generated with [Claude Code](https://claude.com/claude-code)
feat(ci): forgejo workflow for flatpak release (#188)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 8s
PR / Static analysis (pull_request) Successful in 2m15s
PR / OSV-Scanner (pull_request) Successful in 2m35s
PR / pnpm audit (pull_request) Successful in 2m45s
PR / Lint (pull_request) Successful in 3m31s
PR / Client (web export smoke) (pull_request) Successful in 4m0s
PR / Package age policy (soft) (pull_request) Successful in 1m26s
PR / Test (sqlite) (pull_request) Successful in 4m14s
PR / OpenAPI (pull_request) Successful in 4m14s
PR / Build (pull_request) Successful in 4m32s
PR / Typecheck (pull_request) Successful in 4m32s
PR / Test (postgres) (pull_request) Successful in 4m33s
Secrets / gitleaks (pull_request) Successful in 1m3s
PR / Coverage (soft) (pull_request) Successful in 4m23s
PR / Trivy (image) (pull_request) Failing after 6m7s
4b19659727

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.9% ≥ 50%
Branches 76.0% ≥ 75%
Functions 91.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.9% ✅ | ≥ 50% | | Branches | 76.0% ✅ | ≥ 75% | | Functions | 91.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
james merged commit e75e5aad63 into main 2026-06-21 18:22:28 +00:00
james deleted branch 188-linux-flatpak 2026-06-21 18:22:29 +00:00
Sign in to join this conversation.
No description provided.