2026-02-12 12:29:02 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
cat <<EOF
|
|
|
|
|
Run end-to-end release flow in one command.
|
|
|
|
|
|
|
|
|
|
Usage:
|
2026-02-25 13:07:05 +00:00
|
|
|
scripts/release_all.sh <tag> [notarized] [--date YYYY-MM-DD] [--skip-notarized] [--self-hosted] [--github-hosted] [--enterprise-selfhosted] [--autostash] [--dry-run] [--from <step>] [--to <step>] [--retag] [--resume-auto] [--skip-homebrew-wait]
|
2026-02-12 12:29:02 +00:00
|
|
|
|
|
|
|
|
Examples:
|
2026-02-12 23:58:32 +00:00
|
|
|
scripts/release_all.sh v0.4.9
|
2026-02-14 01:38:47 +00:00
|
|
|
scripts/release_all.sh v0.4.9 notarized
|
2026-02-12 23:58:32 +00:00
|
|
|
scripts/release_all.sh 0.4.9 --date 2026-02-12
|
2026-02-13 14:28:48 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --self-hosted
|
2026-02-13 17:53:09 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --enterprise-selfhosted
|
2026-02-12 23:58:32 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --github-hosted
|
2026-02-16 21:12:55 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --autostash
|
2026-02-12 23:58:32 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --dry-run
|
2026-02-18 18:46:29 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --from notarize
|
|
|
|
|
scripts/release_all.sh v0.4.9 --to preflight
|
|
|
|
|
scripts/release_all.sh v0.4.9 --retag
|
|
|
|
|
scripts/release_all.sh v0.4.9 --resume-auto
|
2026-02-25 13:07:05 +00:00
|
|
|
scripts/release_all.sh v0.4.9 --skip-homebrew-wait
|
2026-02-12 12:29:02 +00:00
|
|
|
|
|
|
|
|
What it does:
|
2026-02-12 23:58:32 +00:00
|
|
|
1) Run release preflight checks (docs + build + icon payload + tests)
|
|
|
|
|
2) Prepare README/CHANGELOG docs
|
|
|
|
|
3) Commit docs changes
|
|
|
|
|
4) Create annotated tag
|
|
|
|
|
5) Push main and tag to origin
|
2026-02-13 14:28:48 +00:00
|
|
|
6) Trigger notarized release workflow (GitHub-hosted by default)
|
2026-02-12 23:58:32 +00:00
|
|
|
7) Wait for notarized workflow and verify uploaded release asset payload
|
2026-02-13 17:53:09 +00:00
|
|
|
|
2026-02-12 12:29:02 +00:00
|
|
|
EOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if [[ "${1:-}" == "" || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
|
|
|
usage
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
RAW_TAG="$1"
|
|
|
|
|
shift || true
|
|
|
|
|
|
|
|
|
|
TAG="$RAW_TAG"
|
|
|
|
|
if [[ "$TAG" != v* ]]; then
|
|
|
|
|
TAG="v$TAG"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
DATE_ARG=()
|
2026-02-12 23:58:32 +00:00
|
|
|
TRIGGER_NOTARIZED=1
|
2026-02-13 14:28:48 +00:00
|
|
|
USE_SELF_HOSTED=0
|
2026-02-13 17:53:09 +00:00
|
|
|
ENTERPRISE_SELF_HOSTED=0
|
2026-02-16 21:12:55 +00:00
|
|
|
AUTOSTASH=0
|
2026-02-12 23:58:32 +00:00
|
|
|
DRY_RUN=0
|
2026-02-18 18:46:29 +00:00
|
|
|
START_FROM="docs"
|
|
|
|
|
STOP_AFTER="notarize"
|
|
|
|
|
STOP_AFTER_SET=0
|
|
|
|
|
START_FROM_SET=0
|
|
|
|
|
RETAG=0
|
|
|
|
|
RESUME_AUTO=0
|
2026-02-25 13:07:05 +00:00
|
|
|
WAIT_FOR_HOMEBREW_TAP=1
|
2026-02-18 18:46:29 +00:00
|
|
|
|
|
|
|
|
step_index() {
|
|
|
|
|
case "$1" in
|
|
|
|
|
docs) echo 1 ;;
|
|
|
|
|
preflight) echo 2 ;;
|
|
|
|
|
prep) echo 3 ;;
|
|
|
|
|
notarize) echo 4 ;;
|
|
|
|
|
*)
|
|
|
|
|
echo "Unknown step: $1. Valid steps: docs, preflight, prep, notarize." >&2
|
|
|
|
|
return 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
step_enabled() {
|
|
|
|
|
local step="$1"
|
|
|
|
|
local start_idx stop_idx current_idx
|
|
|
|
|
start_idx="$(step_index "$START_FROM")"
|
|
|
|
|
stop_idx="$(step_index "$STOP_AFTER")"
|
|
|
|
|
current_idx="$(step_index "$step")"
|
|
|
|
|
(( current_idx >= start_idx && current_idx <= stop_idx ))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gh_retry() {
|
|
|
|
|
local attempts="${GH_RETRY_ATTEMPTS:-5}"
|
|
|
|
|
local base_sleep="${GH_RETRY_BASE_SLEEP:-2}"
|
|
|
|
|
local n=1
|
|
|
|
|
|
|
|
|
|
while true; do
|
|
|
|
|
if "$@"; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
if (( n >= attempts )); then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
sleep $((base_sleep * n))
|
|
|
|
|
n=$((n + 1))
|
|
|
|
|
done
|
|
|
|
|
}
|
2026-02-12 12:29:02 +00:00
|
|
|
|
2026-02-25 13:07:05 +00:00
|
|
|
retry_cmd() {
|
|
|
|
|
local attempts="${RETRY_ATTEMPTS:-3}"
|
|
|
|
|
local sleep_seconds="${RETRY_SLEEP_SECONDS:-6}"
|
|
|
|
|
local n=1
|
|
|
|
|
|
|
|
|
|
while true; do
|
|
|
|
|
if "$@"; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
if (( n >= attempts )); then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
echo "Command failed; retrying (${n}/${attempts})..."
|
|
|
|
|
sleep "$sleep_seconds"
|
|
|
|
|
n=$((n + 1))
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 09:50:12 +00:00
|
|
|
release_exists_and_is_published() {
|
|
|
|
|
local tag_name="$1"
|
|
|
|
|
local is_draft
|
|
|
|
|
|
|
|
|
|
if ! gh release view "$tag_name" >/dev/null 2>&1; then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
is_draft="$(gh release view "$tag_name" --json isDraft --jq '.isDraft' 2>/dev/null || echo "false")"
|
|
|
|
|
[[ "$is_draft" != "true" ]]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refresh_download_metrics_if_needed() {
|
|
|
|
|
local tag_name="$1"
|
|
|
|
|
|
|
|
|
|
if ! step_enabled prep; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! release_exists_and_is_published "$tag_name"; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Checking README download metrics freshness for published release ${tag_name}..."
|
|
|
|
|
if scripts/update_download_metrics.py --check --require-traffic-api; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Refreshing stale download metrics before release preflight..."
|
|
|
|
|
scripts/update_download_metrics.py
|
|
|
|
|
git add README.md docs/images/release-download-trend.svg
|
|
|
|
|
|
|
|
|
|
if git diff --cached --quiet; then
|
|
|
|
|
echo "Download metrics refresh produced no staged changes."
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
NVE_SKIP_BUILD_NUMBER_BUMP=1 git commit -m "chore(release): refresh download metrics for ${tag_name}"
|
|
|
|
|
echo "Committed refreshed download metrics for ${tag_name}."
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 12:29:02 +00:00
|
|
|
while [[ "${1:-}" != "" ]]; do
|
|
|
|
|
case "$1" in
|
2026-02-14 01:38:47 +00:00
|
|
|
notarized|--notarized)
|
|
|
|
|
TRIGGER_NOTARIZED=1
|
|
|
|
|
;;
|
2026-02-12 12:29:02 +00:00
|
|
|
--date)
|
|
|
|
|
shift
|
|
|
|
|
if [[ -z "${1:-}" ]]; then
|
|
|
|
|
echo "Missing value for --date" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
DATE_ARG=(--date "$1")
|
|
|
|
|
;;
|
2026-02-12 23:58:32 +00:00
|
|
|
--skip-notarized)
|
|
|
|
|
TRIGGER_NOTARIZED=0
|
2026-02-12 12:29:02 +00:00
|
|
|
;;
|
2026-02-13 14:28:48 +00:00
|
|
|
--self-hosted)
|
|
|
|
|
USE_SELF_HOSTED=1
|
|
|
|
|
;;
|
2026-02-12 23:58:32 +00:00
|
|
|
--github-hosted)
|
|
|
|
|
USE_SELF_HOSTED=0
|
|
|
|
|
;;
|
2026-02-13 17:53:09 +00:00
|
|
|
--enterprise-selfhosted)
|
|
|
|
|
ENTERPRISE_SELF_HOSTED=1
|
|
|
|
|
USE_SELF_HOSTED=1
|
|
|
|
|
;;
|
2026-02-16 21:12:55 +00:00
|
|
|
--autostash)
|
|
|
|
|
AUTOSTASH=1
|
|
|
|
|
;;
|
2026-02-12 23:58:32 +00:00
|
|
|
--dry-run)
|
|
|
|
|
DRY_RUN=1
|
2026-02-12 22:20:39 +00:00
|
|
|
;;
|
2026-02-18 18:46:29 +00:00
|
|
|
--from)
|
|
|
|
|
shift
|
|
|
|
|
if [[ -z "${1:-}" ]]; then
|
|
|
|
|
echo "Missing value for --from" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
START_FROM="$1"
|
|
|
|
|
START_FROM_SET=1
|
|
|
|
|
step_index "$START_FROM" >/dev/null
|
|
|
|
|
;;
|
|
|
|
|
--to)
|
|
|
|
|
shift
|
|
|
|
|
if [[ -z "${1:-}" ]]; then
|
|
|
|
|
echo "Missing value for --to" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
STOP_AFTER="$1"
|
|
|
|
|
STOP_AFTER_SET=1
|
|
|
|
|
step_index "$STOP_AFTER" >/dev/null
|
|
|
|
|
;;
|
|
|
|
|
--retag)
|
|
|
|
|
RETAG=1
|
|
|
|
|
;;
|
|
|
|
|
--resume-auto)
|
|
|
|
|
RESUME_AUTO=1
|
|
|
|
|
;;
|
2026-02-25 13:07:05 +00:00
|
|
|
--skip-homebrew-wait)
|
|
|
|
|
WAIT_FOR_HOMEBREW_TAP=0
|
|
|
|
|
;;
|
2026-02-12 12:29:02 +00:00
|
|
|
*)
|
|
|
|
|
echo "Unknown argument: $1" >&2
|
|
|
|
|
usage
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
shift || true
|
|
|
|
|
done
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if [[ "$TRIGGER_NOTARIZED" -eq 0 && "$STOP_AFTER_SET" -eq 0 ]]; then
|
|
|
|
|
STOP_AFTER="prep"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ "$STOP_AFTER" != "notarize" ]]; then
|
|
|
|
|
TRIGGER_NOTARIZED=0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ "$RESUME_AUTO" -eq 1 ]]; then
|
|
|
|
|
local_tag_exists=0
|
|
|
|
|
remote_tag_exists=0
|
|
|
|
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
local_tag_exists=1
|
|
|
|
|
fi
|
|
|
|
|
if git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null | grep -q "refs/tags/${TAG}$"; then
|
|
|
|
|
remote_tag_exists=1
|
|
|
|
|
fi
|
|
|
|
|
if [[ "$local_tag_exists" -eq 0 && "$remote_tag_exists" -eq 1 ]]; then
|
|
|
|
|
git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}" >/dev/null 2>&1 || true
|
|
|
|
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
local_tag_exists=1
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
if [[ "$START_FROM_SET" -eq 0 ]]; then
|
|
|
|
|
if [[ "$local_tag_exists" -eq 1 && "$remote_tag_exists" -eq 1 && "$TRIGGER_NOTARIZED" -eq 1 ]]; then
|
|
|
|
|
START_FROM="notarize"
|
|
|
|
|
elif [[ "$local_tag_exists" -eq 1 ]]; then
|
|
|
|
|
START_FROM="prep"
|
|
|
|
|
else
|
|
|
|
|
START_FROM="docs"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-02-25 13:07:05 +00:00
|
|
|
if [[ "$local_tag_exists" -eq 1 && "$remote_tag_exists" -eq 0 ]]; then
|
|
|
|
|
CURRENT_BRANCH="$(git branch --show-current)"
|
|
|
|
|
if [[ "$CURRENT_BRANCH" != "main" ]]; then
|
|
|
|
|
echo "Local tag ${TAG} exists but origin tag is missing, and current branch is ${CURRENT_BRANCH}." >&2
|
|
|
|
|
echo "Switch to main and push the tag first:" >&2
|
|
|
|
|
echo " git checkout main && git push origin ${TAG}" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-02-18 18:46:29 +00:00
|
|
|
echo "Resume-auto selected: from=${START_FROM} to=${STOP_AFTER} (local_tag=${local_tag_exists}, remote_tag=${remote_tag_exists})"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if (( "$(step_index "$START_FROM")" > "$(step_index "$STOP_AFTER")" )); then
|
|
|
|
|
echo "--from ${START_FROM} is after --to ${STOP_AFTER}." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-12 12:29:02 +00:00
|
|
|
if ! command -v gh >/dev/null 2>&1; then
|
|
|
|
|
echo "gh CLI is required." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-25 13:07:05 +00:00
|
|
|
if ! gh auth status >/dev/null 2>&1; then
|
|
|
|
|
echo "gh is not authenticated. Run: gh auth login" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-16 21:12:55 +00:00
|
|
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
|
|
|
echo "This command must run inside a git repository." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
|
|
|
cd "$ROOT"
|
|
|
|
|
|
2026-02-25 13:07:05 +00:00
|
|
|
REPO_SLUG="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)"
|
|
|
|
|
if [[ -z "$REPO_SLUG" ]]; then
|
|
|
|
|
echo "Could not resolve repository slug from gh." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-16 21:12:55 +00:00
|
|
|
AUTO_STASHED=0
|
|
|
|
|
cleanup_autostash() {
|
|
|
|
|
if [[ "$AUTO_STASHED" -eq 1 ]]; then
|
|
|
|
|
if git stash pop --index >/dev/null 2>&1; then
|
|
|
|
|
echo "Restored stashed working tree changes."
|
|
|
|
|
else
|
|
|
|
|
echo "Auto-stash restore had conflicts. Changes remain in stash list; resolve manually." >&2
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
trap cleanup_autostash EXIT
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
REQUIRES_CLEAN_TREE=0
|
|
|
|
|
if step_enabled prep; then
|
|
|
|
|
REQUIRES_CLEAN_TREE=1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ "$AUTOSTASH" -eq 1 && "$REQUIRES_CLEAN_TREE" -eq 1 ]]; then
|
2026-02-16 21:12:55 +00:00
|
|
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
|
|
|
STASH_MSG="release_all autostash ${TAG} $(date +%Y-%m-%dT%H:%M:%S)"
|
|
|
|
|
git stash push --include-untracked -m "$STASH_MSG" >/dev/null
|
|
|
|
|
AUTO_STASHED=1
|
|
|
|
|
echo "Auto-stashed dirty working tree before release."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-13 00:34:45 +00:00
|
|
|
wait_for_pre_release_ci() {
|
|
|
|
|
local sha="$1"
|
|
|
|
|
local timeout_seconds=1800
|
|
|
|
|
local interval_seconds=15
|
|
|
|
|
local elapsed=0
|
|
|
|
|
|
|
|
|
|
echo "Waiting for Pre-release CI on ${sha}..."
|
|
|
|
|
while (( elapsed <= timeout_seconds )); do
|
|
|
|
|
local run_line
|
2026-02-18 18:46:29 +00:00
|
|
|
run_line="$(gh_retry gh run list \
|
2026-02-13 00:34:45 +00:00
|
|
|
--workflow pre-release-ci.yml \
|
|
|
|
|
--limit 50 \
|
|
|
|
|
--json databaseId,status,conclusion,headSha,event,createdAt \
|
2026-02-18 18:46:29 +00:00
|
|
|
--jq ".[] | select(.headSha == \"${sha}\" and .event == \"push\") | \"\(.databaseId)\t\(.status)\t\(.conclusion // \"\")\"" | head -n1 || true)"
|
2026-02-13 00:34:45 +00:00
|
|
|
|
|
|
|
|
if [[ -n "$run_line" ]]; then
|
|
|
|
|
local run_id run_status run_conclusion
|
|
|
|
|
run_id="$(echo "$run_line" | awk -F '\t' '{print $1}')"
|
|
|
|
|
run_status="$(echo "$run_line" | awk -F '\t' '{print $2}')"
|
|
|
|
|
run_conclusion="$(echo "$run_line" | awk -F '\t' '{print $3}')"
|
|
|
|
|
|
|
|
|
|
echo "Pre-release CI run ${run_id}: status=${run_status} conclusion=${run_conclusion:-pending}"
|
|
|
|
|
if [[ "$run_status" == "completed" ]]; then
|
|
|
|
|
if [[ "$run_conclusion" == "success" ]]; then
|
|
|
|
|
echo "Pre-release CI passed."
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
echo "Pre-release CI failed for ${sha}. Not starting notarized release." >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo "Pre-release CI run for ${sha} not visible yet; retrying..."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
sleep "$interval_seconds"
|
|
|
|
|
elapsed=$((elapsed + interval_seconds))
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo "Timed out waiting for Pre-release CI on ${sha}. Not starting notarized release." >&2
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
wait_for_homebrew_tap_update() {
|
|
|
|
|
local since="$1"
|
|
|
|
|
local timeout_seconds=1800
|
|
|
|
|
local interval_seconds=15
|
|
|
|
|
local elapsed=0
|
|
|
|
|
|
|
|
|
|
echo "Waiting for homebrew-tap run (since ${since})..."
|
|
|
|
|
while (( elapsed <= timeout_seconds )); do
|
|
|
|
|
local tap_line
|
|
|
|
|
tap_line="$(gh_retry gh run list -R h3pdesign/homebrew-tap \
|
|
|
|
|
--workflow update-cask.yml \
|
|
|
|
|
--event repository_dispatch \
|
|
|
|
|
--limit 30 \
|
|
|
|
|
--json databaseId,status,conclusion,displayTitle,createdAt \
|
|
|
|
|
--jq ".[] | select(.displayTitle == \"notarized_release\" and .createdAt >= \"${since}\") | \"\(.databaseId)\t\(.status)\t\(.conclusion // \"\")\"" | head -n1 || true)"
|
|
|
|
|
|
|
|
|
|
if [[ -n "$tap_line" ]]; then
|
|
|
|
|
local run_id run_status run_conclusion
|
|
|
|
|
run_id="$(echo "$tap_line" | awk -F '\t' '{print $1}')"
|
|
|
|
|
run_status="$(echo "$tap_line" | awk -F '\t' '{print $2}')"
|
|
|
|
|
run_conclusion="$(echo "$tap_line" | awk -F '\t' '{print $3}')"
|
|
|
|
|
echo "homebrew-tap run ${run_id}: status=${run_status} conclusion=${run_conclusion:-pending}"
|
|
|
|
|
if [[ "$run_status" == "completed" ]]; then
|
|
|
|
|
if [[ "$run_conclusion" == "success" ]]; then
|
|
|
|
|
echo "homebrew-tap update passed."
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
echo "homebrew-tap update failed (${run_id})." >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo "homebrew-tap run not visible yet; retrying..."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
sleep "$interval_seconds"
|
|
|
|
|
elapsed=$((elapsed + interval_seconds))
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo "Timed out waiting for homebrew-tap update run." >&2
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 13:07:05 +00:00
|
|
|
assert_workflow_exists() {
|
|
|
|
|
local workflow_name="$1"
|
|
|
|
|
if ! gh_retry gh workflow view "$workflow_name" >/dev/null 2>&1; then
|
|
|
|
|
echo "Workflow ${workflow_name} is not available in this repository." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_online_self_hosted_macos_runner() {
|
|
|
|
|
local runner_line
|
|
|
|
|
runner_line="$(
|
|
|
|
|
gh_retry gh api "repos/${REPO_SLUG}/actions/runners" \
|
|
|
|
|
--jq '.runners[] | select(.status == "online") | [.name, ([.labels[].name] | join(","))] | @tsv' \
|
|
|
|
|
| awk -F '\t' 'index($2, "self-hosted") && index($2, "macOS") { print; exit }'
|
|
|
|
|
)"
|
|
|
|
|
|
|
|
|
|
if [[ -z "$runner_line" ]]; then
|
|
|
|
|
echo "No online self-hosted macOS runner found for ${REPO_SLUG}." >&2
|
|
|
|
|
echo "Check: https://github.com/${REPO_SLUG}/settings/actions/runners" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "Using online runner: $(echo "$runner_line" | awk -F '\t' '{print $1}')"
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if [[ "$REQUIRES_CLEAN_TREE" -eq 1 && "$AUTOSTASH" -eq 0 && -n "$(git status --porcelain)" ]]; then
|
2026-02-16 21:12:55 +00:00
|
|
|
echo "Working tree is not clean. Commit/stash changes first, or rerun with --autostash." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-03-31 09:50:12 +00:00
|
|
|
refresh_download_metrics_if_needed "$TAG"
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if step_enabled docs; then
|
|
|
|
|
echo "Verifying release docs are up to date for ${TAG}..."
|
|
|
|
|
docs_check_cmd=(scripts/prepare_release_docs.py "$TAG" --check)
|
|
|
|
|
if [[ ${#DATE_ARG[@]} -gt 0 ]]; then
|
|
|
|
|
docs_check_cmd+=("${DATE_ARG[@]}")
|
|
|
|
|
fi
|
|
|
|
|
"${docs_check_cmd[@]}"
|
2026-02-14 22:59:13 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if step_enabled preflight; then
|
|
|
|
|
echo "Running release preflight for ${TAG}..."
|
|
|
|
|
scripts/ci/release_preflight.sh "$TAG"
|
|
|
|
|
fi
|
2026-02-12 23:58:32 +00:00
|
|
|
|
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
|
|
|
echo "Dry-run requested. Preflight completed; no commits/tags/workflows were created."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-19 10:45:00 +00:00
|
|
|
assert_tag_matches_head() {
|
|
|
|
|
local tag_name="$1"
|
|
|
|
|
local tag_sha head_sha
|
|
|
|
|
tag_sha="$(git rev-parse "${tag_name}^{commit}")"
|
|
|
|
|
head_sha="$(git rev-parse HEAD)"
|
|
|
|
|
if [[ "$tag_sha" != "$head_sha" ]]; then
|
|
|
|
|
echo "Tag ${tag_name} exists but does not point to HEAD." >&2
|
|
|
|
|
echo " tag: ${tag_sha}" >&2
|
|
|
|
|
echo " head: ${head_sha}" >&2
|
|
|
|
|
echo "Use --retag to repoint the tag before notarized release." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 18:58:03 +00:00
|
|
|
assert_remote_tag_matches_head() {
|
|
|
|
|
local tag_name="$1"
|
|
|
|
|
local remote_sha head_sha
|
|
|
|
|
remote_sha="$(git ls-remote --tags origin "refs/tags/${tag_name}^{}" | awk '{print $1}' | head -n1)"
|
|
|
|
|
if [[ -z "$remote_sha" ]]; then
|
|
|
|
|
remote_sha="$(git ls-remote --tags origin "refs/tags/${tag_name}" | awk '{print $1}' | head -n1)"
|
|
|
|
|
fi
|
|
|
|
|
if [[ -z "$remote_sha" ]]; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
head_sha="$(git rev-parse HEAD)"
|
|
|
|
|
if [[ "$remote_sha" != "$head_sha" ]]; then
|
|
|
|
|
echo "Remote tag ${tag_name} exists but does not point to HEAD." >&2
|
|
|
|
|
echo " tag: ${remote_sha}" >&2
|
|
|
|
|
echo " head: ${head_sha}" >&2
|
|
|
|
|
echo "Use --retag to repoint the tag before notarized release." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if step_enabled prep; then
|
|
|
|
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
if [[ "$RETAG" -eq 1 ]]; then
|
|
|
|
|
echo "Retag requested. Deleting existing ${TAG} locally and on origin (if present)..."
|
|
|
|
|
git tag -d "$TAG" >/dev/null 2>&1 || true
|
|
|
|
|
git push origin ":refs/tags/${TAG}" >/dev/null 2>&1 || true
|
|
|
|
|
else
|
2026-02-19 10:45:00 +00:00
|
|
|
assert_tag_matches_head "$TAG"
|
2026-02-23 18:58:03 +00:00
|
|
|
assert_remote_tag_matches_head "$TAG"
|
2026-02-18 18:46:29 +00:00
|
|
|
echo "Tag ${TAG} already exists. Skipping release prep. Use --retag to recreate it."
|
|
|
|
|
if [[ "$(git branch --show-current)" == "main" ]]; then
|
|
|
|
|
git push origin main
|
2026-02-23 18:58:03 +00:00
|
|
|
git push origin "$TAG"
|
2026-02-18 18:46:29 +00:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-02-12 12:29:02 +00:00
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
echo "Running release prep for ${TAG}..."
|
|
|
|
|
prep_cmd=(scripts/release_prep.sh "$TAG")
|
|
|
|
|
if [[ ${#DATE_ARG[@]} -gt 0 ]]; then
|
|
|
|
|
prep_cmd+=("${DATE_ARG[@]}")
|
|
|
|
|
fi
|
|
|
|
|
prep_cmd+=(--push)
|
|
|
|
|
"${prep_cmd[@]}"
|
|
|
|
|
echo "Tag push completed."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2026-02-12 12:29:02 +00:00
|
|
|
|
2026-02-18 18:46:29 +00:00
|
|
|
if [[ "$TRIGGER_NOTARIZED" -eq 1 ]] && step_enabled notarize; then
|
|
|
|
|
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
echo "Tag ${TAG} does not exist. Run prep first or use --from prep." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
RELEASE_SHA="$(git rev-parse "${TAG}^{commit}")"
|
2026-02-13 00:34:45 +00:00
|
|
|
wait_for_pre_release_ci "$RELEASE_SHA"
|
|
|
|
|
|
2026-02-12 12:29:02 +00:00
|
|
|
echo "Triggering notarized workflow for ${TAG}..."
|
2026-02-13 17:53:09 +00:00
|
|
|
if [[ "$ENTERPRISE_SELF_HOSTED" -eq 1 ]]; then
|
|
|
|
|
echo "Enterprise self-hosted mode enabled (expects self-hosted runner labels and GH_HOST if required)."
|
|
|
|
|
fi
|
2026-02-12 22:20:39 +00:00
|
|
|
if [[ "$USE_SELF_HOSTED" -eq 1 ]]; then
|
2026-02-25 13:07:05 +00:00
|
|
|
assert_workflow_exists "release-notarized-selfhosted.yml"
|
|
|
|
|
assert_online_self_hosted_macos_runner
|
2026-02-18 18:46:29 +00:00
|
|
|
DISPATCHED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
|
|
|
gh_retry gh workflow run release-notarized-selfhosted.yml -f tag="$TAG" -f use_self_hosted=true
|
2026-02-12 23:58:32 +00:00
|
|
|
WORKFLOW_NAME="release-notarized-selfhosted.yml"
|
|
|
|
|
echo "Triggered: ${WORKFLOW_NAME} (tag=${TAG}, use_self_hosted=true)"
|
2026-02-12 22:20:39 +00:00
|
|
|
else
|
2026-02-25 13:07:05 +00:00
|
|
|
assert_workflow_exists "release-notarized.yml"
|
2026-02-18 18:46:29 +00:00
|
|
|
DISPATCHED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
|
|
|
gh_retry gh workflow run release-notarized.yml -f tag="$TAG"
|
2026-02-12 23:58:32 +00:00
|
|
|
WORKFLOW_NAME="release-notarized.yml"
|
|
|
|
|
echo "Triggered: ${WORKFLOW_NAME} (tag=${TAG})"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Waiting for ${WORKFLOW_NAME} run..."
|
|
|
|
|
sleep 6
|
2026-02-18 18:46:29 +00:00
|
|
|
RUN_ID=""
|
|
|
|
|
for _ in {1..20}; do
|
|
|
|
|
RUN_ID="$(gh_retry gh run list --workflow "$WORKFLOW_NAME" --limit 30 --json databaseId,displayTitle,createdAt --jq ".[] | select((.displayTitle | contains(\"${TAG}\")) and .createdAt >= \"${DISPATCHED_AT}\") | .databaseId" | head -n1 || true)"
|
|
|
|
|
if [[ -n "$RUN_ID" ]]; then
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 6
|
|
|
|
|
done
|
2026-02-12 23:58:32 +00:00
|
|
|
if [[ -z "$RUN_ID" ]]; then
|
|
|
|
|
echo "Could not find workflow run for ${TAG}." >&2
|
|
|
|
|
exit 1
|
2026-02-12 22:20:39 +00:00
|
|
|
fi
|
2026-02-25 13:07:05 +00:00
|
|
|
if ! gh_retry gh run watch "$RUN_ID" --exit-status; then
|
|
|
|
|
echo "Workflow run ${RUN_ID} failed. Showing failed job logs..." >&2
|
|
|
|
|
gh_retry gh run view "$RUN_ID" --log-failed || true
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if ! retry_cmd scripts/ci/verify_release_asset.sh "$TAG"; then
|
|
|
|
|
echo "Release asset verification failed for ${TAG} after retries." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if [[ "$WAIT_FOR_HOMEBREW_TAP" -eq 1 ]]; then
|
|
|
|
|
wait_for_homebrew_tap_update "$DISPATCHED_AT"
|
|
|
|
|
else
|
|
|
|
|
echo "Skipping homebrew-tap wait (--skip-homebrew-wait)."
|
|
|
|
|
fi
|
2026-02-12 12:29:02 +00:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "Done."
|
|
|
|
|
echo "Check runs:"
|
2026-02-12 23:58:32 +00:00
|
|
|
echo " gh run list --workflow pre-release-ci.yml --limit 5"
|
|
|
|
|
echo " gh run list --workflow release-dry-run.yml --limit 5"
|
2026-02-12 22:20:39 +00:00
|
|
|
echo " gh run list --workflow release-notarized.yml --limit 5"
|
2026-02-12 19:57:25 +00:00
|
|
|
echo " gh run list --workflow release-notarized-selfhosted.yml --limit 5"
|