All four repo binaries were hand-copied to the phone and owned by no package. secrets/machined ship on both arches; sessiond stays aarch64. secrets unit repointed /usr/local/bin -> /usr/bin.
378 lines
19 KiB
YAML
378 lines
19 KiB
YAML
name: ci
|
|
# CI for souveraine — Rust crate. Tests + clippy on push/PR to primary (dev)
|
|
# and public (published). On push to public, mirror public:public to Codeberg
|
|
# on green. primary stays the private dev branch; public is what publishes.
|
|
#
|
|
# Required repo secrets:
|
|
# CODEBERG_TOKEN — push to codeberg.org/Fimeg/Souveraine (public branch)
|
|
on:
|
|
push:
|
|
branches: [primary, public]
|
|
pull_request:
|
|
branches: [primary, public]
|
|
|
|
jobs:
|
|
rust-test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
- name: Install system deps
|
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq libasound2-dev libchafa-dev
|
|
# 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
|
|
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
|
|
with:
|
|
toolchain: "1.94"
|
|
components: clippy
|
|
- name: cargo test
|
|
run: cargo test
|
|
- name: cargo clippy
|
|
run: cargo clippy -- -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/public'
|
|
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
|
|
|
|
# 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
|
|
for b in souveraine-secrets souveraine-machined; 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
|
|
for b in souveraine-secrets souveraine-machined; 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 is deliberately NOT built for x86_64: the laptop hit
|
|
# lock-screen errors with it, and it is the phone's session authority.
|
|
# Ship it on aarch64 only until the laptop side is sorted.
|
|
|
|
sha256sum souveraine-aarch64 souveraine-x86_64 \
|
|
souveraine-sessiond-aarch64 \
|
|
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
|
|
souveraine-machined-aarch64 souveraine-machined-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; }
|
|
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 packaging/souveraine.service packaging/souveraine-secrets.service \
|
|
packaging/org.freedesktop.secrets.service \
|
|
packaging/souveraine-machined.service LICENSE "$PKG_WORK/"
|
|
# aarch64 only — see the sessiond note in the build step.
|
|
if [ "$ARCH" = aarch64 ]; then
|
|
cp "souveraine-sessiond-$ARCH" "$PKG_WORK/souveraine-sessiond-binary"
|
|
cp packaging/souveraine-sessiond.service "$PKG_WORK/"
|
|
fi
|
|
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'
|
|
if [ "$ARCH" = aarch64 ]; then
|
|
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
|
|
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.
|
|
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="$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")"
|
|
|
|
# Pacman databases are architecture-specific: one database cannot
|
|
# retain two same-named, same-version packages for different
|
|
# architectures. Keep each database and its assets distinct.
|
|
DB="souveraine-${ARCH}"
|
|
repo-add --include-sigs --sign --key "$ARCHIVE_KEY" \
|
|
"$ARCH_REPO/$DB.db.tar.zst" "$ARCH_REPO"/*.pkg.tar.zst
|
|
# repo-add makes .db and .db.sig symlinks. Release assets cannot
|
|
# preserve those, so replace both links with real files.
|
|
rm -f "$ARCH_REPO/$DB.db" "$ARCH_REPO/$DB.db.sig"
|
|
cp "$ARCH_REPO/$DB.db.tar.zst" "$ARCH_REPO/$DB.db"
|
|
cp "$ARCH_REPO/$DB.db.tar.zst.sig" "$ARCH_REPO/$DB.db.sig"
|
|
sha256sum "$ARCH_REPO"/*.pkg.tar.zst "$ARCH_REPO/$DB.db" \
|
|
> "$REPO/$DB-repo.sha256"
|
|
done
|
|
cp packaging/arch/souveraine-archive-key.asc "$REPO/"
|
|
|
|
- name: Publish rolling edge prerelease
|
|
run: |
|
|
set -euo pipefail
|
|
cd "$GITHUB_WORKSPACE/src/Souveraine"
|
|
API="${GITHUB_SERVER_URL}/api/v1"
|
|
AUTH="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
|
|
DESC=$(git describe --tags --always)
|
|
|
|
OLD_ID=$(curl -s -H "$AUTH" "$API/repos/${GITHUB_REPOSITORY}/releases/tags/edge" \
|
|
| python3 -c "import json,sys
|
|
try: print(json.load(sys.stdin).get('id',''))
|
|
except Exception: print('')")
|
|
if [ -n "$OLD_ID" ]; then
|
|
curl -s -X DELETE -H "$AUTH" "$API/repos/${GITHUB_REPOSITORY}/releases/$OLD_ID"
|
|
curl -s -X DELETE -H "$AUTH" "$API/repos/${GITHUB_REPOSITORY}/tags/edge"
|
|
fi
|
|
|
|
REL_ID=$(curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
|
"$API/repos/${GITHUB_REPOSITORY}/releases" \
|
|
-d "{\"tag_name\":\"edge\",\"target_commitish\":\"${GITHUB_SHA}\",\"name\":\"edge (${DESC})\",\"body\":\"Rolling aarch64 build of public HEAD (${GITHUB_SHA}). Built on the archdev node, published only when CI is green.\",\"prerelease\":true}" \
|
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
|
|
|
for f in souveraine-aarch64 souveraine-x86_64 souveraine-binaries.sha256 \
|
|
$(find "$GITHUB_WORKSPACE/pacman-repo" -type f -print); do
|
|
curl -sf -X POST -H "$AUTH" \
|
|
"$API/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets?name=$(basename "$f")" \
|
|
-F "attachment=@${f}" -o /dev/null
|
|
done
|
|
echo "published edge (${DESC}) with aarch64 package + pacman database"
|
|
|
|
# Mirror the published branch only. primary (dev) never reaches Codeberg.
|
|
sync-codeberg:
|
|
runs-on: ubuntu-latest
|
|
needs: [rust-test, no-ai-attribution, action-pins]
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/public'
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Push public to Codeberg
|
|
run: |
|
|
if [ -z "${{ secrets.CODEBERG_TOKEN }}" ]; then
|
|
echo "[INFO] [sync] CODEBERG_TOKEN not set — skipping codeberg push"
|
|
exit 0
|
|
fi
|
|
git remote add codeberg-ci "https://${{ secrets.CODEBERG_TOKEN }}@codeberg.org/Fimeg/Souveraine.git"
|
|
git push codeberg-ci public:public
|
|
echo "Pushed public to codeberg.org/Fimeg/Souveraine"
|