diff --git a/README.md b/README.md index c873c3126..034cc6c31 100644 --- a/README.md +++ b/README.md @@ -465,6 +465,7 @@ The [`utils/`](utils/) directory contains helper scripts for common workflows. S | Script | Description | |--------|-------------| | `release.sh` | Trigger a validated release (major/minor/patch) via GitHub Actions | +| `prepare-patch.sh` | Backport a batch of pending main PRs onto a release branch before a patch release | | `backport-pr.sh` | Cherry-pick a merged PR onto a release branch and open a backport PR | | `patch-dd-java-agent.sh` | Patch `dd-java-agent.jar` with a local ddprof build for quick testing | | `run-containers-tests.sh` | Run tests in containers (musl/glibc, multiple JDKs) | diff --git a/utils/README.md b/utils/README.md index 112176a3a..2d94086a2 100644 --- a/utils/README.md +++ b/utils/README.md @@ -40,20 +40,29 @@ Triggers the Validated Release workflow using GitHub CLI to create a new release **Release flow:** 1. Validates inputs and branch rules -2. Interactive commit selection (or use `--commit`) -3. Triggers GitHub Actions "Validated Release" workflow -4. Workflow runs pre-release tests, creates the annotated tag, and opens an +2. Fetches and verifies the selected branch is up to date with + `origin` (a checked-out local branch that's behind or ahead of origin + fails fast with pull instructions) +3. For patch releases, asks whether to pick PRs from `main` to backport onto + the release branch first; if you accept, you select which PRs, a combined + backport PR is opened, and the script exits so you can merge it and re-run +4. Interactive commit selection (or use `--commit`) +5. Triggers GitHub Actions "Validated Release" workflow +6. Workflow runs pre-release tests, creates the annotated tag, and opens an exact single-commit version-bump PR as `github-actions[bot]` -5. The final commit is pushed through the release SSH identity, producing the +7. The final commit is pushed through the release SSH identity, producing the `synchronize` event that starts normal PR CI even though `GITHUB_TOKEN` created the PR -6. A separate `dd-octo-sts[bot]` identity adds `trivial`; the approval workflow +8. A separate `dd-octo-sts[bot]` identity adds `trivial`; the approval workflow validates permissions, refs, SHAs, and the exact one-line version diff before approving that exact commit -7. The release workflow waits for the exact approval and the aggregate +9. The release workflow waits for the exact approval and the aggregate `release-bump-ci` check, then performs the SHA-locked squash merge itself -8. Tag push triggers GitLab, which publishes the Maven artifacts, and the - GitHub release workflows attach the release assets +10. Tag push triggers GitLab, which publishes the Maven artifacts, and the + GitHub release workflows attach the release assets + +Interactive pickers (branch and commit selection) support ↑/↓/Enter, and +can be cancelled at any time with `q` or Ctrl-C. For a major release, the generated `N.0.0` commit remains on `release/N.0._` and is tagged there. The bump PR moves `main` directly from @@ -114,6 +123,35 @@ Cherry-picks a merged PR onto a release branch, pushes the backport branch, and ./utils/backport-pr.sh --dry-run 1.9._ 420 ``` +### `prepare-patch.sh` + +Finds PRs merged to `main` since a release branch diverged, lets you +multi-select which ones to backport, cherry-picks them onto a single new +branch off the release branch, runs `./gradlew buildDebug` to catch a +combination that doesn't build before pushing, and opens one combined PR. +Also invoked interactively from `release.sh` when preparing a patch release. + +**Prerequisites:** +- [GitHub CLI](https://cli.github.com/) installed and authenticated +- [jq](https://jqlang.github.io/jq/) installed +- Clean working tree + +**Usage:** +```bash +./utils/prepare-patch.sh [--branch release/X.Y._] [--no-dry-run] +``` + +**Options:** +- `--branch `: Release branch to prepare. If omitted, an interactive picker is shown. +- `--no-dry-run`: Actually cherry-pick, push, and open the PR (default is dry-run). + +**Examples:** +```bash +./utils/prepare-patch.sh --branch release/1.9._ # dry-run, preview only +./utils/prepare-patch.sh # interactive branch selection, dry-run +./utils/prepare-patch.sh --no-dry-run --branch release/1.9._ +``` + --- ## Testing diff --git a/utils/prepare-patch.sh b/utils/prepare-patch.sh new file mode 100755 index 000000000..01afbb6ff --- /dev/null +++ b/utils/prepare-patch.sh @@ -0,0 +1,476 @@ +#!/bin/bash +set -euo pipefail + +# Copyright 2026, Datadog, Inc + +# Cherry-picks PRs merged to main since a release branch diverged, to prepare +# that branch for its next patch release. +# +# Usage: +# ./utils/prepare-patch.sh [--branch release/X.Y._] [--no-dry-run] +# +# Flow: +# 1. Select (or take via --branch) the release branch. +# 2. List PRs merged to main after the branch's divergence point. +# 3. Multi-select which PRs to backport. +# 4. Cherry-pick their commits onto a new branch off the release branch. +# 5. Push and open one combined PR against the release branch. + +# --- Colors & helpers -------------------------------------------------------- +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m' + CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; RESET='' +fi +info() { echo -e "${GREEN}✓${RESET} $*"; } +warn() { echo -e "${YELLOW}⚠${RESET} $*"; } +error() { echo -e "${RED}✗${RESET} $*" >&2; } +step() { echo -e "${CYAN}→${RESET} ${BOLD}$*${RESET}"; } + +# --- Cleanup trap ------------------------------------------------------------ +CURRENT_BRANCH="" +PREP_BRANCH="" +CHERRY_PICK_IN_PROGRESS=0 +BUILD_FAILED=0 + +cleanup() { + local exit_code=$? + if [ $exit_code -ne 0 ]; then + if [ $CHERRY_PICK_IN_PROGRESS -eq 1 ]; then + echo "" + error "Cherry-pick failed — likely a conflict." + echo "" + echo -e " ${BOLD}Option 1: Resolve manually${RESET}" + echo -e " 1. Fix the conflicts in the listed files" + echo -e " 2. ${CYAN}git add ${RESET}" + echo -e " 3. ${CYAN}git cherry-pick --continue${RESET}" + echo -e " 4. ${CYAN}git push -u origin $PREP_BRANCH${RESET}" + echo -e " 5. Create the PR manually or re-run this script" + echo "" + echo -e " ${BOLD}Option 2: Abort and go back${RESET}" + echo -e " 1. ${CYAN}git cherry-pick --abort${RESET}" + echo -e " 2. ${CYAN}git checkout $CURRENT_BRANCH${RESET}" + echo -e " 3. ${CYAN}git branch -D $PREP_BRANCH${RESET}" + echo "" + return + fi + if [ $BUILD_FAILED -eq 1 ]; then + echo "" + error "Build failed after cherry-picking onto $PREP_BRANCH — the selected PRs don't build together." + echo "" + echo -e " ${BOLD}Option 1: Fix and continue${RESET}" + echo -e " 1. Fix the build on $PREP_BRANCH (currently checked out)" + echo -e " 2. ${CYAN}./gradlew buildDebug${RESET}" + echo -e " 3. ${CYAN}git push -u origin $PREP_BRANCH${RESET}" + echo -e " 4. Create the PR manually or re-run this script" + echo "" + echo -e " ${BOLD}Option 2: Abort and go back${RESET}" + echo -e " 1. ${CYAN}git checkout $CURRENT_BRANCH${RESET}" + echo -e " 2. ${CYAN}git branch -D $PREP_BRANCH${RESET}" + echo "" + return + fi + if [ -n "$CURRENT_BRANCH" ]; then + warn "Restoring original branch ($CURRENT_BRANCH)" + git checkout "$CURRENT_BRANCH" 2>/dev/null || true + fi + fi +} +trap cleanup EXIT +trap 'echo ""; warn "Interrupted"; exit 130' INT + +# --- Argument parsing -------------------------------------------------------- +usage() { + echo -e "${BOLD}Usage:${RESET} $0 [--branch release/X.Y._] [--no-dry-run]" + echo "" + echo " --branch Release branch to prepare (if omitted, pick from a list)" + echo " --no-dry-run Actually cherry-pick, push, and open the PR (default is dry-run)" + exit 1 +} + +RELEASE_BRANCH="" +DRY_RUN=1 + +while [ $# -gt 0 ]; do + case "$1" in + --branch) + [ -n "${2:-}" ] || usage + RELEASE_BRANCH="$2" + shift 2 + ;; + --no-dry-run) DRY_RUN=0; shift ;; + --help|-h) usage ;; + *) usage ;; + esac +done + +# --- Check requirements ------------------------------------------------------ +step "Checking prerequisites" + +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +for cmd in gh jq; do + $cmd --version 1>/dev/null 2>&1 || { error "$cmd is not installed"; exit 1; } +done +info "gh and jq are available" + +if [ -n "$(git status --porcelain)" ]; then + error "Working tree is not clean. Please stash or commit your changes first." + exit 1 +fi +info "Working tree is clean" + +git fetch --quiet origin +info "Fetched latest from origin" + +# --- Read a single keypress (arrow keys, space, enter, a, q) ---------------- +read_key() { + local key="" + if ! IFS= read -rsn1 key to specify a release branch" + exit 1 + fi + + local selected=0 + local total=${#branches[@]} + local page_size=10 + local visible=$(( total < page_size ? total : page_size )) + + display_menu() { + clear >&2 + echo "" >&2 + echo -e "${CYAN}${BOLD}Select the release branch to prepare${RESET}" >&2 + echo "Use ↑/↓ to navigate, Enter to select, 'q' to quit" >&2 + echo "" >&2 + for ((i = 0; i < visible; i++)); do + if [ "$i" -eq "$selected" ]; then + echo -e "${GREEN}→ ${branches[$i]}${RESET}" >&2 + else + echo -e " ${branches[$i]}" >&2 + fi + done + if [ "$visible" -lt "$total" ]; then + local remaining=$((total - visible)) + if [ "$selected" -eq "$visible" ]; then + echo -e "${GREEN}→ … show more ($remaining remaining)${RESET}" >&2 + else + echo -e " … show more ($remaining remaining)" >&2 + fi + fi + echo "" >&2 + } + + while true; do + display_menu + key=$(read_key) + local menu_rows=$visible + [ "$visible" -lt "$total" ] && menu_rows=$((visible + 1)) + case $key in + up) [ $selected -gt 0 ] && selected=$((selected - 1)) ;; + down) [ $selected -lt $((menu_rows - 1)) ] && selected=$((selected + 1)) ;; + enter) + if [ "$visible" -lt "$total" ] && [ "$selected" -eq "$visible" ]; then + visible=$(( visible + page_size < total ? visible + page_size : total )) + else + echo "${branches[$selected]}" + return 0 + fi + ;; + quit) + echo "" >&2 + info "Selection cancelled" + exit 0 + ;; + esac + done +} + +if [ -z "$RELEASE_BRANCH" ]; then + step "Select a release branch" + RELEASE_BRANCH=$(select_release_branch) + clear +fi + +git show-ref --verify --quiet "refs/remotes/origin/$RELEASE_BRANCH" 2>/dev/null || { + error "Branch $RELEASE_BRANCH does not exist on origin" + exit 1 +} +info "Target branch: $RELEASE_BRANCH" + +# --- Find PRs merged to main after the branch diverged ----------------------- +step "Looking for PRs merged to main since $RELEASE_BRANCH diverged" + +CANDIDATE_SHAS=() +while IFS= read -r sign sha _; do + [ "$sign" == "+" ] && CANDIDATE_SHAS+=("$sha") +done < <(git cherry "origin/$RELEASE_BRANCH" "origin/main") + +if [ ${#CANDIDATE_SHAS[@]} -eq 0 ]; then + info "No commits on main ahead of $RELEASE_BRANCH. Nothing to prepare." + exit 0 +fi + +MERGED_PRS_JSON=$(gh pr list --base main --state merged --limit 200 \ + --json number,title,mergedAt,mergeCommit,labels) + +CANDIDATE_SET=$(printf '%s\n' "${CANDIDATE_SHAS[@]}" | jq -R -s 'split("\n") | map(select(length > 0))') + +PR_LIST_JSON=$(jq -n --argjson prs "$MERGED_PRS_JSON" --argjson shas "$CANDIDATE_SET" ' + $prs + | map(select(.mergeCommit.oid as $c | $shas | index($c) != null)) + | sort_by(.mergedAt) +') + +PR_COUNT=$(echo "$PR_LIST_JSON" | jq 'length') +if [ "$PR_COUNT" -eq 0 ]; then + info "No merged PRs found ahead of $RELEASE_BRANCH. Nothing to prepare." + exit 0 +fi + +PR_NUMBERS=() +while IFS= read -r line; do + PR_NUMBERS+=("$line") +done < <(echo "$PR_LIST_JSON" | jq -r '.[].number') + +PR_TITLES=() +while IFS= read -r line; do + PR_TITLES+=("$line") +done < <(echo "$PR_LIST_JSON" | jq -r '.[].title') + +PR_LABELS=() +while IFS= read -r line; do + PR_LABELS+=("$line") +done < <(echo "$PR_LIST_JSON" | jq -r '[.[].labels | map(.name) | join(",")][]') + +info "Found $PR_COUNT candidate PR(s)" + +# --- Multi-select PR picker --------------------------------------------------- +select_prs() { + local total=${#PR_NUMBERS[@]} + local -a checked + for ((i = 0; i < total; i++)); do checked[i]=0; done + local cursor=0 + + if [ ! -t 0 ]; then + error "Interactive mode requires a terminal" + exit 1 + fi + + display_menu() { + clear >&2 + echo "" >&2 + echo -e "${CYAN}${BOLD}Select PRs to backport to $RELEASE_BRANCH${RESET}" >&2 + echo "↑/↓ navigate, Space toggle, 'a' toggle all, Enter confirm, 'q' cancel" >&2 + echo "" >&2 + for i in "${!PR_NUMBERS[@]}"; do + local box="[ ]" + [ "${checked[$i]}" -eq 1 ] && box="[x]" + local line="${box} #${PR_NUMBERS[$i]} ${PR_TITLES[$i]:0:60}" + [ -n "${PR_LABELS[$i]}" ] && line="$line (${PR_LABELS[$i]})" + if [ "$i" -eq "$cursor" ]; then + echo -e "${GREEN}→ ${line}${RESET}" >&2 + else + echo -e " ${line}" >&2 + fi + done + echo "" >&2 + } + + while true; do + display_menu + key=$(read_key) + case $key in + up) [ $cursor -gt 0 ] && cursor=$((cursor - 1)) ;; + down) [ $cursor -lt $((total - 1)) ] && cursor=$((cursor + 1)) ;; + space) + [ "${checked[$cursor]}" -eq 1 ] && checked[cursor]=0 || checked[cursor]=1 + ;; + all) + local any_unchecked=0 + for ((i = 0; i < total; i++)); do + [ "${checked[$i]}" -eq 0 ] && any_unchecked=1 + done + for ((i = 0; i < total; i++)); do checked[i]=$any_unchecked; done + ;; + enter) + local out=() + for ((i = 0; i < total; i++)); do + [ "${checked[$i]}" -eq 1 ] && out+=("$i") + done + [ ${#out[@]} -gt 0 ] && printf '%s\n' "${out[@]}" + return 0 + ;; + quit) + echo "" >&2 + info "Selection cancelled" + exit 0 + ;; + esac + done +} + +SELECTED_IDX=() +while IFS= read -r line; do + SELECTED_IDX+=("$line") +done < <(select_prs) +clear + +if [ ${#SELECTED_IDX[@]} -eq 0 ]; then + warn "No PRs selected. Nothing to do." + exit 0 +fi + +echo "" +step "Selected PR(s):" +for idx in "${SELECTED_IDX[@]}"; do + echo -e " #${PR_NUMBERS[$idx]} ${PR_TITLES[$idx]}" +done +echo "" + +# --- Dry-run summary ----------------------------------------------------- +PREP_BRANCH="$USER/patch-prep-${RELEASE_BRANCH#release/}" + +if [ $DRY_RUN -eq 1 ]; then + step "Dry-run summary (no changes will be made)" + echo "" + echo -e " Target: ${BOLD}$RELEASE_BRANCH${RESET}" + echo -e " Branch: ${BOLD}$PREP_BRANCH${RESET}" + echo -e " PRs: ${#SELECTED_IDX[@]}" + for idx in "${SELECTED_IDX[@]}"; do + echo " #${PR_NUMBERS[$idx]} — ${PR_TITLES[$idx]}" + done + echo "" + info "Dry run complete. Re-run without --dry-run to execute." + exit 0 +fi + +# --- Handle existing prep branch ---------------------------------------------- +if git show-ref --verify --quiet "refs/remotes/origin/$PREP_BRANCH" 2>/dev/null; then + error "Remote branch $PREP_BRANCH already exists. Delete it or finish that prep first." + exit 1 +fi +if git show-ref --verify --quiet "refs/heads/$PREP_BRANCH" 2>/dev/null; then + warn "Local branch $PREP_BRANCH already exists." + echo -n "Delete it and start fresh? (y/n) " + read -r ANSWER + [ "$ANSWER" == "y" ] || { echo "Aborting."; exit 1; } + git branch -D "$PREP_BRANCH" +fi + +# --- Cherry-pick selected PRs -------------------------------------------------- +step "Creating $PREP_BRANCH from $RELEASE_BRANCH" + +git checkout -b "$PREP_BRANCH" "origin/$RELEASE_BRANCH" + +CHERRY_PICK_IN_PROGRESS=1 +for idx in "${SELECTED_IDX[@]}"; do + num="${PR_NUMBERS[$idx]}" + step "Cherry-picking #$num" + + PR_DATA=$(gh pr view "$num" --json commits,mergeCommit) + PR_COMMITS=$(echo "$PR_DATA" | jq -r '.commits[].oid') + PR_MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty') + + USE_MERGE_COMMIT=0 + for PR_COMMIT in $PR_COMMITS; do + if ! git cat-file -e "$PR_COMMIT" 2>/dev/null; then + USE_MERGE_COMMIT=1 + break + fi + PARENT_COUNT=$(git rev-list --parents -n 1 "$PR_COMMIT" 2>/dev/null | wc -w) + if [ "$PARENT_COUNT" -gt 2 ]; then + USE_MERGE_COMMIT=1 + break + fi + done + + if [ $USE_MERGE_COMMIT -eq 1 ]; then + [ -n "$PR_MERGE_COMMIT" ] || { error "No usable commit found for #$num"; exit 1; } + git cherry-pick -x -m 1 "$PR_MERGE_COMMIT" + else + for PR_COMMIT in $PR_COMMITS; do + git cherry-pick -x "$PR_COMMIT" + done + fi +done +CHERRY_PICK_IN_PROGRESS=0 + +step "Validating build (the selected PRs may not build together)" +if ! ./gradlew buildDebug --console=plain; then + BUILD_FAILED=1 + exit 1 +fi +info "Build validation passed" + +git push -u origin "$PREP_BRANCH" +info "Pushed $PREP_BRANCH" + +# --- Create combined PR -------------------------------------------------- +step "Creating pull request" + +BODY="Backports the following PR(s) to \`$RELEASE_BRANCH\` in preparation for the next patch release:"$'\n\n' +for idx in "${SELECTED_IDX[@]}"; do + BODY+="- #${PR_NUMBERS[$idx]} — ${PR_TITLES[$idx]}"$'\n' +done + +PREP_PR_URL=$(gh pr create --base "$RELEASE_BRANCH" \ + --head "$PREP_BRANCH" \ + --title "🍒 Prepare patch: backport ${#SELECTED_IDX[@]} PR(s) to $RELEASE_BRANCH" \ + --body "$BODY") +[ -n "$PREP_PR_URL" ] || { error "gh pr create did not return a URL"; exit 1; } +info "Created: $PREP_PR_URL" + +for idx in "${SELECTED_IDX[@]}"; do + gh pr comment "${PR_NUMBERS[$idx]}" --body "Backported to \`$RELEASE_BRANCH\` via $PREP_PR_URL" 2>/dev/null || true +done + +# --- Restore ------------------------------------------------------------------ +step "Restoring original branch" +git checkout "$CURRENT_BRANCH" +info "Back on $CURRENT_BRANCH" + +echo "" +echo -e "${GREEN}${BOLD}Done!${RESET}" +echo -e " ${BOLD}Prep PR:${RESET} $PREP_PR_URL" +echo "" +info "Once merged, re-run ./utils/release.sh patch --branch $RELEASE_BRANCH" diff --git a/utils/release.sh b/utils/release.sh index 96f371ea9..5b2440354 100755 --- a/utils/release.sh +++ b/utils/release.sh @@ -54,13 +54,18 @@ print_info() { echo -e "${BLUE}$1${NC}" } +trap 'echo ""; print_warning "Interrupted"; exit 130' INT + # Read a single keypress (arrow keys, enter, q) from /dev/tty read_key() { - local key - IFS= read -rsn1 key /dev/null) + branches=() + while IFS= read -r line; do + branches+=("$line") + done < <(git branch -r --list 'origin/release/[0-9]*.[0-9]*._' \ + | sed 's|[[:space:]]*origin/||' | sort -Vr 2>/dev/null) if [ ${#branches[@]} -eq 0 ]; then print_error "No release branches found matching release/X.Y._" >&2 @@ -93,6 +103,8 @@ select_release_branch() { local selected=0 local total=${#branches[@]} + local page_size=10 + local visible=$(( total < page_size ? total : page_size )) display_branch_menu() { clear >&2 @@ -104,7 +116,7 @@ select_release_branch() { echo "Use ↑/↓ arrow keys to navigate, Enter to select, 'q' to quit" >&2 echo "" >&2 - for i in "${!branches[@]}"; do + for ((i = 0; i < visible; i++)); do if [ "$i" -eq "$selected" ]; then echo -e "${GREEN}→ ${branches[$i]}${NC}" >&2 else @@ -112,6 +124,15 @@ select_release_branch() { fi done + if [ "$visible" -lt "$total" ]; then + local remaining=$((total - visible)) + if [ "$selected" -eq "$visible" ]; then + echo -e "${GREEN}→ … show more ($remaining remaining)${NC}" >&2 + else + echo -e " … show more ($remaining remaining)" >&2 + fi + fi + echo "" >&2 echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}" >&2 } @@ -119,16 +140,22 @@ select_release_branch() { while true; do display_branch_menu key=$(read_key) + local menu_rows=$visible + [ "$visible" -lt "$total" ] && menu_rows=$((visible + 1)) case $key in up) - [ $selected -gt 0 ] && ((selected--)) + [ $selected -gt 0 ] && selected=$((selected - 1)) ;; down) - [ $selected -lt $((total - 1)) ] && ((selected++)) + [ $selected -lt $((menu_rows - 1)) ] && selected=$((selected + 1)) ;; enter) - echo "${branches[$selected]}" - return 0 + if [ "$visible" -lt "$total" ] && [ "$selected" -eq "$visible" ]; then + visible=$(( visible + page_size < total ? visible + page_size : total )) + else + echo "${branches[$selected]}" + return 0 + fi ;; quit) echo "" >&2 @@ -144,7 +171,10 @@ select_commit() { local branch=$1 # Get last 10 commits with format: SHA | DATE | AUTHOR | MESSAGE - mapfile -t commits < <(git log "$branch" -n 10 --pretty=format:"%H|%ar|%an|%s" 2>&1) + commits=() + while IFS= read -r line; do + commits+=("$line") + done < <(git log "$branch" -n 10 --pretty=format:"%H|%ar|%an|%s" 2>&1) if [ ${#commits[@]} -eq 0 ]; then print_error "No commits found on branch $branch" >&2 @@ -245,11 +275,14 @@ Examples: Release Flow: 1. Validates inputs and branch rules - 2. Runs pre-release tests (testDebug + testAsan) unless skipped - 3. Creates annotated git tag - 4. Triggers GitLab build pipeline - 5. GitLab publishes to Maven Central - 6. GitHub creates release with assets + 2. For patch: optionally backports pending main PRs to the release branch + first (see utils/prepare-patch.sh), then exits for you to merge the + backport PR and re-run + 3. Runs pre-release tests (testDebug + testAsan) unless skipped + 4. Creates annotated git tag + 5. Triggers GitLab build pipeline + 6. GitLab publishes to Maven Central + 7. GitHub creates release with assets Branch Rules: - major/minor: Must be run from 'main' branch @@ -349,12 +382,54 @@ else fi fi +# Ensure origin/$BRANCH exists, and that a local branch of the same name (if +# one is checked out) isn't stale or carrying unpushed commits, before doing +# any further work. +print_info "Checking that $BRANCH is up to date with origin..." +git fetch --quiet origin "$BRANCH" 2>/dev/null +ORIGIN_BRANCH_HEAD=$(git rev-parse "origin/$BRANCH" 2>&1) || { + print_error "Branch $BRANCH does not exist on origin" + exit 1 +} +if git show-ref --verify --quiet "refs/heads/$BRANCH"; then + LOCAL_BRANCH_HEAD=$(git rev-parse "$BRANCH") + if [ "$LOCAL_BRANCH_HEAD" != "$ORIGIN_BRANCH_HEAD" ]; then + print_error "Local branch '$BRANCH' is not up to date with origin/$BRANCH" + echo " Local: ${LOCAL_BRANCH_HEAD:0:8}" + echo " Origin: ${ORIGIN_BRANCH_HEAD:0:8}" + echo "" + echo "Update your local branch, e.g.:" + echo " git checkout $BRANCH && git pull --ff-only origin $BRANCH" + exit 1 + fi +fi +print_info "$BRANCH is up to date with origin" + +# For patch releases, offer to backport pending main PRs onto the release +# branch before picking a commit to release. +if [ "$RELEASE_TYPE" == "patch" ]; then + echo "" + if [ -t 0 ]; then + read -p "Pick PRs from main to backport to $BRANCH before releasing? (y/n): " -r &1; then +if ! git merge-base --is-ancestor "$COMMIT_SHA" "origin/$BRANCH" 2>&1; then print_error "Commit $SHORT_SHA is not on branch '$BRANCH'" echo "" echo "The selected commit must be part of the branch history."