PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
13 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.
First package repair — 2026-08-14
souveraine-wireplumber-blueline now owns the known-good blueline
WirePlumber policy: 51-blueline-audio-s16.conf and 90-blueline.conf under
/etc/wireplumber/wireplumber.conf.d/. Its sole source remains the matching
Pixel3Arch/rootfs-overlay/ paths, so a fresh provision and a normal package
update use identical policy rather than two copies.
Pixel3Arch CI builds the package when either source changes and asserts both
paths in the artifact. The distribution graph includes it in the blueline
profile. This is package-source wiring until Gitea publishes it and the phone
adopts the files as owned paths. Do not package the historical asound.state:
its RX1/TX7 baseline is diagnostic evidence, not a restore source.
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),
saf/device/build.md, souveraine/packaging/arch/.