haystack/.github/utils/parse_validate_version.sh
Stefano Fiorucci 0205781af1
ci: add scripts for release automation (#10226)
* ci: add scripts for release automation

* improvements from review
2025-12-11 17:06:24 +01:00

168 lines
5.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# parse_validate_version.sh - Parse and validate version for release
#
# Usage: ./parse_validate_version.sh <version>
# Output: Writes to $GITHUB_OUTPUT if set, otherwise to stdout
#
# Example:
# ./parse_validate_version.sh v2.99.0-rc1
#
# This script is used in the release.yml workflow to parse and validate the version to be released.
# Covers several checks to prevent accidental releases of incorrect versions.
set -euo pipefail
# --- Helpers ---
fail() {
echo ""
echo -e "$1"
echo ""
exit 1
}
ok() {
echo "$1"
}
tag_exists() {
git tag -l "$1" | grep -q "^$1$"
}
branch_exists() {
git ls-remote --heads origin "$1" | grep -q "$1"
}
# --- Parse and validate version ---
VERSION="${1#v}" # Strip 'v' prefix
echo ""
echo " Validating: ${1}"
echo ""
if [[ ! "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-rc([0-9]+))?$ ]]; then
fail "Invalid version format: $1\n\n"\
"Expected format: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rcN\n"\
"Examples: v2.99.0-rc1, v2.99.0, v2.99.1-rc1"
fi
ok "Version format is valid"
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
RC_NUM="${BASH_REMATCH[5]:-0}"
if [[ "${RC_NUM}" == "0" && "${VERSION}" == *"-rc0" ]]; then
fail "Cannot release rc0\n\n"\
"rc0 is an internal marker created automatically during branch-off.\n"\
"Release candidates start at rc1."
fi
MAJOR_MINOR="${MAJOR}.${MINOR}"
RELEASE_BRANCH="v${MAJOR_MINOR}.x"
TAG="v${VERSION}"
IS_FIRST_RC="false"
if [[ "${PATCH}" == "0" && "${RC_NUM}" == "1" ]]; then
IS_FIRST_RC="true"
fi
# 1. Tag must not already exist
if tag_exists "${TAG}"; then
fail "Version ${TAG} was already released\n\n"\
"Each version can only be released once.\n"\
"To publish changes, release the next RC or patch version."
fi
ok "Tag ${TAG} does not exist"
# 2. Checks based on release type
if [[ "${IS_FIRST_RC}" == "true" ]]; then
# First RC of minor: branch must NOT exist yet
if branch_exists "${RELEASE_BRANCH}"; then
fail "Branch ${RELEASE_BRANCH} already exists\n\n"\
"The first RC of a minor (e.g., v${MAJOR_MINOR}.0-rc1) creates the release branch.\n"\
"Since the branch exists, this minor was likely already started.\n"\
"Did you mean to release the next RC (rc2, rc3...) or a patch (v${MAJOR_MINOR}.1-rc1)?"
fi
ok "Branch ${RELEASE_BRANCH} does not exist"
# First RC of minor: VERSION.txt must contain rc0
EXPECTED="${MAJOR_MINOR}.0-rc0"
ACTUAL=$(cat VERSION.txt)
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
ACTUAL_MINOR=$(echo "${ACTUAL}" | cut -d. -f1,2)
fail "Cannot release v${MAJOR_MINOR}.0-rc1 from this branch\n\n"\
"The main branch is prepared for version ${ACTUAL_MINOR}, not ${MAJOR_MINOR}.\n"\
"Check that you're releasing the correct version."
fi
ok "VERSION.txt = ${EXPECTED}"
else
# Not first RC: branch MUST exist
if ! branch_exists "${RELEASE_BRANCH}"; then
if [[ "${PATCH}" == "0" ]]; then
fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\
"For subsequent RCs (rc2, rc3...), the release branch must already exist.\n"\
"Release the first RC (v${MAJOR_MINOR}.0-rc1) first to create the branch."
else
fail "Branch ${RELEASE_BRANCH} does not exist\n\n"\
"For patch releases, the release branch must already exist.\n"\
"The minor version (v${MAJOR_MINOR}.0) must be released before any patches."
fi
fi
ok "Branch ${RELEASE_BRANCH} exists"
# Subsequent RC (rc2, rc3...): previous RC must exist
if [[ "${RC_NUM}" -gt 1 ]]; then
PREV_RC_NUM=$((RC_NUM - 1))
PREV_TAG="v${MAJOR_MINOR}.${PATCH}-rc${PREV_RC_NUM}"
if ! tag_exists "${PREV_TAG}"; then
fail "Cannot release v${MAJOR_MINOR}.${PATCH}-rc${RC_NUM}\n\n"\
"Previous RC (${PREV_TAG}) was not found.\n"\
"RC versions must be sequential. Release rc${PREV_RC_NUM} first."
fi
ok "Previous tag ${PREV_TAG} exists"
fi
# Final release: at least one RC must exist
if [[ "${RC_NUM}" == "0" ]]; then
RC_TAGS=$(git tag -l "v${MAJOR_MINOR}.${PATCH}-rc*" | grep -v "\-rc0$" || true)
if [[ -z "${RC_TAGS}" ]]; then
fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\
"No release candidate found for this version.\n"\
"Stable releases require at least one RC first (e.g., v${MAJOR_MINOR}.${PATCH}-rc1)."
fi
LAST_RC=$(echo "${RC_TAGS}" | sort -V | tail -n1)
ok "Found RC: ${LAST_RC}"
# Check Tests workflow passed (only if credentials available)
if [[ -n "${GH_TOKEN:-}" && -n "${GITHUB_REPOSITORY:-}" ]]; then
RC_SHA=$(git rev-list -n 1 "${LAST_RC}")
RESULT=$(gh api "/repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${RC_SHA}&status=success" \
--jq '.workflow_runs[] | select(.name == "Tests") | .conclusion' 2>/dev/null || true)
if [[ -z "${RESULT}" ]]; then
fail "Cannot release stable version v${MAJOR_MINOR}.${PATCH}\n\n"\
"Tests did not pass on the last RC (${LAST_RC}).\n"\
"Wait for tests to complete, or release a new RC with fixes."
fi
ok "Tests passed on ${LAST_RC}"
fi
fi
fi
echo ""
ok "All validations passed!"
echo ""
# --- Output to GITHUB_OUTPUT (or stdout for local testing) ---
OUTPUT_FILE="${GITHUB_OUTPUT:-/dev/stdout}"
{
echo "version=${VERSION}"
echo "major_minor=${MAJOR_MINOR}"
echo "release_branch=${RELEASE_BRANCH}"
echo "is_first_rc=${IS_FIRST_RC}"
} >> "${OUTPUT_FILE}"