mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
It broke due to the recent rust toolchain update. Also point to radicle.network for desktop downloads in zulip message.
436 lines
14 KiB
Bash
Executable File
436 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Interactive, phased release orchestrator for radicle-desktop.
|
|
#
|
|
# Automates the mechanical steps of the release process and pauses at the human
|
|
# gates (CI/patch approval, DMG smoke-test, the cross-repo apt signing, delegate
|
|
# tag approval, Zulip announce). Each phase prints what to do next and the exact
|
|
# flag to resume with. State is not persisted: the version comes from --version
|
|
# (prep) or tauri.conf.json (later phases), and commit SHAs are derived from git.
|
|
#
|
|
# Usage:
|
|
# scripts/release --phase prep --version X.Y.Z
|
|
# scripts/release --phase build
|
|
# scripts/release --phase apt
|
|
# scripts/release --phase publish
|
|
# scripts/release --phase arch
|
|
# scripts/release --phase tag
|
|
#
|
|
# scripts/release --list List the phases in order
|
|
# scripts/release --help Show this help
|
|
#
|
|
# Add --dry-run (-n) to a phase to print the commands it would run without
|
|
# executing them (read-only lookups still run, so the preview is realistic).
|
|
#
|
|
# Config is auto-detected and overridable via environment variables:
|
|
# FULL_NAME (default: git config user.name)
|
|
# EMAIL (default: git config user.email)
|
|
# USERNAME (default: $USER)
|
|
# SSH_KEY (default: $HOME/.ssh/id_ed25519)
|
|
# SIGNING_KEY (default: $HOME/.ssh/apt_signing_key)
|
|
# FILES_HOST (default: files.radicle.dev)
|
|
|
|
set -Eeuo pipefail
|
|
|
|
DRY_RUN=false
|
|
CURRENT_PHASE=""
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
TAURI_CONF="$PROJECT_DIR/crates/radicle-tauri/tauri.conf.json"
|
|
PKGBUILD="$PROJECT_DIR/arch/radicle-desktop/PKGBUILD"
|
|
CHANGELOG="$PROJECT_DIR/CHANGELOG.md"
|
|
APT_REPO_DIR="$(cd "$PROJECT_DIR/../radicle-apt-repo" 2>/dev/null && pwd || true)"
|
|
|
|
PHASES=(prep build apt publish arch tag)
|
|
|
|
# ---- Output helpers ----
|
|
|
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
step() { echo; bold "==> $*"; }
|
|
info() { echo " $*"; }
|
|
warn() { echo " WARNING: $*" >&2; }
|
|
die() { echo "Error: $*" >&2; trap - ERR; exit 1; }
|
|
|
|
# Fires on any uncaught command failure inside a phase. Points at --dry-run,
|
|
# which prints the phase's full command sequence (with real values) so the
|
|
# remaining steps can be finished by hand.
|
|
on_error() {
|
|
local code=$?
|
|
trap - ERR
|
|
echo >&2
|
|
echo "Error: phase '$CURRENT_PHASE' did not finish (exit $code)." >&2
|
|
echo "Fix the problem above, then review the phase's commands to continue" >&2
|
|
echo "manually from where it stopped:" >&2
|
|
echo >&2
|
|
echo " scripts/release --phase $CURRENT_PHASE --dry-run" >&2
|
|
}
|
|
|
|
confirm() {
|
|
local prompt="$1" reply
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
info "$prompt [dry-run: assuming yes]"
|
|
return 0
|
|
fi
|
|
read -r -p " $prompt [y/N] " reply
|
|
[[ "$reply" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
# Run a mutating command, or just print it under --dry-run.
|
|
run() {
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
printf ' [dry-run] %s\n' "$*"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
# Like run(), but for a command that must run in directory $1.
|
|
run_sh() {
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
printf ' [dry-run] (cd %s && %s)\n' "$1" "${*:2}"
|
|
else
|
|
( cd "$1" && "${@:2}" )
|
|
fi
|
|
}
|
|
|
|
# Abort on a missing prerequisite — but only warn under --dry-run, so later
|
|
# phases can still be previewed without their inputs present.
|
|
fail() {
|
|
if [[ "$DRY_RUN" == true ]]; then warn "$*"; else die "$*"; fi
|
|
}
|
|
|
|
# generate-srcinfo.sh needs a running Docker daemon. Check it before prep
|
|
# mutates anything, so a docker-down situation fails cleanly. Skipped under
|
|
# --dry-run, where the Docker step only prints.
|
|
docker_available() { command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; }
|
|
podman_available() { command -v podman >/dev/null 2>&1 && podman info >/dev/null 2>&1; }
|
|
|
|
require_docker() {
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
docker_available || warn "Docker is not available (a real run needs it to regenerate .SRCINFO)"
|
|
return 0
|
|
fi
|
|
command -v docker >/dev/null 2>&1 || die "docker not found (needed to regenerate .SRCINFO)"
|
|
docker info >/dev/null 2>&1 || die "Docker daemon is not running — start Docker and re-run"
|
|
}
|
|
|
|
# build-artifacts enforces a running Podman itself on a real build. This mirror
|
|
# only surfaces it under --dry-run (build-artifacts isn't executed then), so the
|
|
# preview reports a down/absent Podman too.
|
|
require_podman() {
|
|
[[ "$DRY_RUN" == true ]] || return 0
|
|
podman_available || warn "Podman is not available (a real build needs it for the Linux artifacts)"
|
|
}
|
|
|
|
# Every phase runs on the default branch (prep branches off it; later phases run
|
|
# after the patch is merged back). Bail with a clear message if we're elsewhere,
|
|
# so the user can switch and re-run. Only a warning under --dry-run.
|
|
require_branch() {
|
|
local current
|
|
current="$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD)"
|
|
[[ "$current" == "$EXPECTED_BRANCH" ]] && return 0
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "on branch '$current', expected '$EXPECTED_BRANCH'"
|
|
else
|
|
die "on branch '$current', expected '$EXPECTED_BRANCH' — run 'git checkout $EXPECTED_BRANCH', then re-run"
|
|
fi
|
|
}
|
|
|
|
# Print the command(s) the user should run next to resume the flow.
|
|
# Under --dry-run the hint carries the flag through, so previewing one phase
|
|
# suggests previewing the next.
|
|
next() {
|
|
echo
|
|
bold "Next:"
|
|
local cmd
|
|
for cmd in "$@"; do
|
|
[[ "$DRY_RUN" == true ]] && cmd+=" --dry-run"
|
|
printf ' %s\n' "$cmd"
|
|
done
|
|
}
|
|
|
|
# ---- Config (auto-detect, env-overridable) ----
|
|
|
|
FULL_NAME="${FULL_NAME:-$(git -C "$PROJECT_DIR" config user.name)}"
|
|
EMAIL="${EMAIL:-$(git -C "$PROJECT_DIR" config user.email)}"
|
|
USERNAME="${USERNAME:-${USER:-}}"
|
|
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
|
|
SIGNING_KEY="${SIGNING_KEY:-$HOME/.ssh/apt_signing_key}"
|
|
FILES_HOST="${FILES_HOST:-files.radicle.dev}"
|
|
EXPECTED_BRANCH="${EXPECTED_BRANCH:-main}"
|
|
|
|
# ---- Version helpers ----
|
|
|
|
# Reads the current version from tauri.conf.json.
|
|
conf_version() {
|
|
jq -r '.version' "$TAURI_CONF"
|
|
}
|
|
|
|
# The SHA of the arch-package commit for $1, looked up by its commit subject.
|
|
# This is the second commit of the release patch (its tip) and the one we tag.
|
|
arch_commit_sha() {
|
|
git -C "$PROJECT_DIR" log --grep="^Update arch package to v$1\$" -1 --format=%H
|
|
}
|
|
|
|
# ---- Phases ----
|
|
|
|
phase_prep() {
|
|
local version="$1"
|
|
[[ -n "$version" ]] || die "prep requires --version X.Y.Z"
|
|
require_docker
|
|
|
|
step "Preparing release v$version"
|
|
|
|
info "Syncing and fetching"
|
|
run rad sync
|
|
run git -C "$PROJECT_DIR" fetch
|
|
|
|
info "Creating branch release/v$version"
|
|
run git -C "$PROJECT_DIR" checkout -b "release/v$version"
|
|
|
|
step "Changelog material (for the v$version entry)"
|
|
local prev
|
|
prev="$(git -C "$PROJECT_DIR" log --grep='^Release v' -1 --format=%H || true)"
|
|
if [[ -n "$prev" ]]; then
|
|
info "Commits since $(git -C "$PROJECT_DIR" log -1 --format='%s' "$prev"):"
|
|
echo
|
|
git -C "$PROJECT_DIR" log "$prev"..HEAD --format='- %s (%an <%ae>)'
|
|
echo
|
|
bold " External contributors (map these to DID links for the shout-out):"
|
|
echo
|
|
git -C "$PROJECT_DIR" log "$prev"..HEAD --format='%an <%ae>' \
|
|
| sort -u | grep -v -F "$EMAIL" || true
|
|
echo
|
|
info "Format the list into the user-facing changes for the CHANGELOG entry."
|
|
else
|
|
warn "no previous 'Release v*' commit found; writing the entry from scratch"
|
|
fi
|
|
|
|
if ! confirm "Continue and open CHANGELOG.md in \$EDITOR?"; then
|
|
die "Aborted before editing CHANGELOG."
|
|
fi
|
|
|
|
step "Bumping version to $version"
|
|
run sed -i '' -E "s/^( \"version\": \")[^\"]+(\",)$/\1$version\2/" "$TAURI_CONF"
|
|
[[ "$DRY_RUN" == true ]] || info "tauri.conf.json -> $(conf_version)"
|
|
|
|
step "Editing CHANGELOG.md"
|
|
info "Add the v$version entry (Features, Bug fixes, Shout-out block)."
|
|
run "${EDITOR:-vi}" "$CHANGELOG"
|
|
|
|
step "Creating Release Commit"
|
|
run git -C "$PROJECT_DIR" commit -a -m "Release v$version"
|
|
local sha
|
|
sha="$(git -C "$PROJECT_DIR" rev-parse HEAD)"
|
|
info "Release commit: $sha"
|
|
|
|
step "Updating Arch package to v$version"
|
|
run sed -i '' -E "s/^_commit=.*/_commit=$sha/" "$PKGBUILD"
|
|
run sed -i '' -E "s/^pkgver=.*/pkgver='$version'/" "$PKGBUILD"
|
|
run_sh "$PROJECT_DIR/arch" ./generate-srcinfo.sh
|
|
run git -C "$PROJECT_DIR" add arch/radicle-desktop/.SRCINFO arch/radicle-desktop/PKGBUILD
|
|
run git -C "$PROJECT_DIR" commit -m "Update arch package to v$version"
|
|
|
|
step "Pushing Release Patch"
|
|
if confirm "Push the Release Patch now (git push rad HEAD:refs/patches)?"; then
|
|
run git -C "$PROJECT_DIR" push rad HEAD:refs/patches
|
|
info "Wait for CI to pass, get the patch approved, and merge it into main."
|
|
else
|
|
info "Skipped. Push it yourself when ready: git push rad HEAD:refs/patches"
|
|
fi
|
|
next "scripts/release --phase build"
|
|
}
|
|
|
|
phase_build() {
|
|
local version
|
|
version="$(conf_version)"
|
|
require_podman
|
|
step "Building release artifacts for v$version"
|
|
run "$SCRIPT_DIR/build-artifacts"
|
|
|
|
step "Build complete"
|
|
info "Install releases/v$version/radicle-desktop-aarch64.dmg and verify it runs."
|
|
next "scripts/release --phase apt"
|
|
}
|
|
|
|
phase_apt() {
|
|
local version
|
|
version="$(conf_version)"
|
|
local deb="radicle-desktop_${version}_amd64.deb"
|
|
local src="$PROJECT_DIR/releases/v$version/$deb"
|
|
|
|
step "Staging the Debian package for signing"
|
|
[[ -f "$src" ]] || fail "$src not found — run --phase build first"
|
|
[[ -n "$APT_REPO_DIR" ]] || fail "../radicle-apt-repo not found"
|
|
run cp "$src" "${APT_REPO_DIR:-../radicle-apt-repo}/"
|
|
info "Copied $deb -> ${APT_REPO_DIR:-../radicle-apt-repo}/"
|
|
|
|
step "Sign and publish the package from radicle-apt-repo"
|
|
info "Run these in $APT_REPO_DIR:"
|
|
echo
|
|
cat <<EOF
|
|
rad sync && git fetch
|
|
|
|
docker build -t radicle-apt scripts
|
|
docker run --rm -it \\
|
|
-v "\$(pwd):/src/apt" \\
|
|
-v "$SIGNING_KEY:/src/keys/signing-key" \\
|
|
-e SIGNING_KEY=/src/keys/signing-key \\
|
|
-e DEBEMAIL="$EMAIL" \\
|
|
-e DEBFULLNAME="$FULL_NAME" \\
|
|
-e SSH_USER="$USERNAME" \\
|
|
-v "$SSH_KEY:/root/.ssh/id_ed25519:ro" \\
|
|
radicle-apt
|
|
|
|
# Inside the container:
|
|
RADICLE_APT_SIGNING_KEY=/src/keys/signing-key ./import-deb $deb
|
|
|
|
# Back on the host:
|
|
git checkout -b release-v$version
|
|
git add db/ dists/ pool/
|
|
git commit -m "Update radicle-desktop to $version"
|
|
git push rad HEAD:refs/patches
|
|
# After peer approval:
|
|
git checkout main && git merge --ff-only release-v$version && git push
|
|
make publish
|
|
EOF
|
|
next "scripts/release --phase publish"
|
|
}
|
|
|
|
phase_publish() {
|
|
local version
|
|
version="$(conf_version)"
|
|
local release_dir="$PROJECT_DIR/releases/v$version"
|
|
local remote="/var/www/$FILES_HOST/releases/radicle-desktop"
|
|
local key target
|
|
key="$(rad path)/keys/radicle"
|
|
target="$USERNAME@$FILES_HOST"
|
|
|
|
step "Publishing release files to $FILES_HOST"
|
|
[[ -d "$release_dir" ]] || fail "$release_dir not found — run --phase build first"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
info "Would check whether $remote/$version exists and prompt before overwrite."
|
|
elif ssh -i "$key" "$target" "test -d '$remote/$version'"; then
|
|
warn "$remote/$version already exists on $FILES_HOST"
|
|
confirm "Overwrite it and re-point latest?" || die "Aborted."
|
|
fi
|
|
|
|
info "Uploading to $remote/$version/"
|
|
run ssh -i "$key" "$target" "mkdir -p '$remote/$version'"
|
|
run scp -i "$key" "$release_dir"/* "$target:$remote/$version/"
|
|
|
|
info "Pointing latest -> $version"
|
|
run ssh -i "$key" "$target" "ln -sfn '$version' '$remote/latest'"
|
|
|
|
step "Published"
|
|
info "https://$FILES_HOST/releases/radicle-desktop/$version/"
|
|
info "latest now resolves to $version (update checker reads latest/latest.json)."
|
|
next "scripts/release --phase arch"
|
|
}
|
|
|
|
phase_arch() {
|
|
local version
|
|
version="$(conf_version)"
|
|
step "Publishing the Arch package to the AUR"
|
|
run_sh "$PROJECT_DIR/arch" ./publish.sh "Release v$version"
|
|
|
|
step "AUR package pushed"
|
|
info "Verify the version at https://aur.archlinux.org/packages/radicle-desktop"
|
|
next "scripts/release --phase tag"
|
|
}
|
|
|
|
phase_tag() {
|
|
local version
|
|
version="$(conf_version)"
|
|
step "Tagging canonical release v$version"
|
|
|
|
local sha
|
|
sha="$(arch_commit_sha "$version")"
|
|
[[ -n "$sha" ]] || fail "could not find the 'Update arch package to v$version' commit (is the patch merged into main?)"
|
|
info "Tagging commit: ${sha:-<arch-commit-sha>}"
|
|
|
|
run git -C "$PROJECT_DIR" tag "releases/$version" "${sha:-<arch-commit-sha>}" -m "Release $version"
|
|
run git -C "$PROJECT_DIR" push rad --tags
|
|
run rad sync
|
|
|
|
step "Canonical tag pushed — needs delegate approval"
|
|
info "Ask another delegate to approve the tag by running:"
|
|
echo
|
|
printf ' %s\n' \
|
|
"git push -f self $USERNAME/tags/releases/$version:refs/tags/releases/$version" \
|
|
"rad sync -a"
|
|
|
|
step "Announce on Zulip (once the tag is approved)"
|
|
local entry
|
|
entry="$(awk -v v="### v$version" '
|
|
$0 == v {grab=1}
|
|
grab && NR>1 && /^### v/ && $0 != v {exit}
|
|
grab {print}
|
|
' "$CHANGELOG")"
|
|
[[ -n "$entry" ]] || fail "no '### v$version' section found in CHANGELOG.md"
|
|
|
|
echo
|
|
bold " Copy the message below into the announcements channel:"
|
|
echo " --------------------------------------------------------------------"
|
|
echo "@**all** A new Radicle Desktop release is out :point_right: [give it a try](https://radicle.network/desktop)!"
|
|
echo
|
|
echo "$entry"
|
|
echo " --------------------------------------------------------------------"
|
|
echo
|
|
info "Post it under a topic named: radicle-desktop v$version"
|
|
info "Then resolve the previous release topic."
|
|
echo
|
|
bold "Release v$version complete."
|
|
}
|
|
|
|
# ---- Dispatch ----
|
|
|
|
usage() {
|
|
awk 'NR==1 {next} /^#/ {sub(/^# ?/, ""); print; next} {exit}' "$0"
|
|
}
|
|
|
|
list_phases() {
|
|
echo "Phases (run in order):"
|
|
printf ' %s\n' "${PHASES[@]}"
|
|
}
|
|
|
|
main() {
|
|
local phase="" version=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--phase) [[ $# -ge 2 ]] || die "--phase needs a value (try --list)"
|
|
phase="$2"; shift 2 ;;
|
|
--version) [[ $# -ge 2 ]] || die "--version needs a value, e.g. X.Y.Z"
|
|
version="$2"; shift 2 ;;
|
|
--dry-run|-n) DRY_RUN=true; shift ;;
|
|
--list) list_phases; exit 0 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) die "Unknown option: $1 (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
[[ -n "$phase" ]] || { usage; exit 1; }
|
|
|
|
[[ "$DRY_RUN" == true ]] && bold "DRY RUN — no commands will be executed"
|
|
|
|
require_branch
|
|
|
|
CURRENT_PHASE="$phase"
|
|
trap on_error ERR
|
|
|
|
case "$phase" in
|
|
prep) phase_prep "$version" ;;
|
|
build) phase_build ;;
|
|
apt) phase_apt ;;
|
|
publish) phase_publish ;;
|
|
arch) phase_arch ;;
|
|
tag) phase_tag ;;
|
|
*) die "Unknown phase: $phase (try --list)" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|