#!/usr/bin/env bash set -euo pipefail # -------- configurable defaults -------- REPO_DIR="${REPO_DIR:-/home/hubdeb.qortal.org/public_html}" DEB_NAME="${DEB_NAME:-Qortal-Hub-Setup.deb}" DEB_SUBDIR="${DEB_SUBDIR:-deb-packages}" DEB_URL="${DEB_URL:-https://github.com/Qortal/Qortal-Hub/releases/latest/download/Qortal-Hub-Setup.deb}" # Use your primary key fingerprint (recommended) or specific signing subkey id. # From your output: primary fpr = 20C64216BB5C080569F0F6BA2B4015FB935F5F2A SIGNING_KEY="${SIGNING_KEY:-20C64216BB5C080569F0F6BA2B4015FB935F5F2A}" # Optionally override auto owner/group after publish. If empty, owner+group are inferred # from the directory above REPO_DIR and used as owner:group. CHOWN_TO="${CHOWN_TO:-}" # -------- helpers -------- die() { echo "ERROR: $*" >&2; exit 1; } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } require_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then die "This script must be run as root. Re-run with sudo." fi } sha256_file() { sha256sum "$1" | awk '{print $1}' } # -------- usage -------- usage() { cat <&2 for p in "$DEB_SUBDIR" Packages Packages.gz Release Release.gpg InRelease; do rm -rf "$REPO_DIR/$p" if [[ -e "$BACKUP_DIR/$p" ]]; then mv "$BACKUP_DIR/$p" "$REPO_DIR/$p" fi done fi } cleanup() { rollback_publish rm -rf "${TMP_DEB_DIR:-}" "${STAGE_DIR:-}" } trap cleanup EXIT # Validate repo dirs [[ -d "$REPO_DIR" ]] || die "REPO_DIR not found: $REPO_DIR" [[ -d "$REPO_DIR/$DEB_SUBDIR" ]] || die "Repo deb dir not found: $REPO_DIR/$DEB_SUBDIR" # Determine ownership target from the directory above REPO_DIR unless overridden. if [[ -z "$CHOWN_TO" ]]; then REPO_PARENT="$(dirname "$REPO_DIR")" [[ -d "$REPO_PARENT" ]] || die "Parent directory not found: $REPO_PARENT" OWNER_NAME="$(stat -c '%U' "$REPO_PARENT")" GROUP_NAME="$(stat -c '%G' "$REPO_PARENT")" [[ -n "$OWNER_NAME" && "$OWNER_NAME" != "UNKNOWN" ]] || die "Could not determine owner for: $REPO_PARENT" [[ -n "$GROUP_NAME" && "$GROUP_NAME" != "UNKNOWN" ]] || die "Could not determine group for: $REPO_PARENT" CHOWN_TO="${OWNER_NAME}:${GROUP_NAME}" fi # Ensure signing key exists (public + secret) gpg --list-secret-keys --keyid-format LONG "$SIGNING_KEY" >/dev/null 2>&1 \ || die "Signing secret key not available in this user's keyring: $SIGNING_KEY" # Resolve source deb: # - If arg provided, use it. # - Otherwise download latest release artifact into temp file. if [[ -n "$SRC_DEB" ]]; then [[ -f "$SRC_DEB" ]] || die "Deb not found: $SRC_DEB" [[ "$(basename "$SRC_DEB")" == "$DEB_NAME" ]] || die "Deb must be named exactly: $DEB_NAME (got: $(basename "$SRC_DEB"))" else TMP_DEB_DIR="$(mktemp -d)" SRC_DEB="$TMP_DEB_DIR/$DEB_NAME" echo "Downloading latest $DEB_NAME from:" echo " $DEB_URL" curl -fL --retry 3 --connect-timeout 20 -o "$SRC_DEB" "$DEB_URL" fi # Validate deb integrity dpkg-deb --info "$SRC_DEB" >/dev/null cd "$REPO_DIR" # Create a staging directory on the same filesystem for atomic swaps STAGE_DIR="$(mktemp -d "$REPO_DIR/.stage.XXXXXX")" # Copy current repo metadata into stage (not strictly required, but helps for atomic swap approach) # We'll write new metadata into STAGE_DIR then move into place. mkdir -p "$STAGE_DIR/$DEB_SUBDIR" # Copy all .deb files from current repo into stage, then overwrite with new one # This keeps any other debs you may have, while replacing Qortal-Hub-Setup.deb. rsync -a --delete "$DEB_SUBDIR/" "$STAGE_DIR/$DEB_SUBDIR/" # Compute old hash if existing, for logging OLD_HASH="" if [[ -f "$DEB_SUBDIR/$DEB_NAME" ]]; then OLD_HASH="$(sha256_file "$DEB_SUBDIR/$DEB_NAME" || true)" fi # Put new .deb into stage install -m 0644 "$SRC_DEB" "$STAGE_DIR/$DEB_SUBDIR/$DEB_NAME" NEW_HASH="$(sha256_file "$STAGE_DIR/$DEB_SUBDIR/$DEB_NAME")" # If identical content, you can optionally no-op. if [[ -n "$OLD_HASH" && "$OLD_HASH" == "$NEW_HASH" ]]; then echo "No changes: $DEB_NAME is identical (sha256: $NEW_HASH). Still rebuilding metadata to be safe..." fi # Build Packages + Packages.gz inside stage. # Run from STAGE_DIR so package filenames stay repo-relative (deb-packages/...). ( cd "$STAGE_DIR" dpkg-scanpackages -m "$DEB_SUBDIR" /dev/null > Packages gzip -9c Packages > Packages.gz ) # Build Release inside stage ( cd "$STAGE_DIR" apt-ftparchive release . > Release # Sign Release.gpg + InRelease gpg --batch --yes --default-key "$SIGNING_KEY" --armor --detach-sign --output Release.gpg Release gpg --batch --yes --default-key "$SIGNING_KEY" --clearsign --output InRelease Release ) # Atomic publish: # 1) Move current metadata+deb dir aside (fast) # 2) Move staged into place (fast) # 3) Remove old backup BACKUP_DIR="$REPO_DIR/.backup.$(date -u +%Y%m%dT%H%M%SZ)" mkdir -p "$BACKUP_DIR" PUBLISH_STARTED=1 # Move current repo files that we manage into backup for p in "$DEB_SUBDIR" Packages Packages.gz Release Release.gpg InRelease; do if [[ -e "$REPO_DIR/$p" ]]; then mv "$REPO_DIR/$p" "$BACKUP_DIR/" fi done # Move stage into place mv "$STAGE_DIR/$DEB_SUBDIR" "$REPO_DIR/$DEB_SUBDIR" mv "$STAGE_DIR/Packages" "$REPO_DIR/Packages" mv "$STAGE_DIR/Packages.gz" "$REPO_DIR/Packages.gz" mv "$STAGE_DIR/Release" "$REPO_DIR/Release" mv "$STAGE_DIR/Release.gpg" "$REPO_DIR/Release.gpg" mv "$STAGE_DIR/InRelease" "$REPO_DIR/InRelease" PUBLISH_COMPLETED=1 # If you want to keep the backup, comment the next line rm -rf "$BACKUP_DIR" BACKUP_DIR="" # Ownership fixup on final repo path chown -R "$CHOWN_TO" "$REPO_DIR" echo "Published $DEB_NAME" echo " sha256: $NEW_HASH" echo " repo: $REPO_DIR" echo " owner: $CHOWN_TO" echo " signed: $(date -u)"