Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/42-build-outcomes-are-invisible.md
Fimeg 1b7a14e4c2 tasks: archive what shipped, say what is left, add fingerprint/haptics/camera
13 (grip) and 35 (sessiond deadlock) are done; both moved to archive/ with
closing notes. Index regrouped by state with a one-line what's-left per task
instead of a flat table of history; the archive prose moved to archive/README.

Corrected stale statuses: 29 shipped (read-only pending polkit), 31's component
and squeeze are live and only its contents are open, 42's layer 1 is built, 34
no longer blocks grip, 43 was numbered 42.

New: 44 fingerprint (DT + shim sat unused in staging/ since 07-07), 45 haptics
(settle which chip drives the motor before porting cs40l2x), 46 camera (drivers
and CAMSS are already in-tree; blueline's DT has no camera node).
2026-07-28 18:20:23 -04:00

5.6 KiB

TASK 42 — Build outcomes are invisible, and the silence looks like success

Status: layer 1 built 2026-07-28tools/ci-status.sh + the push hook. Layer 2 (notice publishing stopped) is TASK-29's, layer 3 (say so on the device) is TASK-19's, and pin-rot prevention is unbuilt. Size: small for the detection, larger for the surfacing. Repos: souveraine (updater, CI), Pixel3Arch, tooling.

What happened, because the shape matters

dtolnay/rust-toolchain was pinned to a SHA that was the tip of that repo's stable branch. dtolnay force-moves stable on every Rust release, so the commit was orphaned — still an object on GitHub, reachable from no ref. act cannot resolve it, rust-test dies before compiling anything, and aarch64-artifact is therefore skipped.

Not failed. Skipped. The run finishes, the phone keeps installing whatever was last published, and nothing anywhere says the pipeline stopped delivering. The device sat on 0.1.r260 while primary and public both carried newer code, and the only reason it was caught is that someone went looking for an unrelated reason.

The silence is the bug. A pipeline that fails loudly is a nuisance; one that stops publishing quietly is indistinguishable from one with nothing to publish.

The audit that followed (2026-07-28)

Queried once, by API, across every repo of ours that has Actions:

Repo Recent runs
souveraine 2 failures on public, green after the repin
Pixel3Arch runs 929 and 911 failed on main — kernel.yml, which TASK-25 records as never once green
culver three consecutive failures before the last success
souveraine-viewtop 2 failures
hexagonrpc green
souveraine-updater green

None of those failures had been seen by anyone. This is not one broken pin; it is that nothing reads build outcomes at all.

Gitea already reports everything

This is not a Gitea limitation and no new CI feature is needed. Everything was in the API the whole time:

GET /api/v1/repos/{owner}/{repo}/actions/runs?limit=N     -> status, conclusion, head_sha, branch
GET /api/v1/repos/{owner}/{repo}/actions/runs/{id}/jobs   -> per-job conclusion
GET /api/v1/repos/{owner}/{repo}/actions/jobs/{id}/logs   -> the actual error

Authenticates with the same Gitea token already in ~/.git-credentials on archdev and in the Pixel3Arch remote URL.

What was being read instead was journalctl --user -u gitea-runner, which logs task pickup and never outcome — so it shows activity for a run that failed thirty seconds later. Three wrong diagnoses in a row came out of that one habit. The runner journal is not a status source. Use the API.

Three layers, and they are separable

1. After a push, know what happened (built 2026-07-28)

tools/ci-status.sh in Pixel3Arch — takes a repo name, prints the latest run's conclusion, and on failure prints the failing job and the tail of its log. Wired as a Claude Code PostToolUse hook on git push so it runs without anyone choosing to run it. That closes the immediate loop and is the cheap 80%.

Limitation: it reports the run that exists at that moment. A cross build takes ~20 minutes, so the hook usually reports "in_progress" and the real answer needs a second look. Good enough to catch "failed instantly", which is what pin rot, lint, and attribution failures all look like.

2. Notice that publishing has stopped (TASK-29's natural job)

The layer that would actually have caught this. souveraine-updater already talks to the repo the phone installs from; it is the right place to ask "how old is the newest thing in edge, and how does that compare to the branch head it should be tracking?"

A pipeline that has published nothing in N days while public has moved is the signal. That is one comparison, it needs no new infrastructure, and it does not care why publishing stopped — which is the property that makes it robust against the next failure mode nobody predicted.

3. Surface it on the device

The phone is the thing that suffers a stale build, and it currently has no way to say so. This is the same shape as TASK-08(f) (sensors_degraded reaches no surface) and belongs with it in TASK-19's health readout: last successful publish, installed vs available, how far behind.

Do not

  • Do not treat a green run as a published artifact. A skipped publish job lives inside a run whose overall conclusion can still read success. Check the job, or better, check the artifact.
  • Do not pin an action to a branch tip's SHA. That is what rotted. A tag's SHA is immutable and safe; actions/checkout@34e1148 (a v4 tag) survived the same event untouched. If an action must track a branch, pin to a commit on a branch that is append-only, and say in a comment why that branch was chosen.
  • Do not read the runner journal for status. See above.

Acceptance

  • A push whose CI fails produces a visible failure without anyone deciding to look.
  • Something notices when edge has not moved while its branch has, and says so before the phone is a week behind.
  • The device can answer "am I running the newest build, and if not, why not".
  • The pin-rot class is prevented, not just fixed once: a check that flags any uses: SHA that is no longer reachable from a ref would have caught this the day the branch moved.

Connects to

TASK-29 (souveraine-updater — layer 2 is its job), TASK-25 (one repo all packages; kernel.yml has never been green and nobody knew that either), TASK-27 (pipeline audit), TASK-19 + TASK-08(f) (the device-side readout), TASK-28 (half-upgrades move authority silently — the same disease one layer up).