#!/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/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 < 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 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 whose final tag does not exist yet. Shipping # creates the tag, so "tag exists" is what closes a train. open_train_branch() { local branch best_ver="" best_branch="" ver while IFS= read -r branch; do ver=$(branch_to_version "$branch") || continue tag_exists "v$ver" && continue 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" } 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 } 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 } # --- 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 -------------------------------------------------------------- repo_host() { resolve_repo_host "$REPO_HOST_OVERRIDE"; } host_or_print() { # prints the host, or prints manual instructions and fails local host what="$1" cmd="$2" if host=$(repo_host); then echo "$host" return 0 fi print_warning "No build host configured (set OMARCHY_REPO_HOST, --host, or $BUILD_ROOT/.repo-host)" echo "Run this on the build host to $what:" >&2 echo " $cmd" >&2 return 1 } trigger_rc_build() { local host host=$(host_or_print "build the rc channel" \ "git -C /root/omarchy-pkgs-rc fetch origin rc && git -C /root/omarchy-pkgs-rc reset --hard origin/rc && touch /root/.state/.sync-needed-rc && systemctl start --no-block omarchy-auto-release-rc.service") || return 1 print_info "Triggering rc build on $host..." ssh "$host" 'git -C /root/omarchy-pkgs-rc fetch origin rc && git -C /root/omarchy-pkgs-rc reset --hard origin/rc && mkdir -p /root/.state && touch /root/.state/.sync-needed-rc && systemctl start --no-block omarchy-auto-release-rc.service' } host_advance() { # host_advance [extra args...] local from="$1" to="$2" shift 2 local host host=$(host_or_print "advance $from -> $to" \ "cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --skip-prod-check $*") || return 1 print_info "Advancing $from -> $to on $host..." ssh "$host" "cd /root/omarchy-pkgs && git pull --ff-only && bin/repo advance --from $from --to $to --skip-prod-check $*" } wait_for_published() { # wait_for_published [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 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 [--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 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 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-.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. if [[ "$kind" != "patch" ]]; then host_advance edge rc || true 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" 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 shas+=("$sha") labels+=("PR #$arg: $title") else shas+=("$arg") labels+=("commit $arg") fi done else # Interactive: merged dev-branch PRs whose merge commits are not on the # release branch yet, 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) \(.title)"') local -a cand_shas=() cand_labels=() while IFS=$'\t' read -r sha label; do [[ -z "$sha" || "$sha" == "null" ]] && continue git -C "$WORK_CLONE" merge-base --is-ancestor "$sha" "origin/$branch" 2>/dev/null && 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 print_error "No open release train — nothing to ship" exit 1 fi version=$(branch_to_version "$branch") 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 a commit an RC was actually cut from may ship. if [[ "$pin_ver" != "$version" ]]; then if [[ -z "$pin" || ! "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then print_error "No RC has been cut for $version — 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 "Cut another candidate first: omarchy-release rc" [[ "$ASSUME_YES" == true ]] && exit 1 confirm "Ship anyway from ${head:0:12} WITHOUT an RC of it? (not recommended)" || 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@${head: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 "$head" 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 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 "$head:refs/tags/v$version" print_success "3/7 Tagged v$version at ${head: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="" RC_VER=$(published_version rc 2>/dev/null) || RC_VER="" STABLE_VER=$(published_version stable 2>/dev/null) || STABLE_VER="" 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 "|" if [[ -z "$TRAIN" ]]; then 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: " 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 else print_warning "no build host configured (OMARCHY_REPO_HOST / .repo-host) — 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