Watch
1
0
Fork
You've already forked souveraine
0

split the public gates from the internal packaging half

The three portable jobs stay; the archdev packaging and archive publish move
to packages.yml, which the public branch does not carry. The handoff document
is operator archaeology and does not travel.
This commit is contained in:
Fimeg 2026-08-23 19:42:58 -04:00
commit 76846feb61
4 changed files with 457 additions and 579 deletions

View file

@ -136,448 +136,3 @@ jobs:
# archdev) where the aarch64 sysroot lives — no docker, no node, plain shell.
# Publishes the cross-built binary to a rolling `edge` prerelease; the phone
# updates itself from there instead of waiting on a laptop scp.
aarch64-artifact:
runs-on: archdev
needs: [rust-test, no-ai-attribution, action-pins]
if: github.event_name == 'push' && github.ref == 'refs/heads/primary'
steps:
- name: Clone repo + pinned tuie
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/src"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/src/Souveraine"
git -C "$GITHUB_WORKSPACE/src/Souveraine" checkout "$GITHUB_SHA"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" "$GITHUB_WORKSPACE/src/tuie"
git -C "$GITHUB_WORKSPACE/src/tuie" checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35
- name: Build release binaries
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Persistent target dirs: fresh clones per run, warm compile cache.
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target"
export SOUVERAINE_AARCH64_SYSROOT="$HOME/aarch64-sysroot"
./scripts/build-cross.sh
BIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine"
file "$BIN" | grep -q aarch64 || { echo "built binary is not aarch64" >&2; exit 1; }
cp "$BIN" souveraine-aarch64
# sessiond is a separate bin behind the sessiond feature. It was
# hand-copied to the phone's /usr/local/bin and owned by no package,
# so it never received updates; build and ship it with the rest.
./scripts/build-cross.sh --features sessiond --bin souveraine-sessiond
SBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sessiond"
file "$SBIN" | grep -q aarch64 || { echo "sessiond is not aarch64" >&2; exit 1; }
cp "$SBIN" souveraine-sessiond-aarch64
# The sensor reporter. Same story as sessiond: it replaces shell
# scripts that shipped in rootfs-overlay/ and drifted per-device.
./scripts/build-cross.sh --bin souveraine-sensord
NBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sensord"
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
cp "$NBIN" souveraine-sensord-aarch64
# Full USB KVM receiver: GUD display-in and the HID return surface.
./scripts/build-cross.sh --features usb-kvm --bin souveraine-usb-kvm
UBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-usb-kvm"
file "$UBIN" | grep -q aarch64 || { echo "usb-kvm is not aarch64" >&2; exit 1; }
cp "$UBIN" souveraine-usb-kvm-aarch64
# The wry host: self-hosted sites as apps, and the rig she wears.
# Needs webkit2gtk on the build sysroot; it is already installed on
# the phone (TASK-59 phase 1 measured against it).
# Best effort: it links against the system webview, so it needs
# webkit2gtk in the aarch64 sysroot. Until that is there this is the
# one binary that may legitimately not build, and it must not stop the
# ones that decide whether the phone locks.
if ./scripts/build-cross.sh --features web --bin souveraine-web; then
WBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-web"
if file "$WBIN" | grep -q aarch64; then
cp "$WBIN" souveraine-web-aarch64
else
echo "::warning::web host built but is not aarch64; skipping it"
fi
else
echo "::warning::the wry host did not cross-build (webkit2gtk sysroot?); skipping it"
fi
# secrets rail + machined: also hand-copied on the phone until now.
./scripts/build-cross.sh --features secrets --bin souveraine-secrets
./scripts/build-cross.sh --bin souveraine-machined
./scripts/build-cross.sh --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/$b"
file "$B" | grep -q aarch64 || { echo "$b is not aarch64" >&2; exit 1; }
cp "$B" "$b-aarch64"
done
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target-x86_64"
cargo build --release
BIN="$CARGO_TARGET_DIR/release/souveraine"
file "$BIN" | grep -q 'x86-64' || { echo "built binary is not x86_64" >&2; exit 1; }
cp "$BIN" souveraine-x86_64
cargo build --release --features secrets --bin souveraine-secrets
cargo build --release --bin souveraine-machined
cargo build --release --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/release/$b"
file "$B" | grep -q 'x86-64' || { echo "$b is not x86_64" >&2; exit 1; }
cp "$B" "$b-x86_64"
done
# sessiond for x86_64 as well. It was skipped here with a comment
# calling it deliberate; it never was (Casey, 2026-08-02) — the
# laptop hit lock-screen errors with it once and it stayed
# phone-only by inertia. That inertia is now load-bearing in the
# wrong direction: viewtop targets the laptop screen too
# (VIEWTOP-AND-DENIAL.md), and panel power, the LockedHint report and
# button reporting all assume the daemon is there — so no session
# authority on x86 means no viewtop on the laptop.
#
# Building and packaging are not enabling. The package-owned user
# unit ships on both architectures, but package installation neither
# enables nor starts it; target activation remains separately tested.
cargo build --release --features sessiond --bin souveraine-sessiond
B="$CARGO_TARGET_DIR/release/souveraine-sessiond"
file "$B" | grep -q 'x86-64' || { echo "sessiond is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sessiond-x86_64
# sensord for x86_64 too. It stopped being phone-only when charge
# became a source: sessiond no longer probes the supplies itself, so
# a machine without the reporter has no charge evidence at all — and
# the laptop is a machine with a battery.
cargo build --release --bin souveraine-sensord
B="$CARGO_TARGET_DIR/release/souveraine-sensord"
file "$B" | grep -q 'x86-64' || { echo "sensord is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sensord-x86_64
sha256sum souveraine-aarch64 souveraine-x86_64 \
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
souveraine-usb-kvm-aarch64 \
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
souveraine-machined-aarch64 souveraine-machined-x86_64 \
souveraine-admit-aarch64 souveraine-admit-x86_64 \
souveraine-sessiond-x86_64 souveraine-sensord-x86_64 \
> souveraine-binaries.sha256
- name: Build UPower fork (per-arch install trees)
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# The fork is a git submodule of Souveraine. The runner checkout
# above is not recursive; clone it explicitly with the same LAN
# token used for tuie, pinned to the submodule's recorded commit.
UPOWER_URL=$(git config --file .gitmodules submodule."packaging/upower-souveraine".url)
UPOWER_SHA=$(git ls-tree HEAD packaging/upower-souveraine | awk '{print $3}')
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "$UPOWER_URL" "$GITHUB_WORKSPACE/src/upower"
git -C "$GITHUB_WORKSPACE/src/upower" checkout "$UPOWER_SHA"
SYSROOT="$HOME/aarch64-sysroot"
# .cargo/aarch64-pkg-config reads SOUVERAINE_AARCH64_SYSROOT and falls
# back to /usr/aarch64-linux-gnu, which has no glib. Steps do not share
# environment, so exporting it in the binaries step does not reach here.
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
# --- x86_64: native build on the archdev host ---
(
cd "$GITHUB_WORKSPACE/src/upower"
# gtkdoc-scan is not installed on the runner, so gtk-doc=true fails
# here too; man needs the same toolchain. Introspection is left ON
# for the native build — it works, and costs nothing.
meson setup build-x86_64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false
meson compile -C build-x86_64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-x86_64" meson install -C build-x86_64
)
tar -C "$GITHUB_WORKSPACE/src/upower/dest-x86_64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-x86_64.tar" .
# --- aarch64: cross build against the same sysroot souveraine uses ---
CROSS="$GITHUB_WORKSPACE/src/upower/aarch64-cross.ini"
cat > "$CROSS" <<EOF
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = '$PWD/.cargo/aarch64-pkg-config'
# gobject-introspection is on by default and RUNS the aarch64 binary
# it just built (g-ir-scanner). qemu needs -L to find the aarch64
# loader; binfmt is registered on the Proxmox host so this works
# inside the unprivileged archdev container.
exe_wrapper = ['qemu-aarch64-static', '-L', '$SYSROOT']
[built-in options]
c_args = ['--sysroot=$SYSROOT']
cpp_args = ['--sysroot=$SYSROOT']
# -L as well as --sysroot: libc.so/libbsd.so in the sysroot are linker
# SCRIPTS naming bare paths (libm, libmvec, -lmd), which ld resolves
# against its own prefix and misses without an explicit search dir.
c_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
cpp_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
(
cd "$GITHUB_WORKSPACE/src/upower"
PKG_CONFIG_ALLOW_CROSS=1 \
PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig" \
PKG_CONFIG_SYSROOT_DIR="$SYSROOT" \
# gtk-doc/man need host doc tooling, and introspection needs a
# g-ir-scanner that can scan an aarch64 build (the host copy reads
# its data files out of the sysroot and fails). None of the three
# ship anything the phone uses — quickshell's UPower module is
# native Qt, and the device carries no UPowerGlib typelib.
meson setup build-aarch64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false -Dintrospection=disabled \
--cross-file "$CROSS"
meson compile -C build-aarch64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-aarch64" meson install -C build-aarch64
)
# Sanity: the aarch64 upowerd must actually be aarch64.
# It installs to libexec/, not lib/ — the old path made this check
# fail even on a good build.
file "$GITHUB_WORKSPACE/src/upower/dest-aarch64/usr/libexec/upowerd" \
| grep -q aarch64 || { echo "aarch64 upowerd is not aarch64" >&2; exit 1; }
# Cross-install leak. upower asks pkg-config for the udev dir, and
# with a sysroot in play that answer comes back sysroot-prefixed, so
# the rules and hwdb install under $DESTDIR/home/casey/aarch64-sysroot/
# instead of /usr/lib/udev. systemdsystemunitdir is passed explicitly
# above for exactly this reason; the udev dir was missed. Measured on
# the phone 2026-07-27: all four of 60-upower-battery.{rules,hwdb},
# 95-upower-wup.rules and 95-upower-hid.hwdb were absent from
# /usr/lib/udev and present only under the junk path — so the fork's
# udev quirks had never once been applied on the device.
# Fold any leaked tree back before the tar, then refuse to ship a
# tree containing anything but the three prefixes upower legitimately
# installs into: usr/, etc/ (UPower.conf) and var/ (var/lib/upower).
# Leaving var/ out of that list is what failed run 1234.
DEST="$GITHUB_WORKSPACE/src/upower/dest-aarch64"
LEAK="$DEST$HOME/aarch64-sysroot"
if [ -d "$LEAK" ]; then
echo "cross install leaked into $HOME/aarch64-sysroot — relocating"
cp -a "$LEAK/." "$DEST/"
rm -rf "$LEAK"
# Removing the sysroot subtree leaves its empty parents behind
# ($DEST/home/casey), and an empty directory still ships AND still
# trips the stray check below — which is how runs 1234 and 1248
# failed. The PKGBUILD copy of this had the rmdir; this one did not.
rmdir -p --ignore-fail-on-non-empty "$(dirname "$LEAK")" 2>/dev/null || true
fi
stray=$(find "$DEST" -mindepth 1 -maxdepth 1 \
! -name usr ! -name etc ! -name var -printf '%f\n')
if [ -n "$stray" ]; then
echo "upower tree would ship outside usr/, etc/ and var/: $stray" >&2
exit 1
fi
# And the rules must have landed where udev actually reads them.
for f in usr/lib/udev/rules.d/60-upower-battery.rules \
usr/lib/udev/hwdb.d/60-upower-battery.hwdb; do
[ -e "$DEST/$f" ] || { echo "upower tree is missing $f" >&2; exit 1; }
done
tar -C "$GITHUB_WORKSPACE/src/upower/dest-aarch64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-aarch64.tar" .
- name: Stage the shell surface for packaging
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# deploy.sh composes ~/.config/quickshell/souveraine from repo files
# (symlinks) plus ii dirs borrowed from the ii-base pin. Run it in a
# scratch HOME and tar the result dereferenced, so the package is
# self-contained real files and never ships dangling links.
SHELL_STAGE="$GITHUB_WORKSPACE/shell-stage"
SHELL_TAR="$GITHUB_WORKSPACE/shell-package-input"
rm -rf "$SHELL_STAGE" "$SHELL_TAR"
mkdir -p "$SHELL_STAGE" "$SHELL_TAR"
HOME="$SHELL_STAGE" ./surfaces/quickshell/deploy.sh
test -L "$SHELL_STAGE/.config/quickshell/souveraine/shell.qml"
tar -h --zstd -C "$SHELL_STAGE/.config/quickshell" -cf "$SHELL_TAR/souveraine-shell-config.tar.zst" souveraine
tar -tf "$SHELL_TAR/souveraine-shell-config.tar.zst" | grep -qx 'souveraine/shell.qml'
- name: Package and sign pacman repository
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
PKGVER="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
WORK="$GITHUB_WORKSPACE/pacman-package"
REPO="$GITHUB_WORKSPACE/pacman-repo"
ARCHIVE_KEY="3CD9E99E222C2A174986FC9AFF4949AA20C8E911"
rm -rf "$WORK" "$REPO"
mkdir -p "$WORK" "$REPO"
for ARCH in aarch64 x86_64; do
PKG_WORK="$WORK/$ARCH"
ARCH_REPO="$REPO/$ARCH"
mkdir -p "$PKG_WORK" "$ARCH_REPO"
cp "souveraine-$ARCH" "$PKG_WORK/souveraine-binary"
cp "souveraine-secrets-$ARCH" "$PKG_WORK/souveraine-secrets-binary"
cp "souveraine-machined-$ARCH" "$PKG_WORK/souveraine-machined-binary"
cp "souveraine-admit-$ARCH" "$PKG_WORK/souveraine-admit-binary"
cp "souveraine-sessiond-$ARCH" "$PKG_WORK/souveraine-sessiond-binary"
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
cp packaging/souveraine.service packaging/souveraine-secrets.service \
packaging/org.freedesktop.secrets.service \
packaging/souveraine-machined.service \
packaging/souveraine-sessiond.service \
packaging/souveraine-sensord.service LICENSE "$PKG_WORK/"
cp packaging/arch/souveraine.sysusers "$PKG_WORK/"
# Phone-only reporters and surfaces remain aarch64-only.
if [ "$ARCH" = aarch64 ]; then
cp "souveraine-usb-kvm-$ARCH" "$PKG_WORK/souveraine-usb-kvm-binary"
# The wry host. Guarded, and the PKGBUILD source line is removed
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
# and a missing avatar must not stop sessiond reaching the phone.
# makepkg validates every entry in `source`, so leaving a name
# there with no file fails the whole package.
if [ -f "souveraine-web-$ARCH" ]; then
cp "souveraine-web-$ARCH" "$PKG_WORK/souveraine-web-binary"
fi
fi
cp packaging/souveraine-verify-trail "$PKG_WORK/"
cp packaging/souveraine-button "$PKG_WORK/"
cp packaging/souveraine-stepup.pam packaging/souveraine-sessiond.pam \
packaging/org.souveraine.stepup.policy "$PKG_WORK/"
cp packaging/arch/PKGBUILD.prebuilt "$PKG_WORK/PKGBUILD"
(
cd "$PKG_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
PKG=$(find "$PKG_WORK" -maxdepth 1 -name 'souveraine-*.pkg.tar.zst' -print -quit)
test -n "$PKG"
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/systemd/user/souveraine-sessiond.service'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sensord'
bsdtar -tf "$PKG" | grep -qx 'etc/pam.d/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/sysusers.d/souveraine.conf'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-admit'
if [ "$ARCH" = aarch64 ]; then
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-usb-kvm'
fi
cp "$PKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$PKG")"
# UPower fork package — built from packaging/upower-souveraine
# (git submodule of the Souveraine fork of upstream upower). Built
# natively per-arch by the "Build UPower fork" step above into
# upower-tree-$ARCH.tar, then folded into the SAME per-arch pacman
# database so the phone installs upower-souveraine from the same
# edge archive it already uses for souveraine.
# Version from UPSTREAM upower + the fork commit, NOT souveraine's
# r-count. The hand-built package on the phone was 1.91.3+<sha>;
# reusing souveraine's 0.1.rN here sorts LOWER, so pacman treats
# the repo copy as a downgrade and -Syu silently skips it.
UPOWER_VER=$(sed -nE "s/^ *version *: *'([^']+)'.*/\1/p" \
"$GITHUB_WORKSPACE/src/upower/meson.build" | head -1)
# '.r' not '+': a literal + in a release asset filename cannot be
# fetched from Gitea at all — it is stored decoded as a space, and
# both the raw + and %2B forms 404. Still sorts above the old
# hand-built 1.91.3+<sha> because vercmp reads + as a separator too.
UPOWER_PKGVER="${UPOWER_VER}.r$(git -C "$GITHUB_WORKSPACE/src/upower" rev-parse --short HEAD)"
UPOWER_WORK="$WORK/upower-$ARCH"
mkdir -p "$UPOWER_WORK"
cp "upower-tree-$ARCH.tar" "$UPOWER_WORK/upower-tree.tar"
cp packaging/upower-souveraine/dbus/org.freedesktop.UPower.Device.xml \
"$UPOWER_WORK/" 2>/dev/null \
|| cp "$GITHUB_WORKSPACE/src/upower/dbus/org.freedesktop.UPower.Device.xml" \
"$UPOWER_WORK/"
cp packaging/arch/PKGBUILD.upower.prebuilt "$UPOWER_WORK/PKGBUILD"
(
cd "$UPOWER_WORK"
export SOUVERAINE_PKGVER="$UPOWER_PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
UPKG=$(find "$UPOWER_WORK" -maxdepth 1 -name 'upower-souveraine-*.pkg.tar.zst' -print -quit)
test -n "$UPKG"
# libexec/, not lib/ — same path correction as the sanity check above.
bsdtar -tf "$UPKG" | grep -qx 'usr/libexec/upowerd'
cp "$UPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$UPKG")"
# Shell package — the composed quickshell config, arch=any, from
# the staged tar. Same per-arch database as the binaries above.
SHELL_WORK="$WORK/shell-$ARCH"
mkdir -p "$SHELL_WORK"
cp "$GITHUB_WORKSPACE/shell-package-input/souveraine-shell-config.tar.zst" "$SHELL_WORK/"
cp packaging/arch/PKGBUILD.shell.prebuilt "$SHELL_WORK/PKGBUILD"
cp LICENSE "$SHELL_WORK/"
(
cd "$SHELL_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
SPKG=$(find "$SHELL_WORK" -maxdepth 1 -name 'souveraine-shell-*.pkg.tar.zst' -print -quit)
test -n "$SPKG"
bsdtar -tf "$SPKG" | grep -qx 'etc/xdg/quickshell/souveraine/shell.qml'
bsdtar -tf "$SPKG" | grep -qx 'usr/share/licenses/souveraine-shell/LICENSE'
cp "$SPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$SPKG")"
# The per-arch database is NOT built here. `edge` is a shared archive
# with more than one producer in it, so the database has to be
# read-modify-written against the live copy rather than generated
# fresh — packaging/arch/publish-edge.sh owns that, under a lock.
done
cp packaging/arch/souveraine-archive-key.asc "$REPO/"
- name: Publish rolling edge prerelease
env:
# A PAT rather than the job's own GITHUB_TOKEN, so the identical
# invocation works from a producer repo other than this one.
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Additive: the release and tag are never deleted, the live databases
# are merged into, and only this producer's own superseded packages
# are removed. Before 2026-07-25 this step deleted and recreated the
# release, which erased every other producer's packages on each push.
export EDGE_TARGET_SHA="$GITHUB_SHA"
export PRODUCER_VERSION="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
./packaging/arch/publish-edge.sh souveraine "$GITHUB_WORKSPACE/pacman-repo" \
souveraine-aarch64 souveraine-x86_64 souveraine-binaries.sha256 \
"$GITHUB_WORKSPACE/pacman-repo/souveraine-archive-key.asc"
- name: Mirror into the Gitea Arch registry (alongside edge)
# Best-effort, never fails the publish: the registry sits alongside
# release-assets until proven as the phone's source. A 409 means
# (name, version) is already present immutably -- bump to republish.
# Device-side auth is handled by souveraine-pacman-fetch; this only
# uploads. Covers souveraine + upower-souveraine (both arches).
env:
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
run: |
set +e
REG="$GITHUB_SERVER_URL/api/packages/${GITHUB_REPOSITORY%/*}/arch/edge"
ok=0; skip=0; bad=0
shopt -s nullglob
for pkg in "$GITHUB_WORKSPACE"/pacman-repo/*/*.pkg.tar.zst; do
code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: token $EDGE_TOKEN" --upload-file "$pkg" "$REG")
case "$code" in
200|201) echo "registry: published $(basename "$pkg")"; ok=$((ok+1));;
409) echo "registry: present $(basename "$pkg") (bump to republish)"; skip=$((skip+1));;
*) echo "::warning::registry $(basename "$pkg") -> HTTP $code"; bad=$((bad+1));;
esac
done
echo "registry mirror: $ok new, $skip present, $bad failed"
exit 0

View file

@ -0,0 +1,456 @@
name: packages
# Internal only. Packaging, signing and archive publication run on the
# build host where the aarch64 sysroot and the signing key live. This file
# is not carried on the public branch; the gates in ci.yml are.
on:
push:
branches: [primary, main]
workflow_dispatch:
jobs:
aarch64-artifact:
runs-on: archdev
needs: [rust-test, no-ai-attribution, action-pins]
if: github.event_name == 'push' && github.ref == 'refs/heads/primary'
steps:
- name: Clone repo + pinned tuie
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/src"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/src/Souveraine"
git -C "$GITHUB_WORKSPACE/src/Souveraine" checkout "$GITHUB_SHA"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" "$GITHUB_WORKSPACE/src/tuie"
git -C "$GITHUB_WORKSPACE/src/tuie" checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35
- name: Build release binaries
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Persistent target dirs: fresh clones per run, warm compile cache.
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target"
export SOUVERAINE_AARCH64_SYSROOT="$HOME/aarch64-sysroot"
./scripts/build-cross.sh
BIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine"
file "$BIN" | grep -q aarch64 || { echo "built binary is not aarch64" >&2; exit 1; }
cp "$BIN" souveraine-aarch64
# sessiond is a separate bin behind the sessiond feature. It was
# hand-copied to the phone's /usr/local/bin and owned by no package,
# so it never received updates; build and ship it with the rest.
./scripts/build-cross.sh --features sessiond --bin souveraine-sessiond
SBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sessiond"
file "$SBIN" | grep -q aarch64 || { echo "sessiond is not aarch64" >&2; exit 1; }
cp "$SBIN" souveraine-sessiond-aarch64
# The sensor reporter. Same story as sessiond: it replaces shell
# scripts that shipped in rootfs-overlay/ and drifted per-device.
./scripts/build-cross.sh --bin souveraine-sensord
NBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sensord"
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
cp "$NBIN" souveraine-sensord-aarch64
# Full USB KVM receiver: GUD display-in and the HID return surface.
./scripts/build-cross.sh --features usb-kvm --bin souveraine-usb-kvm
UBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-usb-kvm"
file "$UBIN" | grep -q aarch64 || { echo "usb-kvm is not aarch64" >&2; exit 1; }
cp "$UBIN" souveraine-usb-kvm-aarch64
# The wry host: self-hosted sites as apps, and the rig she wears.
# Needs webkit2gtk on the build sysroot; it is already installed on
# the phone (TASK-59 phase 1 measured against it).
# Best effort: it links against the system webview, so it needs
# webkit2gtk in the aarch64 sysroot. Until that is there this is the
# one binary that may legitimately not build, and it must not stop the
# ones that decide whether the phone locks.
if ./scripts/build-cross.sh --features web --bin souveraine-web; then
WBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-web"
if file "$WBIN" | grep -q aarch64; then
cp "$WBIN" souveraine-web-aarch64
else
echo "::warning::web host built but is not aarch64; skipping it"
fi
else
echo "::warning::the wry host did not cross-build (webkit2gtk sysroot?); skipping it"
fi
# secrets rail + machined: also hand-copied on the phone until now.
./scripts/build-cross.sh --features secrets --bin souveraine-secrets
./scripts/build-cross.sh --bin souveraine-machined
./scripts/build-cross.sh --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/$b"
file "$B" | grep -q aarch64 || { echo "$b is not aarch64" >&2; exit 1; }
cp "$B" "$b-aarch64"
done
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target-x86_64"
cargo build --release
BIN="$CARGO_TARGET_DIR/release/souveraine"
file "$BIN" | grep -q 'x86-64' || { echo "built binary is not x86_64" >&2; exit 1; }
cp "$BIN" souveraine-x86_64
cargo build --release --features secrets --bin souveraine-secrets
cargo build --release --bin souveraine-machined
cargo build --release --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/release/$b"
file "$B" | grep -q 'x86-64' || { echo "$b is not x86_64" >&2; exit 1; }
cp "$B" "$b-x86_64"
done
# sessiond for x86_64 as well. It was skipped here with a comment
# calling it deliberate; it never was (Casey, 2026-08-02) — the
# laptop hit lock-screen errors with it once and it stayed
# phone-only by inertia. That inertia is now load-bearing in the
# wrong direction: viewtop targets the laptop screen too
# (VIEWTOP-AND-DENIAL.md), and panel power, the LockedHint report and
# button reporting all assume the daemon is there — so no session
# authority on x86 means no viewtop on the laptop.
#
# Building and packaging are not enabling. The package-owned user
# unit ships on both architectures, but package installation neither
# enables nor starts it; target activation remains separately tested.
cargo build --release --features sessiond --bin souveraine-sessiond
B="$CARGO_TARGET_DIR/release/souveraine-sessiond"
file "$B" | grep -q 'x86-64' || { echo "sessiond is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sessiond-x86_64
# sensord for x86_64 too. It stopped being phone-only when charge
# became a source: sessiond no longer probes the supplies itself, so
# a machine without the reporter has no charge evidence at all — and
# the laptop is a machine with a battery.
cargo build --release --bin souveraine-sensord
B="$CARGO_TARGET_DIR/release/souveraine-sensord"
file "$B" | grep -q 'x86-64' || { echo "sensord is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sensord-x86_64
sha256sum souveraine-aarch64 souveraine-x86_64 \
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
souveraine-usb-kvm-aarch64 \
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
souveraine-machined-aarch64 souveraine-machined-x86_64 \
souveraine-admit-aarch64 souveraine-admit-x86_64 \
souveraine-sessiond-x86_64 souveraine-sensord-x86_64 \
> souveraine-binaries.sha256
- name: Build UPower fork (per-arch install trees)
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# The fork is a git submodule of Souveraine. The runner checkout
# above is not recursive; clone it explicitly with the same LAN
# token used for tuie, pinned to the submodule's recorded commit.
UPOWER_URL=$(git config --file .gitmodules submodule."packaging/upower-souveraine".url)
UPOWER_SHA=$(git ls-tree HEAD packaging/upower-souveraine | awk '{print $3}')
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "$UPOWER_URL" "$GITHUB_WORKSPACE/src/upower"
git -C "$GITHUB_WORKSPACE/src/upower" checkout "$UPOWER_SHA"
SYSROOT="$HOME/aarch64-sysroot"
# .cargo/aarch64-pkg-config reads SOUVERAINE_AARCH64_SYSROOT and falls
# back to /usr/aarch64-linux-gnu, which has no glib. Steps do not share
# environment, so exporting it in the binaries step does not reach here.
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
# --- x86_64: native build on the archdev host ---
(
cd "$GITHUB_WORKSPACE/src/upower"
# gtkdoc-scan is not installed on the runner, so gtk-doc=true fails
# here too; man needs the same toolchain. Introspection is left ON
# for the native build — it works, and costs nothing.
meson setup build-x86_64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false
meson compile -C build-x86_64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-x86_64" meson install -C build-x86_64
)
tar -C "$GITHUB_WORKSPACE/src/upower/dest-x86_64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-x86_64.tar" .
# --- aarch64: cross build against the same sysroot souveraine uses ---
CROSS="$GITHUB_WORKSPACE/src/upower/aarch64-cross.ini"
cat > "$CROSS" <<EOF
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = '$PWD/.cargo/aarch64-pkg-config'
# gobject-introspection is on by default and RUNS the aarch64 binary
# it just built (g-ir-scanner). qemu needs -L to find the aarch64
# loader; binfmt is registered on the Proxmox host so this works
# inside the unprivileged archdev container.
exe_wrapper = ['qemu-aarch64-static', '-L', '$SYSROOT']
[built-in options]
c_args = ['--sysroot=$SYSROOT']
cpp_args = ['--sysroot=$SYSROOT']
# -L as well as --sysroot: libc.so/libbsd.so in the sysroot are linker
# SCRIPTS naming bare paths (libm, libmvec, -lmd), which ld resolves
# against its own prefix and misses without an explicit search dir.
c_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
cpp_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
(
cd "$GITHUB_WORKSPACE/src/upower"
PKG_CONFIG_ALLOW_CROSS=1 \
PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig" \
PKG_CONFIG_SYSROOT_DIR="$SYSROOT" \
# gtk-doc/man need host doc tooling, and introspection needs a
# g-ir-scanner that can scan an aarch64 build (the host copy reads
# its data files out of the sysroot and fails). None of the three
# ship anything the phone uses — quickshell's UPower module is
# native Qt, and the device carries no UPowerGlib typelib.
meson setup build-aarch64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false -Dintrospection=disabled \
--cross-file "$CROSS"
meson compile -C build-aarch64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-aarch64" meson install -C build-aarch64
)
# Sanity: the aarch64 upowerd must actually be aarch64.
# It installs to libexec/, not lib/ — the old path made this check
# fail even on a good build.
file "$GITHUB_WORKSPACE/src/upower/dest-aarch64/usr/libexec/upowerd" \
| grep -q aarch64 || { echo "aarch64 upowerd is not aarch64" >&2; exit 1; }
# Cross-install leak. upower asks pkg-config for the udev dir, and
# with a sysroot in play that answer comes back sysroot-prefixed, so
# the rules and hwdb install under $DESTDIR/home/casey/aarch64-sysroot/
# instead of /usr/lib/udev. systemdsystemunitdir is passed explicitly
# above for exactly this reason; the udev dir was missed. Measured on
# the phone 2026-07-27: all four of 60-upower-battery.{rules,hwdb},
# 95-upower-wup.rules and 95-upower-hid.hwdb were absent from
# /usr/lib/udev and present only under the junk path — so the fork's
# udev quirks had never once been applied on the device.
# Fold any leaked tree back before the tar, then refuse to ship a
# tree containing anything but the three prefixes upower legitimately
# installs into: usr/, etc/ (UPower.conf) and var/ (var/lib/upower).
# Leaving var/ out of that list is what failed run 1234.
DEST="$GITHUB_WORKSPACE/src/upower/dest-aarch64"
LEAK="$DEST$HOME/aarch64-sysroot"
if [ -d "$LEAK" ]; then
echo "cross install leaked into $HOME/aarch64-sysroot — relocating"
cp -a "$LEAK/." "$DEST/"
rm -rf "$LEAK"
# Removing the sysroot subtree leaves its empty parents behind
# ($DEST/home/casey), and an empty directory still ships AND still
# trips the stray check below — which is how runs 1234 and 1248
# failed. The PKGBUILD copy of this had the rmdir; this one did not.
rmdir -p --ignore-fail-on-non-empty "$(dirname "$LEAK")" 2>/dev/null || true
fi
stray=$(find "$DEST" -mindepth 1 -maxdepth 1 \
! -name usr ! -name etc ! -name var -printf '%f\n')
if [ -n "$stray" ]; then
echo "upower tree would ship outside usr/, etc/ and var/: $stray" >&2
exit 1
fi
# And the rules must have landed where udev actually reads them.
for f in usr/lib/udev/rules.d/60-upower-battery.rules \
usr/lib/udev/hwdb.d/60-upower-battery.hwdb; do
[ -e "$DEST/$f" ] || { echo "upower tree is missing $f" >&2; exit 1; }
done
tar -C "$GITHUB_WORKSPACE/src/upower/dest-aarch64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-aarch64.tar" .
- name: Stage the shell surface for packaging
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# deploy.sh composes ~/.config/quickshell/souveraine from repo files
# (symlinks) plus ii dirs borrowed from the ii-base pin. Run it in a
# scratch HOME and tar the result dereferenced, so the package is
# self-contained real files and never ships dangling links.
SHELL_STAGE="$GITHUB_WORKSPACE/shell-stage"
SHELL_TAR="$GITHUB_WORKSPACE/shell-package-input"
rm -rf "$SHELL_STAGE" "$SHELL_TAR"
mkdir -p "$SHELL_STAGE" "$SHELL_TAR"
HOME="$SHELL_STAGE" ./surfaces/quickshell/deploy.sh
test -L "$SHELL_STAGE/.config/quickshell/souveraine/shell.qml"
tar -h --zstd -C "$SHELL_STAGE/.config/quickshell" -cf "$SHELL_TAR/souveraine-shell-config.tar.zst" souveraine
tar -tf "$SHELL_TAR/souveraine-shell-config.tar.zst" | grep -qx 'souveraine/shell.qml'
- name: Package and sign pacman repository
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
PKGVER="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
WORK="$GITHUB_WORKSPACE/pacman-package"
REPO="$GITHUB_WORKSPACE/pacman-repo"
ARCHIVE_KEY="3CD9E99E222C2A174986FC9AFF4949AA20C8E911"
rm -rf "$WORK" "$REPO"
mkdir -p "$WORK" "$REPO"
for ARCH in aarch64 x86_64; do
PKG_WORK="$WORK/$ARCH"
ARCH_REPO="$REPO/$ARCH"
mkdir -p "$PKG_WORK" "$ARCH_REPO"
cp "souveraine-$ARCH" "$PKG_WORK/souveraine-binary"
cp "souveraine-secrets-$ARCH" "$PKG_WORK/souveraine-secrets-binary"
cp "souveraine-machined-$ARCH" "$PKG_WORK/souveraine-machined-binary"
cp "souveraine-admit-$ARCH" "$PKG_WORK/souveraine-admit-binary"
cp "souveraine-sessiond-$ARCH" "$PKG_WORK/souveraine-sessiond-binary"
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
cp packaging/souveraine.service packaging/souveraine-secrets.service \
packaging/org.freedesktop.secrets.service \
packaging/souveraine-machined.service \
packaging/souveraine-sessiond.service \
packaging/souveraine-sensord.service LICENSE "$PKG_WORK/"
cp packaging/arch/souveraine.sysusers "$PKG_WORK/"
# Phone-only reporters and surfaces remain aarch64-only.
if [ "$ARCH" = aarch64 ]; then
cp "souveraine-usb-kvm-$ARCH" "$PKG_WORK/souveraine-usb-kvm-binary"
# The wry host. Guarded, and the PKGBUILD source line is removed
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
# and a missing avatar must not stop sessiond reaching the phone.
# makepkg validates every entry in `source`, so leaving a name
# there with no file fails the whole package.
if [ -f "souveraine-web-$ARCH" ]; then
cp "souveraine-web-$ARCH" "$PKG_WORK/souveraine-web-binary"
fi
fi
cp packaging/souveraine-verify-trail "$PKG_WORK/"
cp packaging/souveraine-button "$PKG_WORK/"
cp packaging/souveraine-stepup.pam packaging/souveraine-sessiond.pam \
packaging/org.souveraine.stepup.policy "$PKG_WORK/"
cp packaging/arch/PKGBUILD.prebuilt "$PKG_WORK/PKGBUILD"
(
cd "$PKG_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
PKG=$(find "$PKG_WORK" -maxdepth 1 -name 'souveraine-*.pkg.tar.zst' -print -quit)
test -n "$PKG"
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/systemd/user/souveraine-sessiond.service'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sensord'
bsdtar -tf "$PKG" | grep -qx 'etc/pam.d/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/sysusers.d/souveraine.conf'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-admit'
if [ "$ARCH" = aarch64 ]; then
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-usb-kvm'
fi
cp "$PKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$PKG")"
# UPower fork package — built from packaging/upower-souveraine
# (git submodule of the Souveraine fork of upstream upower). Built
# natively per-arch by the "Build UPower fork" step above into
# upower-tree-$ARCH.tar, then folded into the SAME per-arch pacman
# database so the phone installs upower-souveraine from the same
# edge archive it already uses for souveraine.
# Version from UPSTREAM upower + the fork commit, NOT souveraine's
# r-count. The hand-built package on the phone was 1.91.3+<sha>;
# reusing souveraine's 0.1.rN here sorts LOWER, so pacman treats
# the repo copy as a downgrade and -Syu silently skips it.
UPOWER_VER=$(sed -nE "s/^ *version *: *'([^']+)'.*/\1/p" \
"$GITHUB_WORKSPACE/src/upower/meson.build" | head -1)
# '.r' not '+': a literal + in a release asset filename cannot be
# fetched from Gitea at all — it is stored decoded as a space, and
# both the raw + and %2B forms 404. Still sorts above the old
# hand-built 1.91.3+<sha> because vercmp reads + as a separator too.
UPOWER_PKGVER="${UPOWER_VER}.r$(git -C "$GITHUB_WORKSPACE/src/upower" rev-parse --short HEAD)"
UPOWER_WORK="$WORK/upower-$ARCH"
mkdir -p "$UPOWER_WORK"
cp "upower-tree-$ARCH.tar" "$UPOWER_WORK/upower-tree.tar"
cp packaging/upower-souveraine/dbus/org.freedesktop.UPower.Device.xml \
"$UPOWER_WORK/" 2>/dev/null \
|| cp "$GITHUB_WORKSPACE/src/upower/dbus/org.freedesktop.UPower.Device.xml" \
"$UPOWER_WORK/"
cp packaging/arch/PKGBUILD.upower.prebuilt "$UPOWER_WORK/PKGBUILD"
(
cd "$UPOWER_WORK"
export SOUVERAINE_PKGVER="$UPOWER_PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
UPKG=$(find "$UPOWER_WORK" -maxdepth 1 -name 'upower-souveraine-*.pkg.tar.zst' -print -quit)
test -n "$UPKG"
# libexec/, not lib/ — same path correction as the sanity check above.
bsdtar -tf "$UPKG" | grep -qx 'usr/libexec/upowerd'
cp "$UPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$UPKG")"
# Shell package — the composed quickshell config, arch=any, from
# the staged tar. Same per-arch database as the binaries above.
SHELL_WORK="$WORK/shell-$ARCH"
mkdir -p "$SHELL_WORK"
cp "$GITHUB_WORKSPACE/shell-package-input/souveraine-shell-config.tar.zst" "$SHELL_WORK/"
cp packaging/arch/PKGBUILD.shell.prebuilt "$SHELL_WORK/PKGBUILD"
cp LICENSE "$SHELL_WORK/"
(
cd "$SHELL_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
SPKG=$(find "$SHELL_WORK" -maxdepth 1 -name 'souveraine-shell-*.pkg.tar.zst' -print -quit)
test -n "$SPKG"
bsdtar -tf "$SPKG" | grep -qx 'etc/xdg/quickshell/souveraine/shell.qml'
bsdtar -tf "$SPKG" | grep -qx 'usr/share/licenses/souveraine-shell/LICENSE'
cp "$SPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$SPKG")"
# The per-arch database is NOT built here. `edge` is a shared archive
# with more than one producer in it, so the database has to be
# read-modify-written against the live copy rather than generated
# fresh — packaging/arch/publish-edge.sh owns that, under a lock.
done
cp packaging/arch/souveraine-archive-key.asc "$REPO/"
- name: Publish rolling edge prerelease
env:
# A PAT rather than the job's own GITHUB_TOKEN, so the identical
# invocation works from a producer repo other than this one.
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Additive: the release and tag are never deleted, the live databases
# are merged into, and only this producer's own superseded packages
# are removed. Before 2026-07-25 this step deleted and recreated the
# release, which erased every other producer's packages on each push.
export EDGE_TARGET_SHA="$GITHUB_SHA"
export PRODUCER_VERSION="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
./packaging/arch/publish-edge.sh souveraine "$GITHUB_WORKSPACE/pacman-repo" \
souveraine-aarch64 souveraine-x86_64 souveraine-binaries.sha256 \
"$GITHUB_WORKSPACE/pacman-repo/souveraine-archive-key.asc"
- name: Mirror into the Gitea Arch registry (alongside edge)
# Best-effort, never fails the publish: the registry sits alongside
# release-assets until proven as the phone's source. A 409 means
# (name, version) is already present immutably -- bump to republish.
# Device-side auth is handled by souveraine-pacman-fetch; this only
# uploads. Covers souveraine + upower-souveraine (both arches).
env:
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
run: |
set +e
REG="$GITHUB_SERVER_URL/api/packages/${GITHUB_REPOSITORY%/*}/arch/edge"
ok=0; skip=0; bad=0
shopt -s nullglob
for pkg in "$GITHUB_WORKSPACE"/pacman-repo/*/*.pkg.tar.zst; do
code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: token $EDGE_TOKEN" --upload-file "$pkg" "$REG")
case "$code" in
200|201) echo "registry: published $(basename "$pkg")"; ok=$((ok+1));;
409) echo "registry: present $(basename "$pkg") (bump to republish)"; skip=$((skip+1));;
*) echo "::warning::registry $(basename "$pkg") -> HTTP $code"; bad=$((bad+1));;
esac
done
echo "registry mirror: $ok new, $skip present, $bad failed"
exit 0