#!/usr/bin/env bash # /srv/repo/bin/publish.sh — forced-command script for the ciupload SSH key. # # Receives a gzipped tar of signed .deb / .rpm artifacts on stdin from the # packages.yml CI workflow. Validates signatures, integrates each artifact # into the matching repo tree, regenerates + re-signs the index metadata. # # Runs as user `ciupload` (locked-down: nologin shell, no sudo, no groups). # The only mutation it can make is writing into the staging dir under # /srv/repo/incoming and whatever paths aptly/createrepo_c rewrite — and # even those are gated on signature verification before any state change. # # Failure modes (all leave the public repo state unchanged): # - tar extraction fails → exit 1 # - any .deb missing its .asc → exit 1 # - any signature verification fails → exit 1 # - aptly/createrepo_c failure → exit 1; partial state possible. Acceptable # because publish.sh is idempotent: re-running with the same tar overwrites. set -euo pipefail umask 022 GPG_KEY='release@interposed.ai' GPG_PASSPHRASE_FILE="$HOME/.gpg-passphrase" APT_REPO_NAME='interposed-stable' APT_DISTRIBUTION='stable' DNF_DIR='/srv/repo/dnf' LOG_FILE='/var/log/repo-publishes.log' log() { local ts ts="$(date -u +%FT%TZ)" printf '[%s] %s\n' "$ts" "$*" | tee -a "$LOG_FILE" >&2 } die() { log "FATAL: $*" exit 1 } # 1. Receive the tar bundle on stdin TMP="$(mktemp -d -p /srv/repo/incoming staging.XXXXXX)" trap 'rm -rf "$TMP"' EXIT log "publish.sh starting; tmp=$TMP" if ! tar -xzf - -C "$TMP"; then die "tar extraction failed (bundle malformed or empty)" fi shopt -s nullglob DEBS=("$TMP"/*.deb) RPMS=("$TMP"/*.rpm) shopt -u nullglob if [ ${#DEBS[@]} -eq 0 ] && [ ${#RPMS[@]} -eq 0 ]; then die "no .deb or .rpm files in the bundle" fi log "received ${#DEBS[@]} .deb and ${#RPMS[@]} .rpm files" # 2. Validate signatures BEFORE touching any repo state for deb in "${DEBS[@]}"; do [ -f "${deb}.asc" ] || die "missing detached signature for $(basename "$deb")" if ! gpg --batch --verify "${deb}.asc" "$deb" 2>&1 | grep -q "Good signature"; then die "signature verification failed for $(basename "$deb")" fi done for rpm in "${RPMS[@]}"; do if ! rpm --checksig "$rpm" 2>&1 | grep -qiE "pgp.*ok|rsa.*ok|signatures ok|digests signatures ok"; then die "rpm signature check failed for $(basename "$rpm")" fi done log "all signatures verified" # 3. Integrate the .deb files into the apt repo via aptly if [ ${#DEBS[@]} -gt 0 ]; then for deb in "${DEBS[@]}"; do aptly repo add -force-replace "$APT_REPO_NAME" "$deb" >&2 done aptly publish update \ -batch \ -passphrase-file="$GPG_PASSPHRASE_FILE" \ -gpg-key="$GPG_KEY" \ "$APT_DISTRIBUTION" >&2 log "apt repo updated with ${#DEBS[@]} package(s)" fi # 4. Integrate the .rpm files into the dnf tree, regenerate + sign metadata if [ ${#RPMS[@]} -gt 0 ]; then mkdir -p "$DNF_DIR/packages" for rpm in "${RPMS[@]}"; do cp -f "$rpm" "$DNF_DIR/packages/" done createrepo_c --update "$DNF_DIR" >&2 # Sign repomd.xml detached so dnf clients with repo_gpgcheck=1 verify it gpg --batch --yes --pinentry-mode loopback \ --passphrase-file "$GPG_PASSPHRASE_FILE" \ --detach-sign --armor --local-user "$GPG_KEY" \ --output "$DNF_DIR/repodata/repomd.xml.asc" \ "$DNF_DIR/repodata/repomd.xml" log "dnf repo updated with ${#RPMS[@]} package(s)" fi log "publish.sh complete: ${#DEBS[@]} deb + ${#RPMS[@]} rpm" exit 0