pkgs.omarchy.org sits behind Cloudflare, and right after a sync the plain db URL keeps serving the previous file from the edge cache (observed: a cache HIT with age 900s+ returning the pre-release db). That made status report the old version, wait_for_published poll a frozen file for its full timeout, and would let RC auto-numbering count from last release's versions. A unique query string per fetch skips the cached entry.
1077 lines
41 KiB
Bash
Executable File
1077 lines
41 KiB
Bash
Executable File
#!/bin/bash
|
|
# omarchy-release — the front door for cutting Omarchy releases.
|
|
#
|
|
# A release train has three human moments, each one command:
|
|
# start open the train: release branch + staging PR (+ advance edge -> rc)
|
|
# rc cut a candidate: pin PKGBUILDs to the branch head, publish to rc
|
|
# ship make it official: final pins, promote rc -> stable, tag, GH release,
|
|
# ISO, website
|
|
#
|
|
# Run bare `omarchy-release` to be shepherded: it observes reality (branches,
|
|
# pins, published channels, tags) and offers the correct next step. Every
|
|
# subcommand is idempotent — a re-run checks what is already done and skips it.
|
|
#
|
|
# Versions are inferred from branch names: branch v4-0-2 -> RCs 4.0.2rcN ->
|
|
# tag v4.0.2. `start` is the only place a version is typed.
|
|
|
|
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/host-helpers.sh"
|
|
|
|
UPSTREAM_URL="${OMARCHY_UPSTREAM_URL:-https://github.com/basecamp/omarchy.git}"
|
|
UPSTREAM_REPO="${OMARCHY_UPSTREAM_REPO:-basecamp/omarchy}"
|
|
SITE_REPO="${OMARCHY_SITE_REPO:-omacom-io/omarchy-site}"
|
|
ISO_REPO="${OMARCHY_ISO_REPO:-omacom-io/omarchy-iso}"
|
|
DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}"
|
|
|
|
PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}"
|
|
RC_DB_URL="$PKGS_DB_BASE/rc/x86_64/omarchy.db.tar.zst"
|
|
|
|
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
|
MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs)
|
|
WORK_CLONE="$SRCDEST_DIR/omarchy-work" # working clone for pick/cherry-pick
|
|
RC_WORKTREE="$BUILD_ROOT/.worktrees/rc" # pkgs repo rc branch worktree
|
|
|
|
ASSUME_YES=false
|
|
REPO_HOST_OVERRIDE=""
|
|
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: $0 [command] [options]
|
|
|
|
Run with no command to be shepherded through whatever the next step is.
|
|
|
|
Commands:
|
|
start [X.Y.Z] Open a release train: create branch v X-Y-Z on $UPSTREAM_REPO
|
|
(from the previous tag for a patch, from $DEV_BRANCH for a
|
|
minor/major), open the release-notes staging PR, and for a
|
|
minor/major advance edge -> rc on the build host
|
|
pick [pr#|sha ...] Cherry-pick merged PRs onto the release branch; with no
|
|
args, choose from a list of merged $DEV_BRANCH PRs
|
|
rc Cut the next X.Y.ZrcN into the rc channel
|
|
ship Final pins, promote rc -> stable, tag, GitHub release,
|
|
ISO (prompted), website bump
|
|
status Show where the train stands (read-only)
|
|
doctor Verify every credential and connection the flow needs
|
|
self-test Run helper unit tests
|
|
|
|
Options:
|
|
--yes Skip confirmation prompts (for scripting/CI)
|
|
--host <host> Build host ssh destination (else \$OMARCHY_REPO_HOST or .repo-host)
|
|
--iso / --no-iso (rc, ship) Build the ISO without asking / skip it
|
|
--no-wait (rc, ship) Do not poll for the published build
|
|
-h, --help Show this help
|
|
EOF
|
|
}
|
|
|
|
confirm() {
|
|
local prompt="$1" reply
|
|
[[ "$ASSUME_YES" == true ]] && return 0
|
|
read -r -p "$prompt [y/N] " reply
|
|
[[ "$reply" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
# --- version <-> branch ------------------------------------------------------
|
|
|
|
version_to_branch() { # 4.0.2 -> v4-0-2
|
|
local v="${1#v}"
|
|
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
|
|
echo "v${v//./-}"
|
|
}
|
|
|
|
branch_to_version() { # v4-0-2 -> 4.0.2
|
|
local b="$1"
|
|
[[ "$b" =~ ^v([0-9]+)-([0-9]+)-([0-9]+)$ ]] || return 1
|
|
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
|
|
}
|
|
|
|
version_is_patch() { # Z > 0
|
|
[[ "$1" =~ ^[0-9]+\.[0-9]+\.([0-9]+)$ ]] && ((BASH_REMATCH[1] > 0))
|
|
}
|
|
|
|
previous_patch_tag() { # 4.0.2 -> v4.0.1
|
|
[[ "$1" =~ ^([0-9]+\.[0-9]+)\.([0-9]+)$ ]] || return 1
|
|
echo "v${BASH_REMATCH[1]}.$((BASH_REMATCH[2] - 1))"
|
|
}
|
|
|
|
# --- upstream queries --------------------------------------------------------
|
|
|
|
ls_remote() { git ls-remote "$UPSTREAM_URL" "$@"; }
|
|
|
|
tag_exists() { [[ -n "$(ls_remote "refs/tags/$1" | head -1)" ]]; }
|
|
|
|
branch_head() { ls_remote "refs/heads/$1" | awk '{print $1}'; }
|
|
|
|
resolve_tag_commit() {
|
|
local tag="$1" peeled sha
|
|
peeled=$(ls_remote "refs/tags/$tag^{}" | awk '{print $1}')
|
|
sha=$(ls_remote "refs/tags/$tag" | awk '{print $1}')
|
|
echo "${peeled:-$sha}"
|
|
}
|
|
|
|
# Newest v*-*-* release branch, optionally only untagged ones. Shipping tags
|
|
# the version, so "tag exists" is what closes a train — but ship itself also
|
|
# needs to find a tagged train whose remaining steps (release, ISO, website)
|
|
# didn't finish, so it can resume.
|
|
newest_release_branch() { # newest_release_branch [--untagged]
|
|
local untagged_only=false
|
|
[[ "${1:-}" == "--untagged" ]] && untagged_only=true
|
|
local branch best_ver="" best_branch="" ver
|
|
while IFS= read -r branch; do
|
|
ver=$(branch_to_version "$branch") || continue
|
|
if [[ "$untagged_only" == true ]] && tag_exists "v$ver"; then
|
|
continue
|
|
fi
|
|
if [[ -z "$best_ver" ]] || [[ $(vercmp "$ver" "$best_ver") -gt 0 ]]; then
|
|
best_ver="$ver" best_branch="$branch"
|
|
fi
|
|
done < <(ls_remote 'refs/heads/v*' | awk -F/ '{print $3}')
|
|
[[ -n "$best_branch" ]] && echo "$best_branch"
|
|
}
|
|
|
|
open_train_branch() { newest_release_branch --untagged; }
|
|
|
|
# Reads go over anonymous HTTPS; pushes go over SSH like every checkout the
|
|
# operator owns. Pushing over HTTPS would drag in git's credential-helper
|
|
# config, which breaks the moment a stale absolute gh path is baked into it.
|
|
ssh_push_url() { # https://github.com/a/b.git -> git@github.com:a/b.git
|
|
local url="$1"
|
|
if [[ "$url" =~ ^https://github\.com/(.+)$ ]]; then
|
|
echo "git@github.com:${BASH_REMATCH[1]}"
|
|
else
|
|
echo "$url"
|
|
fi
|
|
}
|
|
UPSTREAM_PUSH_URL="${OMARCHY_UPSTREAM_PUSH_URL:-$(ssh_push_url "$UPSTREAM_URL")}"
|
|
|
|
ensure_mirror_clone() {
|
|
if [[ -d "$MIRROR_CLONE" ]]; then
|
|
git -C "$MIRROR_CLONE" fetch --quiet origin
|
|
else
|
|
mkdir -p "$SRCDEST_DIR"
|
|
print_info "Cloning $UPSTREAM_URL (cached in $SRCDEST_DIR)..."
|
|
git clone --mirror --quiet "$UPSTREAM_URL" "$MIRROR_CLONE"
|
|
fi
|
|
git -C "$MIRROR_CLONE" remote set-url --push origin "$UPSTREAM_PUSH_URL"
|
|
}
|
|
|
|
ensure_work_clone() {
|
|
if [[ -d "$WORK_CLONE" ]]; then
|
|
git -C "$WORK_CLONE" fetch --quiet origin
|
|
else
|
|
mkdir -p "$SRCDEST_DIR"
|
|
print_info "Cloning $UPSTREAM_URL working copy..."
|
|
git clone --quiet "$UPSTREAM_URL" "$WORK_CLONE"
|
|
fi
|
|
git -C "$WORK_CLONE" remote set-url --push origin "$UPSTREAM_PUSH_URL"
|
|
}
|
|
|
|
# --- published channel state -------------------------------------------------
|
|
|
|
# Prints omarchy's published version in a channel; empty when absent, rc 2 when
|
|
# the database cannot be read (callers must not mistake an outage for absence).
|
|
published_version() {
|
|
local channel="$1" tmp descs
|
|
tmp=$(mktemp) || return 2
|
|
# A unique query string busts the CDN cache: right after a sync the plain
|
|
# URL can keep serving the previous db for a while, which reads as "not
|
|
# published yet" to status, the wait loop, and ship's pre-checks.
|
|
if ! curl -sf "$PKGS_DB_BASE/$channel/x86_64/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then
|
|
rm -f "$tmp"
|
|
return 2
|
|
fi
|
|
if ! descs=$(tar -xO --zstd -f "$tmp" --wildcards '*/desc' 2>/dev/null); then
|
|
rm -f "$tmp"
|
|
return 2
|
|
fi
|
|
rm -f "$tmp"
|
|
awk '
|
|
function emit() {
|
|
if (!found && name == "omarchy" && version != "") { print version; found = 1 }
|
|
name = ""; version = ""
|
|
}
|
|
$0 == "%FILENAME%" { emit(); next }
|
|
$0 == "%NAME%" { getline; name = $0; next }
|
|
$0 == "%VERSION%" { getline; version = $0; next }
|
|
END { emit() }
|
|
' <<<"$descs"
|
|
}
|
|
|
|
# The rc branch of THIS repo carries the current pins. Read them without
|
|
# touching the working tree.
|
|
rc_branch_pin() { # prints "pkgver commit", empty when no rc branch
|
|
local ref="origin/rc" pkgbuild
|
|
git -C "$BUILD_ROOT" fetch --quiet origin rc 2>/dev/null || true
|
|
pkgbuild=$(git -C "$BUILD_ROOT" show "$ref:pkgbuilds/omarchy/PKGBUILD" 2>/dev/null) || return 0
|
|
local pkgver commit
|
|
pkgver=$(grep -E '^pkgver=' <<<"$pkgbuild" | head -1 | cut -d= -f2)
|
|
commit=$(grep -E '^_commit=' <<<"$pkgbuild" | head -1 | cut -d= -f2 | tr -d "'\"")
|
|
[[ -n "$pkgver" ]] && echo "$pkgver $commit"
|
|
}
|
|
|
|
# --- build host --------------------------------------------------------------
|
|
#
|
|
# Every command runs from anywhere: when this machine IS the build host
|
|
# (on_repo_host — the published database lives here), host operations execute
|
|
# locally; otherwise they go over ssh to the configured destination. The
|
|
# destination is anything ssh accepts — root@<ip>, root@<hostname>, or an
|
|
# ~/.ssh/config Host alias — from --host, $OMARCHY_REPO_HOST, or .repo-host.
|
|
|
|
repo_host() { resolve_repo_host "$REPO_HOST_OVERRIDE"; }
|
|
|
|
print_no_host_help() { # print_no_host_help <what> <script>
|
|
print_warning "Not on the build host, and none configured (--host, OMARCHY_REPO_HOST, or $BUILD_ROOT/.repo-host — any ssh destination, e.g. root@<host> or an ssh-config alias)"
|
|
echo "Run this on the build host to $1:" >&2
|
|
echo "$2" >&2
|
|
}
|
|
|
|
# Builds the pinned release pair for the rc channel from the rc branch's own
|
|
# worktree, creating it on first use. OMARCHY_RC_PINS marks this as the one
|
|
# build allowed to set those packages' rc versions; OMARCHY_REPO_ROOT points
|
|
# the worktree at the primary checkout's channel tree so all three channels
|
|
# stay in one place. Fast-ring packages are not built here — the scheduled rc
|
|
# release covers those from master.
|
|
RC_TRIGGER_SCRIPT='
|
|
set -e
|
|
git -C /root/omarchy-pkgs fetch origin rc
|
|
if [ ! -d /root/omarchy-pkgs-rc ]; then
|
|
git -C /root/omarchy-pkgs worktree add /root/omarchy-pkgs-rc rc 2>/dev/null ||
|
|
git -C /root/omarchy-pkgs worktree add --track -b rc /root/omarchy-pkgs-rc origin/rc
|
|
fi
|
|
git -C /root/omarchy-pkgs-rc fetch origin rc
|
|
git -C /root/omarchy-pkgs-rc reset --hard origin/rc
|
|
cd /root/omarchy-pkgs-rc
|
|
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
|
|
bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check
|
|
'
|
|
|
|
trigger_rc_build() {
|
|
local host
|
|
if host=$(repo_host); then
|
|
print_info "Building the RC on $host (this takes a while)..."
|
|
ssh "$host" "source /root/.omarchy/build-credentials 2>/dev/null; $RC_TRIGGER_SCRIPT"
|
|
elif on_repo_host; then
|
|
print_info "Building the RC locally (this is the build host)..."
|
|
bash -c "$RC_TRIGGER_SCRIPT"
|
|
else
|
|
print_no_host_help "build the release candidate" "$RC_TRIGGER_SCRIPT"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
host_advance() { # host_advance <from> <to> [extra args...]
|
|
local from="$1" to="$2"
|
|
shift 2
|
|
# bin/repo is the remote control: with a host configured it forwards this
|
|
# over ssh itself; on the host it runs locally. Only the "neither" case —
|
|
# a workstation with no host — must be refused here, because running it
|
|
# against this machine's (likely stale) local tree would be wrong.
|
|
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
|
|
print_no_host_help "advance $from -> $to" \
|
|
" cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --skip-prod-check $*"
|
|
return 1
|
|
fi
|
|
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --skip-prod-check "$@"
|
|
}
|
|
|
|
wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds]
|
|
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got
|
|
print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..."
|
|
while ((waited < timeout)); do
|
|
got=$(published_version "$channel" 2>/dev/null) || got=""
|
|
if [[ "${got%-*}" == "$want" ]]; then
|
|
print_success "$channel now serves omarchy $got"
|
|
return 0
|
|
fi
|
|
sleep 60
|
|
waited=$((waited + 60))
|
|
printf '.' >&2
|
|
done
|
|
echo "" >&2
|
|
print_warning "Timed out waiting for $want in $channel (currently: ${got:-unknown})"
|
|
print_info "The build may still be running — re-run this command to resume."
|
|
return 1
|
|
}
|
|
|
|
# --- pkgs rc branch (pin commits) --------------------------------------------
|
|
|
|
# The rc branch is rebuilt as origin/master + pin commit(s) for each cut and
|
|
# force-pushed; the build host follows with reset --hard. History on it is
|
|
# disposable — the pins fully describe the release.
|
|
prepare_rc_worktree() {
|
|
git -C "$BUILD_ROOT" fetch --quiet origin
|
|
if [[ ! -d "$RC_WORKTREE" ]]; then
|
|
mkdir -p "$(dirname "$RC_WORKTREE")"
|
|
git -C "$BUILD_ROOT" worktree add --quiet -B rc "$RC_WORKTREE" origin/master
|
|
else
|
|
git -C "$RC_WORKTREE" checkout --quiet -B rc origin/master
|
|
git -C "$RC_WORKTREE" reset --quiet --hard origin/master
|
|
fi
|
|
}
|
|
|
|
cut_pins() { # cut_pins <omarchy-pkgs release args...>
|
|
prepare_rc_worktree
|
|
# The pin engine's "edge DB" is the ordering floor and rc auto-numbering
|
|
# source; in the pipeline model that floor is the rc channel.
|
|
(cd "$RC_WORKTREE" &&
|
|
OMARCHY_EDGE_DB_URL="$RC_DB_URL" bin/omarchy-pkgs release "$@" --no-push --yes)
|
|
git -C "$RC_WORKTREE" push --force-with-lease origin rc
|
|
}
|
|
|
|
# --- ISO ---------------------------------------------------------------------
|
|
|
|
iso_checkout() { # prints a usable omarchy-iso checkout, cloning to tmp if needed
|
|
if [[ -n "${OMARCHY_ISO_DIR:-}" && -d "${OMARCHY_ISO_DIR:-}" ]]; then
|
|
echo "$OMARCHY_ISO_DIR"
|
|
return 0
|
|
fi
|
|
if [[ -d "$BUILD_ROOT/../omarchy-iso/.git" ]]; then
|
|
realpath "$BUILD_ROOT/../omarchy-iso"
|
|
return 0
|
|
fi
|
|
local tmp="$SRCDEST_DIR/omarchy-iso"
|
|
if [[ -d "$tmp" ]]; then
|
|
git -C "$tmp" pull --ff-only --quiet || true
|
|
else
|
|
print_info "Cloning $ISO_REPO..." >&2
|
|
git clone --quiet "https://github.com/$ISO_REPO.git" "$tmp"
|
|
fi
|
|
echo "$tmp"
|
|
}
|
|
|
|
build_iso() { # build_iso <version> [--rc]
|
|
local version="$1" rc_flag="${2:-}" dir
|
|
dir=$(iso_checkout) || return 1
|
|
if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
|
|
print_warning "Docker unavailable — cannot build the ISO here. Run on a Docker machine:"
|
|
echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version"
|
|
return 1
|
|
fi
|
|
print_info "Building ${rc_flag:+RC }ISO $version (this takes a while)..."
|
|
(cd "$dir" && PATH="$dir/bin:$PATH" bin/omarchy-iso-release ${rc_flag:+"$rc_flag"} "$version")
|
|
}
|
|
|
|
maybe_iso() { # maybe_iso <version> <rc|final> <iso_mode: ask|yes|no>
|
|
local version="$1" kind="$2" mode="$3" rc_flag=""
|
|
[[ "$kind" == "rc" ]] && rc_flag="--rc"
|
|
case "$mode" in
|
|
no) return 0 ;;
|
|
yes) build_iso "$version" "$rc_flag" ;;
|
|
ask)
|
|
if confirm "Build and upload the ${kind} ISO for $version?"; then
|
|
build_iso "$version" "$rc_flag"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- website -----------------------------------------------------------------
|
|
|
|
update_website() { # update_website <version>
|
|
local version="$1" tmp="$SRCDEST_DIR/omarchy-site"
|
|
print_info "Updating $SITE_REPO ISO references to $version..."
|
|
if [[ -d "$tmp" ]]; then
|
|
git -C "$tmp" fetch --quiet origin && git -C "$tmp" reset --quiet --hard origin/HEAD
|
|
else
|
|
git clone --quiet "git@github.com:$SITE_REPO.git" "$tmp" || {
|
|
print_warning "Could not clone $SITE_REPO — update the ISO URL there manually"
|
|
return 1
|
|
}
|
|
fi
|
|
local matches
|
|
matches=$(grep -rIlE 'omarchy-[0-9]+\.[0-9]+\.[0-9]+(-rc)?\.iso' "$tmp" --exclude-dir=.git || true)
|
|
if [[ -z "$matches" ]]; then
|
|
print_warning "No omarchy-<version>.iso references found in $SITE_REPO — update it manually"
|
|
return 1
|
|
fi
|
|
echo "$matches" | while IFS= read -r f; do
|
|
sed -i -E "s/omarchy-[0-9]+\.[0-9]+\.[0-9]+\.iso/omarchy-$version.iso/g" "$f"
|
|
echo " updated: ${f#"$tmp"/}"
|
|
done
|
|
if git -C "$tmp" diff --quiet; then
|
|
print_info "Website already references $version"
|
|
return 0
|
|
fi
|
|
git -C "$tmp" --no-pager diff --stat
|
|
confirm "Push this ISO URL bump to $SITE_REPO?" || {
|
|
print_info "Left uncommitted in $tmp"
|
|
return 1
|
|
}
|
|
git -C "$tmp" commit --quiet -am "Point ISO download at omarchy-$version.iso" &&
|
|
git -C "$tmp" push --quiet origin HEAD
|
|
print_success "Website updated to omarchy-$version.iso"
|
|
}
|
|
|
|
# --- commands ----------------------------------------------------------------
|
|
|
|
cmd_start() {
|
|
local version="${1:-}"
|
|
if [[ -z "$version" ]]; then
|
|
read -r -p "Version to release (X.Y.Z): " version
|
|
fi
|
|
version="${version#v}"
|
|
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
print_error "Invalid version '$version' (expected X.Y.Z)"
|
|
exit 1
|
|
fi
|
|
local branch
|
|
branch=$(version_to_branch "$version")
|
|
|
|
print_header "Open release train $version"
|
|
|
|
if tag_exists "v$version"; then
|
|
print_error "v$version is already tagged on $UPSTREAM_REPO — this train has shipped"
|
|
exit 1
|
|
fi
|
|
|
|
# Base: previous tag for a patch, dev branch head for a minor/major.
|
|
local base_ref base_sha kind
|
|
if version_is_patch "$version"; then
|
|
kind="patch"
|
|
base_ref=$(previous_patch_tag "$version")
|
|
if ! tag_exists "$base_ref"; then
|
|
print_error "Base tag $base_ref not found on $UPSTREAM_REPO"
|
|
exit 1
|
|
fi
|
|
base_sha=$(resolve_tag_commit "$base_ref")
|
|
else
|
|
kind="minor/major"
|
|
base_ref="$DEV_BRANCH"
|
|
base_sha=$(branch_head "$DEV_BRANCH")
|
|
fi
|
|
|
|
if [[ -n "$(branch_head "$branch")" ]]; then
|
|
print_success "Release branch $branch already exists — adopting it"
|
|
else
|
|
print_info "Release type: $kind — branching $branch from $base_ref (${base_sha:0:12})"
|
|
confirm "Create branch $branch on $UPSTREAM_REPO?" || exit 1
|
|
ensure_mirror_clone
|
|
git -C "$MIRROR_CLONE" push --quiet origin "$base_sha:refs/heads/$branch"
|
|
print_success "Created $branch"
|
|
fi
|
|
|
|
# Staging PR: the body is the release-notes staging ground. A branch with no
|
|
# commits beyond its base cannot carry a PR yet; created lazily by pick.
|
|
if command -v gh >/dev/null; then
|
|
if gh pr view "$branch" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
|
print_success "Staging PR for $branch already open"
|
|
elif ! gh pr create --repo "$UPSTREAM_REPO" --draft --base "$DEV_BRANCH" --head "$branch" \
|
|
--title "Release v$version" \
|
|
--body "Staging ground for the v$version release notes. Edit this body — it becomes the GitHub release text." 2>/dev/null; then
|
|
print_info "Staging PR not created yet (no commits on $branch) — it opens after the first pick"
|
|
fi
|
|
fi
|
|
|
|
# Minor/major trains ship new edge packages: carry edge forward into rc so
|
|
# RC testing runs against the set stable users will get. A failed advance
|
|
# must not pass silently — RCs would test against the previous rc set.
|
|
if [[ "$kind" != "patch" ]]; then
|
|
if ! host_advance edge rc; then
|
|
print_error "edge → rc advance did not complete — the train is NOT fully open"
|
|
echo "Fix the advance (it is idempotent), then re-run: omarchy-release start $version"
|
|
echo "start is idempotent too — the branch and PR above are kept."
|
|
exit 1
|
|
fi
|
|
else
|
|
print_info "Patch train: rc already mirrors stable — nothing to advance"
|
|
fi
|
|
|
|
echo ""
|
|
print_success "Train $version is open"
|
|
print_info "Next: omarchy-release pick (cherry-pick fixes), then omarchy-release rc"
|
|
}
|
|
|
|
cmd_pick() {
|
|
local branch version
|
|
branch=$(open_train_branch) || true
|
|
if [[ -z "$branch" ]]; then
|
|
print_error "No open release train — run: omarchy-release start"
|
|
exit 1
|
|
fi
|
|
version=$(branch_to_version "$branch")
|
|
print_header "Pick changes onto $branch ($version)"
|
|
|
|
ensure_work_clone
|
|
git -C "$WORK_CLONE" checkout --quiet -B "$branch" "origin/$branch"
|
|
|
|
# Changes usually reach a release branch as BACKPORTS — cherry-picks with
|
|
# new SHAs — so ancestry of the original quattro merge commit proves nothing
|
|
# on a patch branch. Detect equivalence the way it is actually recorded:
|
|
# the branch's own commit messages since it left quattro name the PR
|
|
# ("backport of #N", squash titles "(#N)") or the source commit
|
|
# ("cherry picked from commit <sha>", which pick -x writes) — or, failing
|
|
# both, repeat its title verbatim.
|
|
local base_commit base_date branch_log
|
|
base_commit=$(git -C "$WORK_CLONE" merge-base "origin/$branch" "origin/$DEV_BRANCH" 2>/dev/null) || base_commit=""
|
|
base_date=""
|
|
if [[ -n "$base_commit" ]]; then
|
|
base_date=$(TZ=UTC git -C "$WORK_CLONE" log -1 --format=%cd \
|
|
--date=format-local:'%Y-%m-%dT%H:%M:%SZ' "$base_commit" 2>/dev/null) || base_date=""
|
|
fi
|
|
branch_log=$(git -C "$WORK_CLONE" log --format='%s %b' "origin/$branch" ${base_commit:+--not "$base_commit"} 2>/dev/null)
|
|
# Subjects alone, with any trailing "(#N)" stripped, so a change re-applied by
|
|
# hand — no PR number, no cherry-pick trailer — is still recognised by title.
|
|
local branch_subjects
|
|
branch_subjects=$(git -C "$WORK_CLONE" log --format='%s' "origin/$branch" ${base_commit:+--not "$base_commit"} 2>/dev/null |
|
|
sed -E 's/ \(#[0-9]+\)$//')
|
|
|
|
already_on_branch() { # already_on_branch <merge-sha> <pr-number-or-empty> <title-or-empty>
|
|
local sha="$1" number="$2" title="$3"
|
|
if [[ -n "$sha" ]]; then
|
|
git -C "$WORK_CLONE" merge-base --is-ancestor "$sha" "origin/$branch" 2>/dev/null && return 0
|
|
grep -q "cherry picked from commit $sha" <<<"$branch_log" && return 0
|
|
fi
|
|
if [[ -n "$number" ]]; then
|
|
grep -qE "#$number([^0-9]|$)" <<<"$branch_log" && return 0
|
|
fi
|
|
if [[ -n "$title" ]]; then
|
|
grep -Fxq "$title" <<<"$branch_subjects" && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
local -a shas=() labels=()
|
|
if [[ $# -gt 0 ]]; then
|
|
local arg sha title
|
|
for arg in "$@"; do
|
|
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
|
sha=$(gh pr view "$arg" --repo "$UPSTREAM_REPO" --json mergeCommit --jq '.mergeCommit.oid' 2>/dev/null)
|
|
title=$(gh pr view "$arg" --repo "$UPSTREAM_REPO" --json title --jq '.title' 2>/dev/null)
|
|
if [[ -z "$sha" || "$sha" == "null" ]]; then
|
|
print_error "PR #$arg has no merge commit (not merged?)"
|
|
exit 1
|
|
fi
|
|
if already_on_branch "$sha" "$arg" "$title"; then
|
|
print_info "PR #$arg is already on $branch — skipping"
|
|
continue
|
|
fi
|
|
shas+=("$sha") labels+=("PR #$arg: $title")
|
|
else
|
|
if already_on_branch "$arg" "" ""; then
|
|
print_info "commit $arg is already on $branch — skipping"
|
|
continue
|
|
fi
|
|
shas+=("$arg") labels+=("commit $arg")
|
|
fi
|
|
done
|
|
else
|
|
# Interactive: merged dev-branch PRs not already on the release branch
|
|
# (by ancestry, backport reference, cherry-pick trailer, or title), oldest
|
|
# first so picks apply in merge order.
|
|
print_info "Loading merged $DEV_BRANCH PRs not yet on $branch..."
|
|
# gh pr list orders by CREATION date, so a plain --limit window silently
|
|
# drops long-lived PRs — opened early, merged recently — which are exactly
|
|
# the ones a release branch still needs. Pull a wide window and bound it by
|
|
# the branch point instead: anything merged into $DEV_BRANCH before $branch
|
|
# left it is already here by ancestry.
|
|
local list jq_pick
|
|
jq_pick='sort_by(.mergedAt) | .[] | "\(.mergeCommit.oid)\t\(.number)\t\(.title)"'
|
|
[[ -n "$base_date" ]] && jq_pick="[.[] | select(.mergedAt >= \"$base_date\")] | $jq_pick"
|
|
list=$(gh pr list --repo "$UPSTREAM_REPO" --state merged --base "$DEV_BRANCH" \
|
|
--limit 300 --json number,title,mergeCommit,mergedAt --jq "$jq_pick")
|
|
local -a cand_shas=() cand_labels=()
|
|
local sha number title
|
|
while IFS=$'\t' read -r sha number title; do
|
|
[[ -z "$sha" || "$sha" == "null" ]] && continue
|
|
already_on_branch "$sha" "$number" "$title" && continue
|
|
cand_shas+=("$sha") cand_labels+=("#$number $title")
|
|
done <<<"$list"
|
|
if [[ ${#cand_shas[@]} -eq 0 ]]; then
|
|
print_success "Nothing to pick — every $DEV_BRANCH PR merged since $branch opened is already on it"
|
|
exit 0
|
|
fi
|
|
echo ""
|
|
local i
|
|
for i in "${!cand_labels[@]}"; do
|
|
printf ' %2d) %s\n' "$((i + 1))" "${cand_labels[$i]}"
|
|
done
|
|
echo ""
|
|
local selection
|
|
read -r -p "Pick which? (e.g. 1,3-5 or 'all'): " selection
|
|
if [[ "$selection" == "all" ]]; then
|
|
shas=("${cand_shas[@]}") labels=("${cand_labels[@]}")
|
|
else
|
|
local part
|
|
for part in ${selection//,/ }; do
|
|
if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
|
for ((i = BASH_REMATCH[1]; i <= BASH_REMATCH[2]; i++)); do
|
|
shas+=("${cand_shas[$((i - 1))]}") labels+=("${cand_labels[$((i - 1))]}")
|
|
done
|
|
elif [[ "$part" =~ ^[0-9]+$ ]]; then
|
|
shas+=("${cand_shas[$((part - 1))]}") labels+=("${cand_labels[$((part - 1))]}")
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
|
|
[[ ${#shas[@]} -eq 0 ]] && { print_info "Nothing selected."; exit 0; }
|
|
|
|
local i
|
|
for i in "${!shas[@]}"; do
|
|
print_info "Cherry-picking ${labels[$i]}..."
|
|
if ! git -C "$WORK_CLONE" cherry-pick -x -m 1 "${shas[$i]}" 2>/dev/null &&
|
|
! { git -C "$WORK_CLONE" cherry-pick --abort 2>/dev/null; git -C "$WORK_CLONE" cherry-pick -x "${shas[$i]}"; }; then
|
|
print_error "Cherry-pick conflict on ${labels[$i]}"
|
|
echo "Resolve it in $WORK_CLONE (git cherry-pick --continue), push, and re-run."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
git -C "$WORK_CLONE" push --quiet origin "$branch"
|
|
print_success "Picked ${#shas[@]} change(s) onto $branch"
|
|
|
|
# The staging PR can exist now that the branch has commits of its own.
|
|
if command -v gh >/dev/null && ! gh pr view "$branch" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
|
gh pr create --repo "$UPSTREAM_REPO" --draft --base "$DEV_BRANCH" --head "$branch" \
|
|
--title "Release v$version" \
|
|
--body "Staging ground for the v$version release notes. Edit this body — it becomes the GitHub release text." 2>/dev/null &&
|
|
print_success "Opened the release-notes staging PR" || true
|
|
fi
|
|
print_info "Next: omarchy-release rc"
|
|
}
|
|
|
|
cmd_rc() {
|
|
local iso_mode="$1" wait="$2"
|
|
local branch version
|
|
branch=$(open_train_branch) || true
|
|
if [[ -z "$branch" ]]; then
|
|
print_error "No open release train — run: omarchy-release start"
|
|
exit 1
|
|
fi
|
|
version=$(branch_to_version "$branch")
|
|
print_header "Cut RC for train $version"
|
|
|
|
local head
|
|
head=$(branch_head "$branch")
|
|
print_info "Branch $branch head: ${head:0:12}"
|
|
|
|
# Idempotence: if the current rc pin already matches the branch head and is
|
|
# published, there is nothing to cut.
|
|
local pin
|
|
pin=$(rc_branch_pin)
|
|
if [[ -n "$pin" ]]; then
|
|
local pin_ver="${pin%% *}" pin_commit="${pin##* }"
|
|
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
|
|
local pub
|
|
pub=$(published_version rc 2>/dev/null) || pub=""
|
|
if [[ "${pub%-*}" == "$pin_ver" ]]; then
|
|
print_success "$pin_ver is already cut from this head and published to rc"
|
|
maybe_iso "$version" rc "$iso_mode"
|
|
return 0
|
|
fi
|
|
print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build"
|
|
trigger_rc_build || true
|
|
[[ "$wait" == true ]] && wait_for_published rc "$pin_ver"
|
|
maybe_iso "$version" rc "$iso_mode"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
confirm "Pin omarchy + omarchy-settings to $branch@${head:0:12} as the next ${version}rcN and publish to rc?" || exit 1
|
|
|
|
cut_pins rc --base "$version" --ref "$branch"
|
|
|
|
local new_pin new_ver
|
|
new_pin=$(rc_branch_pin)
|
|
new_ver="${new_pin%% *}"
|
|
print_success "Pinned $new_ver (rc branch pushed)"
|
|
|
|
trigger_rc_build || true
|
|
if [[ "$wait" == true ]]; then
|
|
wait_for_published rc "$new_ver" || return 1
|
|
fi
|
|
maybe_iso "$version" rc "$iso_mode"
|
|
echo ""
|
|
print_info "Test the candidate, then: omarchy-release ship (or pick + rc again)"
|
|
}
|
|
|
|
cmd_ship() {
|
|
local iso_mode="$1" wait="$2"
|
|
local branch version
|
|
branch=$(open_train_branch) || true
|
|
if [[ -z "$branch" ]]; then
|
|
# A tagged train whose later steps (release, ISO, website) failed is
|
|
# invisible to open_train_branch — find it so a re-run can resume.
|
|
branch=$(newest_release_branch) || true
|
|
if [[ -z "$branch" ]]; then
|
|
print_error "No release train found — nothing to ship"
|
|
exit 1
|
|
fi
|
|
version=$(branch_to_version "$branch")
|
|
local stable_now
|
|
stable_now=$(published_version stable 2>/dev/null) || stable_now=""
|
|
if [[ "${stable_now%-*}" == "$version" ]] &&
|
|
gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
|
print_success "Nothing to ship — $version is tagged, released, and live on stable"
|
|
exit 0
|
|
fi
|
|
print_info "Resuming tagged-but-incomplete train $version"
|
|
else
|
|
version=$(branch_to_version "$branch")
|
|
fi
|
|
print_header "Ship $version"
|
|
|
|
local head pin pin_ver pin_commit
|
|
head=$(branch_head "$branch")
|
|
pin=$(rc_branch_pin)
|
|
pin_ver="${pin%% *}"
|
|
pin_commit="${pin##* }"
|
|
|
|
# The ship guard: only the exact commit an RC was cut from may ship. There
|
|
# is no override — a moved branch means an untested tree; cut another rc.
|
|
if [[ -z "$pin" ]]; then
|
|
print_error "No RC has been cut for $version — run: omarchy-release rc"
|
|
exit 1
|
|
fi
|
|
if [[ "$pin_ver" == "$version" ]]; then
|
|
# Final already pinned (resume path). The artifacts come from pin_commit;
|
|
# a branch that moved afterwards changes nothing already built or tagged.
|
|
if [[ -n "$head" && "$head" != "$pin_commit" ]]; then
|
|
print_warning "$branch moved after the final was pinned — shipping the pinned ${pin_commit:0:12}; newer commits need the next patch train"
|
|
fi
|
|
else
|
|
if [[ ! "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
|
|
print_error "No RC has been cut for $version (rc branch pins $pin_ver) — run: omarchy-release rc"
|
|
exit 1
|
|
fi
|
|
if [[ "$pin_commit" != "$head" ]]; then
|
|
local behind
|
|
behind=$(git -C "$WORK_CLONE" rev-list --count "$pin_commit..origin/$branch" 2>/dev/null || echo "?")
|
|
print_error "$branch has moved since $pin_ver was cut ($behind commit(s) untested)"
|
|
echo "Only a commit an RC was cut from can ship. Cut another candidate:"
|
|
echo " omarchy-release rc"
|
|
exit 1
|
|
fi
|
|
local pub
|
|
pub=$(published_version rc 2>/dev/null) || pub=""
|
|
if [[ "${pub%-*}" != "$pin_ver" ]]; then
|
|
print_error "$pin_ver is pinned but rc serves '${pub:-nothing}' — the candidate build hasn't published"
|
|
echo "Wait for it (or re-run: omarchy-release rc), then ship."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
print_info "This will, in order (steps already done are skipped):"
|
|
echo " 1. Pin the final $version from $branch@${pin_commit:0:12} and publish it to rc"
|
|
echo " 2. Promote the rc channel to stable (packages + signatures + db)"
|
|
echo " 3. Tag v$version on $UPSTREAM_REPO"
|
|
echo " 4. Merge the final pins to master (edge overlap + record)"
|
|
echo " 5. Create the GitHub release from the staging PR body"
|
|
echo " 6. Build + upload the final ISO (${iso_mode})"
|
|
echo " 7. Point the website at the new ISO"
|
|
echo ""
|
|
confirm "Ship $version?" || exit 1
|
|
|
|
# 1. Final pins into rc
|
|
local rc_pub stable_pub
|
|
rc_pub=$(published_version rc 2>/dev/null) || rc_pub=""
|
|
if [[ "${rc_pub%-*}" == "$version" ]]; then
|
|
print_success "1/7 Final $version already published to rc"
|
|
else
|
|
if [[ "$pin_ver" != "$version" ]]; then
|
|
print_info "1/7 Pinning final $version..."
|
|
cut_pins "v$version" --commit "$pin_commit"
|
|
else
|
|
print_info "1/7 Final $version pinned — re-triggering build"
|
|
fi
|
|
trigger_rc_build || true
|
|
wait_for_published rc "$version" || exit 1
|
|
fi
|
|
|
|
# 2. Promote rc -> stable
|
|
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
|
if [[ "${stable_pub%-*}" == "$version" ]]; then
|
|
print_success "2/7 Stable already serves $version"
|
|
else
|
|
host_advance rc stable || exit 1
|
|
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
|
if [[ "${stable_pub%-*}" != "$version" ]]; then
|
|
print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing"
|
|
exit 1
|
|
fi
|
|
print_success "2/7 Promoted to stable: omarchy $stable_pub"
|
|
fi
|
|
|
|
# 3. Tag — at the pinned commit the artifacts were built from, never the
|
|
# branch head (they can differ on a resumed ship).
|
|
if tag_exists "v$version"; then
|
|
print_success "3/7 Tag v$version already exists"
|
|
else
|
|
ensure_mirror_clone
|
|
git -C "$MIRROR_CLONE" push --quiet origin "$pin_commit:refs/tags/v$version"
|
|
print_success "3/7 Tagged v$version at ${pin_commit:0:12}"
|
|
fi
|
|
|
|
# 4. Final pins onto master (keeps edge overlap publishing and the repo record)
|
|
local master_ver
|
|
master_ver=$(git -C "$BUILD_ROOT" show origin/master:pkgbuilds/omarchy/PKGBUILD 2>/dev/null | grep -E '^pkgver=' | head -1 | cut -d= -f2)
|
|
if [[ "$master_ver" == "$version" ]]; then
|
|
print_success "4/7 master already carries the $version pins"
|
|
else
|
|
local pin_sha
|
|
pin_sha=$(git -C "$BUILD_ROOT" rev-parse origin/rc 2>/dev/null || true)
|
|
print_info "4/7 Bringing the final pin commit to master..."
|
|
local sync_wt="$BUILD_ROOT/.worktrees/master-sync"
|
|
git -C "$BUILD_ROOT" fetch --quiet origin
|
|
if [[ ! -d "$sync_wt" ]]; then
|
|
git -C "$BUILD_ROOT" worktree add --quiet -B release-master-sync "$sync_wt" origin/master
|
|
else
|
|
git -C "$sync_wt" checkout --quiet -B release-master-sync origin/master
|
|
fi
|
|
if (cd "$sync_wt" && git cherry-pick "$pin_sha" && git push --quiet origin HEAD:master); then
|
|
print_success "4/7 Final pins merged to master"
|
|
else
|
|
(cd "$sync_wt" && git cherry-pick --abort 2>/dev/null || true)
|
|
print_warning "4/7 Could not fast-path the pins to master — cherry-pick $pin_sha onto master manually"
|
|
fi
|
|
fi
|
|
|
|
# 5. GitHub release from the staging PR body
|
|
if gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
|
print_success "5/7 GitHub release v$version already exists"
|
|
else
|
|
local notes
|
|
notes=$(gh pr view "$branch" --repo "$UPSTREAM_REPO" --json body --jq '.body' 2>/dev/null) || notes=""
|
|
if [[ -z "$notes" || "$notes" == "null" ]]; then
|
|
notes="Omarchy $version"
|
|
print_warning "No staging PR body found — using a bare title; edit the release afterwards"
|
|
fi
|
|
gh release create "v$version" --repo "$UPSTREAM_REPO" --title "v$version" --notes "$notes"
|
|
print_success "5/7 GitHub release v$version created"
|
|
fi
|
|
|
|
# 6. ISO
|
|
maybe_iso "$version" final "$iso_mode" || true
|
|
|
|
# 7. Website (only meaningful once the final ISO exists)
|
|
if [[ "$iso_mode" != "no" ]]; then
|
|
update_website "$version" || true
|
|
else
|
|
print_info "7/7 --no-iso: leaving the website untouched"
|
|
fi
|
|
|
|
echo ""
|
|
print_success "Shipped $version — rc and stable are in parity for the next patch train"
|
|
print_info "Close the loop: merge or close the staging PR for $branch on $UPSTREAM_REPO"
|
|
}
|
|
|
|
# --- status / shepherd -------------------------------------------------------
|
|
|
|
gather_status() {
|
|
EDGE_VER=$(published_version edge 2>/dev/null) || EDGE_VER="<unreachable>"
|
|
RC_VER=$(published_version rc 2>/dev/null) || RC_VER="<unreachable>"
|
|
STABLE_VER=$(published_version stable 2>/dev/null) || STABLE_VER="<unreachable>"
|
|
TRAIN=$(open_train_branch 2>/dev/null) || TRAIN=""
|
|
TRAIN_VER=""
|
|
TRAIN_HEAD=""
|
|
PIN=""
|
|
if [[ -n "$TRAIN" ]]; then
|
|
TRAIN_VER=$(branch_to_version "$TRAIN")
|
|
TRAIN_HEAD=$(branch_head "$TRAIN")
|
|
PIN=$(rc_branch_pin)
|
|
fi
|
|
}
|
|
|
|
next_step() { # prints "<command>|<description>"
|
|
if [[ -z "$TRAIN" ]]; then
|
|
# A tagged train may still have unfinished ship steps (release/ISO/site).
|
|
local last last_ver
|
|
last=$(newest_release_branch 2>/dev/null) || last=""
|
|
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
|
|
last_ver=$(branch_to_version "$last")
|
|
if [[ "${STABLE_VER%-*}" != "$last_ver" ]] ||
|
|
{ command -v gh >/dev/null && ! gh release view "v$last_ver" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; }; then
|
|
echo "ship|$last_ver is tagged but not fully shipped — resume ship"
|
|
return
|
|
fi
|
|
fi
|
|
echo "start|No open train — start the next release"
|
|
return
|
|
fi
|
|
local pin_ver="${PIN%% *}" pin_commit="${PIN##* }"
|
|
if [[ -z "$PIN" || ! "$pin_ver" =~ ^${TRAIN_VER//./\\.}(rc[0-9]+)?$ ]]; then
|
|
echo "rc|Cut the first candidate for $TRAIN_VER"
|
|
elif [[ "$pin_ver" == "$TRAIN_VER" ]]; then
|
|
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
|
|
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
|
|
echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate"
|
|
elif [[ "${RC_VER%-*}" != "$pin_ver" ]]; then
|
|
echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait"
|
|
else
|
|
echo "ship|$pin_ver is published to rc — test it, then ship"
|
|
fi
|
|
}
|
|
|
|
print_status() {
|
|
print_header "Omarchy release status"
|
|
echo " Channels (omarchy):"
|
|
echo " edge: $EDGE_VER"
|
|
echo " rc: $RC_VER"
|
|
echo " stable: $STABLE_VER"
|
|
echo ""
|
|
if [[ -z "$TRAIN" ]]; then
|
|
echo " No open release train."
|
|
else
|
|
echo " Open train: $TRAIN_VER (branch $TRAIN @ ${TRAIN_HEAD:0:12})"
|
|
if [[ -n "$PIN" ]]; then
|
|
echo " Current pin: ${PIN%% *} from commit ${PIN##* }"
|
|
else
|
|
echo " Current pin: <none>"
|
|
fi
|
|
fi
|
|
echo ""
|
|
local step
|
|
step=$(next_step)
|
|
echo " Next: omarchy-release ${step%%|*} — ${step#*|}"
|
|
}
|
|
|
|
cmd_shepherd() {
|
|
gather_status
|
|
print_status
|
|
local step cmd
|
|
step=$(next_step)
|
|
cmd="${step%%|*}"
|
|
echo ""
|
|
if confirm "Run 'omarchy-release $cmd' now?"; then
|
|
case "$cmd" in
|
|
start) cmd_start ;;
|
|
pick) cmd_pick ;;
|
|
rc) cmd_rc "$ISO_MODE" "$WAIT" ;;
|
|
ship) cmd_ship "$ISO_MODE" "$WAIT" ;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# --- doctor ------------------------------------------------------------------
|
|
|
|
cmd_doctor() {
|
|
print_header "omarchy-release doctor"
|
|
local failures=0
|
|
check() {
|
|
local label="$1"; shift
|
|
if "$@" >/dev/null 2>&1; then
|
|
print_success "$label"
|
|
else
|
|
print_error "$label"
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
check "git available" command -v git
|
|
check "gh available" command -v gh
|
|
check "gh authenticated" gh auth status
|
|
check "gh can see $UPSTREAM_REPO" gh repo view "$UPSTREAM_REPO"
|
|
check "gh can see $SITE_REPO" gh repo view "$SITE_REPO"
|
|
check "vercmp available (pacman)" command -v vercmp
|
|
check "makepkg available (checksums)" command -v makepkg
|
|
check "curl available" command -v curl
|
|
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
|
|
local ch
|
|
for ch in edge stable; do
|
|
if published_version "$ch" >/dev/null 2>&1; then
|
|
print_success "$ch channel db readable"
|
|
else
|
|
print_error "$ch channel db readable"
|
|
failures=$((failures + 1))
|
|
fi
|
|
done
|
|
if published_version rc >/dev/null 2>&1; then
|
|
print_success "rc channel db readable"
|
|
else
|
|
print_warning "rc channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)"
|
|
fi
|
|
local host
|
|
if host=$(repo_host); then
|
|
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
|
|
elif on_repo_host; then
|
|
print_success "this machine IS the build host — host operations run locally"
|
|
else
|
|
print_warning "no build host configured — set OMARCHY_REPO_HOST, --host, or write an ssh destination (root@<host> or an ssh-config alias) to $BUILD_ROOT/.repo-host; until then builds trigger on the 6h timer only"
|
|
fi
|
|
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
|
|
print_success "docker available (ISO builds possible here)"
|
|
else
|
|
print_warning "docker unavailable — ISO builds will print instructions instead"
|
|
fi
|
|
echo ""
|
|
if ((failures == 0)); then
|
|
print_success "Ready to release"
|
|
else
|
|
print_error "$failures check(s) failed"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# --- self-test ---------------------------------------------------------------
|
|
|
|
cmd_self_test() {
|
|
local failures=0
|
|
expect() { # expect <label> <got> <want>
|
|
if [[ "$2" == "$3" ]]; then
|
|
echo " ok: $1"
|
|
else
|
|
echo " FAIL: $1 — got '$2', want '$3'"
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
print_header "omarchy-release self-test"
|
|
expect "version_to_branch 4.0.2" "$(version_to_branch 4.0.2)" "v4-0-2"
|
|
expect "version_to_branch v4.1.0" "$(version_to_branch v4.1.0)" "v4-1-0"
|
|
expect "version_to_branch garbage rejects" "$(version_to_branch 4.0 2>/dev/null || echo reject)" "reject"
|
|
expect "branch_to_version v4-0-2" "$(branch_to_version v4-0-2)" "4.0.2"
|
|
expect "branch_to_version v10-2-33" "$(branch_to_version v10-2-33)" "10.2.33"
|
|
expect "branch_to_version quattro rejects" "$(branch_to_version quattro 2>/dev/null || echo reject)" "reject"
|
|
expect "version_is_patch 4.0.2" "$(version_is_patch 4.0.2 && echo yes || echo no)" "yes"
|
|
expect "version_is_patch 4.1.0" "$(version_is_patch 4.1.0 && echo yes || echo no)" "no"
|
|
expect "version_is_patch 5.0.0" "$(version_is_patch 5.0.0 && echo yes || echo no)" "no"
|
|
expect "previous_patch_tag 4.0.2" "$(previous_patch_tag 4.0.2)" "v4.0.1"
|
|
expect "previous_patch_tag 4.0.10" "$(previous_patch_tag 4.0.10)" "v4.0.9"
|
|
echo ""
|
|
if ((failures == 0)); then
|
|
print_success "Self-test passed"
|
|
else
|
|
print_error "$failures self-test failure(s)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# --- dispatch ----------------------------------------------------------------
|
|
|
|
COMMAND=""
|
|
ISO_MODE="ask"
|
|
WAIT=true
|
|
ARGS=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--yes) ASSUME_YES=true; shift ;;
|
|
--host)
|
|
REPO_HOST_OVERRIDE="$2"
|
|
export OMARCHY_REPO_HOST="$2" # child bin/repo invocations forward to it too
|
|
shift 2
|
|
;;
|
|
--iso) ISO_MODE="yes"; shift ;;
|
|
--no-iso) ISO_MODE="no"; shift ;;
|
|
--no-wait) WAIT=false; shift ;;
|
|
-h | --help) show_usage; exit 0 ;;
|
|
start | pick | rc | ship | status | doctor | self-test)
|
|
COMMAND="$1"; shift ;;
|
|
*)
|
|
ARGS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
case "$COMMAND" in
|
|
start) cmd_start "${ARGS[@]}" ;;
|
|
pick) cmd_pick "${ARGS[@]}" ;;
|
|
rc) cmd_rc "$ISO_MODE" "$WAIT" ;;
|
|
ship) cmd_ship "$ISO_MODE" "$WAIT" ;;
|
|
status) gather_status; print_status ;;
|
|
doctor) cmd_doctor ;;
|
|
self-test) cmd_self_test ;;
|
|
"") cmd_shepherd ;;
|
|
*) print_error "Unknown command: $COMMAND"; show_usage; exit 1 ;;
|
|
esac
|