From f17c4d84c5fd72d842e49706de4efc1a51c079f3 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Thu, 18 Jun 2026 14:30:15 +0100 Subject: [PATCH] ADD: Support for Two phase release workflow Signed-off-by: Jim Fitzpatrick --- .github/scripts/parse-version.sh | 31 +++++ .github/scripts/validate-release-yaml.sh | 31 +++++ .github/workflows/build-images.yaml | 23 +++- .github/workflows/pre-release.yaml | 139 +++++++++++++++++++++++ .github/workflows/release.yaml | 133 ++++++++++++++++++++++ .github/workflows/version-gate.yaml | 30 +++++ RELEASE.md | 68 +++++++---- release.yaml | 2 + 8 files changed, 432 insertions(+), 25 deletions(-) create mode 100755 .github/scripts/parse-version.sh create mode 100755 .github/scripts/validate-release-yaml.sh create mode 100644 .github/workflows/pre-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/version-gate.yaml create mode 100644 release.yaml diff --git a/.github/scripts/parse-version.sh b/.github/scripts/parse-version.sh new file mode 100755 index 000000000..062cefc5f --- /dev/null +++ b/.github/scripts/parse-version.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +RELEASE_YAML="${1:-release.yaml}" + +if [[ ! -f "$RELEASE_YAML" ]]; then + echo "::error::File not found: $RELEASE_YAML" + exit 1 +fi + +VERSION=$(yq '.authorino.version' "$RELEASE_YAML") +if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then + echo "::error::No version found in $RELEASE_YAML under authorino.version" + exit 1 +fi + +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Invalid semver: $VERSION" + exit 1 +fi + +MAJOR=$(echo "$VERSION" | cut -d. -f1) +MINOR=$(echo "$VERSION" | cut -d. -f2) +PATCH=$(echo "$VERSION" | cut -d. -f3 | cut -d- -f1) +RELEASE_BRANCH="release-${MAJOR}.${MINOR}" + +echo "version=$VERSION" >> "${GITHUB_OUTPUT:-/dev/stdout}" +echo "major=$MAJOR" >> "${GITHUB_OUTPUT:-/dev/stdout}" +echo "minor=$MINOR" >> "${GITHUB_OUTPUT:-/dev/stdout}" +echo "patch=$PATCH" >> "${GITHUB_OUTPUT:-/dev/stdout}" +echo "release-branch=$RELEASE_BRANCH" >> "${GITHUB_OUTPUT:-/dev/stdout}" diff --git a/.github/scripts/validate-release-yaml.sh b/.github/scripts/validate-release-yaml.sh new file mode 100755 index 000000000..c9f80e0fb --- /dev/null +++ b/.github/scripts/validate-release-yaml.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +BRANCH="${1:?Branch name required}" +ORG="${2:-Kuadrant}" +RELEASE_YAML="${3:-release.yaml}" + +if [[ ! -f "$RELEASE_YAML" ]]; then + echo "::error::File not found: $RELEASE_YAML" + exit 1 +fi + +VERSION=$(yq '.authorino.version' "$RELEASE_YAML") + +if [[ "$BRANCH" != "main" && "$VERSION" == "0.0.0" ]]; then + echo "::error::release.yaml version is 0.0.0 on branch '$BRANCH' -- must specify a release version on non-main branches" + exit 1 +fi + +DEPS=$(yq '.dependencies | keys | .[]' "$RELEASE_YAML" 2>/dev/null || true) +for dep in $DEPS; do + dep_version=$(yq ".dependencies.${dep}" "$RELEASE_YAML") + if [[ "$dep_version" != "0.0.0" && "$dep_version" != "null" && -n "$dep_version" ]]; then + if ! gh release view "v${dep_version}" --repo "${ORG}/${dep}" &>/dev/null; then + echo "::error::Dependency '${dep}' targets version '${dep_version}', but release v${dep_version} does not exist in ${ORG}/${dep}" + exit 1 + fi + fi +done + +echo "release.yaml validation passed" diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index a255e0f5a..c1e096a8d 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -6,6 +6,16 @@ on: - 'main' - 'master' workflow_dispatch: { } + workflow_call: + inputs: + version: + description: "Release version (e.g. 0.26.0)" + type: string + required: true + ref: + description: "Git ref to build from (e.g. v0.26.0)" + type: string + required: true env: IMG_TAGS: ${{ github.sha }} @@ -26,7 +36,10 @@ jobs: - name: Set Authorino build info id: build-info run: | - if [[ ${GITHUB_REF_NAME/\//-} =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then + if [[ -n "${{ inputs.version }}" ]]; then + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "version_tag=v${{ inputs.version }}" >> $GITHUB_OUTPUT + elif [[ ${GITHUB_REF_NAME/\//-} =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then tag=${GITHUB_REF_NAME/\//-} echo "version=${tag#v}" >> $GITHUB_OUTPUT echo "version_tag=${tag}" >> $GITHUB_OUTPUT @@ -59,6 +72,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || '' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to registry @@ -112,9 +127,9 @@ jobs: with: images: ${{ env.IMG_REGISTRY_HOST }}/${{ env.IMG_REGISTRY_ORG }}/authorino tags: | - type=raw,value=${{ github.ref_name }},enable={{is_not_default_branch}} - type=raw,value=${{ github.sha }},enable={{is_default_branch}} - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=${{ needs.prepare.outputs.version_tag }} + type=raw,value=${{ github.sha }},enable=${{ needs.prepare.outputs.version_tag == 'latest' }} + type=raw,value=latest,enable=${{ needs.prepare.outputs.version_tag == 'latest' }} - name: Login to registry if: ${{ !env.ACT }} uses: docker/login-action@v3 diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml new file mode 100644 index 000000000..cbfd3cd34 --- /dev/null +++ b/.github/workflows/pre-release.yaml @@ -0,0 +1,139 @@ +name: Pre-release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (semver, e.g. 1.5.0)" + required: true + type: string + source-branch: + description: "Branch to base the pre-release changes on (default: main)" + required: false + type: string + default: "main" + +permissions: + contents: write + pull-requests: write + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.validate.outputs.version }} + release-branch: ${{ steps.validate.outputs.release-branch }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.source-branch }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate version format + id: validate + run: | + VERSION="${{ inputs.version }}" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Invalid semver version: $VERSION" + exit 1 + fi + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + RELEASE_BRANCH="release-${MAJOR}.${MINOR}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "release-branch=$RELEASE_BRANCH" >> "$GITHUB_OUTPUT" + + - name: Create or verify release branch + run: | + RELEASE_BRANCH="${{ steps.validate.outputs.release-branch }}" + if git ls-remote --exit-code origin "refs/heads/${RELEASE_BRANCH}" >/dev/null 2>&1; then + echo "Release branch '${RELEASE_BRANCH}' already exists" + else + echo "Creating release branch '${RELEASE_BRANCH}' from '${{ inputs.source-branch }}'" + git checkout -b "${RELEASE_BRANCH}" + git push origin "${RELEASE_BRANCH}" + fi + + prepare-release: + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.setup.outputs.release-branch }} + fetch-depth: 0 + + - name: Create pre-release branch + run: | + PRE_RELEASE_BRANCH="pre-release-v${{ needs.setup.outputs.version }}" + if git ls-remote --exit-code origin "refs/heads/${PRE_RELEASE_BRANCH}" >/dev/null 2>&1; then + echo "::error::Pre-release branch '${PRE_RELEASE_BRANCH}' already exists. Delete it first or use a different version." + exit 1 + fi + git checkout -b "${PRE_RELEASE_BRANCH}" + + - name: Update release.yaml + run: | + VERSION="${{ needs.setup.outputs.version }}" + yq -i ".authorino.version = \"${VERSION}\"" release.yaml + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run code generation + run: | + make generate + make manifests + + - name: Commit and push changes + run: | + VERSION="${{ needs.setup.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "chore: prepare release v${VERSION}" + fi + git push origin "pre-release-v${VERSION}" + + open-pr: + needs: [setup, prepare-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Open pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ needs.setup.outputs.version }}" + RELEASE_BRANCH="${{ needs.setup.outputs.release-branch }}" + + gh pr create \ + --base "${RELEASE_BRANCH}" \ + --head "pre-release-v${VERSION}" \ + --title "Release v${VERSION}" \ + --body "$(cat </dev/null; then + echo "::error::GitHub Release v${VERSION} already exists" + exit 1 + fi + + smoke-tests: + needs: read-version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.release-branch }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Lint + run: make lint + + - name: Unit tests + run: make test + + - name: CEL validation tests + run: make test-cel + + tag: + needs: [read-version, smoke-tests] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.release-branch }} + fetch-depth: 0 + + - name: Create and push tag + run: | + VERSION="${{ needs.read-version.outputs.version }}" + TAG="v${VERSION}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "::error::Tag $TAG already exists" + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + + build-image: + needs: [read-version, tag] + uses: ./.github/workflows/build-images.yaml + with: + version: ${{ needs.read-version.outputs.version }} + ref: v${{ needs.read-version.outputs.version }} + secrets: inherit + + create-release: + needs: [read-version, build-image] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.release-branch }} + fetch-depth: 0 + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ needs.read-version.outputs.version }}" + TAG="v${VERSION}" + + PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "") + + NOTES_ARGS="--generate-notes" + if [[ -n "$PREV_TAG" ]]; then + NOTES_ARGS="$NOTES_ARGS --notes-start-tag $PREV_TAG" + fi + + gh release create "$TAG" \ + --title "Release $TAG" \ + $NOTES_ARGS diff --git a/.github/workflows/version-gate.yaml b/.github/workflows/version-gate.yaml new file mode 100644 index 000000000..a6a7d1f23 --- /dev/null +++ b/.github/workflows/version-gate.yaml @@ -0,0 +1,30 @@ +name: Version Gate + +on: + pull_request: + branches: + - "release-**" + paths: + - "release.yaml" + +permissions: + contents: read + +jobs: + validate-release-yaml: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq \ + https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Validate release.yaml + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + chmod +x .github/scripts/validate-release-yaml.sh + .github/scripts/validate-release-yaml.sh "${{ github.head_ref }}" "Kuadrant" diff --git a/RELEASE.md b/RELEASE.md index c16f8eeb2..f6e17167b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,32 +1,58 @@ # How to release Authorino -## Process +Authorino uses a two-phase release workflow. Every release is split into two GitHub Actions workflows with a human review gate between them. See [RFC: Two-Phase Release Workflow](https://github.com/Kuadrant/architecture/pull/178) for the full specification. -To release a version “vX.Y.Z” of Authorino in GitHub and Quay.io, follow these steps: +## Phase 1: Pre-release -1. Pick a `` (SHA-1) as source. +1. Go to **Actions → Pre-release** and click **Run workflow**. +2. Enter the target version (e.g. `0.27.0`). + - For patch releases, set **source-branch** to the existing release branch (e.g. `release-0.27`) if cherry-picks have already been applied there. Otherwise leave it as `main`. +3. The workflow creates the release branch `release-X.Y` (if needed), updates `release.yaml`, runs code generation, and opens a pull request. +4. Review the PR. CI runs tests, code style checks, and the **Version Gate** check. +5. Merge the PR once all checks pass. -```shell -git checkout -``` +## Phase 2: Release -2. Create a new tag and named release `vX.Y.Z`. Push the tag to GitHub. +1. Go to **Actions → Release** and click **Run workflow**. +2. Enter the release branch (e.g. `release-0.27`). +3. The workflow: + - Reads the version from `release.yaml` + - Runs smoke tests (lint, unit tests, CEL tests) + - Creates and pushes the `vX.Y.Z` tag + - Builds and pushes the multi-arch container image to `quay.io/kuadrant/authorino` + - Creates the GitHub Release (final step) -```shell -git tag -a vX.Y.Z -s -m "vX.Y.Z" -git push origin vX.Y.Z -``` +If any step fails, no GitHub Release is created. -Then at the GitHub repository, create a new release from the tag you just pushed. One could start autogenerating the -release notes and then write the change notes highlighting all the new features, bug fixes, enhancements, etc. -([example](https://github.com/Kuadrant/authorino/releases/tag/v0.9.0)). +## Release artifacts -3. Run the GHA ‘Build and push images’ for the `vX.Y.Z` tag. This will cause a new image to be built and pushed to quay.io/kuadrant/authorino. +| Artifact | Location | +|----------|----------| +| Container image | `quay.io/kuadrant/authorino:vX.Y.Z` | +| GitHub Release | `github.com/Kuadrant/authorino/releases/tag/vX.Y.Z` | -## Notes on Authorino’s automated builds +## Version file -* PRs merged to the main branch of Authorino cause a new image to be built (GH Action) and pushed automatically to -`quay.io/kuadrant/authorino:` – the `quay.io/kuadrant/authorino:latest` tag is also moved to match the latest -``. -* Authorino repo owns the manifests required by the operand: AuthConfig CRD + role definitions. A copy of these is merged -into a single deployment file in the [Authorino Operator repository](https://github.com/Kuadrant/authorino-operator). +The `release.yaml` file at the repository root is the source of truth for version information: + +- On `main`: version is always `0.0.0` (active development) +- On release branches: version is the target release (e.g. `0.27.0`) + +## GitHub configuration + +The release workflows require the following repository secrets to be configured in **Settings → Secrets and variables → Actions**: + +| Secret | Used by | Purpose | +|--------|---------|---------| +| `IMG_REGISTRY_USERNAME` | `build-images.yaml` | Username for the `quay.io` container registry | +| `IMG_REGISTRY_TOKEN` | `build-images.yaml` | Auth token/password for the `quay.io` container registry | + +The `GITHUB_TOKEN` secret is provided automatically by GitHub Actions and does not need to be configured. It is used by the pre-release workflow to create branches and open pull requests, and by the release workflow to create tags and GitHub Releases. + +The release workflow passes `secrets: inherit` when calling `build-images.yaml`, so the registry secrets must be set at the repository (or organization) level — not scoped to an environment. + +## Notes on automated builds + +- PRs merged to `main` trigger an image build pushed to `quay.io/kuadrant/authorino:latest` (and `quay.io/kuadrant/authorino:`). +- Smoke tests run automatically after each image build on `main`. +- Authorino owns the AuthConfig CRD and RBAC manifests. A copy is maintained in the [Authorino Operator repository](https://github.com/Kuadrant/authorino-operator). diff --git a/release.yaml b/release.yaml new file mode 100644 index 000000000..abfcc0c4b --- /dev/null +++ b/release.yaml @@ -0,0 +1,2 @@ +authorino: + version: "0.0.0"