parse_validate_version.sh
1 #!/bin/bash 2 # parse_validate_version.sh - Parse and validate version for release 3 # 4 # Usage: ./parse_validate_version.sh <version> 5 # Output: Writes to $GITHUB_OUTPUT if set, otherwise to stdout 6 # 7 # Example: 8 # ./parse_validate_version.sh v2.99.0-rc1 9 # 10 # This script is used in the release.yml workflow to parse and validate the version to be released. 11 # Covers several checks to prevent accidental releases of incorrect versions. 12 13 set -euo pipefail 14 15 # --- Helpers --- 16 17 fail() { 18 echo "" 19 echo -e "â $1" 20 echo "" 21 exit 1 22 } 23 24 ok() { 25 echo "â $1" 26 } 27 28 tag_exists() { 29 git tag -l "$1" | grep -q "^$1$" 30 } 31 32 branch_exists() { 33 git ls-remote --heads origin "$1" | grep -q "$1" 34 } 35 36 # --- Parse and validate version --- 37 38 VERSION="${1#v}" # Strip 'v' prefix 39 40 echo "" 41 echo "âšī¸ Validating: ${1}" 42 echo "" 43 44 if [[ ! "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-rc([0-9]+))?$ ]]; then 45 fail "Invalid version format: $1\n\n"\ 46 "Expected format: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rcN\n"\ 47 "Examples: v2.99.0-rc1, v2.99.0, v2.99.1-rc1" 48 fi 49 ok "Version format is valid" 50 51 MAJOR="${BASH_REMATCH[1]}" 52 MINOR="${BASH_REMATCH[2]}" 53 PATCH="${BASH_REMATCH[3]}" 54 RC_NUM="${BASH_REMATCH[5]:-0}" 55 56 if [[ "${RC_NUM}" == "0" && "${VERSION}" == *"-rc0" ]]; then 57 fail "Cannot release rc0\n\n"\ 58 "rc0 is an internal marker created automatically during branch-off.\n"\ 59 "Release candidates start at rc1." 60 fi 61 62 MAJOR_MINOR="${MAJOR}.${MINOR}" 63 RELEASE_BRANCH="v${MAJOR_MINOR}.x" 64 TAG="v${VERSION}" 65 66 IS_RC="false" 67 [[ "${RC_NUM}" != "0" ]] && IS_RC="true" 68 69 IS_FIRST_RC="false" 70 if [[ "${PATCH}" == "0" && "${RC_NUM}" == "1" ]]; then 71 IS_FIRST_RC="true" 72 fi 73 74 # 1. Tag must not already exist 75 if tag_exists "${TAG}"; then 76 fail "Version ${TAG} was already released\n\n"\ 77 "Each version can only be released once.\n"\ 78 "To publish changes, release the next RC or patch version." 79 fi 80 ok "Tag ${TAG} does not exist" 81 82 # 2. Checks based on release type 83 if [[ "${IS_FIRST_RC}" == "true" ]]; then 84 # First RC of minor: branch must NOT exist yet 85 if branch_exists "${RELEASE_BRANCH}"; then 86 fail "Branch ${RELEASE_BRANCH} already exists\n\n"\ 87 "The first RC of a minor (e.g., v${MAJOR_MINOR}.0-rc1) creates the release branch.\n"\ 88 "Since the branch exists, this minor was likely already started.\n"\ 89 "Did you mean to release the next RC (rc2, rc3...) or a patch (v${MAJOR_MINOR}.1-rc1)?" 90 fi 91 ok "Branch ${RELEASE_BRANCH} does not exist" 92 93 # First RC of minor: VERSION.txt must contain rc0 94 EXPECTED="${MAJOR_MINOR}.0-rc0" 95 ACTUAL=$(cat VERSION.txt) 96 if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then 97 ACTUAL_MINOR=$(echo "${ACTUAL}" | cut -d. -f1,2) 98 fail "Cannot release v${MAJOR_MINOR}.0-rc1 from this branch\n\n"\ 99 "The main branch is prepared for version ${ACTUAL_MINOR}, not ${MAJOR_MINOR}.\n"\ 100 "Check that you're releasing the correct version." 101 fi 102 ok "VERSION.txt = ${EXPECTED}" 103 104 else 105 # Not first RC: branch MUST exist 106 if ! branch_exists "${RELEASE_BRANCH}"; then 107 if [[ "${PATCH}" == "0" ]]; then 108 fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\ 109 "For subsequent RCs (rc2, rc3...), the release branch must already exist.\n"\ 110 "Release the first RC (v${MAJOR_MINOR}.0-rc1) first to create the branch." 111 else 112 fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\ 113 "For patch releases, the release branch must already exist.\n"\ 114 "The minor version (v${MAJOR_MINOR}.0) must be released before any patches." 115 fi 116 fi 117 ok "Branch ${RELEASE_BRANCH} exists" 118 119 # Subsequent RC (rc2, rc3...): previous RC must exist 120 if [[ "${RC_NUM}" -gt 1 ]]; then 121 PREV_RC_NUM=$((RC_NUM - 1)) 122 PREV_TAG="v${MAJOR_MINOR}.${PATCH}-rc${PREV_RC_NUM}" 123 if ! tag_exists "${PREV_TAG}"; then 124 fail "Cannot release v${MAJOR_MINOR}.${PATCH}-rc${RC_NUM}\n\n"\ 125 "Previous RC (${PREV_TAG}) was not found.\n"\ 126 "RC versions must be sequential. Release rc${PREV_RC_NUM} first." 127 fi 128 ok "Previous tag ${PREV_TAG} exists" 129 fi 130 131 # Final release: at least one RC must exist 132 if [[ "${RC_NUM}" == "0" ]]; then 133 RC_TAGS=$(git tag -l "v${MAJOR_MINOR}.${PATCH}-rc*" | grep -v "\-rc0$" || true) 134 if [[ -z "${RC_TAGS}" ]]; then 135 fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\ 136 "No release candidate found for this version.\n"\ 137 "Stable releases require at least one RC first (e.g., v${MAJOR_MINOR}.${PATCH}-rc1)." 138 fi 139 LAST_RC=$(echo "${RC_TAGS}" | sort -V | tail -n1) 140 ok "Found RC: ${LAST_RC}" 141 142 # Check Tests workflow passed (only if credentials available) 143 if [[ -n "${GH_TOKEN:-}" && -n "${GITHUB_REPOSITORY:-}" ]]; then 144 RC_SHA=$(git rev-list -n 1 "${LAST_RC}") 145 RESULT=$(gh api "/repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${RC_SHA}&status=success" \ 146 --jq '.workflow_runs[] | select(.name == "Tests") | .conclusion' 2>/dev/null || true) 147 if [[ -z "${RESULT}" ]]; then 148 fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\ 149 "Tests did not pass on the last RC (${LAST_RC}).\n"\ 150 "Wait for tests to complete, or release a new RC with fixes." 151 fi 152 ok "Tests passed on ${LAST_RC}" 153 fi 154 fi 155 fi 156 157 echo "" 158 ok "All validations passed!" 159 echo "" 160 161 # --- Output to GITHUB_OUTPUT (or stdout for local testing) --- 162 163 OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/stdout}" 164 165 { 166 echo "version=${VERSION}" 167 echo "major_minor=${MAJOR_MINOR}" 168 echo "release_branch=${RELEASE_BRANCH}" 169 echo "is_rc=${IS_RC}" 170 echo "is_first_rc=${IS_FIRST_RC}" 171 } >> "${OUTPUT_FILE}"