Only failures were reported, so a push could reach the mirror with no way to know short of querying the database by hand. Release runs now report: - start: channel, arch, host, and the commit being built - published: the packages and versions that went out, duration, channel URL - no-changes: the run found nothing to build - promoted: what advance moved between channels, including the rc bootstrap and fast-ring replication - failed: unchanged, plus the commit context the other reports carry Release traffic goes to OMARCHY_RELEASE_CHATBOT_URL, falling back to BASECAMP_CHATBOT_URL, so build reports stop drowning the repository chat the sync workflows post to. bin/setup reports which destination is configured. The published list is captured after the build step because promote moves the files out of build-output, and is capped at 25 entries so a full rebuild does not produce an unreadable wall of chat.
356 lines
11 KiB
Bash
Executable File
356 lines
11 KiB
Bash
Executable File
#!/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 <channel> --to <channel> [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 <arch> Target architecture (x86_64 or aarch64, default: x86_64)"
|
|
echo " --package <names...> 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 <path> 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: "name<TAB>base<TAB>filename" 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+="<br>• $(package_file_label "$moved_file" | basecamp_html_escape)"
|
|
shown=$((shown + 1))
|
|
done <<<"$COPIED_FILES"
|
|
((COPIED > shown)) && moved+="<br>• …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)<br><strong>$COPIED package(s) moved:</strong>$moved<br><br>Live at https://pkgs.omarchy.org/$TO/$ARCH/"
|
|
fi
|