ci: publish into edge additively
edge is a shared archive but ci.yml deleted and recreated it each push, erasing other producers. publish-edge.sh merges into the live db under a flock and removes only its own superseded builds. Verifies the merge kept every foreign entry, and reads the db back before trusting it.
This commit is contained in:
parent
9d8bce2a08
commit
044c373cad
2 changed files with 424 additions and 38 deletions
|
|
@ -324,51 +324,31 @@ jobs:
|
|||
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"
|
||||
# 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"
|
||||
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"
|
||||
# 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"
|
||||
|
||||
# Mirror the published branch only. primary (dev) never reaches Codeberg.
|
||||
sync-codeberg:
|
||||
|
|
|
|||
406
packaging/arch/publish-edge.sh
Executable file
406
packaging/arch/publish-edge.sh
Executable file
|
|
@ -0,0 +1,406 @@
|
|||
#!/usr/bin/env bash
|
||||
# Additive publisher for the rolling `edge` pacman archive.
|
||||
#
|
||||
# The `edge` release on Fimeg/souveraine is a MULTI-PRODUCER archive: souveraine
|
||||
# publishes its own binaries there, and so does every other repo that ships a
|
||||
# package to the phone. Until 2026-07-25 souveraine's CI deleted and recreated
|
||||
# the release on every push and uploaded only its own `pacman-repo`, so anything
|
||||
# another job had published was erased — silently, with nothing erroring. This
|
||||
# script is the additive replacement.
|
||||
#
|
||||
# Rules it enforces:
|
||||
# * The release and its tag are NEVER deleted. Created once, patched after.
|
||||
# * The per-arch database is read-modify-written: the live
|
||||
# `souveraine-<arch>.db.tar.zst` is fetched, our packages are `repo-add`ed
|
||||
# into it, and the result is uploaded back. Entries belonging to other
|
||||
# producers survive because repo-add only touches the names it is given.
|
||||
# * The ONLY assets deleted are ones this producer supersedes: for each
|
||||
# package we publish, the `%FILENAME%` recorded in the live database under
|
||||
# the same `%NAME%`. A producer can never delete another's package.
|
||||
# * Everything runs under one exclusive flock on the archdev runner, so two
|
||||
# jobs cannot interleave their read-modify-write of the same database.
|
||||
#
|
||||
# Usage:
|
||||
# publish-edge.sh <producer> <repo-dir> [extra-asset ...]
|
||||
#
|
||||
# <producer> short name for this publisher (souveraine, souveraine-updater).
|
||||
# Used for the manifest line in the release body and for the
|
||||
# per-producer checksum asset. NOT used to decide deletions.
|
||||
# <repo-dir> directory holding one subdirectory per architecture, each with
|
||||
# `*.pkg.tar.zst` and matching `.sig` files. The databases are
|
||||
# built here; do not pre-create them.
|
||||
# extra-asset optional loose files to publish alongside (raw binaries,
|
||||
# checksum manifests). Replaced in place by name.
|
||||
#
|
||||
# Environment:
|
||||
# EDGE_TOKEN required. Gitea token with write access to the ARCHIVE
|
||||
# repo — for a producer other than souveraine this must be
|
||||
# a PAT, since a job's own GITHUB_TOKEN is scoped to its
|
||||
# own repository and cannot upload here.
|
||||
# ARCHIVE_KEY required. GPG key id used for `repo-add --sign`.
|
||||
# GITHUB_SERVER_URL Gitea base URL. Default http://10.10.20.120:4455.
|
||||
# ARCHIVE_REPO owner/name holding the release. Default Fimeg/souveraine.
|
||||
# EDGE_TARGET_SHA commitish for the tag, used only when the release does
|
||||
# not exist yet. Default: the archive repo's default branch.
|
||||
# PRODUCER_VERSION version string recorded in the release body manifest.
|
||||
# EDGE_LOCK lock file path. Default ~/.cache/souveraine-archive/edge.lock.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PRODUCER="${1:?usage: publish-edge.sh <producer> <repo-dir> [extra-asset ...]}"
|
||||
REPO_DIR="${2:?usage: publish-edge.sh <producer> <repo-dir> [extra-asset ...]}"
|
||||
shift 2
|
||||
EXTRA_ASSETS=("$@")
|
||||
|
||||
: "${EDGE_TOKEN:?EDGE_TOKEN must be set (Gitea token with write access to the archive repo)}"
|
||||
: "${ARCHIVE_KEY:?ARCHIVE_KEY must be set (gpg key id for repo-add --sign)}"
|
||||
|
||||
SERVER="${GITHUB_SERVER_URL:-http://10.10.20.120:4455}"
|
||||
ARCHIVE_REPO="${ARCHIVE_REPO:-Fimeg/souveraine}"
|
||||
PRODUCER_VERSION="${PRODUCER_VERSION:-unknown}"
|
||||
LOCK="${EDGE_LOCK:-$HOME/.cache/souveraine-archive/edge.lock}"
|
||||
# Overridable so a change to this script can be rehearsed end-to-end against a
|
||||
# throwaway tag before it is pointed at the archive the phone actually installs
|
||||
# from. Leave it alone in CI.
|
||||
EDGE_TAG="${EDGE_TAG:-edge}"
|
||||
|
||||
# Re-exec under an exclusive lock. The runner is a single host-mode box shared
|
||||
# by every repo's jobs, so this one file serialises all producers. Without it
|
||||
# two concurrent read-modify-writes of the same database both start from the
|
||||
# same base and the second upload silently drops the first's entries — the same
|
||||
# lost-update the delete-and-recreate shape caused, just narrower.
|
||||
if [ "${EDGE_LOCK_HELD:-}" != 1 ]; then
|
||||
mkdir -p "$(dirname "$LOCK")"
|
||||
export EDGE_LOCK_HELD=1
|
||||
exec flock "$LOCK" "$0" "$PRODUCER" "$REPO_DIR" "${EXTRA_ASSETS[@]+"${EXTRA_ASSETS[@]}"}"
|
||||
fi
|
||||
|
||||
API="$SERVER/api/v1"
|
||||
AUTH="Authorization: token $EDGE_TOKEN"
|
||||
|
||||
api() { curl -sf -H "$AUTH" "$@"; }
|
||||
|
||||
log() { echo "[publish-edge] $*"; }
|
||||
|
||||
# --- locate or create the release -------------------------------------------
|
||||
# Note the asymmetry with the old code: there is no DELETE anywhere in this
|
||||
# script for the release or the tag. `edge` outlives every individual push.
|
||||
REL_JSON=$(curl -s -H "$AUTH" "$API/repos/$ARCHIVE_REPO/releases/tags/$EDGE_TAG" || true)
|
||||
REL_ID=$(printf '%s' "$REL_JSON" | python3 -c "
|
||||
import json,sys
|
||||
try: print(json.load(sys.stdin).get('id',''))
|
||||
except Exception: print('')")
|
||||
|
||||
if [ -z "$REL_ID" ]; then
|
||||
TARGET="${EDGE_TARGET_SHA:-}"
|
||||
if [ -z "$TARGET" ]; then
|
||||
TARGET=$(api "$API/repos/$ARCHIVE_REPO" | python3 -c "
|
||||
import json,sys; print(json.load(sys.stdin)['default_branch'])")
|
||||
fi
|
||||
log "no $EDGE_TAG release yet — creating it at $TARGET"
|
||||
REL_ID=$(api -X POST -H "Content-Type: application/json" \
|
||||
"$API/repos/$ARCHIVE_REPO/releases" \
|
||||
-d "$(python3 -c "
|
||||
import json,sys
|
||||
print(json.dumps({
|
||||
'tag_name': sys.argv[2],
|
||||
'target_commitish': sys.argv[1],
|
||||
'name': sys.argv[2],
|
||||
'body': 'Rolling multi-producer pacman archive. Each repo publishes its own '
|
||||
'signed packages into the shared souveraine-<arch> databases; the '
|
||||
'release is never deleted. Producers are listed below.\n',
|
||||
'prerelease': True,
|
||||
}))" "$TARGET" "$EDGE_TAG")" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
else
|
||||
log "publishing into existing $EDGE_TAG release $REL_ID"
|
||||
fi
|
||||
|
||||
ASSETS_JSON=$(api "$API/repos/$ARCHIVE_REPO/releases/$REL_ID/assets")
|
||||
|
||||
asset_id() {
|
||||
printf '%s' "$ASSETS_JSON" | python3 -c "
|
||||
import json,sys
|
||||
name = sys.argv[1]
|
||||
for a in json.load(sys.stdin):
|
||||
if a['name'] == name:
|
||||
print(a['id']); break
|
||||
" "$1"
|
||||
}
|
||||
|
||||
delete_asset() {
|
||||
local name="$1" id
|
||||
id=$(asset_id "$name")
|
||||
[ -n "$id" ] || return 0
|
||||
curl -sf -X DELETE -H "$AUTH" \
|
||||
"$API/repos/$ARCHIVE_REPO/releases/$REL_ID/assets/$id" -o /dev/null
|
||||
log "deleted stale asset $name"
|
||||
}
|
||||
|
||||
# Gitea happily stores two assets with the same name, which would leave pacman
|
||||
# fetching whichever the API returned first. Always delete before uploading.
|
||||
upload_asset() {
|
||||
local path="$1" name
|
||||
name="$(basename "$path")"
|
||||
delete_asset "$name"
|
||||
curl -sf -X POST -H "$AUTH" \
|
||||
"$API/repos/$ARCHIVE_REPO/releases/$REL_ID/assets?name=$name" \
|
||||
-F "attachment=@$path" -o /dev/null
|
||||
log "uploaded $name"
|
||||
}
|
||||
|
||||
# Fetch by the SAME URL shape pacman uses on the phone
|
||||
# (`releases/download/<tag>/<name>`), not by asset id. The by-id API endpoint
|
||||
# returns attachment metadata as JSON in some Gitea versions, and writing that
|
||||
# into a .db.tar.zst would look like a corrupt database rather than an error.
|
||||
# This path is proven: it is exactly what souveraine-pacman-fetch already pulls.
|
||||
download_asset() {
|
||||
local name="$1" dest="$2"
|
||||
curl -sfL -H "$AUTH" \
|
||||
"$SERVER/$ARCHIVE_REPO/releases/download/$EDGE_TAG/$name" -o "$dest"
|
||||
}
|
||||
|
||||
# --- per-arch read-modify-write ---------------------------------------------
|
||||
STAGE=$(mktemp -d)
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
# Dump `NAME<TAB>FILENAME` for every entry in a pacman database. Used three
|
||||
# times: to find our own superseded builds, to snapshot what was in the archive
|
||||
# before we touched it, and to prove after upload that the snapshot survived.
|
||||
cat > "$STAGE/dbentries.py" <<'PY'
|
||||
import subprocess, sys
|
||||
|
||||
# `bsdtar -xO` streams every member concatenated; entry `desc` files are the
|
||||
# only ones we want. Each is a sequence of `%KEY%` header lines followed by
|
||||
# their values, and a `%FILENAME%` header is what starts a new entry.
|
||||
out = subprocess.run(
|
||||
["bsdtar", "-xOf", sys.argv[1], "--include=*/desc"],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
|
||||
entries, cur, key = [], {}, None
|
||||
for line in out.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("%") and line.endswith("%") and len(line) > 2:
|
||||
if line == "%FILENAME%" and cur:
|
||||
entries.append(cur)
|
||||
cur = {}
|
||||
key = line.strip("%")
|
||||
elif line and key and key not in cur:
|
||||
cur[key] = line
|
||||
if cur:
|
||||
entries.append(cur)
|
||||
|
||||
for e in entries:
|
||||
if "NAME" in e and "FILENAME" in e:
|
||||
print(f"{e['NAME']}\t{e['FILENAME']}")
|
||||
PY
|
||||
|
||||
db_entries() { python3 "$STAGE/dbentries.py" "$1"; }
|
||||
|
||||
# Package name is everything before the trailing -pkgver-pkgrel-arch. pkgver and
|
||||
# pkgrel may not contain a hyphen, so the last three fields are always known.
|
||||
pkg_name_of() {
|
||||
local base
|
||||
base="$(basename "$1")"
|
||||
base="${base%.pkg.tar.*}"
|
||||
printf '%s\n' "${base%-*-*-*}"
|
||||
}
|
||||
|
||||
for ARCH_DIR in "$REPO_DIR"/*/; do
|
||||
[ -d "$ARCH_DIR" ] || continue
|
||||
ARCH="$(basename "$ARCH_DIR")"
|
||||
shopt -s nullglob
|
||||
PKGS=("$ARCH_DIR"*.pkg.tar.zst)
|
||||
shopt -u nullglob
|
||||
if [ ${#PKGS[@]} -eq 0 ]; then
|
||||
log "$ARCH: no packages, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
DB="souveraine-$ARCH"
|
||||
WORK="$STAGE/$ARCH"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
# Pull the LIVE database. Every other producer's entries live in here, and
|
||||
# they survive only because we hand this file to repo-add rather than
|
||||
# building a fresh one.
|
||||
#
|
||||
# The distinction below is the whole safety property of this script. Starting
|
||||
# a fresh database is correct ONLY when the archive genuinely has none yet. If
|
||||
# the asset is listed but we cannot fetch or read it — a transient 500, a
|
||||
# truncated body, an HTML error page — falling through to a fresh database
|
||||
# would erase every other producer on upload. That is precisely the clobber
|
||||
# this script exists to remove, so it is a hard failure instead.
|
||||
if [ -n "$(asset_id "$DB.db.tar.zst")" ]; then
|
||||
download_asset "$DB.db.tar.zst" "$WORK/$DB.db.tar.zst" \
|
||||
|| { echo "FATAL: $DB.db.tar.zst is published but could not be fetched;" \
|
||||
"refusing to publish a database that would drop other producers" >&2
|
||||
exit 1; }
|
||||
bsdtar -tf "$WORK/$DB.db.tar.zst" >/dev/null 2>&1 \
|
||||
|| { echo "FATAL: fetched $DB.db.tar.zst is not a readable archive;" \
|
||||
"refusing to overwrite the live database" >&2
|
||||
exit 1; }
|
||||
log "$ARCH: fetched live database ($(bsdtar -tf "$WORK/$DB.db.tar.zst" | grep -c '/desc$') entries)"
|
||||
else
|
||||
log "$ARCH: archive has no $DB database yet — creating the first one"
|
||||
fi
|
||||
|
||||
# The files database is published too (pacman -F). repo-add maintains it
|
||||
# beside the .db automatically, but only merges into one that is already
|
||||
# there — without this fetch it would be rebuilt from just our packages and
|
||||
# every other producer would vanish from `pacman -F` while still installing
|
||||
# fine, which is the kind of half-broken that goes unnoticed for months.
|
||||
if [ -n "$(asset_id "$DB.files.tar.zst")" ]; then
|
||||
download_asset "$DB.files.tar.zst" "$WORK/$DB.files.tar.zst" \
|
||||
|| { echo "FATAL: $DB.files.tar.zst is published but could not be fetched" >&2; exit 1; }
|
||||
bsdtar -tf "$WORK/$DB.files.tar.zst" >/dev/null 2>&1 \
|
||||
|| { echo "FATAL: fetched $DB.files.tar.zst is not a readable archive" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
# Snapshot the archive as it stands BEFORE we touch it. Everything in here
|
||||
# that we do not deliberately supersede must still be in the database we
|
||||
# upload; the check at the end of this loop enforces exactly that.
|
||||
: > "$WORK/before.tsv"
|
||||
if [ -f "$WORK/$DB.db.tar.zst" ]; then
|
||||
db_entries "$WORK/$DB.db.tar.zst" > "$WORK/before.tsv"
|
||||
fi
|
||||
|
||||
# The package NAMES we are publishing. Any entry in the live database under
|
||||
# one of these names is a previous build of OURS — those filenames are the
|
||||
# only assets this script is ever entitled to delete.
|
||||
: > "$WORK/ours.txt"
|
||||
: > "$WORK/newfiles.txt"
|
||||
for p in "${PKGS[@]}"; do
|
||||
pkg_name_of "$p" >> "$WORK/ours.txt"
|
||||
basename "$p" >> "$WORK/newfiles.txt"
|
||||
done
|
||||
|
||||
SUPERSEDED=$(awk -F'\t' '
|
||||
NR==FNR && FILENAME==ARGV[1] { ours[$0]=1; next }
|
||||
FILENAME==ARGV[2] { newf[$0]=1; next }
|
||||
($1 in ours) && !($2 in newf) { print $2 }
|
||||
' "$WORK/ours.txt" "$WORK/newfiles.txt" "$WORK/before.tsv")
|
||||
|
||||
cp "${PKGS[@]}" "$WORK/"
|
||||
for p in "${PKGS[@]}"; do
|
||||
if [ -f "$p.sig" ]; then cp "$p.sig" "$WORK/"; fi
|
||||
done
|
||||
|
||||
shopt -s nullglob
|
||||
STAGED=("$WORK"/*.pkg.tar.zst)
|
||||
shopt -u nullglob
|
||||
repo-add --include-sigs --sign --key "$ARCHIVE_KEY" \
|
||||
"$WORK/$DB.db.tar.zst" "${STAGED[@]}"
|
||||
|
||||
# Before anything is uploaded: the merged database must still carry every
|
||||
# entry the archive had, except the ones we deliberately replaced. A failure
|
||||
# here means repo-add did not merge the way this script assumes, and the
|
||||
# upload is abandoned with the live archive untouched.
|
||||
db_entries "$WORK/$DB.db.tar.zst" > "$WORK/after.tsv"
|
||||
MISSING=$(awk -F'\t' '
|
||||
NR==FNR && FILENAME==ARGV[1] { ours[$0]=1; next }
|
||||
FILENAME==ARGV[2] { kept[$1]=1; next }
|
||||
!($1 in ours) && !($1 in kept) { print $1 }
|
||||
' "$WORK/ours.txt" "$WORK/after.tsv" "$WORK/before.tsv")
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "FATAL: merging into $DB dropped entries this producer does not own:" >&2
|
||||
printf ' %s\n' $MISSING >&2
|
||||
echo "nothing was uploaded; the live archive is unchanged" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "$ARCH: merged — $(wc -l < "$WORK/before.tsv") entries in, $(wc -l < "$WORK/after.tsv") out"
|
||||
|
||||
# repo-add leaves .db and .db.sig as symlinks; release assets cannot carry
|
||||
# those, so publish real files.
|
||||
rm -f "$WORK/$DB.db" "$WORK/$DB.db.sig"
|
||||
cp "$WORK/$DB.db.tar.zst" "$WORK/$DB.db"
|
||||
cp "$WORK/$DB.db.tar.zst.sig" "$WORK/$DB.db.sig"
|
||||
|
||||
# Producer-scoped checksums: a whole-archive manifest would go stale the
|
||||
# moment another producer published, and claim coverage it does not have.
|
||||
( cd "$WORK" && sha256sum ./*.pkg.tar.zst > "$PRODUCER-$ARCH.sha256" )
|
||||
|
||||
# Packages first, database last. A reader that races us then sees either the
|
||||
# old database (pointing at packages that are all still present) or the new
|
||||
# one (pointing at packages already uploaded) — never a database naming a
|
||||
# package that has not landed yet.
|
||||
for f in "${STAGED[@]}"; do
|
||||
upload_asset "$f"
|
||||
if [ -f "$f.sig" ]; then upload_asset "$f.sig"; fi
|
||||
done
|
||||
upload_asset "$WORK/$PRODUCER-$ARCH.sha256"
|
||||
if [ -f "$WORK/$DB.files.tar.zst" ]; then
|
||||
upload_asset "$WORK/$DB.files.tar.zst"
|
||||
if [ -f "$WORK/$DB.files.tar.zst.sig" ]; then
|
||||
upload_asset "$WORK/$DB.files.tar.zst.sig"
|
||||
fi
|
||||
fi
|
||||
upload_asset "$WORK/$DB.db.tar.zst"
|
||||
upload_asset "$WORK/$DB.db.tar.zst.sig"
|
||||
upload_asset "$WORK/$DB.db"
|
||||
upload_asset "$WORK/$DB.db.sig"
|
||||
|
||||
# Read the database back over the SAME URL pacman uses and confirm the archive
|
||||
# really holds what we just merged. Publishing is two dozen HTTP calls against
|
||||
# a box on the far side of a LAN; "the upload returned 201" is not the same
|
||||
# claim as "the phone will see this".
|
||||
if download_asset "$DB.db.tar.zst" "$WORK/live.db.tar.zst"; then
|
||||
db_entries "$WORK/live.db.tar.zst" | sort > "$WORK/live.tsv"
|
||||
sort "$WORK/after.tsv" > "$WORK/expect.tsv"
|
||||
if ! diff -q "$WORK/expect.tsv" "$WORK/live.tsv" >/dev/null; then
|
||||
echo "FATAL: the published $DB database does not match what was uploaded:" >&2
|
||||
diff "$WORK/expect.tsv" "$WORK/live.tsv" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
log "$ARCH: verified live database matches ($(wc -l < "$WORK/live.tsv") entries)"
|
||||
else
|
||||
echo "FATAL: could not read back $DB.db.tar.zst after publishing it" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Now — and only now, with the new database live and verified — drop our own
|
||||
# superseded builds. Doing it earlier would leave a window where the published
|
||||
# database referenced a package that no longer existed.
|
||||
if [ -n "$SUPERSEDED" ]; then
|
||||
while IFS= read -r old; do
|
||||
[ -n "$old" ] || continue
|
||||
delete_asset "$old"
|
||||
delete_asset "$old.sig"
|
||||
done <<< "$SUPERSEDED"
|
||||
fi
|
||||
|
||||
# Legacy whole-archive manifest from the delete-and-recreate era. Harmless but
|
||||
# misleading — it claims to cover packages this producer never built.
|
||||
delete_asset "$DB-repo.sha256"
|
||||
done
|
||||
|
||||
for f in "${EXTRA_ASSETS[@]+"${EXTRA_ASSETS[@]}"}"; do
|
||||
[ -f "$f" ] || { log "extra asset $f missing, skipping"; continue; }
|
||||
upload_asset "$f"
|
||||
done
|
||||
|
||||
# --- manifest line in the release body ---------------------------------------
|
||||
# The body is the only place that records WHO put what in the archive. With the
|
||||
# tag pinned at creation it is also the only place a reader can see that the
|
||||
# archive moved at all.
|
||||
BODY=$(printf '%s' "$REL_JSON" | python3 -c "
|
||||
import json,sys
|
||||
try: print(json.load(sys.stdin).get('body',''))
|
||||
except Exception: print('')")
|
||||
|
||||
NEW_BODY=$(python3 - "$BODY" "$PRODUCER" "$PRODUCER_VERSION" <<'PY'
|
||||
import sys
|
||||
body, producer, version = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
line = f"- {producer}: {version}"
|
||||
kept = [l for l in body.splitlines() if not l.startswith(f"- {producer}: ")]
|
||||
while kept and not kept[-1].strip():
|
||||
kept.pop()
|
||||
print("\n".join(kept + [line]))
|
||||
PY
|
||||
)
|
||||
|
||||
curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
||||
"$API/repos/$ARCHIVE_REPO/releases/$REL_ID" \
|
||||
-d "$(python3 -c "import json,sys; print(json.dumps({'body': sys.argv[1]}))" "$NEW_BODY")" \
|
||||
-o /dev/null
|
||||
|
||||
log "$PRODUCER $PRODUCER_VERSION published into edge additively"
|
||||
Loading…
Reference in a new issue