684 lines
26 KiB
Bash
Executable File
684 lines
26 KiB
Bash
Executable File
#!/bin/bash
|
|
# Omarchy release management for the omarchy + omarchy-settings package pair.
|
|
#
|
|
# Cuts a release by rewriting both PKGBUILDs in lockstep (same _tag/_commit/
|
|
# pkgver/sha256sums), committing, pushing to master, and poking the build host.
|
|
# RCs publish to edge only; stable receives finals via `bin/repo migrate`.
|
|
#
|
|
# Versioning convention (see the PKGBUILD header comments):
|
|
# finals X.Y.Z from upstream tag vX.Y.Z
|
|
# RCs X.Y.ZrcN attached form ONLY — vercmp orders rc1 < rc2 < final;
|
|
# separator forms (X.Y.Z.rcN, X.Y.Z_rcN) sort AFTER final
|
|
# pkgrel resets to 1 on every pkgver change; epoch is never set by tooling.
|
|
|
|
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}"
|
|
EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/x86_64/omarchy.db.tar.zst}"
|
|
RELEASE_PACKAGES=(omarchy omarchy-settings)
|
|
DEFAULT_RC_REF="quattro"
|
|
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
|
CLONE_DIR="$SRCDEST_DIR/omarchy"
|
|
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: $0 <command> [options]
|
|
|
|
Commands:
|
|
release <vX.Y.Z | vX.Y.Z-rcN> Cut a release from an upstream tag; if the
|
|
tag doesn't exist, uses the tip of quattro
|
|
(or --ref/--commit) with that version
|
|
release latest [rc|beta|alpha] Cut from the newest upstream tag, optionally
|
|
restricted to one pre-release channel
|
|
release rc [vX.Y.Z] Newest upstream vX.Y.Z-rcN tag → X.Y.ZrcN
|
|
release beta|alpha [vX.Y.Z] Same for beta/alpha channels
|
|
release rc --commit <sha> Untagged pre-release from a bare commit,
|
|
auto-numbered past published edge + PKGBUILD
|
|
self-test Run version-normalization and ordering tests
|
|
|
|
Options for release:
|
|
--base <X.Y.Z> (rc) Base version the RC leads up to (default: base of
|
|
the current PKGBUILD pkgver)
|
|
--commit <sha> (rc) Upstream commit to pin (default: tip of --ref)
|
|
--ref <branch> (rc) Upstream branch whose tip to pin (default: $DEFAULT_RC_REF)
|
|
--yes Skip confirmation prompts
|
|
--host <host> ssh destination of the repository host to trigger,
|
|
overriding \$OMARCHY_REPO_HOST and .repo-host
|
|
--no-push Rewrite and commit locally; skip push and build trigger
|
|
--pr When releasing from a non-master branch, open a GitHub PR
|
|
to master with gh after pushing
|
|
--rebuild Same version + same commit: bump pkgrel to force a rebuild
|
|
--force Allow a version lower than the current PKGBUILD when that
|
|
version was never published (rewind an unshipped seed)
|
|
--dry-run Resolve, validate, and show the plan; write nothing
|
|
-h, --help Show this help message
|
|
|
|
Every release updates ${RELEASE_PACKAGES[*]} together: same _tag, _commit,
|
|
pkgver, and sha256sums. RCs build for edge only. Promote a final to stable
|
|
after verifying the edge build:
|
|
bin/repo migrate --package omarchy && bin/repo migrate --package omarchy-settings
|
|
EOF
|
|
}
|
|
|
|
# --- version helpers ---------------------------------------------------------
|
|
|
|
# v4.0.0 / v4.0.0-rc1 / v4.0.0-rc.1 / v4.0.0.rc1 / v4.0.0_rc1 → pacman pkgver.
|
|
# Pre-release channels alpha/beta/rc normalize to the ATTACHED form, which is
|
|
# the only one vercmp orders before the final (and alpha < beta < rc < final).
|
|
normalize_tag() {
|
|
local v="${1#v}"
|
|
if [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "$v"
|
|
elif [[ "$v" =~ ^([0-9]+\.[0-9]+\.[0-9]+)[-._](alpha|beta|rc)\.?([0-9]+)$ ]]; then
|
|
echo "${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[3]}"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
version_base() { echo "$1" | sed -E 's/(alpha|beta|rc)[0-9]+$//'; }
|
|
version_is_prerelease() { [[ "$1" =~ (alpha|beta|rc)[0-9]+$ ]]; }
|
|
# Back-compat alias used by channel output
|
|
version_is_rc() { version_is_prerelease "$1"; }
|
|
|
|
pkgbuild_var() {
|
|
local pkg="$1" var="$2"
|
|
(cd "$BUILD_ROOT/pkgbuilds/$pkg" && bash -c "source PKGBUILD 2>/dev/null; echo \"\${$var}\"")
|
|
}
|
|
|
|
# Prints the published version of $pkg from the edge DB. Distinguishes
|
|
# "package absent" (empty output, rc 0) from "could not fetch/read the DB"
|
|
# (rc 2) so callers can fail closed instead of mistaking an outage for a
|
|
# first release.
|
|
published_edge_version() {
|
|
local pkg="$1" tmp descs
|
|
tmp=$(mktemp) || return 2
|
|
if ! curl -sf "$EDGE_DB_URL" -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 -v pkg="$pkg" '
|
|
function emit() {
|
|
if (!found && name == pkg && 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"
|
|
}
|
|
|
|
# --- upstream resolution -----------------------------------------------------
|
|
|
|
resolve_tag_commit() {
|
|
local tag="$1" peeled sha
|
|
peeled=$(git ls-remote "$UPSTREAM_URL" "refs/tags/$tag^{}" | awk '{print $1}')
|
|
sha=$(git ls-remote "$UPSTREAM_URL" "refs/tags/$tag" | awk '{print $1}')
|
|
echo "${peeled:-$sha}"
|
|
}
|
|
|
|
resolve_ref_commit() {
|
|
git ls-remote "$UPSTREAM_URL" "refs/heads/$1" | awk '{print $1}'
|
|
}
|
|
|
|
# Newest upstream tag by vercmp, pre-releases included: v4.0.0-rc1 outranks
|
|
# v3.8.4, and a final outranks its own rc/beta/alpha builds. With a channel
|
|
# argument (rc/beta/alpha), only tags of that channel are considered.
|
|
latest_upstream_tag() {
|
|
local channel_filter="${1:-}" best_tag="" best_ver="" tag ver
|
|
while IFS= read -r tag; do
|
|
ver=$(normalize_tag "$tag") || continue
|
|
if [[ -n "$channel_filter" ]] && [[ ! "$ver" =~ ${channel_filter}[0-9]+$ ]]; then
|
|
continue
|
|
fi
|
|
if [[ -z "$best_ver" ]] || [[ $(vercmp "$ver" "$best_ver") -gt 0 ]]; then
|
|
best_ver="$ver" best_tag="$tag"
|
|
fi
|
|
done < <(git ls-remote --tags "$UPSTREAM_URL" | awk -F/ '!/\^\{\}/{print $3}')
|
|
[[ -n "$best_tag" ]] && echo "$best_tag"
|
|
}
|
|
|
|
|
|
# Newest upstream tag of the form v<base>-<channel>N (any accepted separator),
|
|
# picked by vercmp over normalized candidates.
|
|
latest_channel_tag() {
|
|
local channel="$1" base="$2" best_tag="" best_ver="" tag ver
|
|
while IFS= read -r tag; do
|
|
[[ "${tag#v}" =~ ^${base//./\\.}[-._]${channel}\.?[0-9]+$ ]] || continue
|
|
ver=$(normalize_tag "$tag") || continue
|
|
if [[ -z "$best_ver" ]] || [[ $(vercmp "$ver" "$best_ver") -gt 0 ]]; then
|
|
best_ver="$ver" best_tag="$tag"
|
|
fi
|
|
done < <(git ls-remote --tags "$UPSTREAM_URL" | awk -F/ '!/\^\{\}/{print $3}')
|
|
[[ -n "$best_tag" ]] && echo "$best_tag"
|
|
}
|
|
|
|
ensure_clone() {
|
|
if [[ -d "$CLONE_DIR" ]]; then
|
|
git -C "$CLONE_DIR" fetch --quiet origin
|
|
else
|
|
mkdir -p "$SRCDEST_DIR"
|
|
print_info "Cloning $UPSTREAM_URL (cached in $SRCDEST_DIR for future releases)..."
|
|
git clone --mirror --quiet "$UPSTREAM_URL" "$CLONE_DIR"
|
|
fi
|
|
}
|
|
|
|
# --- guards ------------------------------------------------------------------
|
|
|
|
# Only the two files the release rewrites must be clean — the commit stages
|
|
# and commits exactly those paths, so dirt elsewhere cannot enter the release.
|
|
guard_clean_tree() {
|
|
local dirty
|
|
dirty=$(cd "$BUILD_ROOT" && git status --porcelain -- \
|
|
pkgbuilds/omarchy/PKGBUILD pkgbuilds/omarchy-settings/PKGBUILD)
|
|
if [[ -n "$dirty" ]]; then
|
|
print_error "Uncommitted changes on the release PKGBUILDs — commit or stash them first:"
|
|
echo "$dirty"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Decides how this cut relates to what exists and sets RELEASE_PKGREL:
|
|
# new version -> pkgrel 1 (after the ordering floor)
|
|
# same version, different commit -> re-release: pkgrel +1 (tag was moved)
|
|
# same version, same commit -> no-op, or pkgrel +1 with --rebuild
|
|
guard_version_ordering() {
|
|
local new_pkgver="$1" new_commit="$2" rebuild="$3" force="$4" published
|
|
if ! published=$(published_edge_version omarchy); then
|
|
print_error "Could not read the published edge DB ($EDGE_DB_URL) — refusing to release blind"
|
|
exit 1
|
|
fi
|
|
local current_pkgver current_commit
|
|
current_pkgver=$(pkgbuild_var "${RELEASE_PACKAGES[0]}" pkgver)
|
|
current_commit=$(pkgbuild_var "${RELEASE_PACKAGES[0]}" _commit)
|
|
|
|
local current_pkgrel
|
|
current_pkgrel=$(pkgbuild_var "${RELEASE_PACKAGES[0]}" pkgrel)
|
|
RELEASE_PKGREL=1
|
|
|
|
if [[ "$new_pkgver" == "$current_pkgver" && "$new_commit" == "$current_commit" ]]; then
|
|
if [[ "$rebuild" == true ]]; then
|
|
RELEASE_PKGREL=$((current_pkgrel + 1))
|
|
print_warning "Rebuild: same source ($new_pkgver @ ${new_commit:0:12}), pkgrel $current_pkgrel → $RELEASE_PKGREL"
|
|
return 0
|
|
fi
|
|
print_success "Nothing to release — omarchy is already at $new_pkgver from commit ${new_commit:0:12} (use --rebuild to force a repackage)"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$new_pkgver" == "$current_pkgver" ]]; then
|
|
# Same version, different commit: the upstream tag was moved/recut.
|
|
RELEASE_PKGREL=$((current_pkgrel + 1))
|
|
if [[ -n "$published" ]]; then
|
|
local published_pkgver="${published%-*}"
|
|
if [[ $(vercmp "$new_pkgver" "$published_pkgver") -lt 0 ]]; then
|
|
print_error "Refusing re-release of $new_pkgver: edge has already moved on to $published_pkgver"
|
|
exit 1
|
|
fi
|
|
fi
|
|
print_warning "Re-release: $new_pkgver was cut from ${current_commit:0:12}, now ${new_commit:0:12} — pkgrel $current_pkgrel → $RELEASE_PKGREL"
|
|
return 0
|
|
fi
|
|
|
|
# The floor is the newest version anyone could already have: the published
|
|
# edge DB, or the PKGBUILD itself (a cut that hasn't built/synced yet).
|
|
local floor="$current_pkgver" floor_src="current PKGBUILD"
|
|
if [[ -n "$published" ]]; then
|
|
local published_pkgver="${published%-*}"
|
|
if [[ $(vercmp "$published_pkgver" "$floor") -gt 0 ]]; then
|
|
floor="$published_pkgver" floor_src="published edge DB"
|
|
fi
|
|
else
|
|
print_warning "omarchy not found in the published edge DB yet — guarding against the current PKGBUILD only"
|
|
fi
|
|
if [[ $(vercmp "$new_pkgver" "$floor") -le 0 ]]; then
|
|
if [[ "$force" == true && "$floor_src" == "current PKGBUILD" ]]; then
|
|
# Nothing at or above the floor has shipped — rewinding the unshipped
|
|
# seed is safe. A published floor is never forceable.
|
|
print_warning "--force: rewinding unshipped $floor (current PKGBUILD) → $new_pkgver"
|
|
return 0
|
|
fi
|
|
print_error "Refusing: $new_pkgver does not sort after $floor ($floor_src)"
|
|
if [[ "$floor_src" == "published edge DB" ]]; then
|
|
print_error "That version is already published — users would never see this release. --force cannot override a published floor."
|
|
else
|
|
print_error "Cut a newer version, or use --force to rewind the unshipped PKGBUILD seed."
|
|
fi
|
|
exit 1
|
|
fi
|
|
print_info "Ordering OK: $floor ($floor_src) → $new_pkgver"
|
|
}
|
|
|
|
guard_rc_before_final() {
|
|
local pkgver="$1"
|
|
version_is_rc "$pkgver" || return 0
|
|
local base
|
|
base=$(version_base "$pkgver")
|
|
if [[ $(vercmp "$pkgver" "$base") -ge 0 ]]; then
|
|
print_error "Refusing: RC pkgver $pkgver does not sort before final $base (normalization bug)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
guard_lockstep() {
|
|
local a b
|
|
for var in _tag _commit pkgver pkgrel sha256sums; do
|
|
a=$(pkgbuild_var "${RELEASE_PACKAGES[0]}" "$var")
|
|
b=$(pkgbuild_var "${RELEASE_PACKAGES[1]}" "$var")
|
|
if [[ "$a" != "$b" ]]; then
|
|
print_error "Lockstep violation: $var differs between ${RELEASE_PACKAGES[*]} ('$a' vs '$b')"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# --- PKGBUILD rewriting ------------------------------------------------------
|
|
|
|
rewrite_pkgbuilds() {
|
|
local tag="$1" commit="$2" pkgver="$3" pkg
|
|
for pkg in "${RELEASE_PACKAGES[@]}"; do
|
|
sed -i \
|
|
-e "s|^_tag=.*|_tag='$tag'|" \
|
|
-e "s|^_commit=.*|_commit='$commit'|" \
|
|
-e "s|^pkgver=.*|pkgver=$pkgver|" \
|
|
-e "s|^pkgrel=.*|pkgrel=${RELEASE_PKGREL:-1}|" \
|
|
"$BUILD_ROOT/pkgbuilds/$pkg/PKGBUILD"
|
|
done
|
|
}
|
|
|
|
regenerate_checksums() {
|
|
local sum pkg
|
|
print_info "Generating sha256sums (makepkg -g)..."
|
|
sum=$(cd "$BUILD_ROOT/pkgbuilds/${RELEASE_PACKAGES[0]}" && SRCDEST="$SRCDEST_DIR" makepkg -g 2>/dev/null | grep -oE '[a-f0-9]{64}')
|
|
if [[ -z "$sum" ]]; then
|
|
print_error "makepkg -g produced no checksum — is the pinned commit reachable upstream?"
|
|
exit 1
|
|
fi
|
|
for pkg in "${RELEASE_PACKAGES[@]}"; do
|
|
sed -i -E "s|^(\s*)sha256sums=\('[^']+'\)|\1sha256sums=('$sum')|" "$BUILD_ROOT/pkgbuilds/$pkg/PKGBUILD"
|
|
done
|
|
for pkg in "${RELEASE_PACKAGES[@]}"; do
|
|
print_info "Verifying source integrity for $pkg..."
|
|
(cd "$BUILD_ROOT/pkgbuilds/$pkg" && SRCDEST="$SRCDEST_DIR" makepkg --verifysource --skippgpcheck >/dev/null)
|
|
done
|
|
print_success "sha256sums verified: $sum"
|
|
}
|
|
|
|
# --- trigger -----------------------------------------------------------------
|
|
|
|
trigger_build_host() {
|
|
local host
|
|
if ! host=$(resolve_repo_host "${REPO_HOST_OVERRIDE:-}"); then
|
|
print_info "No repository host configured (set OMARCHY_REPO_HOST or $BUILD_ROOT/.repo-host)."
|
|
print_info "The 6-hourly auto-release timer will pick this up, or trigger manually:"
|
|
echo " ssh <build-host> 'git -C /root/omarchy-pkgs pull --ff-only && touch /root/.state/.sync-needed-edge && systemctl start omarchy-auto-release-edge.service'"
|
|
return 0
|
|
fi
|
|
print_info "Triggering edge build on $host..."
|
|
if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && mkdir -p /root/.state && touch /root/.state/.sync-needed-edge && systemctl start --no-block omarchy-auto-release-edge.service'; then
|
|
print_success "Edge build triggered on $host"
|
|
else
|
|
print_warning "Could not trigger $host — the 6-hourly timer will pick it up"
|
|
fi
|
|
}
|
|
|
|
# --- release command ---------------------------------------------------------
|
|
|
|
cmd_release() {
|
|
local target="" target_arg="" base="" commit_arg="" ref="" dry_run=false assume_yes=false no_push=false open_pr=false rebuild=false force=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--base) base="$2"; shift 2 ;;
|
|
--no-push) no_push=true; shift ;;
|
|
--pr) open_pr=true; shift ;;
|
|
--rebuild) rebuild=true; shift ;;
|
|
--force) force=true; shift ;;
|
|
--commit) commit_arg="$2"; shift 2 ;;
|
|
--ref) ref="$2"; shift 2 ;;
|
|
--host) REPO_HOST_OVERRIDE="$2"; shift 2 ;;
|
|
--yes) assume_yes=true; shift ;;
|
|
--dry-run) dry_run=true; shift ;;
|
|
-h | --help) show_usage; exit 0 ;;
|
|
-*) print_error "Unknown option: $1"; exit 1 ;;
|
|
*)
|
|
if [[ -z "$target" ]]; then
|
|
target="$1"
|
|
elif [[ -z "$target_arg" ]]; then
|
|
target_arg="$1"
|
|
else
|
|
print_error "Unexpected argument: $1"; exit 1
|
|
fi
|
|
shift ;;
|
|
esac
|
|
done
|
|
# A bare-commit release is an RC: --commit/--ref/--base imply rc mode.
|
|
if [[ -z "$target" && ( -n "$commit_arg" || -n "$ref" || -n "$base" ) ]]; then
|
|
target="rc"
|
|
fi
|
|
if [[ -z "$target" ]]; then
|
|
print_error "Usage: $0 release <vX.Y.Z | vX.Y.Z-rcN | latest | rc | beta | alpha> [options]"
|
|
exit 1
|
|
fi
|
|
if [[ ! "$target" =~ ^(rc|beta|alpha)$ && -n "$base" ]]; then
|
|
print_error "--base only applies to pre-release modes (rc/beta/alpha)"
|
|
exit 1
|
|
fi
|
|
if [[ "$target" == "latest" && ( -n "$commit_arg" || -n "$ref" ) ]]; then
|
|
print_error "--commit/--ref do not apply to 'release latest'"
|
|
exit 1
|
|
fi
|
|
|
|
print_header "Omarchy Release"
|
|
|
|
# Fail fast on a dirty tree before touching the network. --no-push commits
|
|
# only the two PKGBUILD files locally, so a dirty tree elsewhere is fine.
|
|
if [[ "$dry_run" != true && "$no_push" != true ]]; then
|
|
guard_clean_tree
|
|
elif [[ "$no_push" == true ]]; then
|
|
print_warning "--no-push: will commit locally to $(cd "$BUILD_ROOT" && git rev-parse --abbrev-ref HEAD); no push, no build trigger"
|
|
fi
|
|
|
|
local tag="" commit="" pkgver=""
|
|
case "$target" in
|
|
latest)
|
|
local channel_filter=""
|
|
if [[ -n "$target_arg" ]]; then
|
|
if [[ "$target_arg" =~ ^(rc|beta|alpha)$ ]]; then
|
|
channel_filter="$target_arg"
|
|
else
|
|
print_error "Unknown argument '$target_arg' — usage: release latest [rc|beta|alpha]"
|
|
exit 1
|
|
fi
|
|
fi
|
|
print_info "Finding newest upstream ${channel_filter:-}${channel_filter:+ }tag..."
|
|
tag=$(latest_upstream_tag "$channel_filter" || true)
|
|
if [[ -z "$tag" ]]; then
|
|
print_error "No ${channel_filter:+$channel_filter }release tags found at $UPSTREAM_URL"
|
|
exit 1
|
|
fi
|
|
pkgver=$(normalize_tag "$tag")
|
|
commit=$(resolve_tag_commit "$tag")
|
|
;;
|
|
rc | beta | alpha)
|
|
local channel="$target"
|
|
# `release rc v4.0.0` → newest upstream v4.0.0-rcN tag
|
|
if [[ -n "$target_arg" && -z "$commit_arg" && -z "$ref" ]]; then
|
|
base="${target_arg#v}"
|
|
if [[ ! "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
print_error "Invalid base '$target_arg' (expected vX.Y.Z or X.Y.Z)"
|
|
exit 1
|
|
fi
|
|
print_info "Finding newest upstream ${channel} tag for $base..."
|
|
tag=$(latest_channel_tag "$channel" "$base" || true)
|
|
if [[ -z "$tag" ]]; then
|
|
print_error "No v$base-${channel}N tag found at $UPSTREAM_URL"
|
|
print_error "Tag one upstream (on quattro), or cut an untagged ${channel} with: $0 release $channel --commit <sha> --base $base"
|
|
exit 1
|
|
fi
|
|
pkgver=$(normalize_tag "$tag")
|
|
commit=$(resolve_tag_commit "$tag")
|
|
if [[ -z "$commit" ]]; then
|
|
print_error "Could not resolve $tag to a commit"
|
|
exit 1
|
|
fi
|
|
else
|
|
if [[ -n "$target_arg" ]]; then base="${base:-${target_arg#v}}"; fi
|
|
if [[ -z "$base" ]]; then
|
|
base=$(version_base "$(pkgbuild_var "${RELEASE_PACKAGES[0]}" pkgver)")
|
|
print_info "No --base given; using current PKGBUILD base: $base"
|
|
fi
|
|
if [[ ! "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
print_error "Invalid --base '$base' (expected X.Y.Z)"
|
|
exit 1
|
|
fi
|
|
if [[ -n "$commit_arg" ]]; then
|
|
ensure_clone
|
|
commit=$(git -C "$CLONE_DIR" rev-parse --verify --quiet "$commit_arg^{commit}") || {
|
|
print_error "Commit '$commit_arg' not found in upstream $UPSTREAM_URL"
|
|
exit 1
|
|
}
|
|
else
|
|
ref="${ref:-$DEFAULT_RC_REF}"
|
|
commit=$(resolve_ref_commit "$ref")
|
|
if [[ -z "$commit" ]]; then
|
|
print_error "Branch '$ref' not found upstream"
|
|
exit 1
|
|
fi
|
|
fi
|
|
# Next rc number: one past the highest of the published edge DB and the
|
|
# current PKGBUILD for this base.
|
|
local published_ver
|
|
if ! published_ver=$(published_edge_version omarchy); then
|
|
print_error "Could not read the published edge DB ($EDGE_DB_URL) — refusing to auto-number the RC blind"
|
|
exit 1
|
|
fi
|
|
local highest=0 candidate
|
|
for candidate in "${published_ver%-*}" "$(pkgbuild_var "${RELEASE_PACKAGES[0]}" pkgver)"; do
|
|
if [[ "$candidate" =~ ^${base//./\\.}${channel}([0-9]+)$ ]] && (( BASH_REMATCH[1] > highest )); then
|
|
highest=${BASH_REMATCH[1]}
|
|
fi
|
|
done
|
|
pkgver="${base}${channel}$((highest + 1))"
|
|
tag=""
|
|
fi
|
|
;;
|
|
v*)
|
|
pkgver=$(normalize_tag "$target") || {
|
|
print_error "Cannot parse '$target' as a release tag."
|
|
print_error "Accepted: vX.Y.Z, vX.Y.Z-rcN, vX.Y.Z-rc.N, vX.Y.Z.rcN, vX.Y.Z_rcN (beta/alpha too)"
|
|
exit 1
|
|
}
|
|
tag="$target"
|
|
if [[ -n "$commit_arg" ]]; then
|
|
# Explicit commit wins — release this version from exactly that commit.
|
|
ensure_clone
|
|
commit=$(git -C "$CLONE_DIR" rev-parse --verify --quiet "$commit_arg^{commit}") || {
|
|
print_error "Commit '$commit_arg' not found in upstream $UPSTREAM_URL"
|
|
exit 1
|
|
}
|
|
tag=""
|
|
print_warning "Releasing $pkgver from explicit commit ${commit:0:12} (no upstream tag)"
|
|
else
|
|
print_info "Resolving $tag upstream..."
|
|
commit=$(resolve_tag_commit "$tag")
|
|
if [[ -z "$commit" ]]; then
|
|
# No such tag: fall back to the branch tip, like the rc flow.
|
|
ref="${ref:-$DEFAULT_RC_REF}"
|
|
commit=$(resolve_ref_commit "$ref")
|
|
if [[ -z "$commit" ]]; then
|
|
print_error "Tag '$tag' not found and branch '$ref' not found at $UPSTREAM_URL"
|
|
exit 1
|
|
fi
|
|
tag=""
|
|
print_warning "Tag '$target' not found upstream — releasing $pkgver from the tip of '$ref' (${commit:0:12})"
|
|
fi
|
|
fi
|
|
;;
|
|
*)
|
|
print_error "Unknown release target '$target' (expected vX.Y.Z, latest, or rc)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
guard_rc_before_final "$pkgver"
|
|
guard_version_ordering "$pkgver" "$commit" "$rebuild" "$force"
|
|
|
|
echo ""
|
|
print_info "Packages: ${RELEASE_PACKAGES[*]}"
|
|
print_info "Tag: ${tag:-<none — cut from bare commit>}"
|
|
print_info "Commit: $commit"
|
|
print_info "Pkgver: $pkgver-$RELEASE_PKGREL"
|
|
if version_is_prerelease "$pkgver"; then
|
|
print_info "Channel: edge only (pre-release)"
|
|
else
|
|
print_info "Channel: edge, then promote to stable via bin/repo migrate"
|
|
fi
|
|
echo ""
|
|
|
|
if [[ "$dry_run" == true ]]; then
|
|
print_success "Dry run complete — nothing written."
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$assume_yes" != true ]]; then
|
|
local reply
|
|
read -r -p "Cut release $pkgver from ${tag:-$commit}? [y/N] " reply
|
|
[[ "$reply" =~ ^[Yy]$ ]] || { print_info "Aborted."; exit 1; }
|
|
fi
|
|
|
|
ensure_clone
|
|
rewrite_pkgbuilds "$tag" "$commit" "$pkgver"
|
|
regenerate_checksums
|
|
guard_lockstep
|
|
|
|
if [[ "$no_push" == true ]]; then
|
|
print_info "Committing locally (--no-push)..."
|
|
(cd "$BUILD_ROOT" &&
|
|
git commit -m "Release omarchy $pkgver" -- pkgbuilds/omarchy/PKGBUILD pkgbuilds/omarchy-settings/PKGBUILD)
|
|
print_success "Committed release omarchy $pkgver (not pushed, build not triggered)"
|
|
else
|
|
local branch
|
|
branch=$(cd "$BUILD_ROOT" && git rev-parse --abbrev-ref HEAD)
|
|
print_info "Committing and pushing to $branch..."
|
|
(cd "$BUILD_ROOT" &&
|
|
git commit -m "Release omarchy $pkgver" -- pkgbuilds/omarchy/PKGBUILD pkgbuilds/omarchy-settings/PKGBUILD &&
|
|
git push origin HEAD)
|
|
print_success "Pushed release omarchy $pkgver to $branch"
|
|
|
|
if [[ "$branch" == "master" ]]; then
|
|
trigger_build_host
|
|
else
|
|
if [[ "$open_pr" == true ]]; then
|
|
if command -v gh >/dev/null; then
|
|
gh pr create --base master --head "$branch" \
|
|
--title "Release omarchy $pkgver" \
|
|
--body "$(printf 'Cut with bin/omarchy-pkgs release.\n\n- pkgver: %s-1\n- tag: %s\n- commit: %s\n\nMerging publishes to edge on the next auto-release cycle.%s' \
|
|
"$pkgver" "${tag:-<none — bare commit>}" "$commit" \
|
|
"$(version_is_prerelease "$pkgver" || echo ' Promote to stable afterwards with: bin/repo migrate --package omarchy && bin/repo migrate --package omarchy-settings')")"
|
|
else
|
|
print_warning "--pr requested but gh is not installed — open the PR manually"
|
|
fi
|
|
else
|
|
print_info "Released on branch '$branch' — merge it to master to go live (or rerun with --pr); the build host follows master."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if version_is_prerelease "$pkgver"; then
|
|
print_info "Pre-release flow: $pkgver builds for edge only. Stable is untouched."
|
|
else
|
|
print_info "After the edge build completes and you have verified it, promote to stable:"
|
|
echo " bin/repo migrate --package omarchy && bin/repo migrate --package omarchy-settings"
|
|
echo " bin/repo sync --mirror stable"
|
|
fi
|
|
}
|
|
|
|
# --- self-test ---------------------------------------------------------------
|
|
|
|
cmd_self_test() {
|
|
local failures=0
|
|
|
|
check_norm() {
|
|
local input="$1" expected="$2" got
|
|
got=$(normalize_tag "$input" 2>/dev/null) || got="<reject>"
|
|
if [[ "$got" == "$expected" ]]; then
|
|
echo " ok: $input → $got"
|
|
else
|
|
echo " FAIL: $input → $got (expected $expected)"
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
|
|
check_vercmp() {
|
|
local a="$1" op="$2" b="$3" got
|
|
got=$(vercmp "$a" "$b")
|
|
local ok=false
|
|
case "$op" in
|
|
"<") [[ "$got" -lt 0 ]] && ok=true ;;
|
|
">") [[ "$got" -gt 0 ]] && ok=true ;;
|
|
"=") [[ "$got" -eq 0 ]] && ok=true ;;
|
|
esac
|
|
if [[ "$ok" == true ]]; then
|
|
echo " ok: $a $op $b"
|
|
else
|
|
echo " FAIL: expected $a $op $b (vercmp said $got)"
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
|
|
print_header "omarchy-pkgs self-test"
|
|
|
|
echo "Tag normalization:"
|
|
check_norm v4.0.0 4.0.0
|
|
check_norm v4.0.0-rc1 4.0.0rc1
|
|
check_norm v4.0.0-rc.2 4.0.0rc2
|
|
check_norm v4.0.0.rc3 4.0.0rc3
|
|
check_norm v4.0.0_rc4 4.0.0rc4
|
|
check_norm 4.1.0 4.1.0
|
|
check_norm v4.0.0-rc10 4.0.0rc10
|
|
check_norm v4.0 "<reject>"
|
|
check_norm v4.0.0-beta1 4.0.0beta1
|
|
check_norm v4.0.0-alpha.2 4.0.0alpha2
|
|
check_norm v4.0.0-preview1 "<reject>"
|
|
check_norm v4.0.0rc "<reject>"
|
|
check_norm garbage "<reject>"
|
|
check_norm v4.0.0-rc "<reject>"
|
|
|
|
echo "Pacman ordering of normalized outputs:"
|
|
check_vercmp 4.0.0rc1 "<" 4.0.0
|
|
check_vercmp 4.0.0rc1 "<" 4.0.0rc2
|
|
check_vercmp 4.0.0rc2 "<" 4.0.0rc10
|
|
check_vercmp 4.0.0 ">" 4.0.0rc99
|
|
check_vercmp 4.0.1 ">" 4.0.0
|
|
check_vercmp 4.0.0 "<" 4.1.0rc1
|
|
check_vercmp 4.0.0alpha1 "<" 4.0.0beta1
|
|
check_vercmp 4.0.0beta1 "<" 4.0.0rc1
|
|
check_vercmp 4.0.0beta2 "<" 4.0.0
|
|
check_vercmp 4.0.0beta2 "<" 4.0.0beta10
|
|
|
|
echo "Version helpers:"
|
|
[[ $(version_base 4.0.0beta3) == 4.0.0 ]] && echo " ok: version_base 4.0.0beta3 → 4.0.0" || { echo " FAIL: version_base beta"; failures=$((failures + 1)); }
|
|
[[ $(version_base 4.0.0rc7) == 4.0.0 ]] && echo " ok: version_base 4.0.0rc7 → 4.0.0" || { echo " FAIL: version_base"; failures=$((failures + 1)); }
|
|
version_is_rc 4.0.0rc1 && echo " ok: 4.0.0rc1 is rc" || { echo " FAIL: version_is_rc positive"; failures=$((failures + 1)); }
|
|
version_is_rc 4.0.0 && { echo " FAIL: version_is_rc negative"; failures=$((failures + 1)); } || echo " ok: 4.0.0 is not rc"
|
|
|
|
echo ""
|
|
if [[ "$failures" -eq 0 ]]; then
|
|
print_success "Self-test passed"
|
|
else
|
|
print_error "$failures self-test failure(s)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# --- dispatch ----------------------------------------------------------------
|
|
|
|
case "${1:-}" in
|
|
release)
|
|
shift
|
|
cmd_release "$@"
|
|
;;
|
|
self-test)
|
|
cmd_self_test
|
|
;;
|
|
-h | --help | "")
|
|
show_usage
|
|
;;
|
|
*)
|
|
print_error "Unknown command: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
esac
|