14 KiB
TASK 25 — One gitea repo for every SouveraineOS package
Status: 2026-08-13 — Pixel3Arch now has one package workflow. Its device
and kernel jobs publish signed artifacts into the additive edge release and
validate every emitted artifact against the central distribution contract. The
former raw kernel-latest release path is retired; its first shared-archive CI
proof remains required before calling the kernel delivery complete.
The problem in one line
souveraine's CI is the only pipeline that does the last mile — repo-add --sign into a per-arch pacman database. Every other repo stops at "here is
a file," so those packages have to be copied to the phone and pacman -U'd
by hand.
| Repo | CI | Publishes | pacman -Syu-able |
|---|---|---|---|
| souveraine | ci.yml (primary/public) |
signed pkgs + souveraine-{arch}.db |
yes |
| Pixel3Arch | kernel.yml (main) |
raw .pkg.tar.zst, no db, no sig |
no |
| firefox-arm | build.yml |
nothing published yet | no |
| culver | ci.yml |
tests only | no |
| souveraine-player | none | — | no |
| upower | none — rides souveraine as a submodule | (into souveraine's db) | yes |
upower is the proof the pattern generalises: a separate repo, built by souveraine's CI, folded into the same database. That is the shape everything else should take.
The design — one database, many producers
Do NOT add a fourth pipeline. Make souveraine-{arch}.db the single archive
and have every producer publish into it.
-
Pixel3Arch
kernel.ymlgains the last mile. It already builds a valid package on the archdev runner and asserts the built hash against the gitea tip. Add therepo-add --include-sigs --sign --key "$ARCHIVE_KEY"step and publish into the sharededgerelease —souveraine/.gitea/workflows/ci.ymllines ~248-256 are the reference implementation. Kernel updates then arrive bypacman -Syu; flashing becomes a recovery tool, not a release channel.repo-add --signis NOT the first blocker (verified 2026-07-25; parked by Casey the same day). Three things sit in front of it:- The job has never once passed. 11 runs, 11 failures, none since
Jul 14;
kernel-latestis a 404 and has never been published. The Jul 14 chain (b1d3226→9c51f3f) was successive gitea-credential fixes tobuild.shand the last fix was never re-run — it may already work. pkgveris static.kernel/PKGBUILD:12-13ispkgver=7.1.1/pkgrel=1; the commit lives only in the kernelrelease, so every build is7.1.1-1and-Syucan never see an upgrade.build.sh:27-28hardcodes both artifact filenames — they must become dynamic in the same change.- The obvious fix is non-monotonic.
vercmp 7.1.1.g588929024 7.1.1.gaaa111222→-1: a bare.g<sha>sorts as hex, so a newer kernel can sort below an older one. Use7.1.1.r<count>.g<sha>(verified monotonic), matching souveraine's own scheme. Never+.
Archive shape — Casey chose "additive" 2026-07-25. Publishing into the shared
edgerelease as written above does not work today:ci.ymldeletes the release and tag and recreates it, uploading only its ownpacman-repo(which itrm -rfs first), so anything another producer published there is erased on the next souveraine push. ci.yml must stop delete-and-recreate; each producer then does read-modify-write onsouveraine-<arch>.dbunder a shared archdev flock (repo-addreplaces the same-pkgname entry, so old versions drop out), uploading its own packages plus the refreshed db and deleting only its own superseded assets. Both jobs areruns-on: archdev, so one flock serialises them.Two things that make it easier than expected: the archive key
3CD9E99E…C8E911is already a secret key in casey's gpg keyring on archdev and souveraine's CI signs with it there, so kernel.yml needs no new signing secret; and~/.git-credentialson archdev holds bothoauth2:andcasey:gitea entries. Pixel3Arch CI does still need a token that can write toFimeg/souveraine. - The job has never once passed. 11 runs, 11 failures, none since
Jul 14;
-
squeekboard + stevia are Pixel3Arch packages — once the kernel job is generalised into a "build these packages, sign, publish" job, they are extra entries in it, not new pipelines.
-
firefox-arm publishes into the same db instead of standing alone.
-
Phone wiring lives in
rootfs-overlay/, never only on the device — see "What bit us" below.
Signing key
One archive key signs the whole database; a pacman db cannot coherently mix
signers. Pixel3Arch's CI needs the same $ARCHIVE_KEY secret souveraine's CI
uses. The key asserts "SouveraineOS published this," which is true regardless
of which repo built it — so sharing it is correct, not a compromise.
This is the package archive key and is deliberately shared across machines.
It is NOT the machine/agent identity key: those stay unique per device and are
never generated implicitly (SouveraineOS/docs/FEDERATION.md, identity tiers).
Different keyring, different trust domain — do not conflate them.
Phone-side wiring (done 2026-07-24, live)
/etc/pacman.d/souveraine-edge.conf—[souveraine-aarch64],SigLevel = Required DatabaseRequired. Template fromsouveraine/packaging/arch/souveraine-edge.conf,__ARCH__→aarch64./usr/local/libexec/souveraine-pacman-fetch— XferCommand shim; attaches the gitea netrc credential for10.10.20.120only, every other mirror unchanged./etc/pacman.d/souveraine-gitea.netrc—root:alpm 0640, token, NOT in git.pacman-key --add+--lsign-key 3CD9E99E222C2A174986FC9AFF4949AA20C8E911./etc/pacman.conf—XferCommandunder[options],Includeat the end.
Two traps found doing it
DownloadUser+ a customXferCommandare incompatible. Pacman execs the external command asDownloadUserbut hands it a path inside the root-owned sync dir; it only creates the alpm-owneddownload-*staging dir for its INTERNAL downloader. Result:EACCESon every repo, failing on upstreamcore.dblong before the souveraine repo is reached. UnsetDownloadUser.DisableSandboxFilesystem/DisableSandboxSyscallsare not pacman options. They were present in the phone'spacman.conf, silently ignored, and leftpacman -Sybroken since ~2026-07-23. pacman 7.1 has exactly one:DisableSandbox— and it governs landlock, not the privilege drop, so it is not the fix here either.
What bit us — why the overlay matters
The phone wiring existed once, was configured live on the device, and was lost
at the next reflash. provision-rootfs.sh copies rootfs-overlay/ wholesale
into a freshly extracted rootfs; anything not represented there does not
survive. The phone is Casey's daily driver — a reprovision costs him his live
system, which is exactly why the repo pipeline matters: it replaces reflashing
with in-place updates.
Next stage — the shell surfaces, settings included
Casey 2026-07-24: souveraine-settings should become a versioned app, and the state-manager settings should live in it.
Settings is QML, not a binary — surfaces/quickshell/settings.qml, run via
qs -p. On the phone it sits in ~/.config/quickshell/souveraine/, unowned,
delivered by rsync + deploy.sh. So "version the settings app" is really
package the shell surfaces, and it is harder than the binaries were:
- A package cannot own
$HOME. The tree would install to a system path (/usr/share/quickshell/souveraine/) and the shell's resolution + the composed-tree symlinks (souveraine/→ stockii/) must survive the move. Seeunify-shell-trees-laptop-phone.md, which is the same tree problem. - It is all-or-nothing: splitting settings out alone puts one QML tree behind two delivery mechanisms.
deploy.shbecomes the dev loop, not the delivery path.
Ordering, and why. TASK-19's rule is that every control is a view over the owning service — "no success-shaped switches". The device state machine is still observational (it logs transitions and confidence, drives nothing; idle timeouts remain hypridle's 300s/600s). Settings pages over it would be lying until TASK-15/08 give it actuators. But this is not a hard block on all of it: charge control is real now (TASK-08b, unblocked tonight) and has a live sysfs + D-Bus backend, so a battery/charge page can be honest today.
So: package the surfaces → expose what already actuates (charge, existing pages) → add state-machine controls as TASK-15/08 wire them up.
2026-08-11 — an update is offered to a body, not pushed through its glass
Observed on the Pixel daily driver: deploy.sh --phone passed its lock check,
snapshotted the tree, and still replaced the live QuickShell composition while
Casey was using it. The reload killed an active Souveraine conversation. A
rollback makes the files recoverable; it does not make destroying live session
state an acceptable delivery protocol.
Casey's required development shape is now explicit:
- CI publishes a signed, immutable surface candidate. The laptop never rsyncs that candidate directly over a live body's active tree.
- A connected-device channel may announce that a development update is available and safe to evaluate. USB Signaller is a possible transport for that offer; it is not the update authority and attachment is not consent.
- Each body answers independently. Its local updater acquires the durable maintenance lease, verifies the package and target profile, stages beside the running candidate, and reports what would change.
- Activation is an explicit local transition with conversation/session state preserved or deliberately quiesced, a health check, and an automatic return to the last working candidate. One connected device accepting must not make every connected device switch.
deploy.sh --phone remains a recovery/developer instrument for an explicitly
quiesced target. It is no longer the ordinary update path. TASK-08 owns the
lease and state projection; the updater owns retrieval and candidate
activation; USB Signaller can carry presence and offers only.
The actual inventory (measured 2026-07-25)
379,153 files under /usr /etc /opt /boot; 5,624 owned by nothing. The
same disease as the eleven binaries, just not yet colliding: 16
/usr/bin/blueline-* scripts, the gadget-* set, clatd, mmsdtng,
qmic, qrtr-*, rmtfs, a hand-installed Qt6Contacts cmake tree,
libcmatrix.so.0u (the ldconfig warning on every transaction), and stale
chatty.orig-pacman / chatty.pacman-orig leftovers.
One of them, blueline-proximity-lock, is not merely unpackaged — it is a
running second authority over proximity. Resolved 2026-07-25: it reports
to sessiond and decides nothing. Only its packaging remains, under TASK-27.
pacman -Syu had been blocked for days by exactly one of these files.
Intersecting the unowned set against the 112,051 files shipped by the 199
pending packages gave a conflict set of ONE:
/usr/include/aquamarine/backend/FrameScheduler.hpp, hand-dropped Jul 9 into
a directory whose every other header belongs to aquamarine 0.12.1-1, and
shipped by extra/aquamarine 0.13.0-2. It was also a version-mismatched
header sitting in the include path the whole time. Backed up to
/var/backups/unowned-preupgrade/ and removed.
Compute conflicts, do not guess at them:
pacman -Fy
pacman -Ql | awk '{print $2}' | sed 's#/$##' | sort -u > owned
find /usr /etc /opt /boot -xdev \( -type f -o -type l \) | sort -u > ondisk
comm -13 owned ondisk > unowned
pacman -Qu | awk '{print $1}' > pending
xargs -a pending pacman -Fl | awk '{print "/" $2}' | sed 's#/$##' | sort -u > newfiles
comm -12 unowned newfiles # <- exactly what will abort -Syu
The phone's login shell is zsh — no mapfile; run this under bash -s
over ssh.
Acceptance
- Pushing to any SouveraineOS repo produces a signed package in
souveraine-{arch}without hand-carrying anything. DONE for every device package except the kernel (2026-07-30; Pixel3Archpackages.yml). - The phone installs kernel, upower, squeekboard, stevia and souveraine from
pacman -Syu. DONE except the kernel — squeekboard/stevia publish alongside the rest; the first real upgrade the pipeline delivered wasusb-signaller 0.3.1-2 → 0.3.1-3. - A reprovision restores the repo wiring from
rootfs-overlay/with no manual steps.
Cross-build shapes (landed 2026-07-30)
Every aarch64 package cross-compiles on the archdev runner against
$HOME/aarch64-sysroot. Four distinct shapes live in packages.yml, each its
own block because the incantation differs:
- arch=any (pulse-native-provider, osk-switch): plain native
makepkg. - meson C (hexagonrpc, tqftpserv): a generated
--cross-file+ sysroot pkg-config. Both PKGBUILDs honourMESON_CROSS_FILEinbuild(). - Makefile C (blueline-edge-sense):
CC="aarch64-linux-gnu-gcc --sysroot=… -L…/usr/lib"; the Makefile honoursCC ?=. - meson+Rust / cargo (stevia, squeekboard via each
build-cross.sh; usb-signaller viaCARGO_BUILD_TARGET+ a per-target cross linker).
Two traps worth not re-deriving: (1) --sysroot alone does NOT link on this
toolchain — ld resolves the sysroot libc.so script's = paths against the
wrong prefix and falls back to the host /lib/libc.so.6 ("file in wrong
format"); an explicit -L$SYSROOT/usr/lib makes ld find the right script
first. (2) the C cross-file is generated inline in the workflow; stevia/
squeekboard's own cross-files (~/build/*.ini) are pets-state, failed loud
if missing.
What broke CI and how it was fixed
First green run was 993. Run 992 failed because stale bare VCS clones and
built *.pkg.tar.zst binaries had been committed (pkgs/stevia/stevia/,
pkgs/squeekboard/squeekboard/, 9 package files incl. a 1.7 MB packfile) —
makepkg rejected the clone as "not a clone of ". Fix: git rm'd the junk,
gitignored *.pkg.tar.zst + the VCS checkout dirs, and set SRCDEST
out-of-tree in CI so clones never land in a package dir. usb-signaller's
PKGBUILD was also made cross-aware (it built for the host tuple before).
Connects to
TASK-24 (keyboard: squeekboard/stevia are two of the packages), the
kernel flash flow in PAF/build.md, and
souveraine/packaging/arch/ which already holds the canonical configs.