Skip to content

ci: build the headless Linux kernel the way it is deployed - #562

Merged
Cloto-dev merged 1 commit into
masterfrom
headless-linux-build
Sep 9, 2026
Merged

ci: build the headless Linux kernel the way it is deployed#562
Cloto-dev merged 1 commit into
masterfrom
headless-linux-build

Conversation

@Cloto-dev

Copy link
Copy Markdown
Owner

A server deployment had no supported way to obtain a kernel binary. release.yml builds the desktop app — installers, .deb, .AppImage — and never emits a bare kernel, and the dashboard the kernel serves is embedded at compile time (rust_embed in handlers/assets.rs), so a binary built against the placeholder index.html that ci.yml writes serves an empty page with nothing left to fix afterwards short of rebuilding it.

This adds a workflow that builds the kernel from the real dashboard build and packages it with the marketplace install engine. The engine has to sit beside the binary (managers/installer.rs resolves exe_dir().join("cloto-installer")) and carry the same version: the kernel refuses a mismatched engine only after the install handler has already answered 200 {"started":true}, so an engine that arrives by a separate route turns a failed install into one that looks successful.

What the job checks, and why each check is there

Check What it catches
dashboard/dist has a real index.html + JS chunks The placeholder compiles just as well as the SPA, so the embed cannot report the wrong input. Runs before the Rust compile.
cloto-installer version names this kernel's version The mismatch that returns 200 and changes nothing.
Kernel starts from a production layout, /api/system/health → 200 The binary runs with no Cargo.toml above it, which is how it resolves its data directory when deployed.
/ returns ≥200 bytes containing <script A placeholder embed would also answer 200 here.
Unauthenticated /api/llm/providers → 401/403 An unknown /api/* path falls through to the SPA and answers 200, so a route that answered 200 there would be indistinguishable from a typo in the path.

Verification

Locally, in a production layout outside the workspace (a copy of the kernel under a directory with no Cargo.toml in any parent):

  • /api/system/health200
  • / → 1475 bytes, 2 <script tags
  • unauthenticated /api/llm/providers403
  • an engine built for another platform → the kernel logged did not identify itself (exit Some(126)), which is the probe path this workflow's engine check guards

The dashboard-shape guard was run in both directions: the placeholder ci.yml writes (27 bytes, 0 chunks) exits 1; the real build (1475 bytes, 26 chunks) exits 0.

scripts/build-installer.sh --target x86_64-unknown-linux-gnu was run on a non-Linux host and produced a static ELF 64-bit LSB executable, x86-64 — the cross-build needs no toolchain beyond Go, as the script documents.

Trigger scope

The pull_request trigger is scoped to this file alone. Compiling the workspace on Linux is already covered for every PR, so repeating it here would buy nothing; what a normal PR cannot prove is this workflow itself. release.yml is the cautionary case — it has no pull_request trigger at all, so a change to it cannot be verified before merge.

glibc

ubuntu-latest links against an older glibc than Debian 13 / Ubuntu 24.04 ship, and glibc is backward compatible, so the artifact runs there. It does not run on a host older than the runner (Debian 12 is glibc 2.36); that would need a container matching the target, and the header comment says so.

A server deployment had no supported way to obtain a kernel binary. The
release workflow builds the desktop app — installers, .deb, .AppImage —
and never emits a bare kernel, and the dashboard the kernel serves is
embedded at compile time, so a binary built against the placeholder
index.html serves an empty page with nothing left to fix afterwards
short of rebuilding it.

This adds a workflow that builds the kernel from the real dashboard
build and packages it with the marketplace install engine. The engine
has to sit beside the binary and carry the same version: the kernel
refuses a mismatched engine only after the install handler has already
answered 200, so an engine that arrives by a separate route turns a
failed install into one that looks successful.

The job proves the bundle rather than assuming it. It rejects a
placeholder dashboard before spending the Rust compile, checks the
engine identifies itself as this kernel's engine, and starts the binary
from a production layout to require a 200 from the health endpoint, a
real SPA on /, and a refusal on an admin route. That last one matters
because an unknown /api/* path falls through to the SPA and answers 200,
so a route that answered 200 there would be indistinguishable from a
typo in the path.

The pull_request trigger is scoped to this file alone. Compiling the
workspace on Linux is already covered for every PR, so repeating it here
would buy nothing; what a normal PR cannot prove is this workflow
itself. The release workflow is the cautionary case — it has no
pull_request trigger at all, so a change to it cannot be verified before
merge.
@Cloto-dev
Cloto-dev merged commit 85e3267 into master Sep 9, 2026
16 checks passed
@Cloto-dev
Cloto-dev deleted the headless-linux-build branch September 9, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant