12 KiB
TASK 27 — Gitea pipeline audit
Status: audited 2026-07-25 against the live Gitea API. Findings below are measured, not assumed. Size: one session for the audit itself; the fixes are separate.
The goal is the one Casey stated: everything built in ~/Projects ships as a
signed package through Gitea, so the phone updates with pacman -Syu and its
integrity is provable. TASK-25 owns the design. This task is the audit — what
is actually wired today, and where a change silently fails to reach the device.
The gap that matters most: not everything is a package
Three delivery mechanisms are in play and only one of them is auditable.
| What | How it reaches the phone | Auditable |
|---|---|---|
souveraine, -secrets, -machined, -sessiond, upower-souveraine |
CI → signed pkg → pacman -Syu |
yes |
rootfs-overlay/, overlays/ (units, scripts, hypr config) |
copied at provision time by provision-rootfs.sh |
no |
| kernel, hyprgrass, squeekboard, stevia, firefox-arm, culver, player | hand-carried pacman -U, or hyprpm, or nothing |
no |
The overlay is not a package. A fix committed to rootfs-overlay/ reaches
a running phone only via a reprovision — which costs Casey his live system, so
it effectively never happens. Two fixes are stuck behind this right now:
blueline-hexagonrpcd-sdsp.service (Restart=always + the readiness gate) and
blueline-proximity-lock. Both are committed in Pixel3Arch 557c8e1 and
neither is on the device.
Deciding what owns the overlay — a blueline-rootfs package, or per-component
packages that each own their own units — is the first real decision here.
Pipeline census, measured from the Gitea API 2026-07-25
61 repos on gitea.wiuf.net (= 10.10.20.120:4455; same box, three URL forms
in use — ssh://…:2222, https://gitea.wiuf.net, http://<token>@10.10.20.120:4455).
Four of them carry a .gitea/workflows/. One produces a package.
| repo | runs | green | publishes |
|---|---|---|---|
souveraine |
48 | yes, #44–48 | signed pacman repo → edge release |
souveraine-updater |
1 | green on its first run, 07-25 | signed pkg → edge, both arches |
culver |
17 | first green ever: #17, 07-25 | nothing — CI has no packaging step |
Pixel3Arch |
11 | never | nothing |
linux-blueline, pocketboot, souveraine-player, stevia, tuie, profile-engine, … |
— | no CI | nothing |
The Gitea package registry is empty of Arch packages — two redflag
containers from June, nothing else. Distribution is entirely release assets on
souveraine's rolling edge tag, which is why the registry is not the thing
to audit.
Pixel3Arch/pkgs/ holds 15 PKGBUILDs — hyprgrass, stevia, squeekboard,
q6voiced, tqftpserv, usb-signaller, pulseaudio-q6, hexagonrpc, paru, pachub and
more. Not one is built by CI. Every one is hand-built on archdev and hand-carried
per pkgs/README.md. Installed that way they are pacman-owned but frozen:
-Syu has no repo to upgrade them from, so ownership records the version
without ever advancing it.
The runner is a single host-mode gitea-runner.service user unit on archdev
(PID 132, Linger=yes, enabled — that part is sound). Host mode means every
job mutates one shared box. That is precisely how the poisoned sysroot in
finding 6 happened, and nothing isolates the next one.
Findings, measured 2026-07-25
-
There is no
primarybranch onsouveraine.ci.ymltriggers on[primary, public]; the API shows onlymain,public,experimental/session-trust-phase1,archive/main-mit-0.1.0. So half the trigger is dead andpublicis the de facto release branch. Worse, the working branchexperimental/session-trust-phase1is not in the trigger list at all — a push there runs no CI and publishes nothing, silently.mainis the repo's default branch and has not moved since 2026-05-22, so a fresh clone gets two-month-old code. (culveris consistent by contrast: defaultprimary, CI triggers onprimary.)Half-fixed 2026-07-25:
primarynow exists at9d8bce2and the local checkout tracks it, so the documented model — dev onprimary, promote topublic, publish gated topublic— is real and the trigger no longer names a ghost. Still open:mainremains the default branch and still points at May's1cf98ef5. Either move it or retire it. -
kernel.ymlhas never once passed. 11 runs, 11 failures, none since Jul 14 — eleven days with nobody retrying.kernel-latestis a 404. The actual failure in the last run (#11, job 1790) is not a credential problem at all — the credential fixes worked and the build got as far asolddefconfig, which then tripped the drift guard atkernel/PKGBUILD:53:-CONFIG_RUST_IS_AVAILABLE=y -# CONFIG_RUST is not set ERROR: olddefconfig changed kernel/config-blueline.aarch64The guard is right to exist and wrong as written:
CONFIG_RUST_IS_AVAILABLEis derived from the build host's rustc, not from our intent. Baking a host-probed symbol into a canonical config makes the check fail whenever the runner's toolchain differs from whoever last regenerated it. Fix by stripping host-probed symbols before the diff, not by chasing the config.Remaining blockers behind it, unchanged:
pkgveris static7.1.1, so-Syucan never see an upgrade- a bare
.g<sha>sorts as hex and is non-monotonic — use7.1.1.r<count>.g<sha> kernel/config-blueline.aarch64was moved toarchive/whilePKGBUILD:53still copies it from$startdir. Restored and verified present 2026-07-25; a stale duplicate remains atarchive/kernel/config-blueline.aarch64and should go.
-
TheFixed 2026-07-25, souveraineedgerelease clobbers other producers.044c373.packaging/arch/publish-edge.shreplaces the delete-and-recreate: release and tag are never deleted, the livesouveraine-<arch>.db(and.files) is fetched and merged withrepo-addunder a shared archdev flock, and only the producer's own superseded builds are removed — matched by%NAME%in the live db, so a producer cannot delete another's asset.Two guards, since the failure is otherwise silent: the merged db must retain every foreign entry (else abort, archive untouched), and the db is read back over pacman's own download URL and diffed. A db that is published but unfetchable is a hard failure, never a fall-through to building a fresh one — that fallback was in the first draft and would have reintroduced the bug on any transient 500.
Databases are fetched by
releases/download/<tag>/<name>, not the asset-id API, which returns attachment metadata rather than bytes on some Gitea versions.Rehearsed against a throwaway tag before adoption, and since proven live:
souveraine-updaterpublished intoedgeon 2026-07-25 and bothsouveraineandupower-souverainesurvived in both per-arch databases. -
Unowned files are still the standing risk. 5,624 under
/usr /etc /opt /boot./var/cache/hyprpmwas one of them, and that is how a routinehyprutils0.13.1 → 0.14.0 upgrade killed every touch gesture with nothing reporting it. hyprgrass is now a package (Pixel3Archpkgs/hyprgrass,557c8e1) — but no pipeline delivers it, because finding 2 blocks the same last mile the kernel needs. -
no-ai-attributionfails on two old commits (a7e909d,bc6ee12) carrying co-author trailers. Fixing means a history rewrite. Casey's call, still pending.
5b. culver had never once been green — 16 red runs from its first on
2026-07-19. Green as of #17, 2026-07-25. The whole blockage was
accumulated clippy lint, in a chain: culver-core → culver-telephony +
culver-contacts → culver-matrix → the cxx-qt app crate. -D warnings
stops at the first failing crate, so each fix only exposed the next and
nobody had walked it. Four commits: 1e3ea12, b28b15f, ab559bc,
8dc8553.
None of it was a real defect, but the rot is worth recording because a working pipeline would have caught each item the day it was written:
- three
unwrap_or_else(|| Utc::now())fallbacks sitting under comments that said "never now" — the dead branch contradicted its own docstring; nowDateTime::UNIX_EPOCH - a
DiffMirror.accountfield kept alive by an_account()accessor that was itself dead - the app crate had never been linted at all: four
pub use qobject::X as QXre-exports referenced nowhere (dead by construction — nothing can import from a binary crate), four matching unused imports,beginInsertRows/endInsertRowsdeclared on two models that never insert (chat.rsgenuinely uses its pair, which is why clippy spared it), apending: VecDeque<ModelUpdate>queue documented as "drained on firstapply" that nothing drained, and an uncalledset_header.
One #[allow] was used, on handle_command's 8 arguments, with the reason
in a comment. Everything else is a real fix.
The structural point outlasts the fix: culver's CI has no packaging step.
It runs test, clippy and build, then stops. Green culver still ships nothing.
Same for any repo whose CI ends at cargo build.
-
Build-host integrity is unaudited, and it lies. The archdev aarch64 sysroot was half-updated: extracting packages unprivileged silently skips every file that already exists root-owned, so
hyprland.pcreadVersion: 0.56.0while the headers were still 0.55's layout. A cross build against that sysroot compiles happily against a mixed ABI. Anything cross-built on archdev in the past few weeks is suspect and worth rebuilding.pkgs/hyprgrass/build-cross.shnow refreshes as root and assertsSONAME == NEEDED; that assertion, or an equivalent, belongs in every cross build. A signed package built from a poisoned sysroot is still a broken package — signing proves origin, not correctness. -
The update path only exists at home. The phone's repo is
Server = http://10.10.20.120:4455/Fimeg/souveraine/releases/download/edge— a LAN address over plain HTTP, reached through aXferCommandwrapper that feeds a token from/etc/pacman.d/souveraine-gitea.netrcbecause pacman will not forward credentials in a Server URL. Package signatures make HTTP acceptable for integrity; they do not make the host reachable. Off the LAN,pacman -Syusimply fails. A daily-driver phone that can only update in one building is a constraint worth naming now rather than discovering on a trip. -
Published ≠ installed, and nothing closes the loop.
edgecurrently carriessouveraine 0.1.r220.g9d8bce2a08cdfor both arches, signed, with a valid db. The phone is onr219. Nothing pulls, notifies, or reports the drift — the last mile is a human remembering. That is TASK-28's subject and it starts here, not at the packaging.
Acceptance
- Every repo in
~/Projectsthat ships anything to the phone has a CI job ending inrepo-add --include-sigs --signintosouveraine-{arch}. - No CI trigger names a branch that does not exist.
pacman -Syuon the phone installs kernel, hyprgrass, squeekboard, stevia, souveraine and the overlay content. Nothing is hand-carried.- A reprovision restores repo wiring from
rootfs-overlay/with no manual steps. - Unowned files under
/usr /etc /opt /boottrend to zero, and the count is reported by a job rather than discovered during an outage. - Cross builds fail loudly on an ABI mismatch instead of shipping one.
Connects to
TASK-25 (the design and the archive shape), the absorbed proximity-authority work (blueline-proximity-lock
as an unowned second authority — now demoted to a reporter, still unpackaged),
PAF/build.md, souveraine/packaging/arch/.