#!/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 < 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 Tag, final pins, promote rc -> stable, draft 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, 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" } # A clone made with `git clone --mirror` sets remote.origin.mirror=true. Git # otherwise rejects an explicit SHA:ref push as an invalid combination with # mirror mode, so disable that remote setting for narrowly targeted pushes. push_upstream_ref() { # push_upstream_ref git -c remote.origin.mirror=false -C "$MIRROR_CLONE" push --quiet origin "$1:$2" } 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@, root@, 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