name: ci # CI for souveraine — Rust crate. Tests + clippy, then the aarch64 artifact, # all on `primary`. # # There used to be a second branch, `public`, which was the only ref that # published, so a green run on `primary` meant the gates passed and NOTHING # SHIPPED — indistinguishable from a run that did. It existed to mirror to # Codeberg; the mirror never worked and nothing else consumed it. Unified # 2026-07-31 after it stranded three commits, including TASK-48's fix. One # branch, one meaning: green on primary means it built and it shipped. on: push: branches: [primary] pull_request: branches: [primary] jobs: rust-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install system deps # Retried because the runner's ephemeral job containers lose DNS for # minutes at a time (four builds lost on 2026-08-13). A transient # resolver blip must not decide whether our build passes, for the same # reason the tuie pin exists below. # # The retry wraps *both* commands: `apt-get update` exits 0 when every # index fails to download — it warns "old ones used instead" — so only # the install reports the failure. run: | for attempt in 1 2 3; do if sudo apt-get update -qq \ && sudo apt-get install -y -qq libasound2-dev libchafa-dev libwayland-dev; then exit 0 fi echo "system deps attempt $attempt failed; retrying in $((attempt * 15))s" sleep $((attempt * 15)) done echo "system deps unavailable after 3 attempts — check runner DNS" exit 1 # Pinned fork on our Gitea (rgb patch committed there) — upstream HEAD # must never decide whether our build passes. - name: Clone tuie (pinned fork) run: | git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \ clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" /workspace/Fimeg/tuie git -C /workspace/Fimeg/tuie checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35 # Pinned to a MASTER commit, deliberately, not to a `stable`-branch tip. # The old pin (29eef336, "toolchain: stable") was the tip of that repo's # `stable` branch when it was taken. dtolnay force-moves `stable` on every # Rust release, which orphaned the commit: it still exists as an object on # GitHub, but is reachable from no ref, so act's clone resolves it as # "reference not found" and rust-test dies before it compiles anything. # That failure skipped aarch64-artifact and silently stopped publishing to # `edge` — the phone sat on an old build with nothing saying why. # master is append-only, so a commit on it stays reachable. The toolchain # is chosen by the `toolchain:` input below, never by the branch. - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c with: toolchain: "1.94" components: clippy - name: cargo test run: cargo test # `souveraine-sessiond` is `required-features = ["sessiond"]`, which is # not in `default` — so the line above never compiled the device state # authority, let alone ran its tests. Every sessiond change since the # daemon existed has been type-checked only by the aarch64 cross-build, # which builds and does not test. A green rust-test meant nothing for the # one binary that decides whether the phone is locked. Same trap as the # old primary/public split: a signal that reads like coverage and isn't. - name: cargo test (sessiond) run: cargo test --features sessiond --bin souveraine-sessiond - name: cargo test (USB KVM receiver) run: cargo test --features usb-kvm --bin souveraine-usb-kvm - name: cargo clippy run: cargo clippy -- -D warnings # Advisory, deliberately. Turning this on for the first time surfaced 9 # pre-existing warnings in code clippy had never seen (dead # `update_sensors`, unused re-exports in device_state, a nul-terminated # string built by hand). None are from the change that added this step, # and blocking the phone's only shipping path on them would be the wrong # trade. Read it, burn the list down, then make it -D warnings and hard. - name: cargo clippy (sessiond) continue-on-error: true run: cargo clippy --features sessiond --bin souveraine-sessiond -- -D warnings - name: cargo clippy (USB KVM receiver) run: cargo clippy --features usb-kvm --bin souveraine-usb-kvm -- -D warnings no-ai-attribution: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Check commit messages for AI attribution run: | if [ "${{ github.event_name }}" = "pull_request" ]; then RANGE="${{ github.event.pull_request.base.sha }}..${{ github.sha }}" else RANGE="${{ github.event.before }}..${{ github.sha }}" if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then RANGE="HEAD~10..HEAD" fi fi # CO assembled from fragments so this guard file does not itself trip # the public-branch pre-commit scanner (which bans the literal token). CO="Co-Authored""-By:" PATTERNS="${CO}.*[Cc]laude|${CO}.*OpenAI|${CO}.*ChatGPT|${CO}.*Copilot|${CO}.*Letta|${CO}.*Cursor|Generated by|Generated with|AI-assisted|Auto-generated by" FAIL=0 while IFS= read -r msg; do if echo "$msg" | grep -qiE "$PATTERNS"; then echo "::error::AI attribution found in commit: $msg" FAIL=1 fi done < <(git log --format='%s%n%b' $RANGE 2>/dev/null) if [ "$FAIL" -eq 1 ]; then echo "::error::Commits contain AI attribution lines. Remove them before merging." exit 1 fi echo "No AI attribution found in commits." action-pins: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Check for floating action refs run: | if grep -rE 'uses:.*@(v[0-9]+|stable|main|master)(\s|$)' .gitea/workflows/; then echo "::error::Floating action refs found — pin every action to a commit SHA" exit 1 fi echo "All action refs are SHA-pinned." # Phone artifact on green. Runs on the ArchDev build node (host mode, label # 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" <&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: 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+; # 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+ 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")" # 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