From 6848ff9531119e7e7bcbedfc86b7f7acf516c916 Mon Sep 17 00:00:00 2001 From: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:51:07 -0700 Subject: [PATCH] ci: adopt dotted RC identifiers Use the SemVer-native rc.N form for new release candidates while preserving historical rcN parsing for verification, upgrades, and release reconciliation. Centralize Radius release-version policy, update operator guidance and active examples, and add focused coverage across tag parsing, selectors, Helm tags, and upgrade ordering. Signed-off-by: Dariusz Porowski <3431813+DariuszPorowski@users.noreply.github.com> --- .github/scripts/checkout-release-codebase.sh | 10 +- .github/scripts/get_release_version.py | 4 +- .github/scripts/release-get-version.sh | 22 +++ .github/scripts/release-get-version_test.sh | 28 ++- .github/scripts/release-verification.sh | 11 +- .../scripts/release-version-format_test.sh | 168 ++++++++++++++++++ .github/scripts/release-version.sh | 54 ++++++ .github/scripts/validate_semver.py | 43 +++-- .github/workflows/functional-test-cloud.yaml | 2 +- .../workflows/functional-test-noncloud.yaml | 2 +- .github/workflows/release-verification.yaml | 2 +- build/test.mk | 6 +- deploy/Chart/tests/helpers_test.yaml | 4 +- .../contributing-releases/README.md | 50 +++--- .../2026-03-goreleaser-release-lifecycle.md | 32 ++-- pkg/upgrade/preflight/version_check.go | 2 +- pkg/upgrade/preflight/version_check_test.go | 27 +-- 17 files changed, 383 insertions(+), 84 deletions(-) create mode 100644 .github/scripts/release-version-format_test.sh create mode 100644 .github/scripts/release-version.sh diff --git a/.github/scripts/checkout-release-codebase.sh b/.github/scripts/checkout-release-codebase.sh index b72385567ef..2962552414c 100755 --- a/.github/scripts/checkout-release-codebase.sh +++ b/.github/scripts/checkout-release-codebase.sh @@ -31,6 +31,10 @@ set -euo pipefail SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME readonly RELEASE_DIR="current_release" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +# shellcheck source=.github/scripts/release-version.sh +source "${SCRIPT_DIR}/release-version.sh" usage() { echo "Usage: ${SCRIPT_NAME}" @@ -75,10 +79,10 @@ main() { exit 1 fi - # Validate version format (should be semver like X.Y.Z or X.Y.Z-rcN) - if ! [[ "${release_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then + # Historical rcN releases remain valid checkout targets. + if ! is_radius_release_version "${release_version}"; then echo "Error: Invalid version format '${release_version}'" - echo "Expected semantic version format (e.g., '0.54.0' or '0.54.0-rc1')" + echo "Expected semantic version format (e.g., '0.61.0' or '0.61.0-rc.1')" exit 1 fi diff --git a/.github/scripts/get_release_version.py b/.github/scripts/get_release_version.py index b545bb44ef4..07277ed5b86 100644 --- a/.github/scripts/get_release_version.py +++ b/.github/scripts/get_release_version.py @@ -23,7 +23,7 @@ # REL_CHANNEL is: # 'edge': for most builds # 'edge': for PR builds -# '1.0.0-rc1' (the full version): for a tagged prerelease +# '1.0.0-rc.1' (the full version): for a tagged prerelease # '1.0' (major.minor): for a tagged release # We set the environment variable UPDATE_RELEASE if it's a full release (tagged and not prerelease) @@ -35,7 +35,7 @@ # # '0.42.42-dev' for most builds # '0.42.42-pr-' for PR builds -# '1.0.0-rc1' (the full version): for a tagged prerelease +# '1.0.0-rc.1' (the full version): for a tagged prerelease # '1.0.0' (major.minor.patch): for a tagged release # # note: we always install the helm chart using the tilde-range syntax to match our behavior diff --git a/.github/scripts/release-get-version.sh b/.github/scripts/release-get-version.sh index 3990b1cf9c8..d74e9567006 100755 --- a/.github/scripts/release-get-version.sh +++ b/.github/scripts/release-get-version.sh @@ -18,11 +18,32 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +# shellcheck source=.github/scripts/release-version.sh +source "${SCRIPT_DIR}/release-version.sh" + fail() { echo "Error: $*" >&2 exit 1 } +validate_release_version() { + local version="$1" + local version_number="${version#v}" + local canonical_version + + if [[ "${version}" != v* ]] || ! is_radius_release_version "${version_number}"; then + fail "unsupported release version ${version}; expected vX.Y.Z or vX.Y.Z-rc.N" + fi + + if is_legacy_rc_version "${version_number}"; then + canonical_version="$(canonical_radius_rc_version "${version_number}")" + printf 'Warning: %s uses the historical RC form; use v%s for new releases.\n' \ + "${version}" "${canonical_version}" >&2 + fi +} + tag_exists() { local repository="$1" local tag="$2" @@ -59,6 +80,7 @@ main() { IFS=',' read -r -a versions <<<"${versions_csv}" for version in "${versions[@]}"; do + validate_release_version "${version}" missing=() for repository in "${repositories[@]}"; do if ! tag_exists "${repository}" "${version}"; then diff --git a/.github/scripts/release-get-version_test.sh b/.github/scripts/release-get-version_test.sh index 0abef68edcd..819ab16dec3 100644 --- a/.github/scripts/release-get-version_test.sh +++ b/.github/scripts/release-get-version_test.sh @@ -208,6 +208,30 @@ test_rc_branch_and_channel() { [[ "$(output_value release-channel)" == "0.62.0-rc.1" ]] || fail_test "RC release channel was parsed incorrectly" } +test_legacy_rc_remains_accepted() { + local rc="v0.62.0-rc2" + + run_selector "${rc}" "${REPOSITORIES[@]}" + [[ "${LAST_STATUS}" -eq 0 ]] || fail_test "legacy RC selection failed: ${LAST_OUTPUT}" + [[ "$(output_value release-branch-name)" == "release/0.62" ]] || fail_test "legacy RC release branch was parsed incorrectly" + [[ "$(output_value release-channel)" == "0.62.0-rc2" ]] || fail_test "legacy RC release channel was parsed incorrectly" + [[ "${LAST_OUTPUT}" == *"use v0.62.0-rc.2 for new releases"* ]] || fail_test "legacy RC selection did not recommend the dotted form" +} + +test_rejects_unsupported_release_versions() { + run_selector "v0.62.0-rc.01" "${REPOSITORIES[@]}" + [[ "${LAST_STATUS}" -ne 0 ]] || fail_test "RC identifiers with leading zeroes should fail" + [[ "${LAST_OUTPUT}" == *"unsupported release version"* ]] || fail_test "invalid dotted RC failed for the wrong reason" + + run_selector "v0.62.0-rc.0" "${REPOSITORIES[@]}" + [[ "${LAST_STATUS}" -ne 0 ]] || fail_test "RC zero should fail" + [[ "${LAST_OUTPUT}" == *"unsupported release version"* ]] || fail_test "RC zero failed for the wrong reason" + + run_selector "v0.62.0-beta.1" "${REPOSITORIES[@]}" + [[ "${LAST_STATUS}" -ne 0 ]] || fail_test "unsupported prerelease identifiers should fail" + [[ "${LAST_OUTPUT}" == *"unsupported release version"* ]] || fail_test "unsupported prerelease failed for the wrong reason" +} + test_rejects_multiple_incomplete_versions() { run_selector "v0.63.0,v0.62.0" "${REPOSITORIES[@]}" [[ "${LAST_STATUS}" -ne 0 ]] || fail_test "multiple incomplete versions should fail" @@ -239,13 +263,15 @@ main() { test_selects_version_missing_from_any_repository test_skips_version_complete_in_every_repository test_rc_branch_and_channel + test_legacy_rc_remains_accepted + test_rejects_unsupported_release_versions test_rejects_multiple_incomplete_versions test_requires_repository_and_output test_remote_query_errors_are_not_treated_as_missing_tags test_main_waits_until_trigger_commit_is_cherry_picked test_main_resumes_branch_created_before_tag test_release_branch_trigger_never_waits_for_cherry_pick - echo "release version selection and resume tests passed (9 tests)" + echo "release version selection and resume tests passed (11 tests)" } main "$@" diff --git a/.github/scripts/release-verification.sh b/.github/scripts/release-verification.sh index 3856b80b2da..3fa385c3398 100755 --- a/.github/scripts/release-verification.sh +++ b/.github/scripts/release-verification.sh @@ -18,6 +18,11 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +# shellcheck source=.github/scripts/release-version.sh +source "${SCRIPT_DIR}/release-version.sh" + # Configuration readonly NAMESPACE="radius-system" readonly GITHUB_ORG="radius-project" @@ -129,9 +134,9 @@ if [[ -z "$RELEASE_VERSION_NUMBER" ]]; then exit 1 fi -# Validate version format -if [[ ! "$RELEASE_VERSION_NUMBER" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then - echo "Error: Invalid version format. Expected format: X.Y.Z or X.Y.Z-rcN" >&2 +# Validate version format. Historical rcN releases remain verifiable. +if ! is_radius_release_version "${RELEASE_VERSION_NUMBER}"; then + echo "Error: Invalid version format. Expected format: X.Y.Z or X.Y.Z-rc.N" >&2 exit 1 fi diff --git a/.github/scripts/release-version-format_test.sh b/.github/scripts/release-version-format_test.sh new file mode 100644 index 00000000000..8e513b3577f --- /dev/null +++ b/.github/scripts/release-version-format_test.sh @@ -0,0 +1,168 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Copyright 2026 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +readonly VALIDATOR="${SCRIPT_DIR}/validate_semver.py" +readonly TAG_PARSER="${SCRIPT_DIR}/get_release_version.py" +readonly RUNBOOK="${SCRIPT_DIR}/../../docs/contributing/contributing-releases/README.md" +# shellcheck source=.github/scripts/release-version.sh +source "${SCRIPT_DIR}/release-version.sh" + +if [[ -z "${PYTHON:-}" ]]; then + if command -v python3 > /dev/null; then + PYTHON="python3" + else + PYTHON="python" + fi +fi +readonly PYTHON + +TEMP_DIR="" + +cleanup() { + if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then + rm -rf "${TEMP_DIR}" + fi +} +trap cleanup EXIT + +fail_test() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_valid() { + local version="$1" + + if ! "${PYTHON}" "${VALIDATOR}" "${version}" > /dev/null; then + fail_test "expected valid SemVer: ${version}" + fi +} + +assert_invalid() { + local version="$1" + + if "${PYTHON}" "${VALIDATOR}" "${version}" > /dev/null 2>&1; then + fail_test "expected invalid SemVer: ${version}" + fi +} + +assert_tag_parser() { + local version="$1" + local environment_file="${TEMP_DIR}/github-env" + + : > "${environment_file}" + GITHUB_REF="refs/tags/v${version}" GITHUB_ENV="${environment_file}" \ + "${PYTHON}" "${TAG_PARSER}" > /dev/null + + if ! grep -Fxq "REL_VERSION=${version}" "${environment_file}"; then + fail_test "tag parser did not preserve release version ${version}" + fi + if ! grep -Fxq "REL_CHANNEL=${version}" "${environment_file}"; then + fail_test "tag parser did not preserve release channel ${version}" + fi + if ! grep -Fxq "CHART_VERSION=${version}" "${environment_file}"; then + fail_test "tag parser did not preserve chart version ${version}" + fi +} + +assert_radius_valid() { + local version="$1" + + if ! is_radius_release_version "${version}"; then + fail_test "expected valid Radius release version: ${version}" + fi +} + +assert_radius_invalid() { + local version="$1" + + if is_radius_release_version "${version}"; then + fail_test "expected invalid Radius release version: ${version}" + fi +} + +assert_policy_callers() { + local script + + for script in \ + release-get-version.sh \ + release-verification.sh \ + checkout-release-codebase.sh; do + if ! grep -Fq "source \"\${SCRIPT_DIR}/release-version.sh\"" \ + "${SCRIPT_DIR}/${script}"; then + fail_test "${script} does not use the shared release-version policy" + fi + done +} + +assert_runbook_uses_dotted_rc() { + local legacy_lines + local version + + legacy_lines="$(grep -nE 'rc[0-9]' "${RUNBOOK}" || true)" + if [[ -n "${legacy_lines}" ]]; then + fail_test "release runbook shows legacy RC identifiers:"$'\n'"${legacy_lines}" + fi + + while read -r version; do + assert_radius_valid "${version#v}" + if is_legacy_rc_version "${version#v}"; then + fail_test "release runbook example uses a legacy RC version: ${version}" + fi + done < <(grep -oE "version: 'v[0-9][^']*'" "${RUNBOOK}" | grep -oE "v[0-9][^']*") +} + +main() { + TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/release-version-format-test-XXXXXX")" + + assert_valid "0.61.0" + assert_valid "0.61.0-rc.1" + assert_valid "0.61.0-rc1" + assert_valid "0.61.0-beta.1" + assert_valid "0.61.0-rc.0" + assert_valid "0.61.0-rc.2+build.7" + assert_invalid "v0.61.0-rc.1" + assert_invalid "0.61.0-rc.01" + assert_invalid "0.61.0-rc..1" + assert_invalid "0.61.0-rc_1" + + assert_tag_parser "0.61.0-rc.1" + assert_tag_parser "0.60.0-rc3" + + assert_radius_valid "0.61.0" + assert_radius_valid "0.61.0-rc.1" + assert_radius_valid "0.60.0-rc3" + assert_radius_invalid "v0.61.0-rc.1" + assert_radius_invalid "0.61.0-beta.1" + assert_radius_invalid "0.61.0-rc.0" + assert_radius_invalid "0.61.0-rc0" + assert_radius_invalid "0.61.0-rc.01" + if [[ "$(canonical_radius_rc_version "0.60.0-rc3")" != "0.60.0-rc.3" ]]; then + fail_test "legacy RC canonicalization failed" + fi + assert_policy_callers + assert_runbook_uses_dotted_rc + + echo "release version format tests passed (24 tests)" +} + +main "$@" diff --git a/.github/scripts/release-version.sh b/.github/scripts/release-version.sh new file mode 100644 index 00000000000..cfffd060047 --- /dev/null +++ b/.github/scripts/release-version.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Copyright 2026 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +# Shared Radius release-version policy; source this file from release scripts. +# The constants below are readonly, so guard against being sourced twice. +if [[ -n "${RADIUS_RELEASE_VERSION_POLICY_LOADED:-}" ]]; then + return 0 +fi +RADIUS_RELEASE_VERSION_POLICY_LOADED=1 + +readonly RADIUS_SEMVER_NUMBER='(0|[1-9][0-9]*)' +readonly RADIUS_RC_NUMBER='[1-9][0-9]*' +RADIUS_RELEASE_VERSION_PATTERN="^${RADIUS_SEMVER_NUMBER}\\.${RADIUS_SEMVER_NUMBER}" +RADIUS_RELEASE_VERSION_PATTERN+="\\.${RADIUS_SEMVER_NUMBER}" +RADIUS_RELEASE_VERSION_PATTERN+="(-rc(\\.${RADIUS_RC_NUMBER}|${RADIUS_RC_NUMBER}))?$" +readonly RADIUS_RELEASE_VERSION_PATTERN +readonly RADIUS_LEGACY_RC_PATTERN="-rc${RADIUS_RC_NUMBER}$" + +is_radius_release_version() { + local version="$1" + + [[ "${version}" =~ ${RADIUS_RELEASE_VERSION_PATTERN} ]] +} + +is_legacy_rc_version() { + local version="$1" + + [[ "${version}" =~ ${RADIUS_LEGACY_RC_PATTERN} ]] +} + +canonical_radius_rc_version() { + local version="$1" + + if is_legacy_rc_version "${version}"; then + printf '%s\n' "${version/-rc/-rc.}" + else + printf '%s\n' "${version}" + fi +} diff --git a/.github/scripts/validate_semver.py b/.github/scripts/validate_semver.py index 8598647775a..b537548ea04 100755 --- a/.github/scripts/validate_semver.py +++ b/.github/scripts/validate_semver.py @@ -14,34 +14,41 @@ # limitations under the License. # ------------------------------------------------------------ -# This script validates that the provided version is a valid semver +# This script validates that the provided version is valid SemVer. -import os import re import sys +# Adapted from the suggested SemVer 2.0.0 regular expression: +# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +# All groups are non-capturing because only match success is used. +SEMVER_PATTERN = re.compile( + r"(?:0|[1-9][0-9]*)\." + r"(?:0|[1-9][0-9]*)\." + r"(?:0|[1-9][0-9]*)" + r"(?:-(?:(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)" + r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?" + r"(?:\+(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?" +) -def main(): - if len(sys.argv) != 2: - print("Usage: validate_semver.py ") - sys.exit(1) - # From https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - SEMVER_REGEX = r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" +def is_valid_semver(version: str) -> bool: + return SEMVER_PATTERN.fullmatch(version) is not None - pattern = re.compile(SEMVER_REGEX) - version = sys.argv[1] - match = pattern.search(version) +def main() -> int: + if len(sys.argv) != 2: + print("Usage: validate_semver.py ") + return 1 - # If no match, then return an error (provided version is not valid semver) - if match is None: + version = sys.argv[1] + if not is_valid_semver(version): print("Provided version is not valid semver") - sys.exit(1) - else: - print("Provided version is valid semver") - sys.exit(0) + return 1 + + print("Provided version is valid semver") + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/.github/workflows/functional-test-cloud.yaml b/.github/workflows/functional-test-cloud.yaml index 06317c8997b..d02013a5f1e 100644 --- a/.github/workflows/functional-test-cloud.yaml +++ b/.github/workflows/functional-test-cloud.yaml @@ -35,7 +35,7 @@ on: # Expected client_payload: # src_image: Source image in ACR (e.g. radiusdeploymentengine.azurecr.io/deployment-engine) # dest_image: Destination image in GHCR (e.g. ghcr.io/radius-project/deployment-engine) - # tag: Tag for the image (e.g. latest, 0.1, 0.1.0-rc1, pr-123) + # tag: Tag for the image (e.g. latest, 0.1, 0.61.0-rc.1, pr-123) # pull_request_target runs in the context of the base branch, providing access to secrets. # For external contributors, the approval-gate job requires manual approval before tests run. # SECURITY: We use pull_request_target but do NOT run any code from the PR until after approval. diff --git a/.github/workflows/functional-test-noncloud.yaml b/.github/workflows/functional-test-noncloud.yaml index 2c4f0e5cf77..0693782871b 100644 --- a/.github/workflows/functional-test-noncloud.yaml +++ b/.github/workflows/functional-test-noncloud.yaml @@ -35,7 +35,7 @@ on: # Expected client_payload: # src_image: Source image in ACR (e.g. radiusdeploymentengine.azurecr.io/deployment-engine) # dest_image: Destination image in GHCR (e.g. ghcr.io/radius-project/deployment-engine) - # tag: Tag for the image (e.g. latest, 0.1, 0.1.0-rc1, pr-123) + # tag: Tag for the image (e.g. latest, 0.1, 0.61.0-rc.1, pr-123) pull_request: branches: - main diff --git a/.github/workflows/release-verification.yaml b/.github/workflows/release-verification.yaml index d482197178e..7cb7bf1e3f7 100644 --- a/.github/workflows/release-verification.yaml +++ b/.github/workflows/release-verification.yaml @@ -22,7 +22,7 @@ on: workflow_dispatch: inputs: version: - description: Radius version number to use (e.g. 0.1.0, 0.1.0-rc1) + description: Radius version number to use (e.g. 0.61.0, 0.61.0-rc.1) required: true default: "" type: string diff --git a/build/test.mk b/build/test.mk index c6878c349ac..45c41fdbc17 100644 --- a/build/test.mk +++ b/build/test.mk @@ -53,7 +53,7 @@ GOTEST_OPTS ?= GOTEST_TOOL ?= go tool gotestsum $(GOTESTSUM_OPTS) -- .PHONY: test -test: test-get-envtools test-helm test-manage-radius-installation test-release-parity-manifest test-changelog-range test-build-summary test-goreleaser-shadow test-capture-release-image-digests test-release-get-version test-release-tag-and-branch test-monitor-remote-workflow ## Runs unit tests, excluding kubernetes controller tests +test: test-get-envtools test-helm test-manage-radius-installation test-release-parity-manifest test-changelog-range test-build-summary test-goreleaser-shadow test-capture-release-image-digests test-release-version-format test-release-get-version test-release-tag-and-branch test-monitor-remote-workflow ## Runs unit tests, excluding kubernetes controller tests KUBEBUILDER_ASSETS="$(shell $(ENV_SETUP) use -p path ${K8S_VERSION} --arch amd64)" CGO_ENABLED=1 $(GOTEST_TOOL) ./pkg/... ./test/validation/... $(GOTEST_OPTS) .PHONY: test-manage-radius-installation @@ -88,6 +88,10 @@ test-release-tag-and-branch: ## Tests release tag and branch reconciliation test-release-get-version: ## Tests release version selection across repositories @bash ./.github/scripts/release-get-version_test.sh +.PHONY: test-release-version-format +test-release-version-format: ## Tests release SemVer validation and tag parsing + @bash ./.github/scripts/release-version-format_test.sh + .PHONY: test-monitor-remote-workflow test-monitor-remote-workflow: ## Tests exact remote workflow dispatch correlation @node --test ./.github/scripts/monitor-remote-workflow_test.mjs diff --git a/deploy/Chart/tests/helpers_test.yaml b/deploy/Chart/tests/helpers_test.yaml index de25886bf12..e6751a37263 100644 --- a/deploy/Chart/tests/helpers_test.yaml +++ b/deploy/Chart/tests/helpers_test.yaml @@ -469,13 +469,13 @@ tests: global.imageRegistry: myregistry.io global.imageTag: 0.48 controller.image: controller - controller.tag: 0.49-rc1 + controller.tag: 0.61.0-rc.1 ucp.image: ucpd asserts: # Controller with override should use its specific tag - equal: path: spec.template.spec.containers[0].image - value: myregistry.io/controller:0.49-rc1 + value: myregistry.io/controller:0.61.0-rc.1 template: controller/deployment.yaml # UCP should use global tag - equal: diff --git a/docs/contributing/contributing-releases/README.md b/docs/contributing/contributing-releases/README.md index e2df21fcc61..ec4c7a400c7 100644 --- a/docs/contributing/contributing-releases/README.md +++ b/docs/contributing/contributing-releases/README.md @@ -23,13 +23,15 @@ Before starting a release, ensure you have: ## Terminology -| Term | Description | Example | -|---------------------|---------------------------------------------------------------------------------------------------------------|------------------------------| -| **RC release** | A release candidate for internal validation before public release. Create additional RCs if validation fails. | `v0.56.0-rc1`, `v0.56.0-rc2` | -| **Final release** | A public release, built from the last validated RC. | `v0.56.0` | -| **Patch release** | A bug-fix release for an existing final release. | `v0.56.1` | -| **Release channel** | A `.` pair that groups all releases for a version. | `0.56` | -| **Release branch** | A branch in the format `release/` that holds release code. | `release/0.56` | +| Term | Description | Example | +|---------------------|---------------------------------------------------------------------------------------------------------------|--------------------------------| +| **RC release** | A release candidate for internal validation before public release. Create additional RCs if validation fails. | `v0.56.0-rc.1`, `v0.56.0-rc.2` | +| **Final release** | A public release, built from the last validated RC. | `v0.56.0` | +| **Patch release** | A bug-fix release for an existing final release. | `v0.56.1` | +| **Release channel** | A `.` pair that groups all releases for a version. | `0.56` | +| **Release branch** | A branch in the format `release/` that holds release code. | `release/0.56` | + +New release candidates use the dotted SemVer form `-rc.N`, starting at `-rc.1`. Historical `-rcN` releases remain valid inputs for verification and upgrade tooling, but do not use that form for new tags. ## How releases work @@ -49,7 +51,7 @@ Two GitHub Actions workflows drive the release process. **No one manually create 1. **[Release Radius](https://github.com/radius-project/radius/actions/workflows/release.yaml)** (`release.yaml`): Triggered whenever `versions.yaml` is pushed to `main` or a `release/*` branch. This workflow: - Scans `versions.yaml` in `.supported[]` order and selects the first `.version` whose tag is missing from any of `radius`, `recipes`, `dashboard`, or `bicep-types-aws` - - **Automatically creates and pushes the version tag** (e.g., `v0.56.0-rc1`) for `radius`, `recipes`, `dashboard`, and `bicep-types-aws` + - **Automatically creates and pushes the version tag** (e.g., `v0.56.0-rc.1`) for `radius`, `recipes`, `dashboard`, and `bicep-types-aws` - Creates the release branch (`release/`) if it does not already exist - Dispatches Deployment Engine image publishing to GHCR - Reconciles matching existing branches and tags as successful state, rejects conflicting tag targets, and resumes any repositories left incomplete by a failed run @@ -109,7 +111,7 @@ When starting the release process, first create an RC release. If validation fai ### Step 1: Start a Teams release thread -Before performing any release actions, start and join a meeting in the team's Microsoft Teams channel dedicated to releases. Title the thread with the target final release version for the entire release cycle (for example, use "Release v0.56.0", not "Release v0.56.0-rc1"). +Before performing any release actions, start and join a meeting in the team's Microsoft Teams channel dedicated to releases. Title the thread with the target final release version for the entire release cycle (for example, use "Release v0.56.0", not "Release v0.56.0-rc.1"). Turn on transcription for the meeting. Recording is not necessary. Verbally announce each step as you perform it, and post updates in the thread with the same information. This creates a detailed timeline of the release process that can be reviewed later for improvements and serves as a record of the release. @@ -123,13 +125,13 @@ This detailed release log helps the team improve future releases by reviewing th ### Step 2: Tag the Deployment Engine -Run the following in a local clone of the [Deployment Engine repo](https://github.com/azure-octo/deployment-engine), replacing `vX.Y.Z-rcN` with the RC version (e.g., `v0.56.0-rc1`): +Run the following in a local clone of the [Deployment Engine repo](https://github.com/azure-octo/deployment-engine), replacing `vX.Y.Z-rc.N` with the RC version (e.g., `v0.56.0-rc.1`): ```bash git checkout main git pull origin main -git tag vX.Y.Z-rcN -git push origin vX.Y.Z-rcN +git tag vX.Y.Z-rc.N +git push origin vX.Y.Z-rc.N ``` > **Note**: This manual tagging step is a temporary workaround. Ideally the [Deployment Engine Release Workflow](https://github.com/azure-octo/deployment-engine/actions/workflows/release.yaml) would handle this, but GPG signing is not yet configured there. See [azure-octo/deployment-engine#456](https://github.com/azure-octo/deployment-engine/issues/456). @@ -141,7 +143,7 @@ Create a branch from `main` in the `radius-project/radius` repo: ```bash git checkout main git pull origin main -git checkout -b /release-X.Y.0-rcN +git checkout -b /release-X.Y.0-rc.N ``` Edit `versions.yaml` to add the new RC as a supported version. Move the oldest supported version to the `deprecated` list if needed ([example PR](https://github.com/radius-project/radius/pull/6077/files)). @@ -149,7 +151,7 @@ Edit `versions.yaml` to add the new RC as a supported version. Move the oldest s ```yaml supported: - channel: '0.56' - version: 'v0.56.0-rc1' + version: 'v0.56.0-rc.1' - channel: '0.55' version: 'v0.55.0' deprecated: @@ -162,7 +164,7 @@ deprecated: Push the branch and create a PR against `main`: ```bash -git push origin /release-X.Y.0-rcN +git push origin /release-X.Y.0-rc.N ``` After approval, merge the PR to `main`. @@ -171,12 +173,12 @@ After approval, merge the PR to `main`. After merging, the [Release Radius](https://github.com/radius-project/radius/actions/workflows/release.yaml) workflow automatically runs because `versions.yaml` changed on `main`. -- **First RC**: The workflow creates the `release/X.Y` branch from `main` and pushes the `vX.Y.Z-rcN` tag. The tag push then triggers the [release build](https://github.com/radius-project/radius/actions/workflows/build-release.yaml) workflow. No manual tag creation is needed. Verify the release using the checklist below. +- **First RC**: The workflow creates the `release/X.Y` branch from `main` and pushes the `vX.Y.Z-rc.N` tag. The tag push then triggers the [release build](https://github.com/radius-project/radius/actions/workflows/build-release.yaml) workflow. No manual tag creation is needed. Verify the release using the checklist below. - **Subsequent RCs**: The workflow detects that the release branch already exists and **skips tag creation**. This is expected — the tag will be created when the cherry-pick lands on the release branch in [Step 6](#step-6-cherry-pick-additional-changes-subsequent-rcs-only). Skip ahead to Step 6 for now and return to verify after completing it. Monitor and verify: -1. The [Release Radius](https://github.com/radius-project/radius/actions/workflows/release.yaml) workflow completes successfully. For the first RC, confirm it created the `release/X.Y` [branch](https://github.com/radius-project/radius/branches) and the `vX.Y.Z-rcN` [tag](https://github.com/radius-project/radius/tags). +1. The [Release Radius](https://github.com/radius-project/radius/actions/workflows/release.yaml) workflow completes successfully. For the first RC, confirm it created the `release/X.Y` [branch](https://github.com/radius-project/radius/branches) and the `vX.Y.Z-rc.N` [tag](https://github.com/radius-project/radius/tags). 2. The [release build](https://github.com/radius-project/radius/actions/workflows/build-release.yaml) workflow (triggered by the tag push) completes successfully. This workflow also dispatches Bicep types publishing automatically. 3. An RC release marked as pre-release appears on [GitHub Releases](https://github.com/radius-project/radius/releases). @@ -184,12 +186,12 @@ Monitor and verify: > **Skip this step for the first RC.** The release branch was just created from `main` and already contains all changes. -For subsequent RCs (`rc2`, `rc3`, etc.), cherry-pick the `versions.yaml` update and any bug fixes onto the release branch: +For subsequent RCs (`rc.2`, `rc.3`, etc.), cherry-pick the `versions.yaml` update and any bug fixes onto the release branch: ```bash git checkout release/X.Y git pull origin release/X.Y -git checkout -b /cherry-pick-rcN-to-release-branch +git checkout -b /cherry-pick-rc.N-to-release-branch git cherry-pick -x git cherry-pick -x ``` @@ -199,14 +201,14 @@ git cherry-pick -x Push and create a PR targeting the release branch: ```bash -git push origin /cherry-pick-rcN-to-release-branch +git push origin /cherry-pick-rc.N-to-release-branch ``` After approval, merge the PR. This triggers the release automation on the release branch, creating the new RC tag. Return to [Step 5](#step-5-verify-the-automated-release) to verify the release completed successfully. ### Step 7: Run validation workflows -1. In `radius-project/radius`, run the [Release verification](https://github.com/radius-project/radius/actions/workflows/release-verification.yaml) workflow from the `release/X.Y` branch. Enter the RC version number without the `v` prefix as the version (e.g., `0.56.0-rc1`). +1. In `radius-project/radius`, run the [Release verification](https://github.com/radius-project/radius/actions/workflows/release-verification.yaml) workflow from the `release/X.Y` branch. Enter the RC version number without the `v` prefix as the version (e.g., `0.56.0-rc.1`). 2. In `radius-project/docs`, run the [Upmerge docs to edge](https://github.com/radius-project/docs/actions/workflows/upmerge.yaml) workflow from the **previous** release branch (e.g., run from `v0.55` when releasing `v0.56`). @@ -216,7 +218,7 @@ After approval, merge the PR. This triggers the release automation on the releas > This generates a PR. Get approval and merge it before proceeding. The PR excludes `bicepconfig.json`. -4. In `radius-project/samples`, run the [Test Samples](https://github.com/radius-project/samples/actions/workflows/test.yaml) workflow from the `edge` branch. Enter the RC version number without the `v` prefix as the version (e.g., `0.56.0-rc1`). +4. In `radius-project/samples`, run the [Test Samples](https://github.com/radius-project/samples/actions/workflows/test.yaml) workflow from the `edge` branch. Enter the RC version number without the `v` prefix as the version (e.g., `0.56.0-rc.1`). > Run this only after the upmerge PR has been merged to `edge`. If tests fail, check logs and existing issues in the samples repo. Flaky tests may pass on re-run. If failures persist, file an issue and raise it with maintainers. @@ -224,7 +226,7 @@ After approval, merge the PR. This triggers the release automation on the releas If all validation workflows pass, proceed to [creating the final release](#creating-the-final-release). -If validation fails, fix the issues on `main`, then create a new RC (increment the RC number, e.g., `rc2`, `rc3`) by repeating the steps above. +If validation fails, fix the issues on `main`, then create a new RC (increment the RC number, e.g., `rc.2`, `rc.3`) by repeating the steps above. ## Creating the final release @@ -262,7 +264,7 @@ git checkout -b /final-release-X.Y.0 ```yaml supported: - channel: '0.56' - version: 'v0.56.0' # was v0.56.0-rc1 + version: 'v0.56.0' # was v0.56.0-rc.1 ``` 2. **Create a draft release notes file**: Add `docs/release-notes/vX.Y.Z.md` using the [release notes template](../../release-notes/template.md). See the [release notes README](../../release-notes/README.md) for instructions ([example PR](https://github.com/radius-project/radius/pull/6092/files)). diff --git a/eng/design-notes/tools/2026-03-goreleaser-release-lifecycle.md b/eng/design-notes/tools/2026-03-goreleaser-release-lifecycle.md index cf5d1f804cc..e8b026299bd 100644 --- a/eng/design-notes/tools/2026-03-goreleaser-release-lifecycle.md +++ b/eng/design-notes/tools/2026-03-goreleaser-release-lifecycle.md @@ -92,7 +92,7 @@ For contributors and reviewers, the main experience improvement is consistency: ```text Workflow: Prepare Radius Release -version: 0.56.0-rc1 +version: 0.56.0-rc.1 source ref: release/0.56 mode: prepare and publish after approval ``` @@ -100,7 +100,7 @@ mode: prepare and publish after approval **Sample Output:** ```text -GitHub Release: Radius v0.56.0-rc1 +GitHub Release: Radius v0.56.0-rc.1 Artifacts: - raw rad binaries for supported operating systems and architectures - per-asset SHA-256 checksums and a combined checksum manifest @@ -260,18 +260,18 @@ The configuration targets the GoReleaser OSS edition and pins a minimum version [GoReleaser Pro](https://goreleaser.com/pro/) is a paid edition with additional features. This design is implementable entirely on the OSS edition: no stage assumes a Pro license, and every Pro capability that would otherwise be attractive has a deliberate OSS substitute. Most substitutes fall out of the release controller, which must own cross-repository orchestration in either edition. -| Capability | GoReleaser Pro feature | OSS workaround in this design | -|--------------------------------------------|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Staged publication with a gate | Release phases: `goreleaser release --prepare`, then separate publish and announce steps | `release.draft: true` stages everything in a draft GitHub Release; the controller publishes the draft only after the publication gate passes | -| Resuming a failed release | `goreleaser continue` | Rerun GoReleaser against the same tag with `use_existing_draft` and `replace_existing_artifacts`; the `Resume Release` workflow reconciles all non-GoReleaser stages | -| Verifying published assets | Built-in verify that re-downloads assets and runs custom checks | The controller's verify stage compares `artifacts.json` and `metadata.json` against the release manifest, checks image digests and platforms, and runs the staged installation check | -| Previous-tag selection and version ordering | Smart SemVer tag sorting | The release plan records the current and previous tags; the controller exports `GORELEASER_CURRENT_TAG` and `GORELEASER_PREVIOUS_TAG` so no tag-sorting heuristic is ever trusted | -| Changelog preview, subgroups, path filters | `goreleaser changelog` command and enhanced changelog options | The version-preparation workflow renders the notes into the reviewable release PR; prepared notes are passed with `--release-notes`, and OSS `changelog.groups` remains the fallback | -| Nightly and edge builds | Nightlies | The separate main-branch edge workflow runs snapshot mode and publishes the mutable `edge` tags outside the release transaction | -| Faster multi-platform releases | Split and merge builds across runners | One release runner with QEMU and Buildx emulation; revisit only if release duration becomes unacceptable, as a license decision rather than a design change | -| Dynamic Dockerfiles and copied files | `templated_dockerfile` and `templated_extra_files` in `dockers_v2` | Static per-component `Dockerfile.goreleaser` files, plain `extra_files` for the UCP manifests, and `build_args` for values that vary per build | -| Consuming binaries built elsewhere | Prebuilt-binaries builder | Not used: GoReleaser builds all six binaries on the release runner itself, which is also why the single-runner model is the OSS baseline | -| Config reuse and templating extras | `include` keyword, custom template variables, templated files, monorepo support | Not required: one product version, one `.goreleaser.yaml`, and all remaining templating stays within OSS template fields | +| Capability | GoReleaser Pro feature | OSS workaround in this design | +|---------------------------------------------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Staged publication with a gate | Release phases: `goreleaser release --prepare`, then separate publish and announce steps | `release.draft: true` stages everything in a draft GitHub Release; the controller publishes the draft only after the publication gate passes | +| Resuming a failed release | `goreleaser continue` | Rerun GoReleaser against the same tag with `use_existing_draft` and `replace_existing_artifacts`; the `Resume Release` workflow reconciles all non-GoReleaser stages | +| Verifying published assets | Built-in verify that re-downloads assets and runs custom checks | The controller's verify stage compares `artifacts.json` and `metadata.json` against the release manifest, checks image digests and platforms, and runs the staged installation check | +| Previous-tag selection and version ordering | Smart SemVer tag sorting | The release plan records the current and previous tags; the controller exports `GORELEASER_CURRENT_TAG` and `GORELEASER_PREVIOUS_TAG` so no tag-sorting heuristic is ever trusted | +| Changelog preview, subgroups, path filters | `goreleaser changelog` command and enhanced changelog options | The version-preparation workflow renders the notes into the reviewable release PR; prepared notes are passed with `--release-notes`, and OSS `changelog.groups` remains the fallback | +| Nightly and edge builds | Nightlies | The separate main-branch edge workflow runs snapshot mode and publishes the mutable `edge` tags outside the release transaction | +| Faster multi-platform releases | Split and merge builds across runners | One release runner with QEMU and Buildx emulation; revisit only if release duration becomes unacceptable, as a license decision rather than a design change | +| Dynamic Dockerfiles and copied files | `templated_dockerfile` and `templated_extra_files` in `dockers_v2` | Static per-component `Dockerfile.goreleaser` files, plain `extra_files` for the UCP manifests, and `build_args` for values that vary per build | +| Consuming binaries built elsewhere | Prebuilt-binaries builder | Not used: GoReleaser builds all six binaries on the release runner itself, which is also why the single-runner model is the OSS baseline | +| Config reuse and templating extras | `include` keyword, custom template variables, templated files, monorepo support | Not required: one product version, one `.goreleaser.yaml`, and all remaining templating stays within OSS template fields | Pro features with no bearing on this design - macOS and Windows installers, notarization, NPM and Homebrew Cask publishing, DockerHub description sync, Cloudsmith and GemFury integrations, Podman builds, and OpenTelemetry trace export - are omitted from the table. @@ -291,7 +291,7 @@ Each image retains its current base-image requirements. Separate `Dockerfile.gor This removes most custom production Docker logic currently encoded in Make and CI while preserving important image differences such as distroless, Alpine, or Debian base images. The Bicep image remains a dedicated non-Go build because it downloads an external binary and generates `bicepconfig.json`. The `testrp` and `magpiego` images remain in test workflows because they use separate Go modules and are not user-facing release artifacts. Migration cannot remove their existing channel tags until repository consumers are inventoried. -GoReleaser publishes full-version tags such as `0.60.0` or `0.60.0-rc1` first. The release controller promotes the mutable aliases - the release channel such as `0.60`, and `latest`, which always points at the most recent stable release - only after all mandatory outputs pass verification. RC releases advance no alias, so alias promotion applies to final and patch releases only. The mutable `edge` tag tracks the `main` branch and is owned by the separate edge workflow, never by the release transaction. The Helm chart for a final or patch release should reference immutable full-version image tags while the channel aliases remain available for backward compatibility. +GoReleaser publishes full-version tags such as `0.60.0` or `0.60.0-rc.1` first. The release controller promotes the mutable aliases - the release channel such as `0.60`, and `latest`, which always points at the most recent stable release - only after all mandatory outputs pass verification. RC releases advance no alias, so alias promotion applies to final and patch releases only. The mutable `edge` tag tracks the `main` branch and is owned by the separate edge workflow, never by the release transaction. The Helm chart for a final or patch release should reference immutable full-version image tags while the channel aliases remain available for backward compatibility. Four properties of this model are constraints, not implementation details: @@ -322,7 +322,7 @@ These concerns share one invariant: exactly one authoritative version record exi No tool can infer compatibility impact from code with complete accuracy. Every automated option moves the human decision to a different reviewable input: a commit type, a change-fragment bump, or a pull request label. Radius must also document how these signals map to its current `0.x` versions and RC prereleases because SemVer intentionally gives projects more latitude before `1.0.0`. -Prerelease identifiers need one explicit decision. Radius currently tags RCs as `-rc1`, `-rc2`, and SemVer compares alphanumeric prerelease identifiers lexically, so `0.56.0-rc10` sorts before `0.56.0-rc2`. Every automation candidate in this design defaults to the dotted `-rc.N` form, which compares numerically and orders correctly. The migration should either adopt `-rc.N` for new releases - the Helm chart's prerelease detection matches any `rc` substring and is unaffected - or keep `-rcN` and enforce an `rc9` ceiling in version-selection validation as an accepted constraint. +Radius historically tagged RCs as `-rc1`, `-rc2`, and SemVer compares those alphanumeric prerelease identifiers lexically, so `0.56.0-rc10` sorts before `0.56.0-rc2`. New releases use the dotted `-rc.N` form, whose numeric identifier orders correctly. Historical `-rcN` tags remain readable for previous-tag selection and upgrade compatibility; they are not rewritten. The Helm chart's prerelease detection matches any `rc` substring and is unaffected. #### Common changelog output contract diff --git a/pkg/upgrade/preflight/version_check.go b/pkg/upgrade/preflight/version_check.go index 1442eb806eb..8909711f18c 100644 --- a/pkg/upgrade/preflight/version_check.go +++ b/pkg/upgrade/preflight/version_check.go @@ -134,7 +134,7 @@ func (v *VersionCompatibilityCheck) isValidUpgradeVersion(currentVersion, target } // Allow upgrades within the same minor version (patch bumps, prerelease upgrades) - // e.g., 0.55.0-rc4 -> 0.55.0-rc5, 0.55.0-rc5 -> 0.55.0, 0.55.0 -> 0.55.1 + // e.g., 0.61.0-rc.2 -> 0.61.0-rc.10, 0.61.0-rc.10 -> 0.61.0, 0.61.0 -> 0.61.1 // Same-version case (e.g., 0.55.0 -> 0.55.0) is already rejected by the Equal check above. if target.Major() == current.Major() && target.Minor() == current.Minor() { return true, "", nil diff --git a/pkg/upgrade/preflight/version_check_test.go b/pkg/upgrade/preflight/version_check_test.go index 4d25bd45a6b..66a88e01042 100644 --- a/pkg/upgrade/preflight/version_check_test.go +++ b/pkg/upgrade/preflight/version_check_test.go @@ -68,17 +68,24 @@ func TestVersionCompatibilityCheck_Run(t *testing.T) { }, { name: "valid prerelease upgrade same version", - currentVersion: "0.55.0-rc4", - targetVersion: "0.55.0-rc5", + currentVersion: "0.61.0-rc.2", + targetVersion: "0.61.0-rc.10", expectSuccess: true, - expectMessage: "Upgrade from 0.55.0-rc4 to 0.55.0-rc5 is valid", + expectMessage: "Upgrade from 0.61.0-rc.2 to 0.61.0-rc.10 is valid", }, { name: "valid prerelease to release upgrade", - currentVersion: "v0.55.0-rc5", - targetVersion: "v0.55.0", + currentVersion: "v0.61.0-rc.10", + targetVersion: "v0.61.0", + expectSuccess: true, + expectMessage: "Upgrade from v0.61.0-rc.10 to v0.61.0 is valid", + }, + { + name: "valid historical prerelease upgrade", + currentVersion: "0.60.0-rc4", + targetVersion: "0.60.0-rc5", expectSuccess: true, - expectMessage: "Upgrade from v0.55.0-rc5 to v0.55.0 is valid", + expectMessage: "Upgrade from 0.60.0-rc4 to 0.60.0-rc5 is valid", }, { name: "valid patch version upgrade", @@ -139,14 +146,14 @@ func TestValidateVersionJump(t *testing.T) { }, { name: "safe prerelease upgrade", - currentVersion: "0.55.0-rc4", - targetVersion: "0.55.0-rc5", + currentVersion: "0.61.0-rc.2", + targetVersion: "0.61.0-rc.10", expectValid: true, }, { name: "safe prerelease to release", - currentVersion: "0.55.0-rc5", - targetVersion: "0.55.0", + currentVersion: "0.61.0-rc.10", + targetVersion: "0.61.0", expectValid: true, }, {