apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Failing after 1m51s
ci / web (pull_request) Successful in 1m25s
ci / bun-nix (pull_request) Successful in 52s
ci / docs-site (pull_request) Successful in 1m53s
android / android (pull_request) Successful in 6m47s
ci / rust (pull_request) Successful in 28m33s
The v0.25.0 rebuild published perfectly — registry has punktfunk-host 0.25.0-2 with
libavcodec.so=63-64, and it resolves on a real ffmpeg-9 box — then failed its last
step with
prune_release_assets: command not found
`. scripts/ci/gitea-release.sh` sources from the CHECKED-OUT TREE, and a release
rebuild checks out the OLD TAG. So the step could only ever see the helpers that
existed when that tag was cut, and the prune is gated on exactly that path: the
helper was guaranteed absent in the only case that calls it. Adding it to a shared
script made it look available at review time while being unreachable at run time.
Only the workflow file is read from the dispatched ref, so the logic moves there,
inline. Same reasoning documented at both ends, including the corollary worth knowing
before the next rebuild: a PKGBUILD fix made after a tag does NOT reach a rebuild of
that tag either — the packaging comes from the tag too.
Verified by executing the one-liner's exact bytes out of arch.yml under /bin/sh (the
shell Gitea actually uses): keeps the new -2 set and gamescope, drops the superseded
-1 packages and their .sha256 sidecars, leaves other legs' .dmg/.deb untouched. The
`'\n'` survives the shell quoting, which was the part worth proving.
Also drops the now-dead helper from gitea-release.sh rather than leaving a function
no caller can reach, and leaves a warning there against the next one.
449 lines
27 KiB
YAML
449 lines
27 KiB
YAML
# Build the punktfunk-host / punktfunk-client / punktfunk-web / punktfunk-scripting pacman packages from
|
|
# packaging/arch/PKGBUILD and publish them to Gitea's Arch package registry, so Arch boxes
|
|
# get new builds via `pacman -Syu`. Counterpart to deb.yml (apt) and rpm.yml (dnf/rpm-ostree).
|
|
# Arch is rolling, so the packages build against whatever the archlinux:base-devel container
|
|
# resolves today — the same sonames an up-to-date Arch box runs.
|
|
#
|
|
# Registry (public, unom org) — box setup (once), see packaging/arch/README.md. The registry
|
|
# SIGNS the DB + packages, so the box imports the registry key first (pacman-key --add +
|
|
# --lsign-key), then no SigLevel line is needed (pacman's default Required verifies):
|
|
# [punktfunk] # or [punktfunk-canary] for main-push builds
|
|
# Server = https://git.unom.io/api/packages/unom/arch/$repo/$arch
|
|
#
|
|
# REGISTRY_TOKEN: repo Actions secret, a PAT with write:package scope (shared with docker.yml).
|
|
# NOTE: this token + the registry-held private key are the trust root — a token holder can
|
|
# publish a validly-signed package (the signature attests "via the registry", not "built by CI").
|
|
name: arch
|
|
# One pending run per workflow+ref: a newer push supersedes the queued/running one and cancels
|
|
# it (a canary only needs the latest commit; each release tag is its own ref so tag runs never
|
|
# cancel each other). Keeps a busy push cadence from piling ~10 queued runs per commit onto the
|
|
# runner fleet. Gitea honors this for push triggers (PR triggers: see gitea#35933).
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
# Scope canary builds to what this artifact is built FROM — a docs-only or
|
|
# web-only push should not light up the whole fleet. Applies to branch pushes;
|
|
# tag runs are matched by `tags:` (proven by flatpak/windows-msix releases).
|
|
paths:
|
|
- 'crates/**'
|
|
- 'clients/linux/**'
|
|
- 'clients/session/**'
|
|
- 'clients/shared/**'
|
|
- 'clients/cli/**'
|
|
- 'web/**'
|
|
- 'sdk/**'
|
|
- 'packaging/arch/**'
|
|
- 'packaging/gamescope/**'
|
|
- 'Cargo.toml'
|
|
- 'Cargo.lock'
|
|
- 'rust-toolchain.toml'
|
|
- 'scripts/ci/**'
|
|
- '.gitea/workflows/arch.yml'
|
|
# Single project version: a `vX.Y.Z` tag is THE release. main publishes to the
|
|
# `punktfunk-canary` pacman repo as X.Y.Z-0.<run#> (sorts below the eventual X.Y.Z-1),
|
|
# tags to `punktfunk` — separate repos, so neither channel can shadow the other.
|
|
tags: ['v*']
|
|
# REBUILDING A PUBLISHED RELEASE, because on a rolling distro the ground moves under one.
|
|
# Arch went FFmpeg 8 -> 9 (every libav soname +1) four minutes before v0.25.0 was tagged, so
|
|
# the release's punktfunk-host was linked in a builder image that still had 8 and shipped
|
|
# `libavcodec.so=62-64`. No up-to-date Arch box can satisfy that — and pacman prepares the
|
|
# whole transaction at once, so it did not merely block our package, it blocked those users'
|
|
# entire `pacman -Syu`. The repair is a rebuild of the SAME upstream version at a HIGHER
|
|
# pkgrel; nothing else reaches a box that already has the broken build recorded in its db.
|
|
# The workflow file at the tag can never carry inputs added after it was tagged, so dispatch
|
|
# this from `main`: it checks the tag's SOURCE out, publishes to the STABLE repo, and
|
|
# replaces the release-page assets. Same lever for any future "the distro moved" rebuild.
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_tag:
|
|
description: 'Rebuild this published release (e.g. v0.25.0) into the stable `punktfunk` repo. Empty = ordinary canary build of the dispatched ref.'
|
|
required: false
|
|
default: ''
|
|
pkgrel:
|
|
description: 'pkgrel for that rebuild — MUST be above the published one (2, 3, …); a same-pkgrel republish is invisible to pacman. Ignored without release_tag.'
|
|
required: false
|
|
default: '2'
|
|
|
|
env:
|
|
REGISTRY: git.unom.io
|
|
OWNER: unom
|
|
# Shared compile cache: sccache -> RustFS S3 (storage.unom.io). NOTE: makepkg runs
|
|
# behind `sudo -u builder env ...`, which strips ambient env — the makepkg step
|
|
# re-exports these explicitly.
|
|
RUSTC_WRAPPER: sccache
|
|
SCCACHE_BUCKET: unom-ci-sccache
|
|
SCCACHE_ENDPOINT: https://storage.unom.io
|
|
SCCACHE_REGION: home-central
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_SECRET_ACCESS_KEY }}
|
|
CARGO_INCREMENTAL: "0"
|
|
|
|
jobs:
|
|
build-publish:
|
|
runs-on: ubuntu-24.04
|
|
container:
|
|
# Everything the two pacman steps below used to download (~1 GB/run) is baked in,
|
|
# plus bun, sccache and node (ci/arch-ci.Dockerfile). The steps stay as --needed
|
|
# no-op guards for the one push where :latest lags an image-content change.
|
|
image: 192.168.1.58:5010/punktfunk-arch-ci:latest
|
|
timeout-minutes: 90
|
|
env:
|
|
CARGO_HOME: /usr/local/cargo
|
|
steps:
|
|
# git + nodejs must exist before actions/checkout — base-devel ships neither, and
|
|
# act_runner runs the action's JS with the CONTAINER's node, it does not inject one.
|
|
- name: Build + runtime-dev deps (no-op guard — baked into arch-ci)
|
|
# No -Syu: the image's snapshot IS the build environment (see the Dockerfile's
|
|
# rolling-release note); with everything installed this resolves locally and
|
|
# does nothing. It only matters on the push that adds a dep before the image
|
|
# rebuild lands — same bootstrap note as ci.yml's GTK4 step.
|
|
run: |
|
|
pacman -S --noconfirm --needed \
|
|
git nodejs rust clang cmake ninja nasm pkgconf python vulkan-headers \
|
|
gtk4 libadwaita sdl3 ffmpeg pipewire wayland libxkbcommon opus libei \
|
|
mesa libglvnd unzip libarchive || echo "::warning::pacman guard failed (stale image db?) — proceeding with baked packages"
|
|
command -v bun >/dev/null || {
|
|
curl -fsSL https://bun.sh/install | bash
|
|
install -m0755 "$HOME/.bun/bin/bun" /usr/local/bin/bun
|
|
}
|
|
bun --version
|
|
|
|
# THE BUILDER'S FFmpeg IS PART OF THE PACKAGE CONTRACT, not merely a build detail.
|
|
# packaging/arch/PKGBUILD binds punktfunk-host to the exact libav sonames it linked
|
|
# (`libavcodec.so=63-64` …), so a builder one FFmpeg major behind Arch emits a package
|
|
# that NOBODY can install — and takes the user's whole `pacman -Syu` down with it, since
|
|
# pacman prepares the transaction as a unit. That is exactly how v0.25.0 shipped: PR #108
|
|
# re-keyed this image for FFmpeg 9, the release tag fired four minutes later, and the job
|
|
# still got the FFmpeg-8 `:latest`. The image is a cache and is allowed to lag — but never
|
|
# on this one axis. So heal it in-job and shout, instead of building a dead package.
|
|
# (Runs BEFORE checkout: a stale image should be repaired before anything depends on it.)
|
|
- name: FFmpeg soname parity with today's Arch (heals a stale builder image)
|
|
run: |
|
|
export LC_ALL=C # `Provides` is a localized field name
|
|
# Piped (never a TTY here) pacman prints each field on ONE line, unwrapped.
|
|
sonames() { sed -n 's/^Provides *: *//p' | tr ' ' '\n' | grep -E '^lib(av|sw)[a-z]*\.so=' | sort | tr '\n' ' '; }
|
|
# A SEPARATE --dbpath: this refreshes only a throwaway view of the repos, so the
|
|
# container's own db never enters the partial-upgrade state a bare `pacman -Sy` leaves.
|
|
mkdir -p /tmp/pf-archsync
|
|
if ! pacman -Sy --dbpath /tmp/pf-archsync --logfile /dev/null >/dev/null 2>&1; then
|
|
echo "::warning::could not refresh the Arch db — skipping the FFmpeg parity check"
|
|
exit 0
|
|
fi
|
|
HAVE="$(pacman -Qi ffmpeg | sonames)"
|
|
WANT="$(pacman -Si --dbpath /tmp/pf-archsync ffmpeg | sonames)"
|
|
echo "builder ffmpeg $(pacman -Q ffmpeg | cut -d' ' -f2): $HAVE"
|
|
echo "arch ffmpeg $(pacman -Si --dbpath /tmp/pf-archsync ffmpeg | sed -n 's/^Version *: *//p'): $WANT"
|
|
if [ "$HAVE" = "$WANT" ]; then
|
|
echo "OK: the builder links the FFmpeg every up-to-date Arch box already has"
|
|
exit 0
|
|
fi
|
|
echo "::warning::arch-ci is stale ACROSS AN FFMPEG SONAME BUMP — upgrading it for this run."
|
|
echo "::warning::Bump the 'refreshed:' date in ci/arch-ci.Dockerfile so the IMAGE carries it."
|
|
pacman -Syu --noconfirm || true
|
|
HAVE="$(pacman -Qi ffmpeg | sonames)"
|
|
if [ "$HAVE" != "$WANT" ]; then
|
|
echo "::error::builder still links $HAVE while Arch ships $WANT."
|
|
echo "::error::Building on would publish a package no Arch box can install."
|
|
exit 1
|
|
fi
|
|
echo "healed: builder now links $HAVE"
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
# A dispatched release rebuild takes its WORKFLOW from the ref you dispatch (the only
|
|
# way it can carry inputs the tag predates) and its SOURCE from the tag. Empty string
|
|
# = checkout's own default, i.e. the triggering ref, for every other trigger.
|
|
ref: ${{ github.event.inputs.release_tag }}
|
|
|
|
# Cache cargo's git dir too, not just the registry: the workspace includes
|
|
# clients/windows, whose windows-reactor/windows deps are git-pinned — cargo must CLONE
|
|
# them (windows-rs is huge) merely to resolve the workspace, even though nothing Windows
|
|
# is ever compiled here. Cached, that cost is paid once per runner.
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
/usr/local/cargo/registry
|
|
/usr/local/cargo/git
|
|
key: cargo-home-arch-${{ hashFiles('Cargo.lock') }}
|
|
restore-keys: cargo-home-arch-
|
|
|
|
- name: Version + channel
|
|
# vX.Y.Z tag -> X.Y.Z-1 in the `punktfunk` repo; main push -> <next-minor>-0.<run#> in
|
|
# `punktfunk-canary` (pkgrel accepts only digits+dots — the run number carries the
|
|
# monotonic ordering; the commit sha is stamped into the binary via the workflow log).
|
|
#
|
|
# The run number is ZERO-PADDED to a fixed width, and that padding is load-bearing.
|
|
# pacman's own vercmp compares numeric segments numerically and gets this right either
|
|
# way, but Gitea's Arch registry picks the version it advertises in `punktfunk-canary.db`
|
|
# by STRING order. Unpadded, the run counter crossing a power of ten inverts that order
|
|
# ("0.9907" > "0.10095" because '9' > '1'), so the db pins itself to the last build
|
|
# before the rollover and every later canary becomes invisible to `pacman -Syu` — the
|
|
# packages publish fine, the index just never names them. That is exactly what happened
|
|
# on 2026-07-29 when run #10000 landed; it cost an evening and needed a manual purge of
|
|
# every 4-digit `0.22.0-0.9xxx` version to unstick. Padding keeps string order and
|
|
# numeric order in agreement, so the two can never disagree again.
|
|
#
|
|
# Keep the leading `0.` — it is what sorts a canary BELOW the eventual `X.Y.Z-1` stable
|
|
# release. (A pkgrel is digits+dots only, so `0.` is the only prefix available; raising
|
|
# it to `1.` would sort canaries ABOVE the release and is not an option.)
|
|
env:
|
|
RELEASE_TAG: ${{ github.event.inputs.release_tag }}
|
|
REBUILD_PKGREL: ${{ github.event.inputs.pkgrel }}
|
|
run: |
|
|
eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE (one minor ahead of latest stable)
|
|
if [ -n "${RELEASE_TAG:-}" ]; then
|
|
# Dispatched rebuild of a published release (see the workflow_dispatch note at the
|
|
# top): same upstream version, higher pkgrel, straight into the stable repo.
|
|
# ⚠ Keep that pkgrel SINGLE-DIGIT. Gitea's Arch registry picks the version its .db
|
|
# advertises by STRING order (the same trap the canary zero-padding below exists for),
|
|
# so "0.25.0-10" sorts BELOW "0.25.0-2" and the rebuild would never be advertised.
|
|
V="${RELEASE_TAG#v}"
|
|
R="${REBUILD_PKGREL:-2}"
|
|
REPO=punktfunk
|
|
case "$R" in
|
|
''|*[!0-9.]*) echo "::error::pkgrel '$R' is not digits+dots"; exit 1 ;;
|
|
1) echo "::error::pkgrel 1 is the published build — a rebuild MUST go up (2, 3, …)"; exit 1 ;;
|
|
esac
|
|
else
|
|
case "$GITHUB_REF" in
|
|
refs/tags/v*) V="${GITHUB_REF_NAME#v}"; R="1"; REPO=punktfunk ;;
|
|
*) V="$PF_BASE"; R="0.$(printf '%08d' "$GITHUB_RUN_NUMBER")"; REPO=punktfunk-canary ;;
|
|
esac
|
|
fi
|
|
echo "PF_PKGVER=$V" >> "$GITHUB_ENV"
|
|
echo "PF_PKGREL=$R" >> "$GITHUB_ENV"
|
|
echo "REPO=$REPO" >> "$GITHUB_ENV"
|
|
echo "pacman $V-$R -> repo '$REPO'"
|
|
|
|
- name: Build packages (makepkg)
|
|
run: |
|
|
git config --global --add safe.directory "$PWD"
|
|
# libcuda link stub — same trick as packaging/rpm/build-rpm.sh: the zerocopy FFI
|
|
# links -lcuda but the builder has no GPU; synthesize every cu* symbol the source
|
|
# references so a newly-added call can't silently break the link.
|
|
CU_SYMS="$(grep -rhoE '\bcu[A-Z][A-Za-z0-9_]*' crates/punktfunk-host/src/ | sort -u || true)"
|
|
if [ -n "$CU_SYMS" ] && [ ! -e /usr/lib/libcuda.so ]; then
|
|
STUB_C="$(mktemp --suffix=.c)"
|
|
for s in $CU_SYMS; do printf 'int %s(void){return 0;}\n' "$s" >> "$STUB_C"; done
|
|
gcc -shared -fPIC -Wl,-soname,libcuda.so.1 -o /usr/lib/libcuda.so.1 "$STUB_C"
|
|
ln -sf libcuda.so.1 /usr/lib/libcuda.so
|
|
rm -f "$STUB_C"; ldconfig
|
|
echo "== libcuda stub: $(printf '%s\n' "$CU_SYMS" | wc -l) symbols =="
|
|
fi
|
|
# makepkg refuses to run as root; deps are already installed above (-d skips the
|
|
# RPM-level check that can't see the script-installed bun anyway).
|
|
useradd -m builder
|
|
mkdir -p "$CARGO_HOME" # actions/cache doesn't create it on a cache miss
|
|
chown -R builder: "$PWD" "$CARGO_HOME"
|
|
sudo -u builder git config --global --add safe.directory "$PWD"
|
|
mkdir -p dist && chown builder: dist
|
|
cd packaging/arch
|
|
# sudo env_reset strips the ambient env, so the sccache wiring must cross the
|
|
# boundary explicitly (same values as the workflow env block).
|
|
sudo -u builder env PF_SRCDIR="$GITHUB_WORKSPACE" PF_WITH_WEB=1 PF_WITH_SCRIPTING=1 \
|
|
PF_PKGVER="$PF_PKGVER" PF_PKGREL="$PF_PKGREL" \
|
|
CARGO_HOME="$CARGO_HOME" PKGDEST="$GITHUB_WORKSPACE/dist" \
|
|
RUSTC_WRAPPER="$RUSTC_WRAPPER" CARGO_INCREMENTAL="$CARGO_INCREMENTAL" \
|
|
SCCACHE_BUCKET="$SCCACHE_BUCKET" SCCACHE_ENDPOINT="$SCCACHE_ENDPOINT" \
|
|
SCCACHE_REGION="$SCCACHE_REGION" \
|
|
AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
|
|
makepkg -f -d --holdver
|
|
ls -lh "$GITHUB_WORKSPACE/dist"
|
|
|
|
# The host must ship a VERSIONED libav soname dep, and nothing else in this pipeline proves
|
|
# it. packaging/arch/PKGBUILD lists bare `libavcodec.so` etc. and relies on makepkg rewriting
|
|
# each into `libavcodec.so=<soname>-<arch>` from the built binary's DT_NEEDED; if that
|
|
# rewrite ever stops happening — Arch dropping the soname `provides`, someone "tidying" the
|
|
# entries out of `depends`, a makepkg change — the dep silently degrades to an unversioned
|
|
# name that ANY ffmpeg satisfies. That is precisely the 2026-08-08 state in which `pacman
|
|
# -Syu` walked every Arch/CachyOS install across the FFmpeg 8 -> 9 soname bump and left the
|
|
# host unable to start (exit 127 before main(), restart loop). The failure is invisible in a
|
|
# green build and only shows up as a bricked box weeks later, so assert it here.
|
|
- name: Assert the host pins the FFmpeg soname
|
|
run: |
|
|
PKG="$(ls "$GITHUB_WORKSPACE"/dist/punktfunk-host-*.pkg.tar.zst | head -1)"
|
|
DEPS="$(bsdtar -xOf "$PKG" .PKGINFO | sed -n 's/^depend = //p')"
|
|
echo "$DEPS" | sed 's/^/ depend = /'
|
|
for lib in libavcodec libavutil; do
|
|
echo "$DEPS" | grep -qE "^$lib\.so=[0-9]+-[0-9]+$" || {
|
|
echo "::error::punktfunk-host declares no VERSIONED $lib.so dependency."
|
|
echo "::error::makepkg did not expand the bare soname from DT_NEEDED, so pacman can"
|
|
echo "::error::upgrade FFmpeg across a soname break and brick the install."
|
|
echo "::error::See the depends comment in packaging/arch/PKGBUILD."
|
|
exit 1
|
|
}
|
|
done
|
|
echo "OK: $(echo "$DEPS" | grep -E '^libav|^libsw' | tr '\n' ' ')"
|
|
|
|
# The optional HDR gamescope companion (packaging/gamescope) — a separate pkgbase with a
|
|
# completely different dependency set, published into the same repo so `pacman -S
|
|
# punktfunk-gamescope` is all an Arch/SteamOS box needs for 10-bit BT.2020 PQ.
|
|
#
|
|
# CACHED on `packaging/gamescope/**`: it depends on nothing else in this repo, so a normal
|
|
# push restores the built package instead of spending ~10 minutes on someone else's C++ tree.
|
|
# Arch is rolling, so the cache is invalidated by our own patch changes only — a stale binary
|
|
# against newer system libs is the same risk the distro's own package carries between rebuilds.
|
|
- uses: actions/cache@v4
|
|
id: gamescope
|
|
with:
|
|
path: dist-gamescope
|
|
key: punktfunk-gamescope-arch-${{ hashFiles('packaging/gamescope/**') }}
|
|
|
|
- name: Build punktfunk-gamescope (makepkg)
|
|
if: steps.gamescope.outputs.cache-hit != 'true'
|
|
# Best-effort: punktfunk-host works without it (SDR on the gamescope backend), and a
|
|
# failure building gamescope must not cost the packages this workflow exists to publish.
|
|
run: |
|
|
set -x
|
|
# Baked into arch-ci — a no-op guard, like the dep step above.
|
|
pacman -S --noconfirm --needed \
|
|
glslang libcap libdrm libinput libx11 libxcomposite libxdamage libxext \
|
|
libxkbcommon libxmu libxrender libxres libxtst libxxf86vm libavif libdecor \
|
|
hwdata luajit pipewire seatd sdl2-compat vulkan-icd-loader wayland \
|
|
xcb-util-errors xcb-util-wm xorg-xwayland \
|
|
meson cmake glm wayland-protocols benchmark libxcursor || true
|
|
mkdir -p dist-gamescope && chown builder: dist-gamescope
|
|
chown -R builder: packaging/gamescope
|
|
if sudo -u builder env PKGDEST="$GITHUB_WORKSPACE/dist-gamescope" \
|
|
bash -c 'cd packaging/gamescope && makepkg -f -d --holdver'; then
|
|
ls -lh dist-gamescope
|
|
else
|
|
echo "::warning::punktfunk-gamescope failed to build — Arch boxes stay SDR on the gamescope backend this run"
|
|
rm -rf dist-gamescope # never cache a failed build (an empty path is not saved)
|
|
fi
|
|
|
|
# THE GATE THIS PIPELINE WAS MISSING. The soname assert above proves the libav dep is
|
|
# VERSIONED; it cannot prove the version is one that EXISTS. v0.25.0 passed it and still
|
|
# shipped `libavcodec.so=62-64` to a world that had moved to 63 — every affected user got
|
|
# "unable to satisfy dependency … required by punktfunk-host", and because pacman prepares
|
|
# one transaction, their whole system upgrade stopped there. So ask the only question that
|
|
# matters before publishing: would a real, up-to-date Arch box install this?
|
|
#
|
|
# An empty --dbpath is what makes the answer honest. It means "nothing is installed", so
|
|
# pacman must satisfy every dependency FROM THE REPOS exactly as a user's box does. Checking
|
|
# against the builder's own installed set instead would let a stale ffmpeg satisfy the stale
|
|
# bound and hide the break completely — the very illusion that shipped v0.25.0. `--print`
|
|
# resolves and prints; it downloads nothing and installs nothing. Verified against the real
|
|
# broken artifact on an ffmpeg-9 box: it reproduces the user-visible failure verbatim.
|
|
- name: Assert every package installs on an up-to-date Arch box
|
|
run: |
|
|
export LC_ALL=C
|
|
mkdir -p /tmp/pf-instcheck
|
|
if ! pacman -Sy --dbpath /tmp/pf-instcheck --logfile /dev/null >/dev/null 2>&1; then
|
|
echo "::error::could not sync the Arch db — cannot prove these packages install"
|
|
exit 1
|
|
fi
|
|
check() { # check FILE -> 0 installable, 1 not (reason on stdout)
|
|
pacman -U --print --noconfirm --dbpath /tmp/pf-instcheck --logfile /dev/null "$1" 2>&1
|
|
}
|
|
ls dist/*.pkg.tar.zst >/dev/null 2>&1 || { echo "::error::nothing in dist/ to check"; exit 1; }
|
|
rc=0
|
|
for pkg in dist/*.pkg.tar.zst; do
|
|
if out="$(check "$pkg")"; then
|
|
echo "OK $(basename "$pkg") ($(echo "$out" | wc -l) targets resolve)"
|
|
else
|
|
rc=1
|
|
echo "::error::$(basename "$pkg") CANNOT be installed on an up-to-date Arch box:"
|
|
echo "$out" | sed 's/^/ /'
|
|
fi
|
|
done
|
|
# gamescope stays best-effort, exactly as its build step is: a companion that cannot
|
|
# install is dropped from the upload with a warning, never a reason to withhold the
|
|
# packages this workflow exists to publish. (It is also the one package that can be
|
|
# restored from a cache older than the current Arch snapshot.)
|
|
for pkg in dist-gamescope/*.pkg.tar.zst; do
|
|
[ -e "$pkg" ] || continue
|
|
if out="$(check "$pkg")"; then
|
|
echo "OK $(basename "$pkg") ($(echo "$out" | wc -l) targets resolve)"
|
|
else
|
|
echo "::warning::$(basename "$pkg") is not installable on current Arch — NOT publishing it"
|
|
echo "$out" | sed 's/^/ /'
|
|
rm -f "$pkg"
|
|
fi
|
|
done
|
|
if [ "$rc" != 0 ]; then
|
|
echo "::error::refusing to publish: pacman would reject this on a current box, and a"
|
|
echo "::error::rejected dependency blocks the user's ENTIRE upgrade, not just punktfunk."
|
|
echo "::error::Usual cause: the arch-ci builder image lags Arch across a soname bump —"
|
|
echo "::error::bump 'refreshed:' in ci/arch-ci.Dockerfile, let docker.yml republish it, re-run."
|
|
exit 1
|
|
fi
|
|
|
|
# NOTE deliberately NO sysext image is built or published here: a prebuilt HOST binary on
|
|
# SteamOS breaks on the next A/B soname bump (and /var — where sysexts live — is
|
|
# per-partition-set), which is the standing packaging verdict behind the on-device
|
|
# distrobox build (scripts/steamdeck/, see scripts/steamdeck/README.md). That flow builds
|
|
# its own HDR gamescope too. packaging/arch/build-sysext.sh remains a by-hand tool for the
|
|
# Deck CLIENT image and for operators who accept the prebuilt-host trade-off.
|
|
|
|
- name: Publish to the Gitea Arch registry
|
|
env:
|
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
# The gamescope companion rides the same loop (same repo, same channel).
|
|
cp -f dist-gamescope/*.pkg.tar.zst dist/ 2>/dev/null || true
|
|
for pkg in dist/*.pkg.tar.zst; do
|
|
echo "uploading $pkg"
|
|
NAME=$(bsdtar -xOf "$pkg" .PKGINFO | sed -n 's/^pkgname = //p')
|
|
VER=$(bsdtar -xOf "$pkg" .PKGINFO | sed -n 's/^pkgver = //p')
|
|
ARCH=$(bsdtar -xOf "$pkg" .PKGINFO | sed -n 's/^arch = //p')
|
|
# A re-tagged release re-fires this workflow and the registry 409s on duplicate
|
|
# package versions — delete any prior copy first (404 on the first publish is fine).
|
|
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE \
|
|
"https://$REGISTRY/api/packages/$OWNER/arch/$REPO/$NAME/$VER/$ARCH" || true
|
|
curl -fsS --user "enricobuehler:$TOKEN" --upload-file "$pkg" \
|
|
"https://$REGISTRY/api/packages/$OWNER/arch/$REPO"
|
|
done
|
|
echo "published to $OWNER/arch/$REPO"
|
|
|
|
# On a real release, also attach the packages to the unified Gitea Release. A dispatched
|
|
# rebuild attaches to that SAME release object: the release page is a distribution surface
|
|
# too, and leaving the superseded .pkg.tar.zst sitting on it is one click away from handing
|
|
# someone the exact break the rebuild exists to fix.
|
|
- name: Attach packages to the Gitea release (stable tags + release rebuilds)
|
|
if: startsWith(gitea.ref, 'refs/tags/v') || github.event.inputs.release_tag != ''
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
RELEASE_TAG: ${{ github.event.inputs.release_tag }}
|
|
run: |
|
|
. scripts/ci/gitea-release.sh
|
|
TAG="${RELEASE_TAG:-$GITHUB_REF_NAME}"
|
|
RID=$(ensure_release "$TAG" "$TAG" auto)
|
|
for pkg in dist/*.pkg.tar.zst; do
|
|
upsert_asset "$RID" "$pkg"
|
|
done
|
|
# A rebuild bumps pkgrel, so its FILENAMES differ from the ones already attached, and
|
|
# upsert_asset only replaces by name — the superseded set would survive untouched.
|
|
# Drop every pacman asset (and .sha256 sidecar) this upload did not just write.
|
|
#
|
|
# ⚠⚠ THIS MUST LIVE IN THE WORKFLOW, NOT IN scripts/ci/gitea-release.sh. The sourced
|
|
# script comes from the CHECKED-OUT TREE, which on a release rebuild is the OLD TAG —
|
|
# so it can only ever offer the helpers that existed when that tag was cut. A helper
|
|
# added for this feature is therefore guaranteed ABSENT in the one code path that
|
|
# calls it: the first attempt failed with `prune_release_assets: command not found`
|
|
# after publishing perfectly. Only the workflow file itself is taken from the ref you
|
|
# dispatch. Same reason a packaging fix made after a tag does NOT reach a rebuild of
|
|
# that tag — the PKGBUILD is the tag's too.
|
|
if [ -n "${RELEASE_TAG:-}" ]; then
|
|
KEEP="$(cd dist && printf '%s ' *.pkg.tar.zst)"
|
|
# An UNMATCHED glob would come through literally and match nothing in the keep set —
|
|
# i.e. "delete every pacman asset on the release". Skip entirely instead.
|
|
case "$KEEP" in *'*'*) KEEP="" ;; esac
|
|
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
|
if [ -n "$KEEP" ]; then
|
|
curl -fsS "$API/releases/$RID/assets" -H "Authorization: token $GITEA_TOKEN" \
|
|
| python3 -c "import json,sys;k=set(sys.argv[1].split());k|={n+'.sha256' for n in k};print('\n'.join('%s %s'%(a['id'],a['name']) for a in json.load(sys.stdin) if a.get('name','').endswith(('.pkg.tar.zst','.pkg.tar.zst.sha256')) and a['name'] not in k))" "$KEEP" \
|
|
| while read -r id name; do
|
|
[ -n "$id" ] || continue
|
|
echo "dropping superseded release asset: $name"
|
|
curl -fsS -o /dev/null -X DELETE "$API/releases/$RID/assets/$id" \
|
|
-H "Authorization: token $GITEA_TOKEN" || true
|
|
done
|
|
fi
|
|
fi
|