#!/bin/bash # Move packages forward through the channel pipeline: edge -> rc -> stable. # # Copies package artifacts AND their detached signatures from one channel to # the next, driven by the source channel's database (not the raw directory, so # historical files never leak forward), then cleans, rebuilds, and syncs the # destination. Published filenames are never overwritten: a same-name file # that already exists at the destination is skipped (and reported when its # bytes differ), because the R2 cache cannot recover from a rewrite. set -e BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh" source "$BUILD_ROOT/helpers/package-metadata.sh" source "$BUILD_ROOT/helpers/basecamp-notifier.sh" FROM="" TO="" DRY_RUN=false SYNC_REMOTE="" SKIP_PROD_CHECK=false FAST_RING_ONLY=false BOOTSTRAP=false PACKAGES="" usage() { echo "Usage: $0 --from --to [OPTIONS]" echo "" echo "Directions:" echo " --from edge --to rc Open a release train: carry edge forward into rc" echo " --from rc --to stable Ship: promote the tested rc channel to stable" echo " --from stable --to rc Only with --fast-ring (parity replication)" echo " or --bootstrap (one-time initial seed)" echo "" echo "Options:" echo " --arch Target architecture (x86_64 or aarch64, default: x86_64)" echo " --package Advance only the named package(s)" echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)" echo " --bootstrap One-time stable -> rc seed before forward-only enforcement" echo " --dry-run Preview without changing files" echo " --sync-remote Rclone remote for sync (default: pkgs.omarchy.org:omarchy-pkgs)" echo " --skip-prod-check Skip production confirmation during sync" echo " -h, --help Show this help message" echo "" echo "What it does:" echo " 1) Read the source channel database for the current package set" echo " 2) Copy eligible packages + .sig files not yet at the destination" echo " 3) Clean the destination (keep 2 versions)" echo " 4) Rebuild the destination database" echo " 5) Sync the destination to the remote (packages first, database last)" exit "${1:-0}" } while [[ $# -gt 0 ]]; do case $1 in --from) FROM="$2" shift 2 ;; --to) TO="$2" shift 2 ;; --arch) ARCH="$2" update_arch_paths shift 2 ;; --package) shift while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do PACKAGES="$PACKAGES $1" shift done PACKAGES="${PACKAGES# }" if [[ -z "$PACKAGES" ]]; then print_error "--package requires at least one package name" exit 1 fi ;; --fast-ring) FAST_RING_ONLY=true shift ;; --bootstrap) BOOTSTRAP=true shift ;; --dry-run) DRY_RUN=true shift ;; --sync-remote) SYNC_REMOTE="$2" shift 2 ;; --skip-prod-check) SKIP_PROD_CHECK=true shift ;; -h | --help) usage 0 ;; *) print_error "Unknown option: $1" usage 1 ;; esac done require_valid_mirror "$FROM" require_valid_mirror "$TO" # The pipeline only moves forward. stable -> rc is allowed for exactly two # labeled purposes: fast-ring parity replication and the one-time bootstrap. # edge -> stable is the legacy migrate path, kept for the transition cycle. case "$FROM->$TO" in "edge->rc" | "rc->stable") ;; "edge->stable") print_warning "edge -> stable is the legacy migrate path; new releases flow edge -> rc -> stable" ;; "stable->rc") if [[ "$FAST_RING_ONLY" != true && "$BOOTSTRAP" != true ]]; then print_error "stable -> rc requires --fast-ring (parity replication) or --bootstrap (initial seed)" exit 1 fi ;; *) print_error "Refusing $FROM -> $TO: packages only move forward (edge -> rc -> stable)" exit 1 ;; esac SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH" TARGET_DIR="$REPO_ROOT/$TO/$ARCH" SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst" print_header "Advance Channel: $FROM -> $TO" print_info "Architecture: $ARCH" print_info "Source: $SOURCE_DIR" print_info "Target: $TARGET_DIR" [[ "$FAST_RING_ONLY" == true ]] && print_info "Scope: fast-ring packages only" [[ "$BOOTSTRAP" == true ]] && print_info "Mode: bootstrap (initial rc seed)" [[ -n "$PACKAGES" ]] && print_info "Packages: $PACKAGES" if [[ ! -f "$SOURCE_DB" ]]; then print_error "Source database not found: $SOURCE_DB" echo "Nothing has been published to the $FROM channel on this host." exit 1 fi if [[ "$DRY_RUN" == true ]]; then print_warning "DRY RUN MODE - No changes will be made" else acquire_release_lock || exit 1 fi # Manifest: "namebasefilename" per current package in the source db. # Each desc record begins with %FILENAME%, so that marker both closes the # previous record and opens the next. read_manifest() { tar -xOf "$SOURCE_DB" --wildcards '*/desc' 2>/dev/null | awk ' function emit() { if (name != "" && filename != "") printf "%s\t%s\t%s\n", name, base, filename name = ""; base = ""; filename = "" } $0 == "%FILENAME%" { emit(); getline; filename = $0; next } $0 == "%NAME%" { getline; name = $0; next } $0 == "%BASE%" { getline; base = $0; next } END { emit() } ' } package_wanted() { local name="$1" base="$2" [[ -z "$PACKAGES" ]] && return 0 local want for want in $PACKAGES; do [[ "$want" == "$name" || "$want" == "$base" ]] && return 0 done return 1 } # Split packages publish under their pkgname; eligibility metadata lives in # the pkgbase directory. Packages whose PKGBUILD has since been removed from # this repo default to moving: they are part of the source channel's set and # leaving them behind would hole the destination. package_eligible() { local name="$1" base="$2" local pkgdir pkgdir=$(package_dir_for_name "$name" 2>/dev/null) || pkgdir=$(package_dir_for_name "$base" 2>/dev/null) || pkgdir="" if [[ -z "$pkgdir" ]]; then [[ "$FAST_RING_ONLY" == true ]] && return 1 return 0 fi if [[ "$FAST_RING_ONLY" == true ]]; then package_is_fast_ring "$pkgdir" || return 1 elif [[ "$FROM" == "edge" && "$TO" == "rc" ]]; then # Fast-ring packages reach rc by replication of the STABLE build (same # bytes stable users get). Carrying the independently built edge artifact # forward would race it under the same filename. package_is_fast_ring "$pkgdir" && return 1 fi # The bootstrap seeds an empty rc from stable, so parity is the whole point: # membership in the destination is enough. Nothing is built in rc yet, so # there is no native artifact to race — and the release pair MUST come along # or rc cannot serve omarchy/omarchy-settings until the first RC is cut. if [[ "$BOOTSTRAP" == true ]]; then package_in_channel "$pkgdir" "$TO" return fi package_moves_to_channel "$pkgdir" "$TO" } mkdir -p "$TARGET_DIR" COPIED=0 COPIED_FILES="" PRESENT=0 SKIPPED=0 MISSING_FILES=() MISSING_SIGS=() DIFFERING=() while IFS=$'\t' read -r name base filename; do package_wanted "$name" "$base" || continue if ! package_eligible "$name" "$base"; then SKIPPED=$((SKIPPED + 1)) continue fi src="$SOURCE_DIR/$filename" sig="$src.sig" dest="$TARGET_DIR/$filename" if [[ ! -f "$src" ]]; then MISSING_FILES+=("$filename") continue fi if [[ ! -f "$sig" ]]; then MISSING_SIGS+=("$filename") continue fi if [[ -f "$dest" ]]; then if cmp -s "$src" "$dest"; then # A crash between the package and signature copies leaves an unsigned # package behind; the bytes are identical, so the source signature is # valid for it. Package + signature resume as one unit. if [[ ! -f "$dest.sig" ]]; then if [[ "$DRY_RUN" == true ]]; then echo " would restore missing signature: $filename.sig" else cp -p "$sig" "$dest.sig" echo " restored missing signature: $filename.sig" fi fi PRESENT=$((PRESENT + 1)) else DIFFERING+=("$filename") fi continue fi if [[ "$DRY_RUN" == true ]]; then echo " would copy: $filename (+ .sig)" else cp -p "$src" "$dest" cp -p "$sig" "$dest.sig" echo " copied: $filename (+ .sig)" fi COPIED=$((COPIED + 1)) COPIED_FILES+="$filename"$'\n' done < <(read_manifest) echo "" print_info "Copied: $COPIED | Already present: $PRESENT | Not eligible: $SKIPPED" if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then print_error "${#MISSING_FILES[@]} package(s) named in the $FROM database are missing on disk:" printf ' %s\n' "${MISSING_FILES[@]}" echo "The $FROM channel is inconsistent; rebuild its database before advancing." exit 1 fi if [[ ${#MISSING_SIGS[@]} -gt 0 ]]; then print_error "${#MISSING_SIGS[@]} package(s) have no detached signature in $FROM:" printf ' %s\n' "${MISSING_SIGS[@]}" echo "Signatures must travel with their packages. Backfill them by copying the" echo "files into build-output/$FROM/$ARCH, running bin/repo sign --mirror $FROM," echo "and moving the .sig files back beside the packages — then re-run." exit 1 fi if [[ ${#DIFFERING[@]} -gt 0 ]]; then print_error "${#DIFFERING[@]} file(s) already published in $TO with DIFFERENT bytes:" printf ' %s\n' "${DIFFERING[@]}" echo "Published filenames are never rewritten, and two artifacts fighting over" echo "one name means something built twice from different inputs. Resolve it" echo "deliberately: bump pkgrel and rebuild so the new artifact gets a new" echo "filename, or remove the source copy if the destination is correct." exit 1 fi if [[ "$DRY_RUN" == true ]]; then print_info "Dry run: skipping clean, database update, and sync" exit 0 fi print_info "Cleaning $TO repository..." "$BUILD_ROOT/bin/clean-repo" --mirror "$TO" --arch "$ARCH" print_info "Updating $TO repository database..." "$BUILD_ROOT/bin/update-repo" --mirror "$TO" --arch "$ARCH" echo "" print_info "Syncing $TO repository to remote..." SYNC_ARGS=("--mirror" "$TO" "--arch" "$ARCH") [[ -n "$SYNC_REMOTE" ]] && SYNC_ARGS+=("--remote" "$SYNC_REMOTE") [[ "$SKIP_PROD_CHECK" == true ]] && SYNC_ARGS+=("--skip-prod-check") "$BUILD_ROOT/bin/sync-repo" "${SYNC_ARGS[@]}" || { print_error "Sync failed" exit 1 } print_success "Advance complete: $FROM -> $TO" # A promotion is how something reaches users, so say what moved and where. if ((COPIED > 0)); then moved="" shown=0 while IFS= read -r moved_file; do [[ -z "$moved_file" ]] && continue ((shown >= 25)) && break moved+="
• $(package_file_label "$moved_file" | basecamp_html_escape)" shown=$((shown + 1)) done <<<"$COPIED_FILES" ((COPIED > shown)) && moved+="
• …and $((COPIED - shown)) more" label="Promoted $FROM → $TO" [[ "$BOOTSTRAP" == true ]] && label="Bootstrapped the $TO channel from $FROM" [[ "$FAST_RING_ONLY" == true ]] && label="Replicated fast-ring $FROM → $TO" notify_info "$label" \ "Arch: $ARCH · $(hostname -s 2>/dev/null || echo host)
$COPIED package(s) moved:$moved

Live at https://pkgs.omarchy.org/$TO/$ARCH/" fi