From 8dd7a6c5b093a97c22b3e3ccb919f43a7ef58a59 Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Wed, 10 Dec 2025 11:16:15 -0800 Subject: [PATCH 1/7] Add Claude Code slash command for patch releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `/patch-release` command that automates the scanner patch release process. The command handles: - Auto-detection of the next patch number - Validation that HEAD is not already a release commit - Sequential patch number verification - Cherry-pick prompts - GitHub release creation with proper --latest flag handling - Release documentation generation Usage: /patch-release [PATCH_NUMBER] Example: /patch-release 2.36 (auto-detects next patch) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/commands/patch-release.md | 132 ++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 .claude/commands/patch-release.md diff --git a/.claude/commands/patch-release.md b/.claude/commands/patch-release.md new file mode 100644 index 000000000..132cf4b6c --- /dev/null +++ b/.claude/commands/patch-release.md @@ -0,0 +1,132 @@ +--- +description: Create a patch release for scanner +allowed-tools: Bash, Read, Write, Edit, Glob, Grep, TodoWrite +argument-hint: [PATCH_NUMBER] +--- + +# Patch Release Process + +You are creating a patch release for scanner. The user has provided: +- RELEASE: $1 (e.g., 2.36) +- PATCH_NUMBER: $2 (optional - will auto-detect if not provided) + +## Prerequisites + +Before proceeding, verify: +1. The user's GitHub account must have "bypass branch protection" permissions on release branches. +2. The release branch `release-$1` must exist. + +## Steps to Execute + +Execute each step in order, documenting progress. + +### Step 1: Determine the patch number + +If PATCH_NUMBER ($2) was not provided, auto-detect the next patch number: +```bash +git fetch origin --tags +git tag --list '$1.*' | grep -E '^$1\.[0-9]+$' | sort -V | tail -1 +``` +Extract the patch number from the latest tag and increment by 1. For example, if the latest tag is `2.36.6`, the next patch number is `7`. + +Set the variables: +- RELEASE = $1 +- PATCH_NUMBER = (provided value or auto-detected next number) +- Full version = ${RELEASE}.${PATCH_NUMBER} + +Create a release log at `/tmp/scanner-${RELEASE}.${PATCH_NUMBER}-release.md` as you go. + +### Step 2: Verify branch state +```bash +git checkout release-${RELEASE} +git pull origin release-${RELEASE} +``` +Ensure the branch is up-to-date with origin. + +### Step 3: Check if release is necessary + +**Check if HEAD is already a release commit:** +```bash +git log -1 --format='%s' +``` +If the latest commit message matches the pattern `Release X.Y.Z`, then HEAD is already a release commit. In this case: +- Inform the user that the latest commit is already a release commit +- Show the commit message +- Ask the user what they want to do: + - Abort (no new release needed) + - Proceed anyway (create another release commit on top) + +**Check if the patch number is sequential:** +Compare the auto-detected next patch number with the calculated expected next number. +- If the user provided a PATCH_NUMBER that is not the next sequential number (e.g., requesting 2.36.10 when 2.36.7 is the latest), warn the user: + - Show the latest existing tag + - Show the expected next patch number + - Show the requested patch number + - Ask the user what to do: + - Use the expected next patch number instead + - Proceed with the requested (non-sequential) patch number + - Abort + +### Step 4: Review commits since last tag +```bash +git log ..HEAD --oneline +``` +Show the user what commits will be included in this release. + +If there are no new commits since the last tag (other than possibly a release commit), warn the user that this release would contain no meaningful changes. + +### Step 5: Confirm version with user +Display the determined version to the user and ask for confirmation before proceeding: +- Show RELEASE and PATCH_NUMBER values +- Show the full version that will be created +- Summarize the commits that will be included +- Ask user to confirm or provide a different patch number + +### Step 6: Ask about cherry-picks +Ask the user if they need to cherry-pick any additional commits before proceeding. + +### Step 7: Create release commit and tag +```bash +git commit --allow-empty -m "Release ${RELEASE}.${PATCH_NUMBER}" +git tag --no-sign ${RELEASE}.${PATCH_NUMBER} +``` + +### Step 8: Sanity checks +```bash +git tag --contains +git log --oneline -5 +``` +Verify: +- The tag is on the current commit +- The commit log shows expected commits + +### Step 9: Push tag and commits +```bash +git push origin ${RELEASE}.${PATCH_NUMBER} +git push --set-upstream origin release-${RELEASE} +``` +Note: This requires "bypass branch protection" permission. The push will bypass PR and status check requirements. + +### Step 10: Create GitHub Release +First, determine if this should be marked as "Latest": +```bash +gh release list --limit 10 +``` + +If a higher semantic version exists (e.g., 2.38.x when releasing 2.36.x), use `--latest=false`: +```bash +# If NOT the latest semantic version: +gh release create ${RELEASE}.${PATCH_NUMBER} --title "${RELEASE}.${PATCH_NUMBER}" --notes-start-tag --generate-notes --latest=false + +# If IS the latest semantic version: +gh release create ${RELEASE}.${PATCH_NUMBER} --title "${RELEASE}.${PATCH_NUMBER}" --notes-start-tag --generate-notes +``` + +### Step 11: Post-release tasks +Inform the user of remaining manual tasks: +- Update SCANNER_VERSION in the stackrox repo +- Manual staging DB fix if needed + +## Documentation + +Update the release log at `/tmp/scanner-$1.$2-release.md` with the status of each step as you complete it. From df0cdd7f8d8481accd1a4f1a0e9daf04e15117c2 Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Wed, 10 Dec 2025 15:48:10 -0800 Subject: [PATCH 2/7] Update .claude/commands/patch-release.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .claude/commands/patch-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/commands/patch-release.md b/.claude/commands/patch-release.md index 132cf4b6c..d6e6ee2e0 100644 --- a/.claude/commands/patch-release.md +++ b/.claude/commands/patch-release.md @@ -25,7 +25,7 @@ Execute each step in order, documenting progress. If PATCH_NUMBER ($2) was not provided, auto-detect the next patch number: ```bash git fetch origin --tags -git tag --list '$1.*' | grep -E '^$1\.[0-9]+$' | sort -V | tail -1 +git tag --list "$1.*" | grep -E "^$1\.[0-9]+$" | sort -V | tail -1 ``` Extract the patch number from the latest tag and increment by 1. For example, if the latest tag is `2.36.6`, the next patch number is `7`. From 0b651bed3ca058939cbb72a23741fbb843a9b63e Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Thu, 11 Dec 2025 12:43:14 -0800 Subject: [PATCH 3/7] Update .claude/commands/patch-release.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .claude/commands/patch-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/commands/patch-release.md b/.claude/commands/patch-release.md index d6e6ee2e0..89b471d9c 100644 --- a/.claude/commands/patch-release.md +++ b/.claude/commands/patch-release.md @@ -129,4 +129,4 @@ Inform the user of remaining manual tasks: ## Documentation -Update the release log at `/tmp/scanner-$1.$2-release.md` with the status of each step as you complete it. +Update the release log at `/tmp/scanner-$1.${PATCH_NUMBER}-release.md` with the status of each step as you complete it. From 67c6a36f44bd71b266ecf14ce0db4cd079a047b5 Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Thu, 11 Dec 2025 23:17:42 -0800 Subject: [PATCH 4/7] Replace Claude slash command with bash script for patch releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LLM-based slash command has been replaced with a deterministic bash script that provides the same functionality with: - Full testability (--dry-run mode) - Auditability (every command visible in source) - Reproducibility (same input → same behavior) - Proper error handling (set -euo pipefail) Usage: ./scripts/patch-release.sh [--dry-run] [-y] [PATCH_NUMBER] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/commands/patch-release.md | 132 -------- scripts/patch-release.sh | 484 ++++++++++++++++++++++++++++++ 2 files changed, 484 insertions(+), 132 deletions(-) delete mode 100644 .claude/commands/patch-release.md create mode 100755 scripts/patch-release.sh diff --git a/.claude/commands/patch-release.md b/.claude/commands/patch-release.md deleted file mode 100644 index 89b471d9c..000000000 --- a/.claude/commands/patch-release.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -description: Create a patch release for scanner -allowed-tools: Bash, Read, Write, Edit, Glob, Grep, TodoWrite -argument-hint: [PATCH_NUMBER] ---- - -# Patch Release Process - -You are creating a patch release for scanner. The user has provided: -- RELEASE: $1 (e.g., 2.36) -- PATCH_NUMBER: $2 (optional - will auto-detect if not provided) - -## Prerequisites - -Before proceeding, verify: -1. The user's GitHub account must have "bypass branch protection" permissions on release branches. -2. The release branch `release-$1` must exist. - -## Steps to Execute - -Execute each step in order, documenting progress. - -### Step 1: Determine the patch number - -If PATCH_NUMBER ($2) was not provided, auto-detect the next patch number: -```bash -git fetch origin --tags -git tag --list "$1.*" | grep -E "^$1\.[0-9]+$" | sort -V | tail -1 -``` -Extract the patch number from the latest tag and increment by 1. For example, if the latest tag is `2.36.6`, the next patch number is `7`. - -Set the variables: -- RELEASE = $1 -- PATCH_NUMBER = (provided value or auto-detected next number) -- Full version = ${RELEASE}.${PATCH_NUMBER} - -Create a release log at `/tmp/scanner-${RELEASE}.${PATCH_NUMBER}-release.md` as you go. - -### Step 2: Verify branch state -```bash -git checkout release-${RELEASE} -git pull origin release-${RELEASE} -``` -Ensure the branch is up-to-date with origin. - -### Step 3: Check if release is necessary - -**Check if HEAD is already a release commit:** -```bash -git log -1 --format='%s' -``` -If the latest commit message matches the pattern `Release X.Y.Z`, then HEAD is already a release commit. In this case: -- Inform the user that the latest commit is already a release commit -- Show the commit message -- Ask the user what they want to do: - - Abort (no new release needed) - - Proceed anyway (create another release commit on top) - -**Check if the patch number is sequential:** -Compare the auto-detected next patch number with the calculated expected next number. -- If the user provided a PATCH_NUMBER that is not the next sequential number (e.g., requesting 2.36.10 when 2.36.7 is the latest), warn the user: - - Show the latest existing tag - - Show the expected next patch number - - Show the requested patch number - - Ask the user what to do: - - Use the expected next patch number instead - - Proceed with the requested (non-sequential) patch number - - Abort - -### Step 4: Review commits since last tag -```bash -git log ..HEAD --oneline -``` -Show the user what commits will be included in this release. - -If there are no new commits since the last tag (other than possibly a release commit), warn the user that this release would contain no meaningful changes. - -### Step 5: Confirm version with user -Display the determined version to the user and ask for confirmation before proceeding: -- Show RELEASE and PATCH_NUMBER values -- Show the full version that will be created -- Summarize the commits that will be included -- Ask user to confirm or provide a different patch number - -### Step 6: Ask about cherry-picks -Ask the user if they need to cherry-pick any additional commits before proceeding. - -### Step 7: Create release commit and tag -```bash -git commit --allow-empty -m "Release ${RELEASE}.${PATCH_NUMBER}" -git tag --no-sign ${RELEASE}.${PATCH_NUMBER} -``` - -### Step 8: Sanity checks -```bash -git tag --contains -git log --oneline -5 -``` -Verify: -- The tag is on the current commit -- The commit log shows expected commits - -### Step 9: Push tag and commits -```bash -git push origin ${RELEASE}.${PATCH_NUMBER} -git push --set-upstream origin release-${RELEASE} -``` -Note: This requires "bypass branch protection" permission. The push will bypass PR and status check requirements. - -### Step 10: Create GitHub Release -First, determine if this should be marked as "Latest": -```bash -gh release list --limit 10 -``` - -If a higher semantic version exists (e.g., 2.38.x when releasing 2.36.x), use `--latest=false`: -```bash -# If NOT the latest semantic version: -gh release create ${RELEASE}.${PATCH_NUMBER} --title "${RELEASE}.${PATCH_NUMBER}" --notes-start-tag --generate-notes --latest=false - -# If IS the latest semantic version: -gh release create ${RELEASE}.${PATCH_NUMBER} --title "${RELEASE}.${PATCH_NUMBER}" --notes-start-tag --generate-notes -``` - -### Step 11: Post-release tasks -Inform the user of remaining manual tasks: -- Update SCANNER_VERSION in the stackrox repo -- Manual staging DB fix if needed - -## Documentation - -Update the release log at `/tmp/scanner-$1.${PATCH_NUMBER}-release.md` with the status of each step as you complete it. diff --git a/scripts/patch-release.sh b/scripts/patch-release.sh new file mode 100755 index 000000000..99359f0f1 --- /dev/null +++ b/scripts/patch-release.sh @@ -0,0 +1,484 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPTS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./lib.sh +source "$SCRIPTS_ROOT/lib.sh" + +# Flags +DRY_RUN=false +YES=false + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +usage() { + cat < [PATCH_NUMBER] + +Create a patch release for scanner. + +Arguments: + RELEASE Release version (e.g., 2.36) + PATCH_NUMBER Optional patch number (auto-detected if omitted) + +Options: + --dry-run Show what would happen without executing + -y, --yes Skip confirmations (for automation) + -h, --help Show this help message + +Examples: + $(basename "$0") 2.36 # Auto-detect next patch number + $(basename "$0") 2.36 7 # Create 2.36.7 specifically + $(basename "$0") --dry-run 2.36 # Preview what would happen + +Prerequisites: + - GitHub CLI (gh) must be installed and authenticated + - User must have "bypass branch protection" permissions on release branches +EOF +} + +log_info() { + echo -e "${GREEN}INFO:${NC} $*" +} + +log_warn() { + echo -e "${YELLOW}WARN:${NC} $*" +} + +log_error() { + echo -e "${RED}ERROR:${NC} $*" +} + +log_dry_run() { + echo -e "${YELLOW}[DRY-RUN]${NC} Would execute: $*" +} + +# Prompt user for confirmation. Returns 0 if confirmed, 1 if declined. +# If --yes flag is set, returns 0 automatically. +confirm() { + local prompt="$1" + local default="${2:-n}" + + if [[ "$YES" == "true" ]]; then + log_info "Auto-confirming (--yes): $prompt" + return 0 + fi + + local yn_prompt + if [[ "$default" == "y" ]]; then + yn_prompt="[Y/n]" + else + yn_prompt="[y/N]" + fi + + read -r -p "$prompt $yn_prompt " response + response="${response:-$default}" + case "$response" in + [yY][eE][sS]|[yY]) return 0 ;; + *) return 1 ;; + esac +} + +# Prompt user to choose from options. Echoes the choice number (1-based). +choose() { + local prompt="$1" + shift + local options=("$@") + + if [[ "$YES" == "true" ]]; then + log_info "Auto-selecting first option (--yes)" + echo "1" + return 0 + fi + + echo "$prompt" + local i=1 + for opt in "${options[@]}"; do + echo " $i) $opt" + ((i++)) + done + + local choice + while true; do + read -r -p "Enter choice [1-${#options[@]}]: " choice + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then + echo "$choice" + return 0 + fi + echo "Invalid choice. Please enter a number between 1 and ${#options[@]}." + done +} + +# Get the latest tag matching a release pattern. +get_latest_tag() { + local release="$1" + git tag --list "${release}.*" | grep -E "^${release}\.[0-9]+$" | sort -V | tail -1 +} + +# Extract patch number from a version tag. +get_patch_number() { + local tag="$1" + echo "$tag" | sed -E 's/^[0-9]+\.[0-9]+\.([0-9]+)$/\1/' +} + +# Compare two semantic versions. Returns: +# 0 if v1 == v2 +# 1 if v1 > v2 +# 2 if v1 < v2 +semver_compare() { + local v1="$1" + local v2="$2" + + if [[ "$v1" == "$v2" ]]; then + return 0 + fi + + local v1_major v1_minor v1_patch + local v2_major v2_minor v2_patch + + IFS='.' read -r v1_major v1_minor v1_patch <<< "$v1" + IFS='.' read -r v2_major v2_minor v2_patch <<< "$v2" + + # Default patch to 0 if not present. + v1_patch="${v1_patch:-0}" + v2_patch="${v2_patch:-0}" + + if (( v1_major > v2_major )); then return 1; fi + if (( v1_major < v2_major )); then return 2; fi + if (( v1_minor > v2_minor )); then return 1; fi + if (( v1_minor < v2_minor )); then return 2; fi + if (( v1_patch > v2_patch )); then return 1; fi + if (( v1_patch < v2_patch )); then return 2; fi + + return 0 +} + +# Check if a version is the latest release (should be marked as GitHub "Latest"). +is_latest_release() { + local version="$1" + + local releases + releases=$(gh release list --limit 20 --json tagName --jq '.[].tagName' 2>/dev/null || echo "") + + if [[ -z "$releases" ]]; then + # No releases exist, this will be the latest. + return 0 + fi + + local highest="$version" + while IFS= read -r tag; do + # Skip non-semver tags. + if [[ ! "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + continue + fi + semver_compare "$tag" "$highest" || true + local result=$? + if [[ "$result" == "1" ]]; then + highest="$tag" + fi + done <<< "$releases" + + [[ "$highest" == "$version" ]] +} + +main() { + # Parse arguments. + local release="" + local patch_number="" + local positional_args=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) + DRY_RUN=true + shift + ;; + -y|--yes) + YES=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + die "Unknown option: $1" + ;; + *) + positional_args+=("$1") + shift + ;; + esac + done + + if [[ ${#positional_args[@]} -lt 1 ]]; then + usage + die "RELEASE argument is required" + fi + + release="${positional_args[0]}" + patch_number="${positional_args[1]:-}" + + # Validate release format. + if [[ ! "$release" =~ ^[0-9]+\.[0-9]+$ ]]; then + die "Invalid RELEASE format: $release (expected X.Y, e.g., 2.36)" + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log_warn "Running in dry-run mode. No changes will be made." + echo + fi + + # Step 1: Validate prerequisites. + log_info "Checking prerequisites..." + require_executable gh "GitHub CLI is required" + require_executable git "Git is required" + + if ! gh auth status &>/dev/null; then + die "GitHub CLI is not authenticated. Run 'gh auth login' first." + fi + + local release_branch="release-${release}" + if ! git ls-remote --heads origin "$release_branch" | grep -q "$release_branch"; then + die "Release branch '$release_branch' does not exist on origin" + fi + + log_info "Prerequisites OK" + echo + + # Step 2: Fetch tags and determine patch number. + log_info "Fetching tags from origin..." + git fetch origin --tags + + local latest_tag + latest_tag=$(get_latest_tag "$release") + local expected_patch=1 + + if [[ -n "$latest_tag" ]]; then + local current_patch + current_patch=$(get_patch_number "$latest_tag") + expected_patch=$((current_patch + 1)) + log_info "Latest tag: $latest_tag" + log_info "Expected next patch number: $expected_patch" + else + log_info "No existing tags for $release, starting at patch 1" + fi + + if [[ -z "$patch_number" ]]; then + patch_number="$expected_patch" + log_info "Auto-detected patch number: $patch_number" + else + log_info "Using provided patch number: $patch_number" + fi + + local version="${release}.${patch_number}" + local previous_tag="${latest_tag:-}" + echo + + # Step 3: Verify branch state. + log_info "Checking out and updating release branch..." + if [[ "$DRY_RUN" == "true" ]]; then + log_dry_run "git checkout $release_branch" + log_dry_run "git pull origin $release_branch" + log_warn "Skipping branch checkout in dry-run mode; remaining checks use current branch state" + else + git checkout "$release_branch" + git pull origin "$release_branch" + fi + echo + + # Step 4: Check if release is necessary. + local head_commit_msg + head_commit_msg=$(git log -1 --format='%s') + if [[ "$DRY_RUN" == "true" ]]; then + log_info "(Note: HEAD commit is from current branch, not $release_branch)" + fi + + if [[ "$head_commit_msg" =~ ^Release\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then + log_warn "HEAD is already a release commit: $head_commit_msg" + if ! confirm "Create another release commit on top?"; then + log_info "Aborted by user." + exit 0 + fi + echo + fi + + if [[ "$patch_number" != "$expected_patch" ]]; then + log_warn "Patch number $patch_number is not sequential." + log_warn "Latest tag: ${latest_tag:-none}" + log_warn "Expected next patch: $expected_patch" + log_warn "Requested patch: $patch_number" + echo + + local choice + choice=$(choose "What would you like to do?" \ + "Use expected patch number ($expected_patch)" \ + "Proceed with requested patch number ($patch_number)" \ + "Abort") + + case "$choice" in + 1) + patch_number="$expected_patch" + version="${release}.${patch_number}" + log_info "Using patch number: $patch_number" + ;; + 2) + log_info "Proceeding with patch number: $patch_number" + ;; + 3) + log_info "Aborted by user." + exit 0 + ;; + esac + echo + fi + + # Step 5: Show commits since last tag. + log_info "Commits to be included in $version:" + echo + if [[ -n "$previous_tag" ]]; then + local commits + commits=$(git log "${previous_tag}..HEAD" --oneline) + if [[ -z "$commits" ]]; then + log_warn "No commits since $previous_tag" + if ! confirm "Create an empty release anyway?"; then + log_info "Aborted by user." + exit 0 + fi + else + echo "$commits" + fi + else + git log --oneline -10 + log_info "(Showing last 10 commits; no previous tag to compare against)" + fi + echo + + # Step 6: Confirm with user. + echo "==========================================" + echo "Release Summary" + echo "==========================================" + echo "Version: $version" + echo "Release branch: $release_branch" + echo "Previous tag: ${previous_tag:-none}" + echo "==========================================" + echo + + if ! confirm "Proceed with creating this release?"; then + log_info "Aborted by user." + exit 0 + fi + echo + + # Step 7: Ask about cherry-picks. + if ! [[ "$YES" == "true" ]]; then + if confirm "Do you need to cherry-pick any commits before proceeding?" "n"; then + log_info "Pausing for cherry-picks. Run your cherry-pick commands, then re-run this script." + log_info "Example: git cherry-pick " + exit 0 + fi + echo + fi + + # Step 8: Create release commit and tag. + log_info "Creating release commit and tag..." + if [[ "$DRY_RUN" == "true" ]]; then + log_dry_run "git commit --allow-empty -m \"Release ${version}\"" + log_dry_run "git tag --no-sign ${version}" + else + git commit --allow-empty -m "Release ${version}" + git tag --no-sign "${version}" + fi + echo + + # Step 9: Sanity checks. + log_info "Running sanity checks..." + if [[ "$DRY_RUN" == "true" ]]; then + log_info "(Skipping tag verification in dry-run mode)" + else + local tags_on_head + tags_on_head=$(git tag --contains HEAD 2>/dev/null || echo "") + if ! echo "$tags_on_head" | grep -q "^${version}$"; then + die "Sanity check failed: tag $version is not on HEAD" + fi + log_info "Tag $version is on HEAD" + fi + + log_info "Recent commits:" + git log --oneline -5 + echo + + # Step 10: Push. + if ! confirm "Push tag and commits to origin? (Requires bypass branch protection)" "y"; then + log_info "Aborted by user." + log_info "To push manually:" + log_info " git push origin ${version}" + log_info " git push --set-upstream origin ${release_branch}" + exit 0 + fi + + log_info "Pushing tag and commits..." + if [[ "$DRY_RUN" == "true" ]]; then + log_dry_run "git push origin ${version}" + log_dry_run "git push --set-upstream origin ${release_branch}" + else + git push origin "${version}" + git push --set-upstream origin "${release_branch}" + fi + echo + + # Step 11: Create GitHub release. + log_info "Creating GitHub release..." + + local latest_flag="" + if [[ "$DRY_RUN" != "true" ]]; then + if ! is_latest_release "$version"; then + latest_flag="--latest=false" + log_info "This is not the highest semantic version; using --latest=false" + fi + fi + + local gh_cmd="gh release create ${version} --title ${version} --generate-notes" + if [[ -n "$previous_tag" ]]; then + gh_cmd="$gh_cmd --notes-start-tag ${previous_tag}" + fi + if [[ -n "$latest_flag" ]]; then + gh_cmd="$gh_cmd $latest_flag" + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log_dry_run "$gh_cmd" + else + if [[ -n "$previous_tag" ]]; then + if [[ -n "$latest_flag" ]]; then + gh release create "${version}" --title "${version}" --generate-notes --notes-start-tag "${previous_tag}" --latest=false + else + gh release create "${version}" --title "${version}" --generate-notes --notes-start-tag "${previous_tag}" + fi + else + if [[ -n "$latest_flag" ]]; then + gh release create "${version}" --title "${version}" --generate-notes --latest=false + else + gh release create "${version}" --title "${version}" --generate-notes + fi + fi + fi + echo + + # Step 12: Post-release tasks. + log_info "Release $version created successfully!" + echo + echo "==========================================" + echo "Remaining Manual Tasks" + echo "==========================================" + echo "1. Update SCANNER_VERSION in the stackrox repo" + echo "2. Manual staging DB fix if needed" + echo "==========================================" +} + +main "$@" From 0092bf0a4575dedc1de351feace235f310a02ced Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Mon, 15 Dec 2025 15:38:50 -0800 Subject: [PATCH 5/7] Update scripts/patch-release.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/patch-release.sh | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/scripts/patch-release.sh b/scripts/patch-release.sh index 99359f0f1..a9e051c04 100755 --- a/scripts/patch-release.sh +++ b/scripts/patch-release.sh @@ -451,22 +451,19 @@ main() { gh_cmd="$gh_cmd $latest_flag" fi + # Build gh release create command as an array for safe argument handling + gh_args=(release create "${version}" --title "${version}" --generate-notes) + if [[ -n "$previous_tag" ]]; then + gh_args+=(--notes-start-tag "${previous_tag}") + fi + if [[ -n "$latest_flag" ]]; then + gh_args+=($latest_flag) + fi + if [[ "$DRY_RUN" == "true" ]]; then - log_dry_run "$gh_cmd" + log_dry_run "gh ${gh_args[*]}" else - if [[ -n "$previous_tag" ]]; then - if [[ -n "$latest_flag" ]]; then - gh release create "${version}" --title "${version}" --generate-notes --notes-start-tag "${previous_tag}" --latest=false - else - gh release create "${version}" --title "${version}" --generate-notes --notes-start-tag "${previous_tag}" - fi - else - if [[ -n "$latest_flag" ]]; then - gh release create "${version}" --title "${version}" --generate-notes --latest=false - else - gh release create "${version}" --title "${version}" --generate-notes - fi - fi + gh "${gh_args[@]}" fi echo From f9ae6f6d492f096d6456a15a78fd9580a1b90b8d Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Mon, 15 Dec 2025 15:39:08 -0800 Subject: [PATCH 6/7] Update scripts/patch-release.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/patch-release.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/patch-release.sh b/scripts/patch-release.sh index a9e051c04..5f57a247b 100755 --- a/scripts/patch-release.sh +++ b/scripts/patch-release.sh @@ -274,6 +274,10 @@ main() { log_info "Auto-detected patch number: $patch_number" else log_info "Using provided patch number: $patch_number" + if [[ ! "$patch_number" =~ ^[0-9]+$ ]]; then + log_error "Invalid patch number: '$patch_number'. Must be a non-negative integer." + exit 1 + fi fi local version="${release}.${patch_number}" From 588ee265263f04bc77289494dd2009d49a7e1c56 Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Mon, 15 Dec 2025 15:39:22 -0800 Subject: [PATCH 7/7] Update scripts/patch-release.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/patch-release.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/patch-release.sh b/scripts/patch-release.sh index 5f57a247b..afa8e0552 100755 --- a/scripts/patch-release.sh +++ b/scripts/patch-release.sh @@ -282,6 +282,16 @@ main() { local version="${release}.${patch_number}" local previous_tag="${latest_tag:-}" + + # Check if the tag already exists + if git rev-parse "refs/tags/$version" >/dev/null 2>&1; then + if [[ "$DRY_RUN" == "true" ]]; then + log_warn "Tag '$version' already exists (dry-run mode, continuing)." + else + log_error "Tag '$version' already exists. Aborting release." + exit 1 + fi + fi echo # Step 3: Verify branch state.