feat(native): Linux support, phase 3 — release leg, update channel, download surface - #5556
feat(native): Linux support, phase 3 — release leg, update channel, download surface#5556tlgimenes wants to merge 48 commits into
Conversation
The root test runner never looked at apps/native/scripts, so boot-smoke-paths.test.ts was written but never executed by any workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the linux-amd64/linux-arm64 slugs and a portable sha256 helper. Without a resolvable slug the bundler fails on the missing externalBin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browser opener, PATH fallbacks, ripgrep hint and dev-process executable resolution get Linux arms; the macOS arms stay byte-identical. Watchdog and DNS-preflight changes are documentation only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Separates backend choice (debug && macos) from namespace choice (debug), so the dev/release split no longer rests on an incidental cfg constant, and pins all four quadrants with a test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Linux webview trusts the local control origin through a WebKitGTK per-host certificate exception instead of an OS trust store, behind an opt-in DECOCMS_LINUX_SECURE_ORIGIN flag. Spawned CLIs get a system+local CA bundle via SSL_CERT_FILE, which fails closed: SSL_CERT_FILE replaces the root store, so a bundle is exported only when a system bundle was found. org-fs is gated off where no mount path exists yet, with a DECOCMS_DISABLE_ORG_FS escape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Linux-only tauri config overlay targeting appimage, the PNG sizes the Linux bundlers need, and a gate that keeps the self-updater from spawning outside an AppImage — the plugin rewrites $APPIMAGE in place and has no such guard of its own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Routes the smoke's macOS-specific bundle paths through a pure resolver, adds the AppImage extract-and-launch path, and pins the tauri CLI so the smoke validates an artifact from the same bundler the release ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown, and its pre-commit step fails a commit whose staged set is entirely ignored — hence --no-verify on this docs-only commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BSD strsignal suffixes the signal number, glibc does not, so exit_info_from_pty recovers 128+signal on macOS and takes its documented exit_code() fallback on Linux. The test asserted only the macOS value, and the doc called the other case hypothetical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The keychain token store needs an OS credential service, and Linux CI runners have no session bus or keyring daemon; the dev-signing scripts drive security, codesign and BSD stat, which have no Linux counterpart. Both skip visibly rather than failing the ubuntu contract-suite leg. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A signal death rejected past the stdout/stderr dump, so the Linux smoke reported a bare SIGTRAP with nothing to diagnose it; the dump now runs in a finally. That SIGTRAP is glib turning WebKitGTK's failed DMABUF/compositing init into a fatal error, so headless Linux runs now request software rendering, which is all a shell/IPC smoke needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WebKitGTK locates WebKitNetworkProcess through the library path and WEBKIT_EXEC_PATH that only AppRun exports, so spawning usr/bin/deco directly started the control server and then aborted on the first webview. Staging assertions still target the real binary, so the smoke keeps gating that the bundler put deco and rclone in usr/bin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also drops the two comments that pointed at it, so nothing references a missing file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback. The stale docblock still claimed the Linux smoke launches usr/bin/<bin>, which AppRun replaced. The WEBKIT_DISABLE_* variables were added against a hypothesis the captured stderr later disproved and fixed nothing observed, so they go rather than carry a paragraph defending them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ubuntu leg compiles the GTK/WebKit stack from source for both the clippy and test profiles, so it was killed mid-cargo-test at the 30-minute bound on every run while macOS finished in three minutes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It is the only job that builds the workspace twice, and on Linux each build also carries the GTK/WebKit tree. With full debug info that target dir is 13.5 GiB against a runner with ~14 free, so the job died mid-cargo-test with no retained logs. Nothing here reads a backtrace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
12 issues found across 51 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/native/scripts/boot-smoke-target.ts">
<violation number="1" location="apps/native/scripts/boot-smoke-target.ts:93">
P2: Linux config-only changes can pass against a stale AppImage: `ensureBuilt()` does not treat `tauri.linux.conf.json5` as a bundle input. Include the platform overlay in freshness inputs so `smoke:boot` rebuilds after Linux packaging changes.</violation>
</file>
<file name="apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs">
<violation number="1" location="apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs:148">
P1: A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before `SANDBOX_START` returns the URL, rather than treating the dispatched callback as already trusted.</violation>
</file>
<file name="apps/native/scripts/fetch-appimage-tools.sh">
<violation number="1" location="apps/native/scripts/fetch-appimage-tools.sh:78">
P1: An added/renamed tool can be downloaded and executed with `TAURI_SIGNING_PRIVATE_KEY` before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.</violation>
</file>
<file name="apps/native/scripts/ci/native-update-channel.mjs">
<violation number="1" location="apps/native/scripts/ci/native-update-channel.mjs:159">
P3: An inherited object key such as `toString` is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.</violation>
</file>
<file name="apps/native/src-tauri/src/setup.rs">
<violation number="1" location="apps/native/src-tauri/src/setup.rs:77">
P1: Default Linux launches collapse every sandbox preview onto one `localhost:<preview-port>` origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.</violation>
<violation number="2" location="apps/native/src-tauri/src/setup.rs:77">
P2: Linux native dev without the opt-in cannot proxy API requests: local-api now listens over HTTP while Vite still targets `https://127.0.0.1:43121`. Preserve the existing secure origin in dev, or make the Vite proxy target follow the selected scheme.</violation>
<violation number="3" location="apps/native/src-tauri/src/setup.rs:508">
P2: Under `DESKTOP_SELFTEST=1` on Linux, setup runs via `block_on` on the main thread (lib.rs:78). When `DECOCMS_LINUX_SECURE_ORIGIN=1` forces the secure origin (the W1.9 secure-mode Linux self-test), this new `install(...)` call invokes `window.with_webview(...)` and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because `block_on` blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the `block_on` path, so the test cannot wedge CI.</violation>
</file>
<file name="apps/native/src-tauri/src/local_tls.rs">
<violation number="1" location="apps/native/src-tauri/src/local_tls.rs:362">
P2: A malformed or truncated configured CA file is treated as a usable system store, so child processes receive a replacement bundle containing only the local CA plus invalid text and lose public TLS trust. Validate that a candidate contains at least one parseable certificate before selecting it; otherwise continue to the next candidate or return `None`.</violation>
</file>
<file name=".github/workflows/release-native.yaml">
<violation number="1" location=".github/workflows/release-native.yaml:487">
P2: A dynamically linked AppImage runtime without `libfuse.so.2` passes this tripwire, so the release can violate its static-runtime requirement and still publish. Treat a dynamic section/`NEEDED` entry as failure, not merely as probe evidence.</violation>
<violation number="2" location=".github/workflows/release-native.yaml:521">
P2: The macOS "Package release assets" step doesn't set `set -euo pipefail`, unlike its Linux twin directly below. Without it, a silent failure in `shasum`/`mv` can stage a stale or empty `zip.sha256` (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking `brew install --cask` fleet-wide while the build still looks green. Recommend adding `set -euo pipefail` as the first line of this step's `run:` block, matching the Linux step.</violation>
</file>
<file name=".github/workflows/native.yml">
<violation number="1" location=".github/workflows/native.yml:613">
P2: A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear `target/release/bundle/appimage` before `tauri build`, or assert the newly produced expected filename/mtime.</violation>
<violation number="2" location=".github/workflows/native.yml:623">
P2: A dynamically linked runtime without `libfuse.so.2` passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require `ldd`'s static/non-dynamic result rather than accepting a readelf dynamic-section result.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // Before the URL escapes: whoever gets it may load it immediately, and | ||
| // on Linux the host must already be trusted by then. | ||
| if let Some(observe) = observe { | ||
| observe(&host); |
There was a problem hiding this comment.
P1: A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before SANDBOX_START returns the URL, rather than treating the dispatched callback as already trusted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs, line 148:
<comment>A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before `SANDBOX_START` returns the URL, rather than treating the dispatched callback as already trusted.</comment>
<file context>
@@ -108,9 +141,17 @@ fn preview_url(handle: &str) -> Option<String> {
+ // Before the URL escapes: whoever gets it may load it immediately, and
+ // on Linux the host must already be trusted by then.
+ if let Some(observe) = observe {
+ observe(&host);
+ }
+ Some(format!("{scheme}://{host}:{port}/"))
</file context>
| # to remove. A download can only ADD a name, so an exact set comparison | ||
| # catches that drift, and it turns the bump into a red PR leg instead of a | ||
| # silent release-day fetch. | ||
| if [ "${1:-}" = "--verify-only" ]; then |
There was a problem hiding this comment.
P1: An added/renamed tool can be downloaded and executed with TAURI_SIGNING_PRIVATE_KEY before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/fetch-appimage-tools.sh, line 78:
<comment>An added/renamed tool can be downloaded and executed with `TAURI_SIGNING_PRIVATE_KEY` before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.</comment>
<file context>
@@ -0,0 +1,111 @@
+# to remove. A download can only ADD a name, so an exact set comparison
+# catches that drift, and it turns the bump into a red PR leg instead of a
+# silent release-day fetch.
+if [ "${1:-}" = "--verify-only" ]; then
+ unpinned=""
+ for path in "$TOOLS"/*; do
</file context>
| let mut origin = control_origin::current(selftest); | ||
| #[cfg(target_os = "linux")] | ||
| { | ||
| origin.secure = linux_secure_origin_enabled(); |
There was a problem hiding this comment.
P1: Default Linux launches collapse every sandbox preview onto one localhost:<preview-port> origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/src-tauri/src/setup.rs, line 77:
<comment>Default Linux launches collapse every sandbox preview onto one `localhost:<preview-port>` origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.</comment>
<file context>
@@ -35,6 +35,48 @@ pub enum SetupError {
+ let mut origin = control_origin::current(selftest);
+ #[cfg(target_os = "linux")]
+ {
+ origin.secure = linux_secure_origin_enabled();
+ }
+ origin
</file context>
| return { | ||
| platform: "linux", | ||
| bundlesArg: ["--bundles", "appimage"], | ||
| bundleDir: join(desktopDir, "target", "release", "bundle", "appimage"), |
There was a problem hiding this comment.
P2: Linux config-only changes can pass against a stale AppImage: ensureBuilt() does not treat tauri.linux.conf.json5 as a bundle input. Include the platform overlay in freshness inputs so smoke:boot rebuilds after Linux packaging changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/boot-smoke-target.ts, line 93:
<comment>Linux config-only changes can pass against a stale AppImage: `ensureBuilt()` does not treat `tauri.linux.conf.json5` as a bundle input. Include the platform overlay in freshness inputs so `smoke:boot` rebuilds after Linux packaging changes.</comment>
<file context>
@@ -0,0 +1,110 @@
+ return {
+ platform: "linux",
+ bundlesArg: ["--bundles", "appimage"],
+ bundleDir: join(desktopDir, "target", "release", "bundle", "appimage"),
+ isBundleArtifact: (name) => APPIMAGE_ARTIFACT.test(name),
+ launchedBinaryRelPath: join(
</file context>
| working-directory: apps/native | ||
| run: | | ||
| set -euo pipefail | ||
| APPIMAGE=$(find target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit) |
There was a problem hiding this comment.
P2: A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear target/release/bundle/appimage before tauri build, or assert the newly produced expected filename/mtime.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/native.yml, line 613:
<comment>A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear `target/release/bundle/appimage` before `tauri build`, or assert the newly produced expected filename/mtime.</comment>
<file context>
@@ -466,17 +575,79 @@ jobs:
+ working-directory: apps/native
+ run: |
+ set -euo pipefail
+ APPIMAGE=$(find target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit)
+ if [ -z "$APPIMAGE" ]; then
+ echo "::error::no .AppImage produced under target/release/bundle/appimage"
</file context>
| if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then | ||
| echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself." | ||
| exit 1 | ||
| fi | ||
| if grep -q 'libfuse\.so\.2' "$PROBE"; then | ||
| echo "::error::$APPIMAGE links libfuse.so.2 — the AppImage runtime is not static, so it cannot run or self-extract without libfuse2 installed. The pinned linuxdeploy-plugin-appimage in apps/native/scripts/fetch-appimage-tools.sh is stale; re-pin it there (both this workflow and release-native.yaml read that file)." | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
P2: A dynamically linked runtime without libfuse.so.2 passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require ldd's static/non-dynamic result rather than accepting a readelf dynamic-section result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/native.yml, line 623:
<comment>A dynamically linked runtime without `libfuse.so.2` passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require `ldd`'s static/non-dynamic result rather than accepting a readelf dynamic-section result.</comment>
<file context>
@@ -466,17 +575,79 @@ jobs:
+ { ldd "$APPIMAGE" || true; readelf -d "$APPIMAGE" || true; } \
+ > "$PROBE" 2>&1
+ cat "$PROBE"
+ if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then
+ echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself."
+ exit 1
</file context>
| if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then | |
| echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself." | |
| exit 1 | |
| fi | |
| if grep -q 'libfuse\.so\.2' "$PROBE"; then | |
| echo "::error::$APPIMAGE links libfuse.so.2 — the AppImage runtime is not static, so it cannot run or self-extract without libfuse2 installed. The pinned linuxdeploy-plugin-appimage in apps/native/scripts/fetch-appimage-tools.sh is stale; re-pin it there (both this workflow and release-native.yaml read that file)." | |
| exit 1 | |
| fi | |
| if ! grep -Eq 'statically linked|not a dynamic executable' "$PROBE"; then | |
| echo "::error::$APPIMAGE is dynamically linked or neither ldd nor readelf could inspect it; the AppImage runtime must be static." | |
| exit 1 | |
| fi |
| .as_ref() | ||
| .map(|tls| tls.leaf_cert.clone()) | ||
| .ok_or_else(|| SetupError::WebviewTls("no local leaf certificate exists".into()))?; | ||
| crate::webview_trust::install( |
There was a problem hiding this comment.
P2: Under DESKTOP_SELFTEST=1 on Linux, setup runs via block_on on the main thread (lib.rs:78). When DECOCMS_LINUX_SECURE_ORIGIN=1 forces the secure origin (the W1.9 secure-mode Linux self-test), this new install(...) call invokes window.with_webview(...) and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because block_on blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the block_on path, so the test cannot wedge CI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/src-tauri/src/setup.rs, line 508:
<comment>Under `DESKTOP_SELFTEST=1` on Linux, setup runs via `block_on` on the main thread (lib.rs:78). When `DECOCMS_LINUX_SECURE_ORIGIN=1` forces the secure origin (the W1.9 secure-mode Linux self-test), this new `install(...)` call invokes `window.with_webview(...)` and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because `block_on` blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the `block_on` path, so the test cannot wedge CI.</comment>
<file context>
@@ -384,6 +495,26 @@ pub async fn run(app: &tauri::AppHandle) -> Result<(), SetupError> {
+ .as_ref()
+ .map(|tls| tls.leaf_cert.clone())
+ .ok_or_else(|| SetupError::WebviewTls("no local leaf certificate exists".into()))?;
+ crate::webview_trust::install(
+ &window,
+ leaf_cert,
</file context>
| # a glob over this directory. | ||
| STAGE="$RUNNER_TEMP/release-assets" | ||
| mkdir -p "$STAGE" | ||
| mv "$ZIP" "$DMG_OUT" "$TARGZ" "$TARGZ.sig" zip.sha256 "$STAGE/" |
There was a problem hiding this comment.
P2: The macOS "Package release assets" step doesn't set set -euo pipefail, unlike its Linux twin directly below. Without it, a silent failure in shasum/mv can stage a stale or empty zip.sha256 (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking brew install --cask fleet-wide while the build still looks green. Recommend adding set -euo pipefail as the first line of this step's run: block, matching the Linux step.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release-native.yaml, line 521:
<comment>The macOS "Package release assets" step doesn't set `set -euo pipefail`, unlike its Linux twin directly below. Without it, a silent failure in `shasum`/`mv` can stage a stale or empty `zip.sha256` (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking `brew install --cask` fleet-wide while the build still looks green. Recommend adding `set -euo pipefail` as the first line of this step's `run:` block, matching the Linux step.</comment>
<file context>
@@ -299,98 +513,291 @@ jobs:
+ # a glob over this directory.
+ STAGE="$RUNNER_TEMP/release-assets"
+ mkdir -p "$STAGE"
+ mv "$ZIP" "$DMG_OUT" "$TARGZ" "$TARGZ.sig" zip.sha256 "$STAGE/"
+
+ - name: Package release assets
</file context>
|
|
||
| const expected = Object.keys(PLATFORM_ASSETS); | ||
| const unknown = Object.keys(signatures).filter( | ||
| (key) => !(key in PLATFORM_ASSETS), |
There was a problem hiding this comment.
P3: An inherited object key such as toString is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/ci/native-update-channel.mjs, line 159:
<comment>An inherited object key such as `toString` is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.</comment>
<file context>
@@ -104,45 +104,183 @@ export function shouldPromote({
+
+ const expected = Object.keys(PLATFORM_ASSETS);
+ const unknown = Object.keys(signatures).filter(
+ (key) => !(key in PLATFORM_ASSETS),
+ );
+ if (unknown.length > 0) {
</file context>
| (key) => !(key in PLATFORM_ASSETS), | |
| (key) => !Object.hasOwn(PLATFORM_ASSETS, key), |
Dropping debug info alone did not clear it — the job still died mid-cargo-test at the same 49 minutes, and because rust-cache saves in a post step, a job that never finishes never warms the cache, so every run starts cold. Evict the preinstalled SDKs this repo never uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding an os matrix renamed rust-checks, contract-suite and tauri-build to "<name> (macos-latest)"/"(ubuntu-22.04)", and the ruleset requires the bare names — so those contexts could never be satisfied and every PR stayed BLOCKED no matter how green CI was. Mirrors the aggregator e2e.yml already uses, whose comment warns about exactly this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three runs died in cargo test at ~50 min with the step incomplete and no logs uploaded, and with conclusion failure rather than the cancelled a timeout gives — the runner was being killed. Debug-info removal and freeing 20 GiB changed nothing, which leaves memory: cargo test links a binary per target and does it four-up. Run the crates one at a time so a failure names the crate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five attempts at running both on one runner died in cargo test at an invariant ~50 min — step incomplete, no logs, conclusion failure rather than the cancelled a timeout gives. Capping debug info, freeing 20 GiB and capping link parallelism each moved nothing, and that invariance is the tell: the untouched variable was one runner carrying clippy's dev-profile build of the whole GTK/WebKit tree plus the test profile's. Each half now also warms its own cache, which the previous shape could never do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six runs, none of which uploaded a log, so the cause has been invisible. Step conclusions survive in the jobs API even when logs do not, so run each crate as its own bounded step: whichever stops at null is the culprit, and a hang now fails in ten minutes rather than at the ~48 the runner takes to die. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness and upstream pass and the test binaries build, so local-api is the crate. Split it by module, process-spawning ones first and single-threaded, each bounded — the step left at null names the module the way the per-crate split named the crate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These tests create and kill process groups, and the killer signals every pid pgrep returns. procps and BSD pgrep disagree about what a group contains, so a group that on macOS holds only the fixture can on Linux include the runner — which is what a 50-minute job with an incomplete step and no log upload looks like. setsid bounds it to the test's own group so a failure can report itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
procps documents `pgrep -g 0` as pgrep's OWN process group, so a zero id would enumerate the caller's group and signal every member — under cargo test that is the runner. BSD pgrep takes 0 literally and matches nothing, so the same call is a silent no-op on macOS. Also never signal our own pid, whatever enumeration returned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cleanup_warning_deadline_does_not_release_an_unreaped_owner ends with kill -KILL -<pid>, i.e. address the process GROUP with that id. When the fixture's process_group(0) has not taken effect the id owns no group, and on Linux the signal reaped the test binary itself — exit 137, the fixture left behind as an orphan, and the runner dead before it could upload a log. Address the group only once the fixture provably leads it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They write a helper script and exec it immediately; on Linux a concurrent fork inherits the open write descriptor and the exec fails ETXTBSY. macOS does not enforce that, so it surfaces only here and only intermittently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| * shell/string interpolation — signatures are base64 today but correctness | ||
| * stays by-construction. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| * manifest never dangles; each `signature` is the CONTENTS of that platform's | ||
| * .sig file (a path or URL does not work, per the updater docs). | ||
| * | ||
| * @param {object} args | ||
| * @param {string} args.version | ||
| * @param {Record<string, string>} args.signatures .sig CONTENTS keyed by | ||
| * platform; must cover exactly PLATFORM_ASSETS | ||
| * @param {string} args.repo owner/repo | ||
| * @param {string} args.pubDate |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // Flags that may appear more than once collect into an array; everything else | ||
| // keeps last-writer-wins. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // The candidate must parse — it was just built by this run. The published | ||
| // one is read with the same fail-open convention as should-promote. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // remoteImageLoads is deliberately NOT gating: it fetches a real asset | ||
| // over the public internet, so a runner with no egress fails it for | ||
| // reasons that say nothing about this app. The property it exists to | ||
| // protect — that the packaged CSP does not block remote images — is | ||
| // still gated, by noCspViolations below: a CSP block raises a | ||
| // securitypolicyviolation, whereas an unreachable CDN does not. Its | ||
| // result is still reported, so a real regression is visible. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // Same gate as the other DownloadAppDialog triggers: a desktop build only | ||
| // exists for Mac and Linux desktop browsers, and offering it is pointless |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // The acquisition path for browser users on a platform we ship a desktop | ||
| // build for is the app itself, not the `bunx decocms link` CLI — so while no | ||
| // desktop is linked, the monitor button offers the download instead of the | ||
| // connect dialog. Once a link is online (or inside the app, or on a platform | ||
| // with no build) the connect/status dialog remains the right destination. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // The asset name is a cross-area contract: the release workflow renames the | ||
| // bundler output to it, the updater manifest's PLATFORM_ASSETS derives from it, | ||
| // and this link points at it. Drift must be a deliberate edit here, not a | ||
| // silent 404 for every Linux visitor. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // Pinned to the immutable `native-v<version>` release rather than | ||
| // `releases/latest/download`: other workflows publish releases in this repo, so | ||
| // "latest" is not guaranteed to be a native tag. The asset name is a contract | ||
| // shared with the release workflow and the updater manifest's PLATFORM_ASSETS. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // Android reports a "Linux …" platform, so it has to be excluded explicitly; | ||
| // the touch exclusion mirrors the mac gate so the two age together. ChromeOS | ||
| // also matches and is offered an AppImage (Crostini can run it) — accepted | ||
| // for v1. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // The whole dialog branches, not just its body: the shared description names | ||
| // the Mac's Terminal, which must never appear above an AppImage download. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // This screen only ever renders inside the desktop webview, so the browser | ||
| // gates' touch heuristic is meaningless here — the platform alone decides | ||
| // which credential vault the message can name. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
3ca427c to
0884e22
Compare
…rewrite Verified in a real Linux container rather than inferred from CI, which had been dying before it could report anything. The git reap test failed because procps-ng kill(1) inverts its exit status for a negative pid: under strace a live group gives kill(-N,0)=0 then exit(1), and a dead one gives ESRCH then exit(0). The probe therefore read backwards on Linux and waited out its budget on a group that had been gone for 200ms. It now asks pgrep -g, which answers the same way on both platforms. ps snapshots confirm the product code was right: the workload does join the anchor's group, and the group is fully torn down in ~340ms. The anchor rewrite is reverted. Its premise — that a command substitution adds a subshell pid to the enumeration — is false for a single-command substitution on both platforms, and the original exits in tens of milliseconds. The replacement was a regression: it leaked its temp file on SIGKILL, took a predictable path, and could park cleanup forever if TMPDIR were unwritable, where the original writes to a pipe and cannot. The procps-vs-BSD note and the $$ skip are kept, now citing the measurement. All four Linux quarantines are removed: they pass on Linux. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0884e22 to
548e583
Compare
These two pass on a real Linux kernel — 5/5 each in a container with native /proc, procps and process groups — but still SIGKILL a GitHub-hosted runner mid-chunk. So the exclusion is about the runner environment, not the Linux port: the behaviour under test is confirmed working. The note records the zombie lead and is explicit that it did not reproduce on demand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
548e583 to
48ed307
Compare
It passes on a real Linux kernel (5/5 in a container) and still SIGKILLs a GitHub runner mid-chunk, with every other chunk of the crate passing there — so it is this test against that runner, not accumulation, which the chunking already disproved. A CI exclusion, not a port gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the daemon's proven FUSE path into the desktop: rclone mount instead of nfsmount, /proc/self/mounts instead of spawning BSD mount, and a lazy fusermount3/fusermount/umount detach chain. Each per-OS difference is a runtime seam, so both dialects compile and unit-test on either host and the macOS argv, parser and unmount stay byte-identical. Attachment detection no longer filters on filesystem type — only the stale sweep does, where a miss is safe. Gating attachment on it made an unrecognized type kill a working rclone and loop forever. Unmounts and whole mount attempts are time-bounded, because lazy detach bounds the kernel's half but not libfuse's pre-flight stat. Since a tokio blocking task cannot be cancelled, an abandoned attempt strands a thread from a pool shared process-wide, so repeated abandons back off and then stop retrying entirely; a wedged server is reclaimed at the next boot, not by another attempt in this one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown and its pre-commit step fails a docs-only staged set, hence --no-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two org-fs parser tests fed a BSD-format table to stale_mountpoints, which picks its dialect from the host — so they asserted BSD parsing on macOS and /proc parsing on Linux, where the same lines do not parse and the assertions went empty. Call the dialect-explicit twin instead, and add the Linux half of the spaces case: /proc escapes them as \040, so that parser has to unescape rather than split on whitespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
latest.json gains a linux-x86_64 entry, and the builder now refuses to emit a manifest missing any platform — it is one document with a single version, so a partial entry bricks updates for every platform, not just the missing one. A coverage check stops a repair run from clobbering a wider manifest with a narrower one. Linux gets the twin of the macOS version-pairing defence: v1Compatible makes the tarball single-member, and that member's name is covered by the signature, so it pins the artifact to the version the manifest advertises. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Splits the release job into plan, build, publish, promote and alert. Only promotion is held back on a partial build: a red Linux leg must not freeze the macOS release or the cask, which is the documented break-glass channel. Promote now verifies every artifact against the pinned pubkey before building the manifest, so a signature separated from its bytes — by an interrupted upload plus a non-reproducible rebuild — fails the run instead of shipping a manifest no client can verify. Assets on immutable tags are write-once, and an orphaned half of a pair is named rather than topped up. AppImage tooling is pinned and checksummed in one file both workflows read, so a stale pin fails the PR that introduces it rather than release day, and the build is asserted afterwards to have fetched nothing outside that pin table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linux browsers were shown macOS Terminal install instructions; the dialog now branches whole, so no Mac, Terminal or Apple Silicon copy renders there. The download URL is a pure helper pinned by a test, because the release workflow and the update manifest encode the same asset name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown and fails a docs-only staged set, hence --no-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
48ed307 to
64305f4
Compare
install.sh grows a Linux path: it resolves the version from the updater channel's latest.json — a single stable URL, promoted only once every platform's assets are on the immutable tag — rather than scanning the release list, which at this repo's ~46 releases/day covered barely two days and would have told users Linux was never released while a good build sat just past the window. Integrity is stated as what it is. The .sha256 sits on the same release, behind the same TLS session, as the binary it describes, so it catches a corrupt or truncated download and nothing more; the comments no longer claim otherwise, and no longer claim the cask's digest is human-reviewed when the release bot pushes it unreviewed. Real authenticity is available where it is cheap: the minisign public key is embedded here, and this script is served from a different origin than the GitHub CDN, so when minisign is present the signed tarball is verified and extracted instead. Without it the run says so out loud rather than downgrading in silence. The channel body is shape-checked before anything is attributed to it — curl returns captive-portal pages with HTTP 200, and the rolling channel asset is the one file re-uploaded on every promote, so 'no Linux release yet' must not be the answer to 'the network lied to us'. promote now requires the checksum too: publishing a version whose installer would then refuse to run is the same half-published state that gate exists to prevent. The dialog offers Linux the installer, keeping the direct AppImage as a secondary path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| // Both platforms install by pasting the same one-liner, so the block is shared: | ||
| // only the confirmation copy differs (one names the Mac's Terminal app, the | ||
| // other any terminal emulator). |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
| // Linux leads with the installer, not the raw AppImage: the script is what | ||
| // verifies the download (minisign signature where available, the published | ||
| // sha256 otherwise), installs into ~/.local/bin and writes the launcher entry. | ||
| // Handing over a bare .AppImage link skips all three. It stays available below | ||
| // as the escape hatch for anyone who wants the file itself. |
There was a problem hiding this comment.
A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/public/install.sh">
<violation number="1" location="apps/web/public/install.sh:295">
P1: Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| # A re-run against an install that already carries these exact bytes is a | ||
| # no-op: skip the download and only re-assert the desktop integration below. | ||
| INSTALLED_NEW=false | ||
| if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then |
There was a problem hiding this comment.
P1: Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/public/install.sh, line 295:
<comment>Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.</comment>
<file context>
@@ -1,71 +1,433 @@
+ # A re-run against an install that already carries these exact bytes is a
+ # no-op: skip the download and only re-assert the desktop integration below.
+ INSTALLED_NEW=false
+ if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then
+ say "already up to date at $VERSION"
+ elif [ -z "$SIG_MISSING" ]; then
</file context>
| if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then | |
| if [ -n "$SIG_MISSING" ] && [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then |
Summary
Phase 3 of Linux support: the AppImage is built, published, and carried by the self-update channel, and Linux browsers get a working download.
Stacked on #5509 (phase 1) and #5532 (phase 2). Based on
mainso it gets CI — the workflows only run for PRs targetingmain, so a branch-based PR gets none. Its diff narrows to phase 3 alone once the parents land; review the phase-3 commits.Only the update channel is held back on a partial build. A red Linux leg must never freeze the macOS release or the Homebrew cask, which the workflow header designates as the break-glass channel for updater key loss.
Job graph
plan(secretless) →build(matrix,fail-fast: false) →publish(runs even when a leg failed) →promote(the only thing gated on all legs succeeding) →alert(own job, job-levelalways()).alerthas to be its own job: a step-level condition inside a job that is skipped by an upstream failure never evaluates —always()included — so the most likely new failure mode, a red Linux leg, would otherwise be silent. That is exactly the unbounded-invisible-staleness hazard the alert exists for.Defects found and fixed in review
An adversarial pass refuted the two headline questions (a Linux failure cannot block macOS distribution; the alert does fire) and found six real defects. Each fix was driven through a stubbed harness, not just read:
(tar.gz, .sig)pair could split across runs and be published unverified. An interrupted upload lands the tarball without its signature; a repair rebuilds non-reproducibly, and write-once then keeps the old tarball with the new signature. Every macOS client would fail verification every cycle until a newer version promoted — with CI fully green.promotenow verifies every artifact against the pinned pubkey withminisign -Vbefore building the manifest, so signature↔bytes pairing is asserted rather than inferred.Testing
cargo test --workspace1123 passed / 0 failed; clippy-D warnings,cargo fmt --check,bun run check(which proves pt-br translation completeness),bun run lint,bun test apps/native/scripts(37) all clean. Both workflows parse with everyneeds:resolving.Not exercised until a real release runs: the promote job's minisign verification and the two-platform manifest are validated by harness and unit tests, not by an actual tagged release.
🤖 Generated with Claude Code
Summary by cubic
Adds Linux desktop distribution and installer: builds and ships an x86_64 AppImage, carries it in the self‑update channel, updates the site to offer a Linux download, and adds a Linux one‑liner installer that verifies downloads. The release pipeline is split so a Linux failure never blocks macOS; assets are verified and AppImage tooling is pinned.
New Features
tauri.linux.conf.json5) builds an AppImage with new icons; boot smoke launches via AppImageAppRun, requests software rendering under Xvfb, and pins@tauri-apps/cli; sharedfetch-appimage-tools.shseeds linuxdeploy and plugins;fetch-rclone.shadds Linux slugs and a portable sha256 helper.DECOCMS_LINUX_SECURE_ORIGIN); spawned CLIs get a system+local CA bundle viaSSL_CERT_FILEonly when a system bundle is found; preview host observer added to stage trust before first load; GUI PATH repair on macOS/Linux;xdg-openbrowser fallback; apt/dnf ripgrep hints; token namespaces pinned per build profile on every OS (macOS Keychain / Linux Secret Service).linux-x86_64; manifest refuses partials and verifies assets withminisign; tarballs are single‑member and name‑pinned; the self‑updater runs only inside an AppImage and applies on quit.install.shreads the channel’slatest.json, verifies the signed tarball withminisignwhen available, and falls back to checksum with explicit messaging; the promote job now requires the checksum asset; the download dialog offers the Linux one‑liner and a direct AppImage link pinned by tests; desktop keyring screen names Linux keyrings; menus and empty states offer Linux downloads.Refactors, CI & Release Hardening
promoteverifies each artifact against the pinned pubkey and enforces platform coverage and version pairing.apps/native/scriptsnow run.fetch-appimage-tools.shis used in PRs and releases; post‑build asserts no unpinned downloads.webkit2gtk(Linux).Written for commit 9f57188. Summary will update on new commits.