The rc channel's Arch base can sit anywhere between stable's snapshot and edge's, so a package built against stable's libraries is not necessarily correct for rc. Copying stable's fast-ring artifacts into rc therefore shipped possibly-mislinked packages to RC testers. Fast-ring packages now build natively for all three channels, each in its own image against its own base mirror, and the stable release's replication step is gone. That required separating 'may be built here' from 'whose version wins'. The release pair is now marked "pinned": its version is set per release on the rc branch, so it builds for rc only from that branch's worktree (OMARCHY_RC_PINS=1, set by omarchy-release rc) — master's shipped pins can never overwrite an in-flight RC, even though check-versions now discovers rc work like it does for edge and stable.
1053 lines
39 KiB
Bash
Executable File
1053 lines
39 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
|
|
if ! curl -sf "$PKGS_DB_BASE/$channel/x86_64/omarchy.db.tar.zst" -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).
|
|
local base_commit branch_log
|
|
base_commit=$(git -C "$WORK_CLONE" merge-base "origin/$branch" "origin/$DEV_BRANCH" 2>/dev/null) || base_commit=""
|
|
branch_log=$(git -C "$WORK_CLONE" log --format='%s %b' "origin/$branch" ${base_commit:+--not "$base_commit"} 2>/dev/null)
|
|
|
|
already_on_branch() { # already_on_branch <merge-sha> <pr-number-or-empty>
|
|
local sha="$1" number="$2"
|
|
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
|
|
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"; 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, or cherry-pick trailer), oldest first
|
|
# so picks apply in merge order.
|
|
print_info "Loading merged $DEV_BRANCH PRs not yet on $branch..."
|
|
local list
|
|
list=$(gh pr list --repo "$UPSTREAM_REPO" --state merged --base "$DEV_BRANCH" \
|
|
--limit 30 --json number,title,mergeCommit,mergedAt \
|
|
--jq 'sort_by(.mergedAt) | .[] | "\(.mergeCommit.oid)\t\(.number)\t#\(.number) \(.title)"')
|
|
local -a cand_shas=() cand_labels=()
|
|
while IFS=$'\t' read -r sha number label; do
|
|
[[ -z "$sha" || "$sha" == "null" ]] && continue
|
|
already_on_branch "$sha" "$number" && continue
|
|
cand_shas+=("$sha") cand_labels+=("$label")
|
|
done <<<"$list"
|
|
if [[ ${#cand_shas[@]} -eq 0 ]]; then
|
|
print_success "Nothing to pick — every recent merged PR is already on $branch"
|
|
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
|