diff --git a/.github/actions/install-yq/action.yaml b/.github/actions/install-yq/action.yaml new file mode 100644 index 00000000..9fd6bf04 --- /dev/null +++ b/.github/actions/install-yq/action.yaml @@ -0,0 +1,18 @@ +name: Install yq +description: Install mikefarah/yq at a pinned version + +inputs: + version: + description: "yq version to install" + required: false + default: "4.53.3" + +runs: + using: composite + steps: + - name: Install yq v${{ inputs.version }} + shell: bash + run: | + sudo wget -qO /usr/local/bin/yq \ + "https://github.com/mikefarah/yq/releases/download/v${{ inputs.version }}/yq_linux_amd64" + sudo chmod +x /usr/local/bin/yq diff --git a/.github/scripts/parse-version.sh b/.github/scripts/parse-version.sh new file mode 100755 index 00000000..062cefc5 --- /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 00000000..5baedcd9 --- /dev/null +++ b/.github/scripts/validate-release-yaml.sh @@ -0,0 +1,45 @@ +#!/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 + +ERRORS=0 +ENTRIES=$(yq -o=json '.dependencies // {} | to_entries[]' "$RELEASE_YAML" 2>/dev/null || true) +if [[ -n "$ENTRIES" ]]; then + while IFS= read -r entry; do + dep=$(echo "$entry" | jq -r '.key') + dep_version=$(echo "$entry" | jq -r '.value') + if [[ "$dep_version" != "0.0.0" && "$dep_version" != "null" && -n "$dep_version" ]]; then + draft_status=$(gh release view "v${dep_version}" --repo "${ORG}/${dep}" --json isDraft -q '.isDraft' 2>/dev/null) || { + echo "::error::Dependency '${dep}' targets version '${dep_version}', but release v${dep_version} does not exist in ${ORG}/${dep}" + ERRORS=$((ERRORS + 1)) + continue + } + if [[ "$draft_status" == "true" ]]; then + echo "::error::Dependency '${dep}' targets version '${dep_version}', but release v${dep_version} in ${ORG}/${dep} is a draft" + ERRORS=$((ERRORS + 1)) + fi + fi + done < <(echo "$ENTRIES" | jq -c '.') +fi + +if [[ "$ERRORS" -gt 0 ]]; then + echo "::error::release.yaml validation failed with ${ERRORS} dependency error(s)" + exit 1 +fi + +echo "release.yaml validation passed" diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 7f1ab74f..dd15725f 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 }} @@ -29,7 +39,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 @@ -62,6 +75,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4 + with: + ref: ${{ inputs.ref || '' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # ratchet:docker/setup-buildx-action@v3 - name: Login to registry @@ -119,9 +134,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@c94ce9fb468520275223c153574b00df6fe4bcc9 # ratchet:docker/login-action@v3 diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml new file mode 100644 index 00000000..cf11c354 --- /dev/null +++ b/.github/workflows/pre-release.yaml @@ -0,0 +1,141 @@ +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@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet: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@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet: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}" + + - uses: ./.github/actions/install-yq + + - 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@40f1582b2485089dde7abd97c1529aa768e1baff # ratchet: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@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet: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@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4 + with: + ref: ${{ inputs.release-branch }} + + - name: Set up Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # ratchet: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@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet: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 + + smoke-test: + needs: [read-version, build-image] + uses: ./.github/workflows/smoke-test.yaml + with: + authorino-image: ${{ env.IMG_REGISTRY_HOST }}/${{ env.IMG_REGISTRY_ORG }}/authorino:v${{ needs.read-version.outputs.version }} + ref: ${{ inputs.release-branch }} + + create-release: + needs: [read-version, build-image, smoke-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet: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/smoke-test.yaml b/.github/workflows/smoke-test.yaml index dcff0cc2..7ee2b6a0 100644 --- a/.github/workflows/smoke-test.yaml +++ b/.github/workflows/smoke-test.yaml @@ -6,6 +6,17 @@ on: - Build and push image types: - completed + workflow_call: + inputs: + authorino-image: + description: "Full image reference (e.g. quay.io/kuadrant/authorino:v1.5.0)" + required: false + type: string + default: "quay.io/kuadrant/authorino:latest" + ref: + description: "Git ref to checkout (e.g. release-1.5, v1.5.0)" + required: false + type: string permissions: contents: read @@ -17,13 +28,17 @@ jobs: matrix: platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} - if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: >- + github.event_name == 'workflow_call' || + github.event.workflow_run.conclusion == 'success' defaults: run: shell: bash steps: - name: Check out code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # ratchet:actions/checkout@v4 + with: + ref: ${{ inputs.ref || '' }} - name: Set up Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # ratchet:actions/setup-go@v6 with: @@ -48,7 +63,7 @@ jobs: kubectl create namespace authorino - name: Create Authorino TLS certificates run: | - curl -sSL https://raw.githubusercontent.com/Kuadrant/authorino/main/deploy/certs.yaml | sed "s/\$(AUTHORINO_INSTANCE)/authorino/g;s/\$(NAMESPACE)/authorino/g" | kubectl -n authorino apply -f - + sed "s/\$(AUTHORINO_INSTANCE)/authorino/g;s/\$(NAMESPACE)/authorino/g" deploy/certs.yaml | kubectl -n authorino apply -f - - name: Create Authorino instance run: | kubectl -n authorino apply -f -<` (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 00000000..abfcc0c4 --- /dev/null +++ b/release.yaml @@ -0,0 +1,2 @@ +authorino: + version: "0.0.0"