From ec13353f443d1b5f9c4873a916b94355472cd743 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Thu, 2 Jul 2026 13:58:16 +0200 Subject: [PATCH 01/17] Add ACS smoke test --- .github/workflows/acs-smoke-test.yml | 139 ++++++++++++++++ smoke-test/lib.sh | 230 +++++++++++++++++++++++++++ smoke-test/upgrade-latest-test.sh | 60 +++++++ smoke-test/upgrade-oldest-test.sh | 53 ++++++ 4 files changed, 482 insertions(+) create mode 100644 .github/workflows/acs-smoke-test.yml create mode 100755 smoke-test/lib.sh create mode 100755 smoke-test/upgrade-latest-test.sh create mode 100755 smoke-test/upgrade-oldest-test.sh diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml new file mode 100644 index 00000000..4bb51f8b --- /dev/null +++ b/.github/workflows/acs-smoke-test.yml @@ -0,0 +1,139 @@ +name: "ACS Smoke Test" + +on: + workflow_dispatch: + inputs: + operator-index-image: + description: 'ACS Operator index image (e.g., quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-...)' + required: true + type: string + acs-version: + description: 'ACS minor version under test (e.g. 4.10). Determines channel and Y-2 oldest.' + required: true + type: string + cluster-lifespan: + description: 'Cluster lifespan (e.g., 2h, 4h, 8h)' + required: false + default: '2h' + type: string + +env: + CLUSTER_NAME: smoke-${{ github.run_number }} + GH_TOKEN: ${{ github.token }} + GH_NO_UPDATE_NOTIFIER: 1 + +run-name: >- + ${{ format('ACS Smoke Test - {0}', inputs.operator-index-image) }} + +jobs: + create-cluster: + name: Create OpenShift cluster + runs-on: ubuntu-latest + outputs: + cluster-name: ${{ steps.info.outputs.name }} + steps: + - name: Set cluster name + id: info + run: echo "name=${{ env.CLUSTER_NAME }}" >> "$GITHUB_OUTPUT" + + - name: Create OpenShift cluster + uses: stackrox/actions/infra/create-cluster@v1 + with: + token: ${{ secrets.INFRA_TOKEN }} + flavor: ocp-default + name: ${{ env.CLUSTER_NAME }} + lifespan: ${{ inputs.cluster-lifespan }} + args: nodes=3,machine-type=e2-standard-8 + wait: true + + run-smoke-tests: + name: Run ACS smoke tests + needs: create-cluster + runs-on: ubuntu-latest + container: + image: quay.io/stackrox-io/apollo-ci:stackrox-test-0.5.11@sha256:df3459cf038e73775314b3a5d36d14dda41e6df438a0273c9efa8d23e0f2358a + steps: + - name: Check out code + uses: actions/checkout@v7 + + - name: Install infractl + uses: stackrox/actions/infra/install-infractl@v1 + + - name: Authenticate to GCP + uses: google-github-actions/auth@v3 + with: + credentials_json: ${{ secrets.GCP_RELEASE_AUTOMATION_SA }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v3 + + - name: Download cluster kubeconfig + run: | + infractl artifacts \ + --cluster-name=${{ needs.create-cluster.outputs.cluster-name }} \ + --download-dir=./artifacts + + - name: Set KUBECONFIG + run: echo "KUBECONFIG=${{ github.workspace }}/artifacts/kubeconfig" >> "$GITHUB_ENV" + + - name: "Test 1: upgrade oldest-supported → provided" + working-directory: ./smoke-test + env: + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} + ACS_VERSION: ${{ inputs.acs-version }} + run: bash upgrade-oldest-test.sh + + - name: Reset operator state between tests + working-directory: ./smoke-test + run: | + source lib.sh + reset_operator + shell: bash + + - name: "Test 2: install provided → optionally upgrade to latest GA" + working-directory: ./smoke-test + env: + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} + ACS_VERSION: ${{ inputs.acs-version }} + run: bash upgrade-latest-test.sh + + cleanup-cluster: + name: Cleanup cluster + needs: [create-cluster, run-smoke-tests] + runs-on: ubuntu-latest + if: always() && needs.create-cluster.result != 'cancelled' + steps: + - name: Install infractl + uses: stackrox/actions/infra/install-infractl@v1 + + - name: Authenticate to GCP + uses: google-github-actions/auth@v3 + with: + credentials_json: ${{ secrets.GCP_RELEASE_AUTOMATION_SA }} + + - name: Delete cluster + run: infractl delete --cluster-name=${{ needs.create-cluster.outputs.cluster-name }} + + report-status: + name: Report test status + needs: [create-cluster, run-smoke-tests, cleanup-cluster] + runs-on: ubuntu-latest + if: always() + steps: + - name: Write summary + run: | + cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" + # ACS Smoke Test Results + + | Field | Value | + |-------|-------| + | **Operator image** | `${{ inputs.operator-index-image }}` | + | **Cluster** | `${{ needs.create-cluster.outputs.cluster-name }}` | + | **Create cluster** | ${{ needs.create-cluster.result }} | + | **Smoke tests** | ${{ needs.run-smoke-tests.result }} | + | **Cleanup** | ${{ needs.cleanup-cluster.result }} | + + ## Tests run: + 1. **Test 1** — install oldest supported ACS version, upgrade to the provided operator-index image + 2. **Test 2** — install provided operator-index image, upgrade to latest GA if minor is behind + EOF diff --git a/smoke-test/lib.sh b/smoke-test/lib.sh new file mode 100755 index 00000000..fda048d7 --- /dev/null +++ b/smoke-test/lib.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# Shared functions for ACS operator smoke tests. + +set -euo pipefail + +# Logging helpers +step() { echo; echo "══════════════════════════════════════════════════════"; echo " $*"; echo "══════════════════════════════════════════════════════"; } +info() { echo " $*"; } +ok() { echo " ✅ $*"; } +warn() { echo " ⚠️ $*"; } +fail() { echo " ❌ $*" >&2; return 1; } + +# Splits "4.10" → ACS_MAJOR=4, ACS_MINOR=10 +parse_acs_version() { + local ver="$1" + ACS_MAJOR=$(echo "$ver" | cut -d. -f1) + ACS_MINOR=$(echo "$ver" | cut -d. -f2) + [[ "$ACS_MAJOR" =~ ^[0-9]+$ && "$ACS_MINOR" =~ ^[0-9]+$ ]] \ + || fail "ACS_VERSION must be MAJOR.MINOR (e.g. 4.10), got: ${ver}" +} + +# Prints the highest minor version in rhacs-MAJOR.N channels from the official catalog. +get_latest_official_minor() { + local major="${1:-4}" + oc get packagemanifest -n openshift-marketplace \ + -l catalog=redhat-operators \ + -o jsonpath='{range .items[?(@.metadata.name=="rhacs-operator")].status.channels[*]}{.name}{"\n"}{end}' \ + | grep -oE "rhacs-${major}\.[0-9]+" \ + | grep -oE '[0-9]+$' \ + | sort -n \ + | tail -1 \ + || true +} + +disable_default_sources() { + info "Disabling default OperatorHub sources..." + oc patch OperatorHub cluster --type json \ + -p '[{"op":"add","path":"/spec/disableAllDefaultSources","value":true}]' + ok "Default sources disabled" +} + +enable_default_sources() { + info "Enabling default OperatorHub sources..." + oc patch OperatorHub cluster --type json \ + -p '[{"op":"add","path":"/spec/disableAllDefaultSources","value":false}]' + ok "Default sources enabled" +} + +apply_custom_catalog() { + local index_image="$1" + info "Applying custom CatalogSource (image: ${index_image})..." + oc apply -f - </dev/null || true) + if [[ "$state" == "READY" ]]; then + ok "CatalogSource/${name} is READY" + return 0 + fi + info " state=${state:-pending}, retrying in 10s..." + sleep 10 + done + fail "CatalogSource/${name} not READY within ${timeout}s" +} + +apply_subscription() { + local channel="$1" source="$2" source_ns="${3:-openshift-marketplace}" + info "Applying Subscription (channel=${channel}, source=${source})..." + oc apply -f - </dev/null || true) + if [[ -n "$TARGET_CSV" ]]; then + info "Target CSV: ${TARGET_CSV}" + return 0 + fi + info " packagemanifest not ready yet, retrying in 10s..." + sleep 10 + done + fail "Could not resolve currentCSV for channel ${channel} from ${catalog_label} after 60s" +} + +# Install from official redhat-operators at channel rhacs-MAJOR.MINOR. +# Sets TARGET_CSV to the channel's currentCSV. +install_from_official() { + local major="$1" minor="$2" + local channel="rhacs-${major}.${minor}" + info "Installing ACS Operator from redhat-operators, channel ${channel}..." + wait_for_catalog "redhat-operators" "openshift-marketplace" 120 + _resolve_target_csv "redhat-operators" "$channel" + apply_subscription "$channel" "redhat-operators" +} + +# Install from the custom index image on the given channel (disables default sources). +# Sets TARGET_CSV to the channel's currentCSV. +install_from_custom() { + local index_image="$1" channel="$2" + info "Installing ACS Operator from custom index, channel ${channel}..." + disable_default_sources + apply_custom_catalog "$index_image" + wait_for_catalog "my-operator-catalog" + _resolve_target_csv "my-operator-catalog" "$channel" + apply_subscription "$channel" "my-operator-catalog" +} + +# Upgrade by refreshing the custom CatalogSource and pointing the subscription at it. +# Sets TARGET_CSV to the channel's currentCSV. +upgrade_via_custom() { + local index_image="$1" channel="$2" + info "Upgrading via custom catalog (channel: ${channel})..." + apply_custom_catalog "$index_image" + disable_default_sources + wait_for_catalog "my-operator-catalog" + _resolve_target_csv "my-operator-catalog" "$channel" + apply_subscription "$channel" "my-operator-catalog" + ok "Subscription updated — OLM will upgrade to ${TARGET_CSV}" +} + +# Upgrade to latest official GA via redhat-operators. +# Sets TARGET_CSV to the latest channel's currentCSV. +upgrade_to_latest_official() { + local major="${1:-4}" + enable_default_sources + wait_for_catalog "redhat-operators" "openshift-marketplace" 180 + local latest_minor + latest_minor=$(get_latest_official_minor "$major") || true + [[ -n "$latest_minor" ]] || fail "Could not determine latest GA minor from redhat-operators" + local channel="rhacs-${major}.${latest_minor}" + info "Upgrading to latest GA channel: ${channel}..." + _resolve_target_csv "redhat-operators" "$channel" + apply_subscription "$channel" "redhat-operators" + ok "Subscription updated to ${channel}, target: ${TARGET_CSV}" +} + +get_current_csv() { + oc get csv -n openshift-operators --no-headers 2>/dev/null \ + | awk '/rhacs-operator/ && /Succeeded/ {print $1}' \ + | head -1 +} + +# Waits for a specific CSV to reach Succeeded phase. +# Use after install/upgrade to wait for the exact target version, not just any change. +# This correctly handles multi-hop OLM upgrade graphs (e.g. 4.8→4.9→4.10). +wait_for_csv() { + local target_csv="$1" timeout="${2:-600}" + local deadline + deadline=$(( $(date +%s) + timeout )) + info "Waiting for ${target_csv} to reach Succeeded (timeout: ${timeout}s)..." + while (( $(date +%s) < deadline )); do + local phase + phase=$(oc get csv "$target_csv" -n openshift-operators \ + -o jsonpath='{.status.phase}' 2>/dev/null || true) + if [[ "$phase" == "Succeeded" ]]; then + ok "${target_csv} is Succeeded" + return 0 + fi + # Show in-progress CSVs so logs show the hop chain + local progress + progress=$(oc get csv -n openshift-operators --no-headers 2>/dev/null \ + | awk '/rhacs-operator/ {printf "%s(%s) ", $1, $7}' || true) + info " ${progress:+CSVs: ${progress}}phase=${phase:-not found}, waiting 15s..." + sleep 15 + done + fail "${target_csv} did not reach Succeeded within ${timeout}s" +} + +log_csv() { + local csv + csv=$(oc get csv -n openshift-operators --no-headers 2>/dev/null \ + | grep rhacs-operator || echo "") + info "Current CSV: ${csv}" +} + +# Reset between tests +reset_operator() { + info "Resetting operator state for next test..." + oc delete subscription rhacs-operator -n openshift-operators --ignore-not-found + oc get csv -n openshift-operators --no-headers 2>/dev/null \ + | awk '/rhacs-operator/ {print $1}' \ + | xargs -r oc delete csv -n openshift-operators --ignore-not-found || true + oc delete catalogsource my-operator-catalog -n openshift-marketplace --ignore-not-found || true + enable_default_sources + ok "Operator state reset" +} diff --git a/smoke-test/upgrade-latest-test.sh b/smoke-test/upgrade-latest-test.sh new file mode 100755 index 00000000..97a8b861 --- /dev/null +++ b/smoke-test/upgrade-latest-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Test 2 — Install provided version, optionally upgrade to latest GA +# +# 1. Installs ACS Operator from OPERATOR_INDEX_IMAGE on the ACS_VERSION channel +# 2. If ACS_VERSION minor < latest GA minor: upgrades to latest GA via redhat-operators +# 3. Verifies CSV reaches Succeeded after each step +# +# Required env vars: +# OPERATOR_INDEX_IMAGE custom operator index image +# ACS_VERSION ACS minor version to test, e.g. "4.10" + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "${SCRIPT_DIR}/lib.sh" + +OPERATOR_INDEX_IMAGE="${OPERATOR_INDEX_IMAGE:-${MY_INDEX_IMAGE:-}}" +ACS_VERSION="${ACS_VERSION:-}" + +[[ -n "$OPERATOR_INDEX_IMAGE" ]] || fail "OPERATOR_INDEX_IMAGE is required" +[[ -n "$ACS_VERSION" ]] || fail "ACS_VERSION is required (e.g. 4.10)" + +parse_acs_version "$ACS_VERSION" +CHANNEL="rhacs-${ACS_MAJOR}.${ACS_MINOR}" + +echo "======================================================================" +echo " TEST 2: Install provided → optionally upgrade to latest GA" +echo " Image: ${OPERATOR_INDEX_IMAGE}" +echo " Version: ${ACS_MAJOR}.${ACS_MINOR} | Channel: ${CHANNEL}" +echo "======================================================================" + +# Query latest GA minor NOW while redhat-operators is still enabled. +# install_from_custom() disables default sources, making this unavailable later. +LATEST_MINOR="" +wait_for_catalog "redhat-operators" "openshift-marketplace" 180 +if latest=$(get_latest_official_minor "$ACS_MAJOR") && [[ -n "$latest" ]]; then + LATEST_MINOR="$latest" + info "Latest GA in redhat-operators: ${ACS_MAJOR}.${LATEST_MINOR}" +else + warn "Could not determine latest GA minor — upgrade check will be skipped" +fi + +step "Step 1: Install ACS Operator ${CHANNEL} from custom index" +install_from_custom "$OPERATOR_INDEX_IMAGE" "$CHANNEL" +wait_for_csv "$TARGET_CSV" 600 +log_csv + +if [[ -n "$LATEST_MINOR" ]] && (( ACS_MINOR < LATEST_MINOR )); then + step "Step 2: Upgrade ${ACS_MAJOR}.${ACS_MINOR} → ${ACS_MAJOR}.${LATEST_MINOR} (latest GA)" + upgrade_to_latest_official "$ACS_MAJOR" + wait_for_csv "$TARGET_CSV" 600 + log_csv +elif [[ -n "$LATEST_MINOR" ]]; then + info "${ACS_MAJOR}.${ACS_MINOR} is already at latest GA minor (${LATEST_MINOR}) — no upgrade needed" +fi + +echo +echo "======================================================================" +echo " ✅ TEST 2 PASSED" +echo "======================================================================" diff --git a/smoke-test/upgrade-oldest-test.sh b/smoke-test/upgrade-oldest-test.sh new file mode 100755 index 00000000..e587234b --- /dev/null +++ b/smoke-test/upgrade-oldest-test.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Test 1 — Upgrade from oldest supported to provided version +# +# 1. Installs ACS Operator (oldest_supported_version from bundles.yaml) from official redhat-operators +# 2. Upgrades to the provided OPERATOR_INDEX_IMAGE on the ACS_VERSION channel +# 3. Verifies CSV reaches Succeeded after each step +# +# Required env vars: +# OPERATOR_INDEX_IMAGE custom operator index image +# ACS_VERSION ACS minor version to test, e.g. "4.10" + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "${SCRIPT_DIR}/lib.sh" + +OPERATOR_INDEX_IMAGE="${OPERATOR_INDEX_IMAGE:-${MY_INDEX_IMAGE:-}}" +ACS_VERSION="${ACS_VERSION:-}" + +[[ -n "$OPERATOR_INDEX_IMAGE" ]] || fail "OPERATOR_INDEX_IMAGE is required" +[[ -n "$ACS_VERSION" ]] || fail "ACS_VERSION is required (e.g. 4.10)" + +parse_acs_version "$ACS_VERSION" +CHANNEL="rhacs-${ACS_MAJOR}.${ACS_MINOR}" + +BUNDLES_YAML="${SCRIPT_DIR}/../bundles.yaml" +[[ -f "$BUNDLES_YAML" ]] || fail "bundles.yaml not found at ${BUNDLES_YAML}" +OLDEST_VERSION=$(awk '/^oldest_supported_version:/{print $2; exit}' "$BUNDLES_YAML") +[[ -n "$OLDEST_VERSION" ]] || fail "oldest_supported_version not found in bundles.yaml" +OLDEST_MAJOR=$(echo "$OLDEST_VERSION" | cut -d. -f1) +OLDEST_MINOR=$(echo "$OLDEST_VERSION" | cut -d. -f2) + +echo "======================================================================" +echo " TEST 1: Upgrade oldest-supported → provided" +echo " Image: ${OPERATOR_INDEX_IMAGE}" +echo " Version: ${ACS_MAJOR}.${ACS_MINOR} | Channel: ${CHANNEL}" +echo " Oldest: ${OLDEST_MAJOR}.${OLDEST_MINOR} (from bundles.yaml)" +echo "======================================================================" + +step "Step 1: Install ACS Operator ${OLDEST_MAJOR}.${OLDEST_MINOR} from redhat-operators" +install_from_official "$OLDEST_MAJOR" "$OLDEST_MINOR" +wait_for_csv "$TARGET_CSV" 300 +log_csv + +step "Step 2: Upgrade to ${CHANNEL} via custom index" +upgrade_via_custom "$OPERATOR_INDEX_IMAGE" "$CHANNEL" +wait_for_csv "$TARGET_CSV" 1200 +log_csv + +echo +echo "======================================================================" +echo " ✅ TEST 1 PASSED" +echo "======================================================================" From 629ad02fd1a93bf16ae70730e2b7d38cc4628e3f Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 11:52:46 +0200 Subject: [PATCH 02/17] Add auto test --- .github/workflows/acs-smoke-test.yml | 15 ++++ .github/workflows/auto-smoke-test.yml | 114 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 .github/workflows/auto-smoke-test.yml diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index 4bb51f8b..b9b62b3b 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -16,6 +16,21 @@ on: required: false default: '2h' type: string + workflow_call: + inputs: + operator-index-image: + description: 'ACS Operator index image' + required: true + type: string + acs-version: + description: 'ACS minor version under test (e.g. 4.10). Determines channel and Y-2 oldest.' + required: true + type: string + cluster-lifespan: + description: 'Cluster lifespan (e.g., 2h, 4h, 8h)' + required: false + default: '2h' + type: string env: CLUSTER_NAME: smoke-${{ github.run_number }} diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-smoke-test.yml new file mode 100644 index 00000000..718cbf6a --- /dev/null +++ b/.github/workflows/auto-smoke-test.yml @@ -0,0 +1,114 @@ +name: "Auto Smoke Test" + +on: + workflow_dispatch: + inputs: + acs-version-override: + description: 'Override ACS version, skips bundles.yaml diff (e.g. "4.10")' + required: false + type: string + operator-index-image-override: + description: 'Override operator-index image, skips Quay polling (e.g. quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-...)' + required: false + type: string + push: + branches: [master] + paths: ['bundles.yaml'] + pull_request: + paths: ['bundles.yaml'] + +env: + # OCP version used to construct the Konflux-built operator-index image tag. + # Update when infra cluster uses a newer OCP version. + OCP_VERSION: v4-22 + +jobs: + derive-inputs: + name: Derive smoke test inputs + runs-on: ubuntu-latest + outputs: + acs-version: ${{ steps.version.outputs.acs-version }} + operator-index-image: ${{ steps.image.outputs.operator-index-image }} + should-run: ${{ steps.version.outputs.should-run }} + steps: + - name: Check out code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Determine ACS version + id: version + run: | + if [ -n "${{ inputs.acs-version-override }}" ]; then + echo "acs-version=${{ inputs.acs-version-override }}" >> "$GITHUB_OUTPUT" + echo "should-run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "${{ github.event_name }}" = "pull_request" ]; then + DIFF_BASE="origin/${{ github.base_ref }}" + else + DIFF_BASE="HEAD~1" + fi + + NEW_VERSIONS=$(git diff "$DIFF_BASE" -- bundles.yaml \ + | grep '^+.*version:' \ + | grep -v '^+++' \ + | sed 's/.*version: *//' \ + | sort -V) + + if [ -z "$NEW_VERSIONS" ]; then + echo "No new versions found in bundles.yaml diff, skipping smoke test" + echo "should-run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + LATEST=$(echo "$NEW_VERSIONS" | tail -1) + MINOR=$(echo "$LATEST" | cut -d. -f1-2) + echo "New versions added: $(echo "$NEW_VERSIONS" | tr '\n' ' '), testing minor: $MINOR" + echo "acs-version=$MINOR" >> "$GITHUB_OUTPUT" + echo "should-run=true" >> "$GITHUB_OUTPUT" + + - name: Determine operator-index image + id: image + if: steps.version.outputs.should-run == 'true' + run: | + if [ -n "${{ inputs.operator-index-image-override }}" ]; then + echo "operator-index-image=${{ inputs.operator-index-image-override }}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "${{ github.event_name }}" = "pull_request" ]; then + SHA="${{ github.event.pull_request.head.sha }}" + else + SHA="${{ github.sha }}" + fi + + TAG="ocp-${OCP_VERSION}-${SHA}-fast" + IMAGE="quay.io/rhacs-eng/stackrox-operator-index:${TAG}" + echo "Waiting for Konflux to build: $IMAGE" + + for i in $(seq 1 40); do + RESPONSE=$(curl -sf "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}" || echo '{}') + FOUND=$(echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print('true' if d.get('tags') else 'false')" 2>/dev/null || echo 'false') + if [ "$FOUND" = "true" ]; then + echo "Image available after attempt $i: $IMAGE" + echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Attempt $i/40: image not yet available, retrying in 3 minutes..." + sleep 180 + done + + echo "Timed out after 2 hours waiting for: $IMAGE" + exit 1 + + smoke-test: + name: Run smoke test + needs: derive-inputs + if: needs.derive-inputs.outputs.should-run == 'true' + uses: ./.github/workflows/acs-smoke-test.yml + with: + operator-index-image: ${{ needs.derive-inputs.outputs.operator-index-image }} + acs-version: ${{ needs.derive-inputs.outputs.acs-version }} + secrets: inherit From 76416946882d39238732a89af47ace386692d0e5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 11:53:09 +0200 Subject: [PATCH 03/17] Fix testing --- smoke-test/lib.sh | 16 ++++++++-------- smoke-test/upgrade-latest-test.sh | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/smoke-test/lib.sh b/smoke-test/lib.sh index fda048d7..03bf234f 100755 --- a/smoke-test/lib.sh +++ b/smoke-test/lib.sh @@ -8,7 +8,7 @@ step() { echo; echo "═══════════════════ info() { echo " $*"; } ok() { echo " ✅ $*"; } warn() { echo " ⚠️ $*"; } -fail() { echo " ❌ $*" >&2; return 1; } +fail() { echo " ❌ $*" >&2; exit 1; } # Splits "4.10" → ACS_MAJOR=4, ACS_MINOR=10 parse_acs_version() { @@ -105,11 +105,11 @@ EOF } # Sets TARGET_CSV to the currentCSV for a given channel from the specified catalog label. -# Retries for up to 60s because packagemanifests can lag behind catalog READY state. +# Retries for up to timeout (default 60s) because packagemanifests can lag behind catalog READY state. _resolve_target_csv() { - local catalog_label="$1" channel="$2" + local catalog_label="$1" channel="$2" timeout="${3:-60}" local deadline - deadline=$(( $(date +%s) + 60 )) + deadline=$(( $(date +%s) + timeout )) info "Resolving currentCSV for channel ${channel} from ${catalog_label}..." while (( $(date +%s) < deadline )); do TARGET_CSV=$(oc get packagemanifest -n openshift-marketplace \ @@ -123,7 +123,7 @@ _resolve_target_csv() { info " packagemanifest not ready yet, retrying in 10s..." sleep 10 done - fail "Could not resolve currentCSV for channel ${channel} from ${catalog_label} after 60s" + fail "Could not resolve currentCSV for channel ${channel} from ${catalog_label} after ${timeout}s" } # Install from official redhat-operators at channel rhacs-MAJOR.MINOR. @@ -133,7 +133,7 @@ install_from_official() { local channel="rhacs-${major}.${minor}" info "Installing ACS Operator from redhat-operators, channel ${channel}..." wait_for_catalog "redhat-operators" "openshift-marketplace" 120 - _resolve_target_csv "redhat-operators" "$channel" + _resolve_target_csv "redhat-operators" "$channel" 120 apply_subscription "$channel" "redhat-operators" } @@ -154,8 +154,8 @@ install_from_custom() { upgrade_via_custom() { local index_image="$1" channel="$2" info "Upgrading via custom catalog (channel: ${channel})..." - apply_custom_catalog "$index_image" disable_default_sources + apply_custom_catalog "$index_image" wait_for_catalog "my-operator-catalog" _resolve_target_csv "my-operator-catalog" "$channel" apply_subscription "$channel" "my-operator-catalog" @@ -173,7 +173,7 @@ upgrade_to_latest_official() { [[ -n "$latest_minor" ]] || fail "Could not determine latest GA minor from redhat-operators" local channel="rhacs-${major}.${latest_minor}" info "Upgrading to latest GA channel: ${channel}..." - _resolve_target_csv "redhat-operators" "$channel" + _resolve_target_csv "redhat-operators" "$channel" 180 apply_subscription "$channel" "redhat-operators" ok "Subscription updated to ${channel}, target: ${TARGET_CSV}" } diff --git a/smoke-test/upgrade-latest-test.sh b/smoke-test/upgrade-latest-test.sh index 97a8b861..18a756dc 100755 --- a/smoke-test/upgrade-latest-test.sh +++ b/smoke-test/upgrade-latest-test.sh @@ -31,9 +31,10 @@ echo "======================================================================" # Query latest GA minor NOW while redhat-operators is still enabled. # install_from_custom() disables default sources, making this unavailable later. +# Use || true so a catalog timeout degrades gracefully to skipping the upgrade check. LATEST_MINOR="" -wait_for_catalog "redhat-operators" "openshift-marketplace" 180 -if latest=$(get_latest_official_minor "$ACS_MAJOR") && [[ -n "$latest" ]]; then +if wait_for_catalog "redhat-operators" "openshift-marketplace" 180 && + latest=$(get_latest_official_minor "$ACS_MAJOR") && [[ -n "$latest" ]]; then LATEST_MINOR="$latest" info "Latest GA in redhat-operators: ${ACS_MAJOR}.${LATEST_MINOR}" else From d58a4d854ccb917012a3c5f63f50b21f7aaf738c Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 13:40:53 +0200 Subject: [PATCH 04/17] Fix auto smoke test --- .github/workflows/auto-smoke-test.yml | 57 +++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-smoke-test.yml index 718cbf6a..a7096d6a 100644 --- a/.github/workflows/auto-smoke-test.yml +++ b/.github/workflows/auto-smoke-test.yml @@ -38,9 +38,18 @@ jobs: - name: Determine ACS version id: version + env: + ACS_VERSION_OVERRIDE: ${{ inputs.acs-version-override }} + OPERATOR_INDEX_IMAGE_OVERRIDE: ${{ inputs.operator-index-image-override }} run: | - if [ -n "${{ inputs.acs-version-override }}" ]; then - echo "acs-version=${{ inputs.acs-version-override }}" >> "$GITHUB_OUTPUT" + # Image override without version override is ambiguous — fail fast. + if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ] && [ -z "$ACS_VERSION_OVERRIDE" ]; then + echo "::error::operator-index-image-override requires acs-version-override to also be set" + exit 1 + fi + + if [ -n "$ACS_VERSION_OVERRIDE" ]; then + echo "acs-version=$ACS_VERSION_OVERRIDE" >> "$GITHUB_OUTPUT" echo "should-run=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -48,14 +57,21 @@ jobs: if [ "${{ github.event_name }}" = "pull_request" ]; then DIFF_BASE="origin/${{ github.base_ref }}" else - DIFF_BASE="HEAD~1" + # Use event.before to capture all commits in the push, not just the tip commit. + DIFF_BASE="${{ github.event.before }}" + # event.before is all-zeros on first push to a branch; fall back to parent. + if [[ "$DIFF_BASE" =~ ^0+$ ]]; then + DIFF_BASE="HEAD~1" + fi fi + # Exclude diff headers first, then match only the `version:` key (not + # `oldest_supported_version:` or other keys that contain "version"). NEW_VERSIONS=$(git diff "$DIFF_BASE" -- bundles.yaml \ - | grep '^+.*version:' \ | grep -v '^+++' \ - | sed 's/.*version: *//' \ - | sort -V) + | grep '^+[[:space:]]*version:' \ + | sed 's/.*version:[[:space:]]*//' \ + | sort -V || true) if [ -z "$NEW_VERSIONS" ]; then echo "No new versions found in bundles.yaml diff, skipping smoke test" @@ -72,9 +88,11 @@ jobs: - name: Determine operator-index image id: image if: steps.version.outputs.should-run == 'true' + env: + OPERATOR_INDEX_IMAGE_OVERRIDE: ${{ inputs.operator-index-image-override }} run: | - if [ -n "${{ inputs.operator-index-image-override }}" ]; then - echo "operator-index-image=${{ inputs.operator-index-image-override }}" >> "$GITHUB_OUTPUT" + if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ]; then + echo "operator-index-image=$OPERATOR_INDEX_IMAGE_OVERRIDE" >> "$GITHUB_OUTPUT" exit 0 fi @@ -89,15 +107,21 @@ jobs: echo "Waiting for Konflux to build: $IMAGE" for i in $(seq 1 40); do - RESPONSE=$(curl -sf "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}" || echo '{}') - FOUND=$(echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print('true' if d.get('tags') else 'false')" 2>/dev/null || echo 'false') - if [ "$FOUND" = "true" ]; then - echo "Image available after attempt $i: $IMAGE" - echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" - exit 0 + if RESPONSE=$(curl -sf --max-time 30 --connect-timeout 10 \ + "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}"); then + FOUND=$(echo "$RESPONSE" | python3 -c \ + "import json,sys; d=json.load(sys.stdin); print('true' if d.get('tags') else 'false')" \ + 2>/dev/null || echo 'false') + if [ "$FOUND" = "true" ]; then + echo "Image available after attempt $i: $IMAGE" + echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Attempt $i/40: image not yet in Quay, retrying in 3 minutes..." + else + echo "Attempt $i/40: Quay API request failed (network or HTTP error), retrying in 3 minutes..." fi - echo "Attempt $i/40: image not yet available, retrying in 3 minutes..." - sleep 180 + [ "$i" -lt 40 ] && sleep 180 done echo "Timed out after 2 hours waiting for: $IMAGE" @@ -111,4 +135,5 @@ jobs: with: operator-index-image: ${{ needs.derive-inputs.outputs.operator-index-image }} acs-version: ${{ needs.derive-inputs.outputs.acs-version }} + cluster-lifespan: 3h secrets: inherit From d2c4fc7fc73bab9785eb7172ee4ddc912054bcf5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 13:46:14 +0200 Subject: [PATCH 05/17] Fix env --- .github/workflows/auto-smoke-test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-smoke-test.yml index a7096d6a..3712cab9 100644 --- a/.github/workflows/auto-smoke-test.yml +++ b/.github/workflows/auto-smoke-test.yml @@ -41,6 +41,9 @@ jobs: env: ACS_VERSION_OVERRIDE: ${{ inputs.acs-version-override }} OPERATOR_INDEX_IMAGE_OVERRIDE: ${{ inputs.operator-index-image-override }} + BASE_REF: ${{ github.base_ref }} + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} run: | # Image override without version override is ambiguous — fail fast. if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ] && [ -z "$ACS_VERSION_OVERRIDE" ]; then @@ -54,11 +57,11 @@ jobs: exit 0 fi - if [ "${{ github.event_name }}" = "pull_request" ]; then - DIFF_BASE="origin/${{ github.base_ref }}" + if [ "$EVENT_NAME" = "pull_request" ]; then + DIFF_BASE="origin/$BASE_REF" else # Use event.before to capture all commits in the push, not just the tip commit. - DIFF_BASE="${{ github.event.before }}" + DIFF_BASE="$BEFORE_SHA" # event.before is all-zeros on first push to a branch; fall back to parent. if [[ "$DIFF_BASE" =~ ^0+$ ]]; then DIFF_BASE="HEAD~1" From 16d53aa87c4b2249c4c374793ad4eb71d06f0cde Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 14:02:56 +0200 Subject: [PATCH 06/17] Add manual PR trigger --- .github/workflows/acs-smoke-test.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index b9b62b3b..c4949213 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -1,6 +1,9 @@ name: "ACS Smoke Test" on: + # TODO: remove pull_request trigger before merging + pull_request: + branches: [master] workflow_dispatch: inputs: operator-index-image: @@ -57,7 +60,7 @@ jobs: token: ${{ secrets.INFRA_TOKEN }} flavor: ocp-default name: ${{ env.CLUSTER_NAME }} - lifespan: ${{ inputs.cluster-lifespan }} + lifespan: ${{ inputs.cluster-lifespan || '2h' }} args: nodes=3,machine-type=e2-standard-8 wait: true @@ -94,8 +97,10 @@ jobs: - name: "Test 1: upgrade oldest-supported → provided" working-directory: ./smoke-test env: - OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} - ACS_VERSION: ${{ inputs.acs-version }} + # Add a || operator-index image and version for testing in the PR. + # TODO: remove before merging! + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image || 'quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-7a9fcba929e5858cca91a84c3e5da8c829a3c990-fast' }} + ACS_VERSION: ${{ inputs.acs-version || '4.11' }} run: bash upgrade-oldest-test.sh - name: Reset operator state between tests @@ -108,8 +113,10 @@ jobs: - name: "Test 2: install provided → optionally upgrade to latest GA" working-directory: ./smoke-test env: - OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} - ACS_VERSION: ${{ inputs.acs-version }} + # Add a || operator-index image and version for testing in the PR. + # TODO: remove before merging! + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image || 'quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-7a9fcba929e5858cca91a84c3e5da8c829a3c990-fast' }} + ACS_VERSION: ${{ inputs.acs-version || '4.11' }} run: bash upgrade-latest-test.sh cleanup-cluster: From efec9d97b8a6f8a7cebcdc34c5159b07b94f81b6 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 14:30:06 +0200 Subject: [PATCH 07/17] Fix infra provisioning --- .github/workflows/acs-smoke-test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index c4949213..037b2078 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -58,10 +58,9 @@ jobs: uses: stackrox/actions/infra/create-cluster@v1 with: token: ${{ secrets.INFRA_TOKEN }} - flavor: ocp-default + flavor: openshift-4-demo name: ${{ env.CLUSTER_NAME }} lifespan: ${{ inputs.cluster-lifespan || '2h' }} - args: nodes=3,machine-type=e2-standard-8 wait: true run-smoke-tests: From 3d73d432150570e47e609c55b861b99b62f2b2a0 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 14:50:22 +0200 Subject: [PATCH 08/17] Fix flavor --- .github/workflows/acs-smoke-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index 037b2078..cdb193c8 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -58,7 +58,7 @@ jobs: uses: stackrox/actions/infra/create-cluster@v1 with: token: ${{ secrets.INFRA_TOKEN }} - flavor: openshift-4-demo + flavor: openshift-4 name: ${{ env.CLUSTER_NAME }} lifespan: ${{ inputs.cluster-lifespan || '2h' }} wait: true From 417c1809867ec66301dafca525a54f7a35001a80 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 19:34:08 +0200 Subject: [PATCH 09/17] Drop redundant GCP auth --- .github/workflows/acs-smoke-test.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index cdb193c8..7cbd6de6 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -76,14 +76,6 @@ jobs: - name: Install infractl uses: stackrox/actions/infra/install-infractl@v1 - - name: Authenticate to GCP - uses: google-github-actions/auth@v3 - with: - credentials_json: ${{ secrets.GCP_RELEASE_AUTOMATION_SA }} - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v3 - - name: Download cluster kubeconfig run: | infractl artifacts \ From f002468da488d4f2142d48ac4358f4f546499ee8 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 20:30:00 +0200 Subject: [PATCH 10/17] Fix infra cluster artifact fetch --- .github/workflows/acs-smoke-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index 7cbd6de6..3872b100 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -79,8 +79,8 @@ jobs: - name: Download cluster kubeconfig run: | infractl artifacts \ - --cluster-name=${{ needs.create-cluster.outputs.cluster-name }} \ - --download-dir=./artifacts + --download-dir=./artifacts \ + ${{ needs.create-cluster.outputs.cluster-name }} - name: Set KUBECONFIG run: echo "KUBECONFIG=${{ github.workspace }}/artifacts/kubeconfig" >> "$GITHUB_ENV" @@ -125,7 +125,7 @@ jobs: credentials_json: ${{ secrets.GCP_RELEASE_AUTOMATION_SA }} - name: Delete cluster - run: infractl delete --cluster-name=${{ needs.create-cluster.outputs.cluster-name }} + run: infractl delete ${{ needs.create-cluster.outputs.cluster-name }} report-status: name: Report test status From e4e3123f6c8fde3c5c75e6da0936dee83f0a91aa Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Mon, 6 Jul 2026 22:10:32 +0200 Subject: [PATCH 11/17] Pass INFRA_TOKEN --- .github/workflows/acs-smoke-test.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index 3872b100..db21b93b 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -67,6 +67,8 @@ jobs: name: Run ACS smoke tests needs: create-cluster runs-on: ubuntu-latest + env: + INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }} container: image: quay.io/stackrox-io/apollo-ci:stackrox-test-0.5.11@sha256:df3459cf038e73775314b3a5d36d14dda41e6df438a0273c9efa8d23e0f2358a steps: @@ -115,15 +117,12 @@ jobs: needs: [create-cluster, run-smoke-tests] runs-on: ubuntu-latest if: always() && needs.create-cluster.result != 'cancelled' + env: + INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }} steps: - name: Install infractl uses: stackrox/actions/infra/install-infractl@v1 - - name: Authenticate to GCP - uses: google-github-actions/auth@v3 - with: - credentials_json: ${{ secrets.GCP_RELEASE_AUTOMATION_SA }} - - name: Delete cluster run: infractl delete ${{ needs.create-cluster.outputs.cluster-name }} From 190bb3f6aa3c3130792c5940d6369d7be0d73584 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 7 Jul 2026 02:58:13 +0200 Subject: [PATCH 12/17] Simplify auto smoke test --- .github/workflows/auto-smoke-test.yml | 87 ++++++++++++++------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-smoke-test.yml index 3712cab9..b646359f 100644 --- a/.github/workflows/auto-smoke-test.yml +++ b/.github/workflows/auto-smoke-test.yml @@ -27,7 +27,7 @@ jobs: name: Derive smoke test inputs runs-on: ubuntu-latest outputs: - acs-version: ${{ steps.version.outputs.acs-version }} + acs-versions: ${{ steps.version.outputs.acs-versions }} operator-index-image: ${{ steps.image.outputs.operator-index-image }} should-run: ${{ steps.version.outputs.should-run }} steps: @@ -43,16 +43,16 @@ jobs: OPERATOR_INDEX_IMAGE_OVERRIDE: ${{ inputs.operator-index-image-override }} BASE_REF: ${{ github.base_ref }} EVENT_NAME: ${{ github.event_name }} - BEFORE_SHA: ${{ github.event.before }} run: | - # Image override without version override is ambiguous — fail fast. + # Image override without version override is ambiguous then fail fast. if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ] && [ -z "$ACS_VERSION_OVERRIDE" ]; then echo "::error::operator-index-image-override requires acs-version-override to also be set" exit 1 fi if [ -n "$ACS_VERSION_OVERRIDE" ]; then - echo "acs-version=$ACS_VERSION_OVERRIDE" >> "$GITHUB_OUTPUT" + echo "Version override provided, skipping bundles.yaml diff: $ACS_VERSION_OVERRIDE" + echo "acs-versions=[\"$ACS_VERSION_OVERRIDE\"]" >> "$GITHUB_OUTPUT" echo "should-run=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -60,21 +60,15 @@ jobs: if [ "$EVENT_NAME" = "pull_request" ]; then DIFF_BASE="origin/$BASE_REF" else - # Use event.before to capture all commits in the push, not just the tip commit. - DIFF_BASE="$BEFORE_SHA" - # event.before is all-zeros on first push to a branch; fall back to parent. - if [[ "$DIFF_BASE" =~ ^0+$ ]]; then - DIFF_BASE="HEAD~1" - fi + # Merge to master then diff the previous commit. + DIFF_BASE="HEAD~1" fi - # Exclude diff headers first, then match only the `version:` key (not - # `oldest_supported_version:` or other keys that contain "version"). + # Match only the `version:` key, ignore `oldest_supported_version:` and others. NEW_VERSIONS=$(git diff "$DIFF_BASE" -- bundles.yaml \ - | grep -v '^+++' \ | grep '^+[[:space:]]*version:' \ | sed 's/.*version:[[:space:]]*//' \ - | sort -V || true) + || true) if [ -z "$NEW_VERSIONS" ]; then echo "No new versions found in bundles.yaml diff, skipping smoke test" @@ -82,10 +76,13 @@ jobs: exit 0 fi - LATEST=$(echo "$NEW_VERSIONS" | tail -1) - MINOR=$(echo "$LATEST" | cut -d. -f1-2) - echo "New versions added: $(echo "$NEW_VERSIONS" | tr '\n' ' '), testing minor: $MINOR" - echo "acs-version=$MINOR" >> "$GITHUB_OUTPUT" + # Build version JSON array for the matrix. + MINORS_JSON=$(echo "$NEW_VERSIONS" \ + | sed 's/^\([0-9]*\.[0-9]*\).*/\1/' \ + | sort -uV \ + | jq -Rc '[.,inputs]') + echo "New versions added: $(echo "$NEW_VERSIONS" | tr '\n' ' '), testing minors: $MINORS_JSON" + echo "acs-versions=$MINORS_JSON" >> "$GITHUB_OUTPUT" echo "should-run=true" >> "$GITHUB_OUTPUT" - name: Determine operator-index image @@ -93,50 +90,58 @@ jobs: if: steps.version.outputs.should-run == 'true' env: OPERATOR_INDEX_IMAGE_OVERRIDE: ${{ inputs.operator-index-image-override }} + SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} run: | if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ]; then echo "operator-index-image=$OPERATOR_INDEX_IMAGE_OVERRIDE" >> "$GITHUB_OUTPUT" exit 0 fi - if [ "${{ github.event_name }}" = "pull_request" ]; then - SHA="${{ github.event.pull_request.head.sha }}" - else - SHA="${{ github.sha }}" - fi - TAG="ocp-${OCP_VERSION}-${SHA}-fast" IMAGE="quay.io/rhacs-eng/stackrox-operator-index:${TAG}" - echo "Waiting for Konflux to build: $IMAGE" + + CHECK_NAME="Red Hat Konflux / operator-index-ocp-${OCP_VERSION}-on-push" + echo "Waiting for Konflux check '$CHECK_NAME' on $SHA..." for i in $(seq 1 40); do - if RESPONSE=$(curl -sf --max-time 30 --connect-timeout 10 \ - "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}"); then - FOUND=$(echo "$RESPONSE" | python3 -c \ - "import json,sys; d=json.load(sys.stdin); print('true' if d.get('tags') else 'false')" \ - 2>/dev/null || echo 'false') - if [ "$FOUND" = "true" ]; then - echo "Image available after attempt $i: $IMAGE" - echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "Attempt $i/40: image not yet in Quay, retrying in 3 minutes..." - else - echo "Attempt $i/40: Quay API request failed (network or HTTP error), retrying in 3 minutes..." - fi + RESULT=$(gh api /repos/stackrox/operator-index/commits/"$SHA"/check-runs \ + | jq -r --arg name "$CHECK_NAME" \ + '.check_runs[] | select(.name == $name) | "\(.status) \(.conclusion)"') + case "$RESULT" in + "completed success") + echo "Konflux build succeeded, verifying image in Quay..." + if curl -sf --max-time 30 --connect-timeout 10 \ + "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}" \ + | jq -e '.tags | length > 0' > /dev/null; then + echo "Image confirmed in Quay: $IMAGE" + echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "Konflux check passed but image not found in Quay: $IMAGE" + exit 1 ;; + "completed "*) + echo "Konflux build failed: $RESULT" + exit 1 ;; + esac + echo "Attempt $i/40: ${RESULT:-check not yet started}, retrying in 3 minutes..." [ "$i" -lt 40 ] && sleep 180 done - echo "Timed out after 2 hours waiting for: $IMAGE" + echo "Timed out after 2 hours waiting for Konflux build: $CHECK_NAME" exit 1 smoke-test: name: Run smoke test needs: derive-inputs if: needs.derive-inputs.outputs.should-run == 'true' + strategy: + matrix: + acs-version: ${{ fromJson(needs.derive-inputs.outputs.acs-versions) }} + # if any matrix job fails, continue to run the rest of the jobs + fail-fast: false uses: ./.github/workflows/acs-smoke-test.yml with: operator-index-image: ${{ needs.derive-inputs.outputs.operator-index-image }} - acs-version: ${{ needs.derive-inputs.outputs.acs-version }} + acs-version: ${{ matrix.acs-version }} cluster-lifespan: 3h secrets: inherit From a8e5cd78177023eaaca785e5d7bb672b337e186e Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 14 Jul 2026 01:13:10 +0200 Subject: [PATCH 13/17] Apply suggestions from code review Co-authored-by: Tom Martensen --- .github/workflows/acs-smoke-test.yml | 19 ++++++++----------- .github/workflows/auto-smoke-test.yml | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/acs-smoke-test.yml index db21b93b..676ffd3f 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/acs-smoke-test.yml @@ -47,12 +47,6 @@ jobs: create-cluster: name: Create OpenShift cluster runs-on: ubuntu-latest - outputs: - cluster-name: ${{ steps.info.outputs.name }} - steps: - - name: Set cluster name - id: info - run: echo "name=${{ env.CLUSTER_NAME }}" >> "$GITHUB_OUTPUT" - name: Create OpenShift cluster uses: stackrox/actions/infra/create-cluster@v1 @@ -82,7 +76,7 @@ jobs: run: | infractl artifacts \ --download-dir=./artifacts \ - ${{ needs.create-cluster.outputs.cluster-name }} + ${{ env.CLUSTER_NAME }} - name: Set KUBECONFIG run: echo "KUBECONFIG=${{ github.workspace }}/artifacts/kubeconfig" >> "$GITHUB_ENV" @@ -94,11 +88,14 @@ jobs: # TODO: remove before merging! OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image || 'quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-7a9fcba929e5858cca91a84c3e5da8c829a3c990-fast' }} ACS_VERSION: ${{ inputs.acs-version || '4.11' }} - run: bash upgrade-oldest-test.sh + run: | + set -uo pipefail + bash upgrade-oldest-test.sh - name: Reset operator state between tests working-directory: ./smoke-test run: | + set -uo pipefail source lib.sh reset_operator shell: bash @@ -112,8 +109,8 @@ jobs: ACS_VERSION: ${{ inputs.acs-version || '4.11' }} run: bash upgrade-latest-test.sh - cleanup-cluster: - name: Cleanup cluster + delete-test-cluster: + name: Delete infra test cluster needs: [create-cluster, run-smoke-tests] runs-on: ubuntu-latest if: always() && needs.create-cluster.result != 'cancelled' @@ -147,5 +144,5 @@ jobs: ## Tests run: 1. **Test 1** — install oldest supported ACS version, upgrade to the provided operator-index image - 2. **Test 2** — install provided operator-index image, upgrade to latest GA if minor is behind + 2. **Test 2** — install provided operator-index image, upgrade to latest GA if provided ACS version is behind EOF diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-smoke-test.yml index b646359f..ed637a19 100644 --- a/.github/workflows/auto-smoke-test.yml +++ b/.github/workflows/auto-smoke-test.yml @@ -44,7 +44,7 @@ jobs: BASE_REF: ${{ github.base_ref }} EVENT_NAME: ${{ github.event_name }} run: | - # Image override without version override is ambiguous then fail fast. + # Image override without version override is ambiguous, thus fail fast. if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ] && [ -z "$ACS_VERSION_OVERRIDE" ]; then echo "::error::operator-index-image-override requires acs-version-override to also be set" exit 1 @@ -60,7 +60,7 @@ jobs: if [ "$EVENT_NAME" = "pull_request" ]; then DIFF_BASE="origin/$BASE_REF" else - # Merge to master then diff the previous commit. + # For runs on master branch, diff the previous commit. DIFF_BASE="HEAD~1" fi From 140b7f736687d15ba9a96c825b1ea285ea317f1a Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 14 Jul 2026 14:13:23 +0200 Subject: [PATCH 14/17] Remove bash based test suit --- smoke-test/lib.sh | 230 ------------------------------ smoke-test/upgrade-latest-test.sh | 61 -------- smoke-test/upgrade-oldest-test.sh | 53 ------- 3 files changed, 344 deletions(-) delete mode 100755 smoke-test/lib.sh delete mode 100755 smoke-test/upgrade-latest-test.sh delete mode 100755 smoke-test/upgrade-oldest-test.sh diff --git a/smoke-test/lib.sh b/smoke-test/lib.sh deleted file mode 100755 index 03bf234f..00000000 --- a/smoke-test/lib.sh +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env bash -# Shared functions for ACS operator smoke tests. - -set -euo pipefail - -# Logging helpers -step() { echo; echo "══════════════════════════════════════════════════════"; echo " $*"; echo "══════════════════════════════════════════════════════"; } -info() { echo " $*"; } -ok() { echo " ✅ $*"; } -warn() { echo " ⚠️ $*"; } -fail() { echo " ❌ $*" >&2; exit 1; } - -# Splits "4.10" → ACS_MAJOR=4, ACS_MINOR=10 -parse_acs_version() { - local ver="$1" - ACS_MAJOR=$(echo "$ver" | cut -d. -f1) - ACS_MINOR=$(echo "$ver" | cut -d. -f2) - [[ "$ACS_MAJOR" =~ ^[0-9]+$ && "$ACS_MINOR" =~ ^[0-9]+$ ]] \ - || fail "ACS_VERSION must be MAJOR.MINOR (e.g. 4.10), got: ${ver}" -} - -# Prints the highest minor version in rhacs-MAJOR.N channels from the official catalog. -get_latest_official_minor() { - local major="${1:-4}" - oc get packagemanifest -n openshift-marketplace \ - -l catalog=redhat-operators \ - -o jsonpath='{range .items[?(@.metadata.name=="rhacs-operator")].status.channels[*]}{.name}{"\n"}{end}' \ - | grep -oE "rhacs-${major}\.[0-9]+" \ - | grep -oE '[0-9]+$' \ - | sort -n \ - | tail -1 \ - || true -} - -disable_default_sources() { - info "Disabling default OperatorHub sources..." - oc patch OperatorHub cluster --type json \ - -p '[{"op":"add","path":"/spec/disableAllDefaultSources","value":true}]' - ok "Default sources disabled" -} - -enable_default_sources() { - info "Enabling default OperatorHub sources..." - oc patch OperatorHub cluster --type json \ - -p '[{"op":"add","path":"/spec/disableAllDefaultSources","value":false}]' - ok "Default sources enabled" -} - -apply_custom_catalog() { - local index_image="$1" - info "Applying custom CatalogSource (image: ${index_image})..." - oc apply -f - </dev/null || true) - if [[ "$state" == "READY" ]]; then - ok "CatalogSource/${name} is READY" - return 0 - fi - info " state=${state:-pending}, retrying in 10s..." - sleep 10 - done - fail "CatalogSource/${name} not READY within ${timeout}s" -} - -apply_subscription() { - local channel="$1" source="$2" source_ns="${3:-openshift-marketplace}" - info "Applying Subscription (channel=${channel}, source=${source})..." - oc apply -f - </dev/null || true) - if [[ -n "$TARGET_CSV" ]]; then - info "Target CSV: ${TARGET_CSV}" - return 0 - fi - info " packagemanifest not ready yet, retrying in 10s..." - sleep 10 - done - fail "Could not resolve currentCSV for channel ${channel} from ${catalog_label} after ${timeout}s" -} - -# Install from official redhat-operators at channel rhacs-MAJOR.MINOR. -# Sets TARGET_CSV to the channel's currentCSV. -install_from_official() { - local major="$1" minor="$2" - local channel="rhacs-${major}.${minor}" - info "Installing ACS Operator from redhat-operators, channel ${channel}..." - wait_for_catalog "redhat-operators" "openshift-marketplace" 120 - _resolve_target_csv "redhat-operators" "$channel" 120 - apply_subscription "$channel" "redhat-operators" -} - -# Install from the custom index image on the given channel (disables default sources). -# Sets TARGET_CSV to the channel's currentCSV. -install_from_custom() { - local index_image="$1" channel="$2" - info "Installing ACS Operator from custom index, channel ${channel}..." - disable_default_sources - apply_custom_catalog "$index_image" - wait_for_catalog "my-operator-catalog" - _resolve_target_csv "my-operator-catalog" "$channel" - apply_subscription "$channel" "my-operator-catalog" -} - -# Upgrade by refreshing the custom CatalogSource and pointing the subscription at it. -# Sets TARGET_CSV to the channel's currentCSV. -upgrade_via_custom() { - local index_image="$1" channel="$2" - info "Upgrading via custom catalog (channel: ${channel})..." - disable_default_sources - apply_custom_catalog "$index_image" - wait_for_catalog "my-operator-catalog" - _resolve_target_csv "my-operator-catalog" "$channel" - apply_subscription "$channel" "my-operator-catalog" - ok "Subscription updated — OLM will upgrade to ${TARGET_CSV}" -} - -# Upgrade to latest official GA via redhat-operators. -# Sets TARGET_CSV to the latest channel's currentCSV. -upgrade_to_latest_official() { - local major="${1:-4}" - enable_default_sources - wait_for_catalog "redhat-operators" "openshift-marketplace" 180 - local latest_minor - latest_minor=$(get_latest_official_minor "$major") || true - [[ -n "$latest_minor" ]] || fail "Could not determine latest GA minor from redhat-operators" - local channel="rhacs-${major}.${latest_minor}" - info "Upgrading to latest GA channel: ${channel}..." - _resolve_target_csv "redhat-operators" "$channel" 180 - apply_subscription "$channel" "redhat-operators" - ok "Subscription updated to ${channel}, target: ${TARGET_CSV}" -} - -get_current_csv() { - oc get csv -n openshift-operators --no-headers 2>/dev/null \ - | awk '/rhacs-operator/ && /Succeeded/ {print $1}' \ - | head -1 -} - -# Waits for a specific CSV to reach Succeeded phase. -# Use after install/upgrade to wait for the exact target version, not just any change. -# This correctly handles multi-hop OLM upgrade graphs (e.g. 4.8→4.9→4.10). -wait_for_csv() { - local target_csv="$1" timeout="${2:-600}" - local deadline - deadline=$(( $(date +%s) + timeout )) - info "Waiting for ${target_csv} to reach Succeeded (timeout: ${timeout}s)..." - while (( $(date +%s) < deadline )); do - local phase - phase=$(oc get csv "$target_csv" -n openshift-operators \ - -o jsonpath='{.status.phase}' 2>/dev/null || true) - if [[ "$phase" == "Succeeded" ]]; then - ok "${target_csv} is Succeeded" - return 0 - fi - # Show in-progress CSVs so logs show the hop chain - local progress - progress=$(oc get csv -n openshift-operators --no-headers 2>/dev/null \ - | awk '/rhacs-operator/ {printf "%s(%s) ", $1, $7}' || true) - info " ${progress:+CSVs: ${progress}}phase=${phase:-not found}, waiting 15s..." - sleep 15 - done - fail "${target_csv} did not reach Succeeded within ${timeout}s" -} - -log_csv() { - local csv - csv=$(oc get csv -n openshift-operators --no-headers 2>/dev/null \ - | grep rhacs-operator || echo "") - info "Current CSV: ${csv}" -} - -# Reset between tests -reset_operator() { - info "Resetting operator state for next test..." - oc delete subscription rhacs-operator -n openshift-operators --ignore-not-found - oc get csv -n openshift-operators --no-headers 2>/dev/null \ - | awk '/rhacs-operator/ {print $1}' \ - | xargs -r oc delete csv -n openshift-operators --ignore-not-found || true - oc delete catalogsource my-operator-catalog -n openshift-marketplace --ignore-not-found || true - enable_default_sources - ok "Operator state reset" -} diff --git a/smoke-test/upgrade-latest-test.sh b/smoke-test/upgrade-latest-test.sh deleted file mode 100755 index 18a756dc..00000000 --- a/smoke-test/upgrade-latest-test.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Test 2 — Install provided version, optionally upgrade to latest GA -# -# 1. Installs ACS Operator from OPERATOR_INDEX_IMAGE on the ACS_VERSION channel -# 2. If ACS_VERSION minor < latest GA minor: upgrades to latest GA via redhat-operators -# 3. Verifies CSV reaches Succeeded after each step -# -# Required env vars: -# OPERATOR_INDEX_IMAGE custom operator index image -# ACS_VERSION ACS minor version to test, e.g. "4.10" - -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -OPERATOR_INDEX_IMAGE="${OPERATOR_INDEX_IMAGE:-${MY_INDEX_IMAGE:-}}" -ACS_VERSION="${ACS_VERSION:-}" - -[[ -n "$OPERATOR_INDEX_IMAGE" ]] || fail "OPERATOR_INDEX_IMAGE is required" -[[ -n "$ACS_VERSION" ]] || fail "ACS_VERSION is required (e.g. 4.10)" - -parse_acs_version "$ACS_VERSION" -CHANNEL="rhacs-${ACS_MAJOR}.${ACS_MINOR}" - -echo "======================================================================" -echo " TEST 2: Install provided → optionally upgrade to latest GA" -echo " Image: ${OPERATOR_INDEX_IMAGE}" -echo " Version: ${ACS_MAJOR}.${ACS_MINOR} | Channel: ${CHANNEL}" -echo "======================================================================" - -# Query latest GA minor NOW while redhat-operators is still enabled. -# install_from_custom() disables default sources, making this unavailable later. -# Use || true so a catalog timeout degrades gracefully to skipping the upgrade check. -LATEST_MINOR="" -if wait_for_catalog "redhat-operators" "openshift-marketplace" 180 && - latest=$(get_latest_official_minor "$ACS_MAJOR") && [[ -n "$latest" ]]; then - LATEST_MINOR="$latest" - info "Latest GA in redhat-operators: ${ACS_MAJOR}.${LATEST_MINOR}" -else - warn "Could not determine latest GA minor — upgrade check will be skipped" -fi - -step "Step 1: Install ACS Operator ${CHANNEL} from custom index" -install_from_custom "$OPERATOR_INDEX_IMAGE" "$CHANNEL" -wait_for_csv "$TARGET_CSV" 600 -log_csv - -if [[ -n "$LATEST_MINOR" ]] && (( ACS_MINOR < LATEST_MINOR )); then - step "Step 2: Upgrade ${ACS_MAJOR}.${ACS_MINOR} → ${ACS_MAJOR}.${LATEST_MINOR} (latest GA)" - upgrade_to_latest_official "$ACS_MAJOR" - wait_for_csv "$TARGET_CSV" 600 - log_csv -elif [[ -n "$LATEST_MINOR" ]]; then - info "${ACS_MAJOR}.${ACS_MINOR} is already at latest GA minor (${LATEST_MINOR}) — no upgrade needed" -fi - -echo -echo "======================================================================" -echo " ✅ TEST 2 PASSED" -echo "======================================================================" diff --git a/smoke-test/upgrade-oldest-test.sh b/smoke-test/upgrade-oldest-test.sh deleted file mode 100755 index e587234b..00000000 --- a/smoke-test/upgrade-oldest-test.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# Test 1 — Upgrade from oldest supported to provided version -# -# 1. Installs ACS Operator (oldest_supported_version from bundles.yaml) from official redhat-operators -# 2. Upgrades to the provided OPERATOR_INDEX_IMAGE on the ACS_VERSION channel -# 3. Verifies CSV reaches Succeeded after each step -# -# Required env vars: -# OPERATOR_INDEX_IMAGE custom operator index image -# ACS_VERSION ACS minor version to test, e.g. "4.10" - -set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -source "${SCRIPT_DIR}/lib.sh" - -OPERATOR_INDEX_IMAGE="${OPERATOR_INDEX_IMAGE:-${MY_INDEX_IMAGE:-}}" -ACS_VERSION="${ACS_VERSION:-}" - -[[ -n "$OPERATOR_INDEX_IMAGE" ]] || fail "OPERATOR_INDEX_IMAGE is required" -[[ -n "$ACS_VERSION" ]] || fail "ACS_VERSION is required (e.g. 4.10)" - -parse_acs_version "$ACS_VERSION" -CHANNEL="rhacs-${ACS_MAJOR}.${ACS_MINOR}" - -BUNDLES_YAML="${SCRIPT_DIR}/../bundles.yaml" -[[ -f "$BUNDLES_YAML" ]] || fail "bundles.yaml not found at ${BUNDLES_YAML}" -OLDEST_VERSION=$(awk '/^oldest_supported_version:/{print $2; exit}' "$BUNDLES_YAML") -[[ -n "$OLDEST_VERSION" ]] || fail "oldest_supported_version not found in bundles.yaml" -OLDEST_MAJOR=$(echo "$OLDEST_VERSION" | cut -d. -f1) -OLDEST_MINOR=$(echo "$OLDEST_VERSION" | cut -d. -f2) - -echo "======================================================================" -echo " TEST 1: Upgrade oldest-supported → provided" -echo " Image: ${OPERATOR_INDEX_IMAGE}" -echo " Version: ${ACS_MAJOR}.${ACS_MINOR} | Channel: ${CHANNEL}" -echo " Oldest: ${OLDEST_MAJOR}.${OLDEST_MINOR} (from bundles.yaml)" -echo "======================================================================" - -step "Step 1: Install ACS Operator ${OLDEST_MAJOR}.${OLDEST_MINOR} from redhat-operators" -install_from_official "$OLDEST_MAJOR" "$OLDEST_MINOR" -wait_for_csv "$TARGET_CSV" 300 -log_csv - -step "Step 2: Upgrade to ${CHANNEL} via custom index" -upgrade_via_custom "$OPERATOR_INDEX_IMAGE" "$CHANNEL" -wait_for_csv "$TARGET_CSV" 1200 -log_csv - -echo -echo "======================================================================" -echo " ✅ TEST 1 PASSED" -echo "======================================================================" From dda9eddf065fe86e4d8d61cc34add1102739ef16 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 14 Jul 2026 14:13:43 +0200 Subject: [PATCH 15/17] Add golang based test --- operator-index-upgrade-test/main_test.go | 13 + operator-index-upgrade-test/olm.go | 328 ++++++++++++++++++ .../upgrade_latest_test.go | 56 +++ .../upgrade_oldest_test.go | 49 +++ 4 files changed, 446 insertions(+) create mode 100644 operator-index-upgrade-test/main_test.go create mode 100644 operator-index-upgrade-test/olm.go create mode 100644 operator-index-upgrade-test/upgrade_latest_test.go create mode 100644 operator-index-upgrade-test/upgrade_oldest_test.go diff --git a/operator-index-upgrade-test/main_test.go b/operator-index-upgrade-test/main_test.go new file mode 100644 index 00000000..dadb010b --- /dev/null +++ b/operator-index-upgrade-test/main_test.go @@ -0,0 +1,13 @@ +package upgradetest + +import ( + "os" + "testing" +) + +// TestMain resets any leftover operator state before running the suite. +// This makes local re-runs on the same cluster safe without manual cleanup. +func TestMain(m *testing.M) { + _ = ResetOperator() + os.Exit(m.Run()) +} diff --git a/operator-index-upgrade-test/olm.go b/operator-index-upgrade-test/olm.go new file mode 100644 index 00000000..6261dcec --- /dev/null +++ b/operator-index-upgrade-test/olm.go @@ -0,0 +1,328 @@ +package upgradetest + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "time" + + yaml "github.com/goccy/go-yaml" +) + +func ocRun(args ...string) error { + cmd := exec.Command("oc", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func ocOutput(args ...string) (string, error) { + var stderr bytes.Buffer + cmd := exec.Command("oc", args...) + cmd.Stderr = &stderr + out, err := cmd.Output() + if err != nil && stderr.Len() > 0 { + return strings.TrimSpace(string(out)), fmt.Errorf("%w\nstderr: %s", err, strings.TrimSpace(stderr.String())) + } + return strings.TrimSpace(string(out)), err +} + +func ocApply(manifest string) error { + cmd := exec.Command("oc", "apply", "-f", "-") + cmd.Stdin = strings.NewReader(manifest) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +type bundlesYAML struct { + OldestSupportedVersion string `yaml:"oldest_supported_version"` +} + +// ReadOldestSupportedVersion parses oldest_supported_version from bundles.yaml. +func ReadOldestSupportedVersion() (major, minor int, err error) { + data, err := os.ReadFile("../bundles.yaml") + if err != nil { + return 0, 0, fmt.Errorf("reading bundles.yaml: %w", err) + } + var b bundlesYAML + if err := yaml.Unmarshal(data, &b); err != nil { + return 0, 0, fmt.Errorf("parsing bundles.yaml: %w", err) + } + // Strip optional patch component ("4.9.0" → "4.9") before delegating to ParseACSVersion. + parts := strings.SplitN(b.OldestSupportedVersion, ".", 3) + if len(parts) < 2 { + return 0, 0, fmt.Errorf("invalid oldest_supported_version: %s", b.OldestSupportedVersion) + } + return ParseACSVersion(parts[0] + "." + parts[1]) +} + +// ParseACSVersion parses "4.10" into (major=4, minor=10). +func ParseACSVersion(ver string) (major, minor int, err error) { + parts := strings.SplitN(ver, ".", 2) + if len(parts) != 2 { + return 0, 0, fmt.Errorf("ACS_VERSION must be MAJOR.MINOR (e.g. 4.10), got: %q", ver) + } + maj, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("invalid major in %q: %w", ver, err) + } + min, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, fmt.Errorf("invalid minor in %q: %w", ver, err) + } + return maj, min, nil +} + +// GetLatestOfficialMinor returns the highest available minor for the given major +// from the official redhat-operators catalog, retrying until timeout because +// packagemanifests can lag behind the catalog's READY state. +func GetLatestOfficialMinor(major int, timeout time.Duration) (int, error) { + deadline := time.Now().Add(timeout) + prefix := fmt.Sprintf("rhacs-%d.", major) + fmt.Printf(" Looking for latest rhacs-%d.* channel in redhat-operators (timeout: %v)...\n", major, timeout) + for time.Now().Before(deadline) { + out, err := exec.Command("oc", "get", "packagemanifest", + "-n", "openshift-marketplace", + "-l", "catalog=redhat-operators", + "-o", `jsonpath={range .items[?(@.metadata.name=="rhacs-operator")].status.channels[*]}{.name}{"\n"}{end}`, + ).Output() + if err != nil { + fmt.Printf(" packagemanifest query failed: %v, retrying in 10s...\n", err) + time.Sleep(10 * time.Second) + continue + } + maxMinor := -1 + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if !strings.HasPrefix(line, prefix) { + continue + } + n, err := strconv.Atoi(strings.TrimPrefix(line, prefix)) + if err != nil { + continue + } + if n > maxMinor { + maxMinor = n + } + } + if maxMinor >= 0 { + return maxMinor, nil + } + fmt.Printf(" rhacs-%d.* channels not yet in packagemanifest, retrying in 10s...\n", major) + time.Sleep(10 * time.Second) + } + return 0, fmt.Errorf("no rhacs-%d.* channels found in redhat-operators after %v", major, timeout) +} + +// DisableDefaultSources disables all default OperatorHub catalog sources. +func DisableDefaultSources() error { + fmt.Println(" Disabling default OperatorHub sources...") + return ocRun("patch", "OperatorHub", "cluster", + "--type", "json", + "-p", `[{"op":"add","path":"/spec/disableAllDefaultSources","value":true}]`) +} + +// EnableDefaultSources re-enables all default OperatorHub catalog sources. +func EnableDefaultSources() error { + fmt.Println(" Enabling default OperatorHub sources...") + return ocRun("patch", "OperatorHub", "cluster", + "--type", "json", + "-p", `[{"op":"add","path":"/spec/disableAllDefaultSources","value":false}]`) +} + +// ApplyCustomCatalog creates or updates the custom CatalogSource. +func ApplyCustomCatalog(indexImage string) error { + fmt.Printf(" Applying custom CatalogSource (image: %s)...\n", indexImage) + return ocApply(fmt.Sprintf(`apiVersion: operators.coreos.com/v1alpha1 +kind: CatalogSource +metadata: + name: my-operator-catalog + namespace: openshift-marketplace +spec: + sourceType: grpc + image: %s + displayName: My Operator Catalog + publisher: Custom`, indexImage)) +} + +// WaitForCatalog polls until the named CatalogSource reaches READY state. +func WaitForCatalog(name, ns string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + fmt.Printf(" Waiting for CatalogSource/%s to be READY (timeout: %v)...\n", name, timeout) + for time.Now().Before(deadline) { + state, _ := ocOutput("get", "catalogsource", name, "-n", ns, + "-o", "jsonpath={.status.connectionState.lastObservedState}") + if state == "READY" { + fmt.Printf(" ✅ CatalogSource/%s is READY\n", name) + return nil + } + fmt.Printf(" state=%s, retrying in 10s...\n", state) + time.Sleep(10 * time.Second) + } + return fmt.Errorf("CatalogSource/%s not READY within %v", name, timeout) +} + +// ApplySubscription creates or updates the rhacs-operator Subscription. +func ApplySubscription(channel, source, sourceNS string) error { + fmt.Printf(" Applying Subscription (channel=%s, source=%s)...\n", channel, source) + return ocApply(fmt.Sprintf(`apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: rhacs-operator + namespace: openshift-operators +spec: + channel: %s + installPlanApproval: Automatic + name: rhacs-operator + source: %s + sourceNamespace: %s`, channel, source, sourceNS)) +} + +// ResolveTargetCSV returns the currentCSV for a channel from the specified catalog, +// retrying until timeout because packagemanifests can lag behind catalog READY. +func ResolveTargetCSV(catalogLabel, channel string, timeout time.Duration) (string, error) { + deadline := time.Now().Add(timeout) + fmt.Printf(" Resolving currentCSV for channel %s from %s...\n", channel, catalogLabel) + jsonpath := fmt.Sprintf( + `{range .items[?(@.metadata.name=="rhacs-operator")].status.channels[?(@.name=="%s")]}{.currentCSV}{end}`, + channel) + for time.Now().Before(deadline) { + csv, _ := ocOutput("get", "packagemanifest", + "-n", "openshift-marketplace", + "-l", "catalog="+catalogLabel, + "-o", "jsonpath="+jsonpath) + if csv != "" { + fmt.Printf(" Target CSV: %s\n", csv) + return csv, nil + } + fmt.Println(" packagemanifest not ready yet, retrying in 10s...") + time.Sleep(10 * time.Second) + } + return "", fmt.Errorf("could not resolve currentCSV for channel %s from %s after %v", + channel, catalogLabel, timeout) +} + +// InstallFromOfficial installs the ACS Operator from redhat-operators at channel rhacs-MAJOR.MINOR. +func InstallFromOfficial(major, minor int) (targetCSV string, err error) { + channel := fmt.Sprintf("rhacs-%d.%d", major, minor) + fmt.Printf(" Installing ACS Operator from redhat-operators, channel %s...\n", channel) + if err := WaitForCatalog("redhat-operators", "openshift-marketplace", 2*time.Minute); err != nil { + return "", err + } + csv, err := ResolveTargetCSV("redhat-operators", channel, 2*time.Minute) + if err != nil { + return "", err + } + if err := ApplySubscription(channel, "redhat-operators", "openshift-marketplace"); err != nil { + return "", err + } + return csv, nil +} + +// applyCustomCatalogAndSubscribe is the shared implementation for InstallFromCustom and UpgradeViaCustom. +func applyCustomCatalogAndSubscribe(indexImage, channel string) (string, error) { + if err := DisableDefaultSources(); err != nil { + return "", err + } + if err := ApplyCustomCatalog(indexImage); err != nil { + return "", err + } + if err := WaitForCatalog("my-operator-catalog", "openshift-marketplace", 3*time.Minute); err != nil { + return "", err + } + csv, err := ResolveTargetCSV("my-operator-catalog", channel, time.Minute) + if err != nil { + return "", err + } + if err := ApplySubscription(channel, "my-operator-catalog", "openshift-marketplace"); err != nil { + return "", err + } + return csv, nil +} + +// InstallFromCustom installs from the custom index on the given channel, disabling default sources. +func InstallFromCustom(indexImage, channel string) (targetCSV string, err error) { + fmt.Printf(" Installing ACS Operator from custom index, channel %s...\n", channel) + return applyCustomCatalogAndSubscribe(indexImage, channel) +} + +// UpgradeViaCustom upgrades by refreshing the custom CatalogSource and updating the subscription. +func UpgradeViaCustom(indexImage, channel string) (targetCSV string, err error) { + fmt.Printf(" Upgrading via custom catalog (channel: %s)...\n", channel) + csv, err := applyCustomCatalogAndSubscribe(indexImage, channel) + if err != nil { + return "", err + } + fmt.Printf(" ✅ Subscription updated — OLM will upgrade to %s\n", csv) + return csv, nil +} + +// UpgradeToLatestOfficial upgrades to the latest GA via redhat-operators. +func UpgradeToLatestOfficial(major int) (targetCSV string, err error) { + if err := EnableDefaultSources(); err != nil { + return "", err + } + if err := WaitForCatalog("redhat-operators", "openshift-marketplace", 3*time.Minute); err != nil { + return "", err + } + latestMinor, err := GetLatestOfficialMinor(major, 3*time.Minute) + if err != nil { + return "", err + } + channel := fmt.Sprintf("rhacs-%d.%d", major, latestMinor) + fmt.Printf(" Upgrading to latest GA channel: %s...\n", channel) + csv, err := ResolveTargetCSV("redhat-operators", channel, 3*time.Minute) + if err != nil { + return "", err + } + if err := ApplySubscription(channel, "redhat-operators", "openshift-marketplace"); err != nil { + return "", err + } + fmt.Printf(" ✅ Subscription updated to %s, target: %s\n", channel, csv) + return csv, nil +} + +// WaitForCSV polls until the target CSV reaches Succeeded phase. +// It correctly handles multi-hop OLM upgrade graphs (e.g. 4.8→4.9→4.10). +func WaitForCSV(targetCSV string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + fmt.Printf(" Waiting for %s to reach Succeeded (timeout: %v)...\n", targetCSV, timeout) + for time.Now().Before(deadline) { + phase, _ := ocOutput("get", "csv", targetCSV, + "-n", "openshift-operators", + "-o", "jsonpath={.status.phase}") + if phase == "Succeeded" { + fmt.Printf(" ✅ %s is Succeeded\n", targetCSV) + return nil + } + if phase == "Failed" { + return fmt.Errorf("%s reached Failed phase — OLM will not recover without a resource change", targetCSV) + } + // Show all in-progress rhacs CSVs so logs show the hop chain. + progress, _ := ocOutput("get", "csv", "-n", "openshift-operators", "--no-headers") + fmt.Printf(" phase=%s, csvs=%s, waiting 15s...\n", phase, + strings.ReplaceAll(progress, "\n", " | ")) + time.Sleep(15 * time.Second) + } + return fmt.Errorf("%s did not reach Succeeded within %v", targetCSV, timeout) +} + +// ResetOperator removes the operator subscription, CSVs, and custom catalog between tests. +func ResetOperator() error { + fmt.Println(" Resetting operator state...") + _ = ocRun("delete", "subscription", "rhacs-operator", + "-n", "openshift-operators", "--ignore-not-found") + out, _ := ocOutput("get", "csv", "-n", "openshift-operators", "--no-headers") + for _, line := range strings.Split(out, "\n") { + if fields := strings.Fields(line); len(fields) > 0 && strings.HasPrefix(fields[0], "rhacs-operator.") { + _ = ocRun("delete", "csv", fields[0], + "-n", "openshift-operators", "--ignore-not-found") + } + } + _ = ocRun("delete", "catalogsource", "my-operator-catalog", + "-n", "openshift-marketplace", "--ignore-not-found") + return EnableDefaultSources() +} diff --git a/operator-index-upgrade-test/upgrade_latest_test.go b/operator-index-upgrade-test/upgrade_latest_test.go new file mode 100644 index 00000000..c92ccf0b --- /dev/null +++ b/operator-index-upgrade-test/upgrade_latest_test.go @@ -0,0 +1,56 @@ +package upgradetest + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestUpgradeLatest tests the install + optional upgrade to latest GA path: +// 1. Installs ACS Operator from OPERATOR_INDEX_IMAGE on the ACS_VERSION channel. +// 2. If ACS_VERSION minor < latest GA minor for the same major: upgrades to latest GA via redhat-operators. +// 3. Verifies each CSV reaches Succeeded. +func TestUpgradeLatest(t *testing.T) { + operatorIndexImage := requireEnv(t, "OPERATOR_INDEX_IMAGE") + acsVersion := requireEnv(t, "ACS_VERSION") + + major, minor, err := ParseACSVersion(acsVersion) + require.NoError(t, err, "parse ACS_VERSION") + channel := fmt.Sprintf("rhacs-%d.%d", major, minor) + + t.Cleanup(func() { _ = ResetOperator() }) + + t.Logf("Image: %s", operatorIndexImage) + t.Logf("Version: %d.%d | Channel: %s", major, minor, channel) + + // Query latest GA minor BEFORE disabling default sources (install_from_custom disables them). + // Use || to degrade gracefully: a catalog timeout just skips the upgrade check. + var latestMinor int + if err := WaitForCatalog("redhat-operators", "openshift-marketplace", 3*time.Minute); err != nil { + t.Logf("Warning: could not wait for redhat-operators: %v — upgrade check will be skipped", err) + } else if lm, err := GetLatestOfficialMinor(major, 3*time.Minute); err != nil { + t.Logf("Warning: could not determine latest GA minor for %d.x: %v — upgrade check will be skipped", major, err) + } else { + latestMinor = lm + t.Logf("Latest GA in redhat-operators: %d.%d", major, latestMinor) + } + + t.Logf("Step 1: Install ACS Operator %s from custom index", channel) + targetCSV, err := InstallFromCustom(operatorIndexImage, channel) + require.NoError(t, err) + require.NoError(t, WaitForCSV(targetCSV, 10*time.Minute)) + + // Upgrade only when the tested major matches and our minor is behind latest. + // Comparing major prevents a spurious upgrade when testing a pre-release (e.g. 5.0 vs 4.12). + if latestMinor > 0 && minor < latestMinor { + t.Logf("Step 2: Upgrade %d.%d → %d.%d (latest GA)", major, minor, major, latestMinor) + targetCSV, err = UpgradeToLatestOfficial(major) + require.NoError(t, err) + require.NoError(t, WaitForCSV(targetCSV, 10*time.Minute)) + } else if latestMinor > 0 { + t.Logf("%d.%d is already at or ahead of the latest GA minor (%d) — no upgrade needed", + major, minor, latestMinor) + } +} diff --git a/operator-index-upgrade-test/upgrade_oldest_test.go b/operator-index-upgrade-test/upgrade_oldest_test.go new file mode 100644 index 00000000..d71731fe --- /dev/null +++ b/operator-index-upgrade-test/upgrade_oldest_test.go @@ -0,0 +1,49 @@ +package upgradetest + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func requireEnv(t *testing.T, name string) string { + t.Helper() + val := os.Getenv(name) + require.NotEmptyf(t, val, "%s env var must be set", name) + return val +} + +// TestUpgradeOldest tests the upgrade path from the oldest supported version to the provided index: +// 1. Installs ACS Operator (oldest_supported_version from bundles.yaml) from official redhat-operators. +// 2. Upgrades to OPERATOR_INDEX_IMAGE on the ACS_VERSION channel. +// 3. Verifies each CSV reaches Succeeded. +func TestUpgradeOldest(t *testing.T) { + operatorIndexImage := requireEnv(t, "OPERATOR_INDEX_IMAGE") + acsVersion := requireEnv(t, "ACS_VERSION") + + major, minor, err := ParseACSVersion(acsVersion) + require.NoError(t, err, "parse ACS_VERSION") + channel := fmt.Sprintf("rhacs-%d.%d", major, minor) + + oldestMajor, oldestMinor, err := ReadOldestSupportedVersion() + require.NoError(t, err, "read oldest_supported_version from bundles.yaml") + + t.Cleanup(func() { _ = ResetOperator() }) + + t.Logf("Image: %s", operatorIndexImage) + t.Logf("Version: %d.%d | Channel: %s", major, minor, channel) + t.Logf("Oldest: %d.%d (from bundles.yaml)", oldestMajor, oldestMinor) + + t.Logf("Step 1: Install ACS Operator %d.%d from redhat-operators", oldestMajor, oldestMinor) + targetCSV, err := InstallFromOfficial(oldestMajor, oldestMinor) + require.NoError(t, err) + require.NoError(t, WaitForCSV(targetCSV, 10*time.Minute)) + + t.Logf("Step 2: Upgrade to %s via custom index", channel) + targetCSV, err = UpgradeViaCustom(operatorIndexImage, channel) + require.NoError(t, err) + require.NoError(t, WaitForCSV(targetCSV, 30*time.Minute)) +} From fdddf2afea9adf4393d998d05d0c333292b96e0d Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 14 Jul 2026 14:13:58 +0200 Subject: [PATCH 16/17] Add makefile target --- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Makefile b/Makefile index 4ba1ce0c..209b7951 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,18 @@ catalog-template.yaml: bundles.yaml $(wildcard $(GENERATE_SCRIPT_FOLDER)/*.go) go-test: @$(GO) test -cover -v ./cmd/... +# Operator index upgrade tests — require OPERATOR_INDEX_IMAGE and ACS_VERSION env vars. +# Example: make upgrade-test-oldest OPERATOR_INDEX_IMAGE=quay.io/rhacs-eng/stackrox-operator-index:... ACS_VERSION=4.11 +.PHONY: upgrade-test-oldest upgrade-test-latest upgrade-test + +upgrade-test-oldest: + $(GO) test -v -count=1 -run TestUpgradeOldest -timeout 45m ./operator-index-upgrade-test/ + +upgrade-test-latest: + $(GO) test -v -count=1 -run TestUpgradeLatest -timeout 30m ./operator-index-upgrade-test/ + +upgrade-test: upgrade-test-oldest upgrade-test-latest + $(OPM): mkdir -p "$$(dirname $@)" os_name="$$(uname | tr '[:upper:]' '[:lower:]')"; \ From b220d9b769fd6f1f354ea518e22c48bb65628c27 Mon Sep 17 00:00:00 2001 From: Aleksandr Kurlov Date: Tue, 14 Jul 2026 14:14:17 +0200 Subject: [PATCH 17/17] Address feedback --- ...l => auto-operator-index-upgrade-test.yml} | 78 ++++++-------- ...st.yml => operator-index-upgrade-test.yml} | 100 ++++++++++-------- 2 files changed, 91 insertions(+), 87 deletions(-) rename .github/workflows/{auto-smoke-test.yml => auto-operator-index-upgrade-test.yml} (65%) rename .github/workflows/{acs-smoke-test.yml => operator-index-upgrade-test.yml} (59%) diff --git a/.github/workflows/auto-smoke-test.yml b/.github/workflows/auto-operator-index-upgrade-test.yml similarity index 65% rename from .github/workflows/auto-smoke-test.yml rename to .github/workflows/auto-operator-index-upgrade-test.yml index ed637a19..a51306d8 100644 --- a/.github/workflows/auto-smoke-test.yml +++ b/.github/workflows/auto-operator-index-upgrade-test.yml @@ -1,4 +1,4 @@ -name: "Auto Smoke Test" +name: "Auto Operator Index Upgrade Test" on: workflow_dispatch: @@ -8,7 +8,7 @@ on: required: false type: string operator-index-image-override: - description: 'Override operator-index image, skips Quay polling (e.g. quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-...)' + description: 'Override operator-index image, skips Konflux polling (e.g. quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-...)' required: false type: string push: @@ -17,24 +17,24 @@ on: pull_request: paths: ['bundles.yaml'] -env: - # OCP version used to construct the Konflux-built operator-index image tag. - # Update when infra cluster uses a newer OCP version. - OCP_VERSION: v4-22 +permissions: + contents: read jobs: derive-inputs: - name: Derive smoke test inputs + name: Derive operator-index-upgrade test inputs runs-on: ubuntu-latest outputs: acs-versions: ${{ steps.version.outputs.acs-versions }} operator-index-image: ${{ steps.image.outputs.operator-index-image }} + ocp-version: ${{ steps.image.outputs.ocp-version }} should-run: ${{ steps.version.outputs.should-run }} steps: - name: Check out code uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - name: Determine ACS version id: version @@ -64,14 +64,14 @@ jobs: DIFF_BASE="HEAD~1" fi - # Match only the `version:` key, ignore `oldest_supported_version:` and others. + # Lines starting with '+' are additions in the diff; anchor to 'version:' to skip 'oldest_supported_version:' etc. NEW_VERSIONS=$(git diff "$DIFF_BASE" -- bundles.yaml \ | grep '^+[[:space:]]*version:' \ | sed 's/.*version:[[:space:]]*//' \ || true) if [ -z "$NEW_VERSIONS" ]; then - echo "No new versions found in bundles.yaml diff, skipping smoke test" + echo "No new versions found in bundles.yaml diff, skipping operator-index-upgrade test" echo "should-run=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -79,7 +79,6 @@ jobs: # Build version JSON array for the matrix. MINORS_JSON=$(echo "$NEW_VERSIONS" \ | sed 's/^\([0-9]*\.[0-9]*\).*/\1/' \ - | sort -uV \ | jq -Rc '[.,inputs]') echo "New versions added: $(echo "$NEW_VERSIONS" | tr '\n' ' '), testing minors: $MINORS_JSON" echo "acs-versions=$MINORS_JSON" >> "$GITHUB_OUTPUT" @@ -94,44 +93,35 @@ jobs: run: | if [ -n "$OPERATOR_INDEX_IMAGE_OVERRIDE" ]; then echo "operator-index-image=$OPERATOR_INDEX_IMAGE_OVERRIDE" >> "$GITHUB_OUTPUT" + echo "ocp-version=" >> "$GITHUB_OUTPUT" + echo "needs-wait=false" >> "$GITHUB_OUTPUT" exit 0 fi + # Use the highest OCP version from .tekton/ pipelines to build the image tag. + OCP_VERSION=$(ls .tekton/operator-index-ocp-v*-build.yaml \ + | sed 's/.*operator-index-ocp-\(v[0-9]*-[0-9]*\)-build\.yaml/\1/' \ + | sort -V | tail -1) + echo "Derived OCP_VERSION: $OCP_VERSION" + TAG="ocp-${OCP_VERSION}-${SHA}-fast" IMAGE="quay.io/rhacs-eng/stackrox-operator-index:${TAG}" + echo "Image: $IMAGE" + echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" + # Convert v4-22 → 4.22 for the infra create-cluster args. + echo "ocp-version=$(echo "$OCP_VERSION" | sed 's/^v//' | tr '-' '.')" >> "$GITHUB_OUTPUT" + echo "needs-wait=true" >> "$GITHUB_OUTPUT" - CHECK_NAME="Red Hat Konflux / operator-index-ocp-${OCP_VERSION}-on-push" - echo "Waiting for Konflux check '$CHECK_NAME' on $SHA..." - - for i in $(seq 1 40); do - RESULT=$(gh api /repos/stackrox/operator-index/commits/"$SHA"/check-runs \ - | jq -r --arg name "$CHECK_NAME" \ - '.check_runs[] | select(.name == $name) | "\(.status) \(.conclusion)"') - case "$RESULT" in - "completed success") - echo "Konflux build succeeded, verifying image in Quay..." - if curl -sf --max-time 30 --connect-timeout 10 \ - "https://quay.io/api/v1/repository/rhacs-eng/stackrox-operator-index/tag/?specificTag=${TAG}" \ - | jq -e '.tags | length > 0' > /dev/null; then - echo "Image confirmed in Quay: $IMAGE" - echo "operator-index-image=$IMAGE" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "Konflux check passed but image not found in Quay: $IMAGE" - exit 1 ;; - "completed "*) - echo "Konflux build failed: $RESULT" - exit 1 ;; - esac - echo "Attempt $i/40: ${RESULT:-check not yet started}, retrying in 3 minutes..." - [ "$i" -lt 40 ] && sleep 180 - done - - echo "Timed out after 2 hours waiting for Konflux build: $CHECK_NAME" - exit 1 + - name: Wait for operator-index image + if: steps.version.outputs.should-run == 'true' && steps.image.outputs.needs-wait == 'true' + uses: stackrox/actions/release/wait-for-image@v1 + with: + image: ${{ steps.image.outputs.operator-index-image }} + interval: "60" + limit: "7200" - smoke-test: - name: Run smoke test + run-upgrade-tests: + name: Run operator-index upgrade tests needs: derive-inputs if: needs.derive-inputs.outputs.should-run == 'true' strategy: @@ -139,9 +129,11 @@ jobs: acs-version: ${{ fromJson(needs.derive-inputs.outputs.acs-versions) }} # if any matrix job fails, continue to run the rest of the jobs fail-fast: false - uses: ./.github/workflows/acs-smoke-test.yml + uses: ./.github/workflows/operator-index-upgrade-test.yml with: operator-index-image: ${{ needs.derive-inputs.outputs.operator-index-image }} acs-version: ${{ matrix.acs-version }} + ocp-version: ${{ needs.derive-inputs.outputs.ocp-version }} cluster-lifespan: 3h - secrets: inherit + secrets: + INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }} diff --git a/.github/workflows/acs-smoke-test.yml b/.github/workflows/operator-index-upgrade-test.yml similarity index 59% rename from .github/workflows/acs-smoke-test.yml rename to .github/workflows/operator-index-upgrade-test.yml index 676ffd3f..b97fb7b9 100644 --- a/.github/workflows/acs-smoke-test.yml +++ b/.github/workflows/operator-index-upgrade-test.yml @@ -1,9 +1,6 @@ -name: "ACS Smoke Test" +name: "Operator Index Upgrade Test" on: - # TODO: remove pull_request trigger before merging - pull_request: - branches: [master] workflow_dispatch: inputs: operator-index-image: @@ -14,6 +11,11 @@ on: description: 'ACS minor version under test (e.g. 4.10). Determines channel and Y-2 oldest.' required: true type: string + ocp-version: + description: 'OCP version for the infra cluster (e.g. 4.22). Derived from operator-index-image tag if not set.' + required: false + default: '' + type: string cluster-lifespan: description: 'Cluster lifespan (e.g., 2h, 4h, 8h)' required: false @@ -29,24 +31,50 @@ on: description: 'ACS minor version under test (e.g. 4.10). Determines channel and Y-2 oldest.' required: true type: string + ocp-version: + description: 'OCP version for the infra cluster (e.g. 4.22). Derived from operator-index-image tag if not set.' + required: false + default: '' + type: string cluster-lifespan: description: 'Cluster lifespan (e.g., 2h, 4h, 8h)' required: false default: '2h' type: string + secrets: + INFRA_TOKEN: + required: true + +permissions: + contents: read + +defaults: + run: + shell: bash env: - CLUSTER_NAME: smoke-${{ github.run_number }} + CLUSTER_NAME: upgrade-test-${{ github.run_id }}-${{ github.run_attempt }} GH_TOKEN: ${{ github.token }} GH_NO_UPDATE_NOTIFIER: 1 run-name: >- - ${{ format('ACS Smoke Test - {0}', inputs.operator-index-image) }} + ${{ format('Operator Index Upgrade Test v{0}', inputs.acs-version) }} jobs: create-cluster: name: Create OpenShift cluster runs-on: ubuntu-latest + steps: + - name: Set cluster args + id: ocp + env: + OCP_VERSION: ${{ inputs.ocp-version }} + run: | + if [ -n "$OCP_VERSION" ]; then + echo "infra-args=openshift-version=ocp/stable-${OCP_VERSION}" >> "$GITHUB_OUTPUT" + else + echo "infra-args=" >> "$GITHUB_OUTPUT" + fi - name: Create OpenShift cluster uses: stackrox/actions/infra/create-cluster@v1 @@ -56,62 +84,45 @@ jobs: name: ${{ env.CLUSTER_NAME }} lifespan: ${{ inputs.cluster-lifespan || '2h' }} wait: true + args: ${{ steps.ocp.outputs.infra-args }} - run-smoke-tests: - name: Run ACS smoke tests + run-upgrade-tests: + name: Run operator-index upgrade tests needs: create-cluster runs-on: ubuntu-latest env: INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }} container: - image: quay.io/stackrox-io/apollo-ci:stackrox-test-0.5.11@sha256:df3459cf038e73775314b3a5d36d14dda41e6df438a0273c9efa8d23e0f2358a + image: quay.io/stackrox-io/apollo-ci:stackrox-test-stable steps: - name: Check out code uses: actions/checkout@v7 + with: + persist-credentials: false - name: Install infractl uses: stackrox/actions/infra/install-infractl@v1 - name: Download cluster kubeconfig run: | - infractl artifacts \ - --download-dir=./artifacts \ - ${{ env.CLUSTER_NAME }} - - - name: Set KUBECONFIG - run: echo "KUBECONFIG=${{ github.workspace }}/artifacts/kubeconfig" >> "$GITHUB_ENV" + infractl artifacts --download-dir=./artifacts "$CLUSTER_NAME" + echo "KUBECONFIG=${{ github.workspace }}/artifacts/kubeconfig" >> "$GITHUB_ENV" - name: "Test 1: upgrade oldest-supported → provided" - working-directory: ./smoke-test env: - # Add a || operator-index image and version for testing in the PR. - # TODO: remove before merging! - OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image || 'quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-7a9fcba929e5858cca91a84c3e5da8c829a3c990-fast' }} - ACS_VERSION: ${{ inputs.acs-version || '4.11' }} - run: | - set -uo pipefail - bash upgrade-oldest-test.sh - - - name: Reset operator state between tests - working-directory: ./smoke-test - run: | - set -uo pipefail - source lib.sh - reset_operator - shell: bash + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} + ACS_VERSION: ${{ inputs.acs-version }} + run: make upgrade-test-oldest - name: "Test 2: install provided → optionally upgrade to latest GA" - working-directory: ./smoke-test env: - # Add a || operator-index image and version for testing in the PR. - # TODO: remove before merging! - OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image || 'quay.io/rhacs-eng/stackrox-operator-index:ocp-v4-22-7a9fcba929e5858cca91a84c3e5da8c829a3c990-fast' }} - ACS_VERSION: ${{ inputs.acs-version || '4.11' }} - run: bash upgrade-latest-test.sh + OPERATOR_INDEX_IMAGE: ${{ inputs.operator-index-image }} + ACS_VERSION: ${{ inputs.acs-version }} + run: make upgrade-test-latest delete-test-cluster: name: Delete infra test cluster - needs: [create-cluster, run-smoke-tests] + needs: [create-cluster, run-upgrade-tests] runs-on: ubuntu-latest if: always() && needs.create-cluster.result != 'cancelled' env: @@ -121,26 +132,27 @@ jobs: uses: stackrox/actions/infra/install-infractl@v1 - name: Delete cluster - run: infractl delete ${{ needs.create-cluster.outputs.cluster-name }} + run: infractl delete "$CLUSTER_NAME" report-status: name: Report test status - needs: [create-cluster, run-smoke-tests, cleanup-cluster] + needs: [create-cluster, run-upgrade-tests, delete-test-cluster] runs-on: ubuntu-latest if: always() steps: - name: Write summary run: | cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" - # ACS Smoke Test Results + # Operator Index Upgrade Test Results | Field | Value | |-------|-------| | **Operator image** | `${{ inputs.operator-index-image }}` | - | **Cluster** | `${{ needs.create-cluster.outputs.cluster-name }}` | + | **ACS version** | `${{ inputs.acs-version }}` | + | **Cluster** | `${{ env.CLUSTER_NAME }}` | | **Create cluster** | ${{ needs.create-cluster.result }} | - | **Smoke tests** | ${{ needs.run-smoke-tests.result }} | - | **Cleanup** | ${{ needs.cleanup-cluster.result }} | + | **Upgrade tests** | ${{ needs.run-upgrade-tests.result }} | + | **Delete cluster** | ${{ needs.delete-test-cluster.result }} | ## Tests run: 1. **Test 1** — install oldest supported ACS version, upgrade to the provided operator-index image