/ .github / utils / parse_validate_version.sh
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}"