From 96be95cf330b0dbced7ff77db414895d1f011ba9 Mon Sep 17 00:00:00 2001 From: AnnaZivkovic Date: Fri, 1 May 2026 16:08:08 -0700 Subject: [PATCH 1/2] Add run-script task to multi-arch build pipeline Add version bumping to multi-arch operator builds to match single-arch. Add release pre and final pipeline for the operator and FBC --- .tekton/fbc-update-final-pipeline.yaml | 847 ++++++++++++++++++++++ .tekton/multi-arch-build-pipeline.yaml | 60 +- .tekton/single-arch-build-pipeline.yaml | 16 +- .tekton/snapshot-validation-pipeline.yaml | 248 +++++++ .tekton/tag-release-commit-pipeline.yaml | 334 +++++++++ hack/bump-version-manual.sh | 51 ++ hack/bump-version.sh | 4 +- 7 files changed, 1549 insertions(+), 11 deletions(-) create mode 100644 .tekton/fbc-update-final-pipeline.yaml create mode 100644 .tekton/snapshot-validation-pipeline.yaml create mode 100644 .tekton/tag-release-commit-pipeline.yaml create mode 100644 hack/bump-version-manual.sh diff --git a/.tekton/fbc-update-final-pipeline.yaml b/.tekton/fbc-update-final-pipeline.yaml new file mode 100644 index 000000000..3227dd7b4 --- /dev/null +++ b/.tekton/fbc-update-final-pipeline.yaml @@ -0,0 +1,847 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: fbc-update-final-pipeline + annotations: + description: | + Final pipeline that runs after release completion to update FBC branch + with new bundle references. Uses existing hack scripts from FBC branch. +spec: + description: | + This pipeline extracts bundle information from a release snapshot and creates + a pull request to update the FBC branch using the existing build-indexs.sh + and update-graph.sh scripts. + params: + - name: snapshot + description: JSON string of the Snapshot from the release + type: string + - name: release + description: JSON string of the Release + type: string + - name: git-url + description: Repository URL + type: string + default: https://github.com/openshift/multiarch-tuning-operator + - name: fbc-branch + description: FBC branch to update (default is 'fbc') + type: string + default: fbc + + results: + - name: VERSION + description: Release version + value: $(tasks.extract-snapshot-name.results.VERSION) + - name: FBC_PR_URL + description: URL to the FBC update pull request + value: $(tasks.update-fbc-and-create-pr.results.PR_URL) + + tasks: + - name: extract-snapshot-name + taskSpec: + params: + - name: snapshot + results: + - name: SNAPSHOT_NAME + description: Name of the snapshot + - name: BUNDLE_IMAGE + description: Bundle image reference + - name: VERSION + description: Bundle version + steps: + - name: parse-snapshot + image: quay.io/konflux-ci/appstudio-utils:latest + script: | + #!/bin/bash + set -euo pipefail + + # Install jq to temp directory (no permissions needed) + TOOL_DIR="/tmp/tools" + mkdir -p "$TOOL_DIR" + JQ_VERSION=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + echo "Latest jq version: $JQ_VERSION" + curl -L "https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64" -o "$TOOL_DIR/jq" + chmod +x "$TOOL_DIR/jq" + export PATH="$TOOL_DIR:$PATH" + + echo "Fetching snapshot..." + SNAPSHOT_REF='$(params.snapshot)' + echo "Snapshot reference: $SNAPSHOT_REF" + + # Parse namespace/name from reference + if [[ "$SNAPSHOT_REF" == *"/"* ]]; then + SNAPSHOT_NAMESPACE="${SNAPSHOT_REF%/*}" + SNAPSHOT_NAME="${SNAPSHOT_REF#*/}" + else + # If no namespace, assume current namespace + SNAPSHOT_NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + SNAPSHOT_NAME="$SNAPSHOT_REF" + fi + + echo "Fetching snapshot $SNAPSHOT_NAME from namespace $SNAPSHOT_NAMESPACE..." + + # Fetch the snapshot using kubectl + SNAPSHOT=$(oc get snapshot "$SNAPSHOT_NAME" -n "$SNAPSHOT_NAMESPACE" -o json) + + # Extract snapshot name + echo "Snapshot name: $SNAPSHOT_NAME" + echo -n "$SNAPSHOT_NAME" > $(results.SNAPSHOT_NAME.path) + + # Extract bundle image (find component with "bundle" in name) + BUNDLE_IMAGE=$(echo "$SNAPSHOT" | jq -r '.spec.components[] | select(.name | contains("bundle")) | .containerImage' | head -n1) + echo "Bundle image: $BUNDLE_IMAGE" + echo -n "$BUNDLE_IMAGE" > $(results.BUNDLE_IMAGE.path) + + # Extract version from bundle CSV (the authoritative source) + echo "Extracting version from bundle CSV..." + + # Install skopeo and yq + echo "Installing skopeo..." + curl -L "https://github.com/lework/skopeo-binary/releases/download/v1.17.0/skopeo-linux-amd64" -o "$TOOL_DIR/skopeo" + chmod +x "$TOOL_DIR/skopeo" + + echo "Installing yq..." + YQ_VERSION=$(curl -s https://api.github.com/repos/mikefarah/yq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + curl -L "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "$TOOL_DIR/yq" + chmod +x "$TOOL_DIR/yq" + + # Extract bundle image to get CSV + BUNDLE_DIR="/tmp/bundle" + mkdir -p "$BUNDLE_DIR" + + echo "Copying bundle image..." + skopeo copy "docker://$BUNDLE_IMAGE" "dir:$BUNDLE_DIR" + + # Find and extract the manifest layers + MANIFEST_FILE=$(find "$BUNDLE_DIR" -name "manifest.json" | head -1) + if [ -z "$MANIFEST_FILE" ]; then + echo "❌ ERROR: Could not find manifest.json in bundle image" + exit 1 + fi + + # Extract layers + LAYERS_DIR="/tmp/bundle-extracted" + mkdir -p "$LAYERS_DIR" + for layer in $(jq -r '.layers[].digest' "$MANIFEST_FILE" | cut -d: -f2); do + if [ -f "$BUNDLE_DIR/$layer" ]; then + tar -xzf "$BUNDLE_DIR/$layer" -C "$LAYERS_DIR" 2>/dev/null || \ + tar -xf "$BUNDLE_DIR/$layer" -C "$LAYERS_DIR" 2>/dev/null || true + fi + done + + # Find CSV file + CSV_FILE=$(find "$LAYERS_DIR" -name "*clusterserviceversion.yaml" | head -1) + if [ -z "$CSV_FILE" ]; then + echo "❌ ERROR: Could not find CSV file in bundle" + exit 1 + fi + + # Extract version from CSV + VERSION=$(yq eval '.spec.version' "$CSV_FILE") + if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then + echo "❌ ERROR: Could not extract version from CSV" + exit 1 + fi + + # Add 'v' prefix if not present + if [[ ! "$VERSION" =~ ^v ]]; then + VERSION="v${VERSION}" + fi + + echo "Extracted version from bundle CSV: $VERSION" + echo -n "$VERSION" > $(results.VERSION.path) + params: + - name: snapshot + value: $(params.snapshot) + + - name: update-fbc-and-create-pr + runAfter: [extract-snapshot-name] + taskSpec: + params: + - name: BUNDLE_IMAGE + - name: VERSION + - name: FBC_BRANCH + - name: GIT_URL + results: + - name: PR_URL + description: URL of created pull request + steps: + # Step 1: Clone FBC branch + - name: clone-fbc-branch + image: quay.io/konflux-ci/appstudio-utils:latest + script: | + #!/bin/bash + set -euo pipefail + + GIT_URL="$(params.GIT_URL)" + FBC_BRANCH="$(params.FBC_BRANCH)" + + echo "Cloning FBC branch: $FBC_BRANCH" + git clone --branch "$FBC_BRANCH" --single-branch "$GIT_URL" /workspace/fbc-repo + cd /workspace/fbc-repo + + echo "Repository cloned successfully" + git log -1 --oneline + + # Step 2: Check if version already exists in CHANNEL GRAPH + - name: check-version-in-channel + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + VERSION="$(params.VERSION)" + # Remove 'v' prefix for catalog entry name + VERSION_NO_V="${VERSION#v}" + ENTRY_NAME="multiarch-tuning-operator.v${VERSION_NO_V}" + + echo "Checking if version $ENTRY_NAME exists in channel graph..." + + # Check if version exists in channel entries (entries with schema: olm.channel) + # These are the upgrade graph entries, not the bundle metadata + if grep -A 10 "schema: olm.channel" fbc-v*/catalog/multiarch-tuning-operator/index.yaml | grep -q "name: $ENTRY_NAME"; then + echo "✅ Version $ENTRY_NAME already exists in channel graph" + echo "This version has already been released - skipping all FBC updates" + touch /workspace/skip-all-updates + else + echo "Version $ENTRY_NAME not found in channel graph - will add it" + fi + + # Step 3: Remove existing bundle metadata (only if re-running pipeline for same version) + - name: remove-existing-bundle + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + # Check if we should skip all updates (version already in channel graph = already committed) + if [ -f /workspace/skip-all-updates ]; then + echo "Skipping bundle removal - version already released and committed" + exit 0 + fi + + VERSION="$(params.VERSION)" + VERSION_NO_V="${VERSION#v}" + BUNDLE_NAME="multiarch-tuning-operator.v${VERSION_NO_V}" + + echo "Checking if bundle $BUNDLE_NAME exists from a previous pipeline run..." + + # Check if any catalog has this bundle entry + BUNDLE_EXISTS=false + for catalog_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + if grep -q "^name: $BUNDLE_NAME$" "$catalog_file"; then + echo "Found existing bundle entry in $catalog_file (from previous run)" + BUNDLE_EXISTS=true + break + fi + done + + if [ "$BUNDLE_EXISTS" = false ]; then + echo "No existing bundle found - this is a new version" + exit 0 + fi + + echo "Removing bundle entries from previous pipeline run to avoid duplicates..." + + # Remove bundle entries using awk to preserve file formatting + for catalog_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + if ! grep -q "^name: $BUNDLE_NAME$" "$catalog_file"; then + continue + fi + + echo "Removing from $catalog_file..." + temp_file=$(mktemp) + + # Remove the bundle entry block (from '---' or 'schema: olm.bundle' to next schema) + awk -v bundle="$BUNDLE_NAME" ' + BEGIN { in_bundle = 0; skip_lines = 0 } + /^---$/ { + if (in_bundle) { + in_bundle = 0 + next + } + print + next + } + /^schema: olm\.bundle$/ { + getline + if ($0 == "name: " bundle) { + in_bundle = 1 + skip_lines = 1 + next + } else { + print "schema: olm.bundle" + print + } + next + } + /^schema: / { + if (in_bundle) { + in_bundle = 0 + } + print + next + } + !in_bundle { print } + ' "$catalog_file" > "$temp_file" + + mv "$temp_file" "$catalog_file" + echo "✅ Removed from $catalog_file" + done + + echo "Cleanup complete - ready to append new bundle" + + # Step 4: Run build-indexs.sh to append bundle to catalogs + - name: build-indexes + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + # Check if we should skip all updates + if [ -f /workspace/skip-all-updates ]; then + echo "Skipping bundle addition - version already released" + exit 0 + fi + + BUNDLE_IMAGE="$(params.BUNDLE_IMAGE)" + + # Verify pre-installed tools + echo "Checking pre-installed tools..." + jq --version + yq --version + + # Install opm (not included in appstudio-utils) + echo "Installing opm..." + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + + # Use fixed version to avoid API calls and ensure reproducibility + OPM_VERSION="v1.48.0" + echo "Installing opm ${OPM_VERSION}..." + curl -sL "https://github.com/operator-framework/operator-registry/releases/download/${OPM_VERSION}/linux-amd64-opm" -o /tmp/tools/opm + chmod +x /tmp/tools/opm + opm version + + echo "" + echo "Running build-indexs.sh with bundle: $BUNDLE_IMAGE" + + # Make script executable + chmod +x hack/build-indexs.sh + + # Run the script with bundle image + # appstudio-utils has container credentials to pull the image + hack/build-indexs.sh "$BUNDLE_IMAGE" + + echo "Catalog indexes updated successfully" + + # Step 5: Commit bundle metadata updates + - name: commit-index-updates + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + VERSION="$(params.VERSION)" + BUNDLE_IMAGE="$(params.BUNDLE_IMAGE)" + + # Configure git + git config user.name "Konflux Release Bot" + git config user.email "konflux-release@redhat.com" + + # Check if there are changes to the indexes + if ! git diff --quiet fbc-v*/catalog/multiarch-tuning-operator/index.yaml; then + echo "Committing bundle metadata updates..." + + git add fbc-v*/catalog/multiarch-tuning-operator/index.yaml + + git commit -m "Update bundle metadata for ${VERSION} + + Updated bundle image reference in catalog indexes. + + Bundle image: ${BUNDLE_IMAGE} + Version: ${VERSION} + + Co-Authored-By: Konflux Release Bot " + + echo "✅ Bundle metadata committed" + git log -1 --oneline + else + echo "No changes to bundle metadata - skipping commit" + fi + + # Step 6: Update channel graph (insert into existing entries array) + - name: update-graph + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + # Check if we should skip all updates (version already released) + if [ -f /workspace/skip-all-updates ]; then + echo "Skipping update-graph - version already exists in channel graph" + exit 0 + fi + + VERSION="$(params.VERSION)" + VERSION_NO_V="${VERSION#v}" + ENTRY_NAME="multiarch-tuning-operator.v${VERSION_NO_V}" + + echo "Updating channel graph with version: $VERSION_NO_V" + + # Verify pre-installed tools + echo "Checking pre-installed tools..." + jq --version + yq --version + + # Extract base version and determine if this is a clean or commit-based version + if [[ "$VERSION_NO_V" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\+(.+)$ ]]; then + BASE_VERSION="${BASH_REMATCH[1]}" + COMMIT_SUFFIX="${BASH_REMATCH[2]}" + IS_CLEAN_VERSION=false + echo "Commit-based patch version: $VERSION_NO_V (base: $BASE_VERSION, commit: $COMMIT_SUFFIX)" + else + # Clean version (no commit suffix) + BASE_VERSION="$VERSION_NO_V" + COMMIT_SUFFIX="" + IS_CLEAN_VERSION=true + echo "Clean version detected: $BASE_VERSION" + fi + + # Determine strategy based on version type + # Release pattern: Clean version FIRST (1.4.0), then patches (1.4.0+abc, 1.4.0+def, ...) + if [ "$IS_CLEAN_VERSION" = true ]; then + # Adding a clean version - this is a new base version release + # Replaces: latest version from previous base (any version, clean or commit) + # Skips: none (clean versions are upgrade milestones and start a new series) + + echo "Clean version - new base version release" + echo "Will replace latest version from previous base" + USE_SKIPS=false + + # Check if this clean version already exists + for check_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + if grep -q "name: multiarch-tuning-operator.v${BASE_VERSION}\$" "$check_file"; then + echo "ERROR: Clean version $BASE_VERSION already exists in catalog" + echo "Cannot add duplicate clean version" + exit 1 + fi + done + else + # Adding a commit-based patch version + # These come AFTER the clean version for this base + # Replaces: + # - If first patch: replaces clean version (e.g., 1.4.0+abc replaces 1.4.0) + # - If subsequent patch: replaces previous patch (e.g., 1.4.0+def replaces 1.4.0+abc) + # Skips: older patches (but NOT the clean version - it's a milestone) + + echo "Patch version - adds to existing base $BASE_VERSION" + + # Verify clean version exists for this base + CLEAN_VERSION_EXISTS=false + for check_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + if grep -q "name: multiarch-tuning-operator.v${BASE_VERSION}\$" "$check_file"; then + CLEAN_VERSION_EXISTS=true + break + fi + done + + if [ "$CLEAN_VERSION_EXISTS" = false ]; then + echo "ERROR: Clean version $BASE_VERSION does not exist in catalog" + echo "Cannot add patch versions before the clean version is released" + echo "Release pattern: clean version FIRST (1.4.0), then patches (1.4.0+abc, 1.4.0+def)" + exit 1 + fi + + # Check if other patches exist for this base + COMMIT_VERSIONS_EXIST=false + for check_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + if grep -q "name: multiarch-tuning-operator.v${BASE_VERSION}+" "$check_file"; then + COMMIT_VERSIONS_EXIST=true + break + fi + done + + if [ "$COMMIT_VERSIONS_EXIST" = true ]; then + echo "Other patches exist for base $BASE_VERSION - will skip older patches" + USE_SKIPS=true + else + echo "First patch for base $BASE_VERSION - will replace clean version" + USE_SKIPS=false + fi + fi + + # Update each catalog's channel graph + for index_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + echo "Processing $index_file..." + temp_file=$(mktemp) + + if [ "$USE_SKIPS" = true ]; then + # Adding a patch version when other patches exist + # Replaces: the latest patch (first in file) + # Skips: all older patches (excluding clean version - it's a milestone) + + # Find all existing patch versions for this base version + # IMPORTANT: Only collect patch versions (with +), NEVER clean versions + # Clean versions are permanent milestones that cannot be skipped + # File order is insertion order (newest first) + # Exclude the version we're currently adding + ALL_PATCHES=$(grep "name: multiarch-tuning-operator.v${BASE_VERSION}+" "$index_file" | \ + sed 's/.*name: //' | sed 's/^[[:space:]]*//' | \ + grep -v "^${ENTRY_NAME}$" || true) + + if [ -n "$ALL_PATCHES" ]; then + echo "Found existing patches for base $BASE_VERSION:" + echo "$ALL_PATCHES" + + # Get the first (most recent, newest in file) patch to replace + # File order is insertion order with newest first + REPLACES_TARGET=$(echo "$ALL_PATCHES" | head -1) + + # Build skip list: all patches except the one we're replacing + # We want users to auto-upgrade through the chain, but allow skipping older patches + # So we skip everything except the immediate predecessor + SKIP_VERSIONS=$(echo "$ALL_PATCHES" | tail -n +2 || true) + + echo "Will replace: $REPLACES_TARGET" + if [ -n "$SKIP_VERSIONS" ]; then + echo "Will skip (older patches only): $SKIP_VERSIONS" + + # Insert new entry with skips using awk + awk -v entry="$ENTRY_NAME" -v replaces="$REPLACES_TARGET" -v skips="$SKIP_VERSIONS" ' + /^ - name: / && !inserted && in_channel { + # Found first entry in channel, insert new one before it + print " - name: " entry + print " replaces: " replaces + print " skips:" + # Split and print skip versions + n = split(skips, skip_array, "\n") + for (i = 1; i <= n; i++) { + if (skip_array[i] != "") { + print " - " skip_array[i] + } + } + inserted = 1 + } + /^schema: olm.channel$/ { in_channel = 1 } + /^schema: / && !/^schema: olm.channel$/ { in_channel = 0 } + { print } + ' "$index_file" > "$temp_file" + else + echo "No older patches to skip (only one existing patch)" + + # Insert new entry without skips (only replaces) + awk -v entry="$ENTRY_NAME" -v replaces="$REPLACES_TARGET" ' + /^ - name: / && !inserted && in_channel { + print " - name: " entry + print " replaces: " replaces + inserted = 1 + } + /^schema: olm.channel$/ { in_channel = 1 } + /^schema: / && !/^schema: olm.channel$/ { in_channel = 0 } + { print } + ' "$index_file" > "$temp_file" + fi + else + echo "ERROR: USE_SKIPS=true but no existing patches found for base $BASE_VERSION" + echo "This should not happen - existence check should have prevented this" + exit 1 + fi + else + # No skips - either clean version or first patch + # For clean version: replaces latest from previous base + # For first patch: replaces clean version of same base + + if [ "$IS_CLEAN_VERSION" = true ]; then + # Clean version - replaces latest version overall (from any base) + PREV_VERSION=$(grep -A 5 "schema: olm.channel" "$index_file" | \ + grep "name: multiarch-tuning-operator.v" | head -1 | \ + sed 's/.*name: multiarch-tuning-operator.v//' | sed 's/^[[:space:]]*//' || true) + + if [ -n "$PREV_VERSION" ]; then + echo "Clean version - replaces latest version: $PREV_VERSION" + + # Insert new entry + awk -v entry="$ENTRY_NAME" -v prev="$PREV_VERSION" ' + /^ - name: / && !inserted && in_channel { + print " - name: " entry + print " replaces: multiarch-tuning-operator.v" prev + inserted = 1 + } + /^schema: olm.channel$/ { in_channel = 1 } + /^schema: / && !/^schema: olm.channel$/ { in_channel = 0 } + { print } + ' "$index_file" > "$temp_file" + else + echo "No previous version found - adding as initial version" + + # Insert new entry without replaces + awk -v entry="$ENTRY_NAME" ' + /^ - name: / && !inserted && in_channel { + print " - name: " entry + inserted = 1 + } + /^schema: olm.channel$/ { in_channel = 1 } + /^schema: / && !/^schema: olm.channel$/ { in_channel = 0 } + { print } + ' "$index_file" > "$temp_file" + fi + else + # First patch for this base - replaces clean version + echo "First patch for base $BASE_VERSION - replaces clean version" + + # Insert new entry + awk -v entry="$ENTRY_NAME" -v base="$BASE_VERSION" ' + /^ - name: / && !inserted && in_channel { + print " - name: " entry + print " replaces: multiarch-tuning-operator.v" base + inserted = 1 + } + /^schema: olm.channel$/ { in_channel = 1 } + /^schema: / && !/^schema: olm.channel$/ { in_channel = 0 } + { print } + ' "$index_file" > "$temp_file" + fi + fi + + # Replace original file + mv "$temp_file" "$index_file" + echo "✅ Updated $index_file" + done + + echo "Channel graph update complete" + + # Step 7: Commit channel graph updates + - name: commit-channel-updates + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + VERSION="$(params.VERSION)" + + # Configure git + git config user.name "Konflux Release Bot" + git config user.email "konflux-release@redhat.com" + + # Check if there are uncommitted changes (channel graph updates) + if ! git diff --quiet fbc-v*/catalog/multiarch-tuning-operator/index.yaml; then + echo "Committing channel graph updates..." + + git add fbc-v*/catalog/multiarch-tuning-operator/index.yaml + + git commit -m "Add ${VERSION} to channel graph + + Added version entry to upgrade channel graph. + + Version: ${VERSION} + + Co-Authored-By: Konflux Release Bot " + + echo "✅ Channel graph committed" + git log -1 --oneline + else + echo "No changes to channel graph - skipping commit" + fi + + # Step 8: Show changes for verification + - name: verify-changes + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + echo "=== Changes made to FBC catalogs ===" + + git status + echo "" + echo "=== Diff of changes ===" + git diff --stat + + # Step 9: Push branch and create/update PR + - name: push-and-create-pr + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + env: + - name: APP_ID + valueFrom: + secretKeyRef: + name: github-app-credentials + key: app-id + optional: true + - name: INSTALLATION_ID + valueFrom: + secretKeyRef: + name: github-app-credentials + key: installation-id + optional: true + - name: PRIVATE_KEY + valueFrom: + secretKeyRef: + name: github-app-credentials + key: private-key + optional: true + script: | + #!/bin/bash + set -euo pipefail + + VERSION="$(params.VERSION)" + FBC_BRANCH="$(params.FBC_BRANCH)" + + # Install gh CLI to user directory + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + + echo "Downloading gh CLI..." + GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | sed -E 's/.*"v([^"]+)".*/\1/') + curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o /tmp/gh.tar.gz + tar -xzf /tmp/gh.tar.gz -C /tmp + mv /tmp/gh_${GH_VERSION}_linux_amd64/bin/gh /tmp/tools/ + chmod +x /tmp/tools/gh + + # Generate GitHub App installation token if credentials are available + if [ -n "${APP_ID:-}" ] && [ -n "${INSTALLATION_ID:-}" ] && [ -n "${PRIVATE_KEY:-}" ]; then + echo "Generating GitHub App installation token..." + + # Install jq if not already available + if ! command -v jq &> /dev/null; then + echo "Installing jq..." + JQ_VERSION=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + curl -L "https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64" -o /tmp/tools/jq + chmod +x /tmp/tools/jq + fi + + # Generate JWT + # Header + HEADER='{"alg":"RS256","typ":"JWT"}' + HEADER_B64=$(echo -n "$HEADER" | base64 -w0 | tr '+/' '-_' | tr -d '=') + + # Payload (expires in 10 minutes) + NOW=$(date +%s) + EXPIRES=$((NOW + 600)) + PAYLOAD="{\"iat\":$NOW,\"exp\":$EXPIRES,\"iss\":\"$APP_ID\"}" + PAYLOAD_B64=$(echo -n "$PAYLOAD" | base64 -w0 | tr '+/' '-_' | tr -d '=') + + # Signature + SIGNATURE_INPUT="${HEADER_B64}.${PAYLOAD_B64}" + echo -n "$PRIVATE_KEY" > /tmp/private-key.pem + SIGNATURE=$(echo -n "$SIGNATURE_INPUT" | openssl dgst -sha256 -sign /tmp/private-key.pem | base64 -w0 | tr '+/' '-_' | tr -d '=') + rm -f /tmp/private-key.pem + + JWT="${SIGNATURE_INPUT}.${SIGNATURE}" + + # Exchange JWT for installation access token + echo "Exchanging JWT for installation token..." + RESPONSE=$(curl -s -X POST \ + -H "Authorization: Bearer $JWT" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens") + + GITHUB_TOKEN=$(echo "$RESPONSE" | jq -r '.token') + + if [ "$GITHUB_TOKEN" = "null" ] || [ -z "$GITHUB_TOKEN" ]; then + echo "❌ ERROR: Failed to generate installation token" + echo "Response: $RESPONSE" + exit 1 + fi + + echo "✅ Generated installation token successfully" + # Export for gh CLI (which uses GH_TOKEN instead of GITHUB_TOKEN) + export GH_TOKEN="$GITHUB_TOKEN" + else + echo "❌ ERROR: GitHub App credentials not found" + echo "Cannot push branch or create PR without GitHub authentication" + exit 1 + fi + + # Configure git + git config user.name "Konflux Release Bot" + git config user.email "konflux-release@redhat.com" + + # Configure git authentication + echo "Configuring GitHub authentication..." + git remote set-url origin "https://oauth2:${GITHUB_TOKEN}@github.com/openshift/multiarch-tuning-operator.git" + + # Use consistent branch name for all FBC updates + PR_BRANCH="auto-fbc-update" + + # Save current commit (includes our index and channel commits) + CURRENT_COMMIT=$(git rev-parse HEAD) + echo "Current HEAD: $CURRENT_COMMIT" + + # Check if branch exists locally or remotely and create/update it + if git ls-remote --heads origin "$PR_BRANCH" | grep -q "$PR_BRANCH"; then + echo "Branch $PR_BRANCH exists on remote, fetching..." + git fetch origin "$PR_BRANCH" + git checkout -B "$PR_BRANCH" "$CURRENT_COMMIT" + else + echo "Creating new branch $PR_BRANCH..." + git checkout -b "$PR_BRANCH" + fi + + # Show what we're about to push + echo "Branch $PR_BRANCH is now at:" + git log --oneline -5 + + # Always force push the branch to keep it updated + echo "Pushing branch $PR_BRANCH (force push)..." + git push --force origin "$PR_BRANCH" + + # Check if PR already exists for this branch + EXISTING_PR=$(gh pr list --head "$PR_BRANCH" --json number,url --jq '.[0].url' 2>/dev/null || echo "") + + if [ -n "$EXISTING_PR" ]; then + echo "✅ Updated existing pull request: $EXISTING_PR" + PR_URL="$EXISTING_PR" + else + echo "Creating new pull request..." + PR_URL=$(gh pr create \ + --title "chore: Update FBC catalogs for ${VERSION}" \ + --body "## Automated FBC Catalog Update + + This PR was automatically generated by the Konflux release pipeline. + + **Version:** \`${VERSION}\` + **Base Branch:** \`${FBC_BRANCH}\` + + ### Commits + + This PR includes separate commits for: + 1. Bundle metadata update (always updated with latest image digest) + 2. Channel graph update (only if version is new) + + ### Verification + + - [ ] Bundle metadata is correct + - [ ] Channel graph properly ordered + - [ ] \`opm validate\` passes for all catalogs + + 🤖 Generated by Konflux Release Pipeline" \ + --base "${FBC_BRANCH}" \ + --head "$PR_BRANCH" | grep -oP 'https://[^\s]+' || echo "") + + if [ -n "$PR_URL" ]; then + echo "✅ Pull request created: $PR_URL" + else + echo "⚠️ PR creation completed but URL not captured" + PR_URL="unknown" + fi + fi + + # Save PR_URL result + echo -n "$PR_URL" > $(results.PR_URL.path) + params: + - name: BUNDLE_IMAGE + value: $(tasks.extract-snapshot-name.results.BUNDLE_IMAGE) + - name: VERSION + value: $(tasks.extract-snapshot-name.results.VERSION) + - name: FBC_BRANCH + value: $(params.fbc-branch) + - name: GIT_URL + value: $(params.git-url) \ No newline at end of file diff --git a/.tekton/multi-arch-build-pipeline.yaml b/.tekton/multi-arch-build-pipeline.yaml index 91931b28f..fea3d7198 100644 --- a/.tekton/multi-arch-build-pipeline.yaml +++ b/.tekton/multi-arch-build-pipeline.yaml @@ -171,6 +171,47 @@ spec: workspace: git-auth - name: netrc workspace: netrc + - name: run-script + params: + - name: ociStorage + value: $(params.output-image).script + - name: ociArtifactExpiresAfter + value: $(params.image-expires-after) + - name: SCRIPT_RUNNER_IMAGE + value: registry.access.redhat.com/ubi9/go-toolset:1.25 + - name: SCRIPT + value: | + COMMIT_MSG=$(git log -1 --format=%s) + + # Check if this commit requests clean version + if [[ "$COMMIT_MSG" =~ \[clean-version\] ]]; then + VERSION=$(grep -E "^VERSION \?=" Makefile | awk '{print $3}') + echo "Clean version requested: $VERSION" + export VERSION + else + # Normal commit - generate commit-based version + BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | awk '{print $3}') + COMMIT_SHA=$(tasks.clone-repository.results.commit) + export VERSION="${BASE_VERSION}+${COMMIT_SHA:0:7}" + echo "Generated commit-based version: $VERSION" + fi + + exec ./hack/bump-version.sh + - name: HERMETIC + value: "true" + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + runAfter: + - prefetch-dependencies + taskRef: + params: + - name: name + value: run-script-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-run-script-oci-ta:0.1@sha256:0e13a74cc02c945e7119ecd4cc0c9148e7591b50f87e415b212154caad0479c0 + - name: kind + value: task + resolver: bundles - matrix: params: - name: PLATFORM @@ -208,13 +249,16 @@ spec: - name: NO_PROXY value: $(tasks.init.results.no-proxy) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + - name: ADDITIONAL_BASE_IMAGES + value: + - $(tasks.run-script.results.SCRIPT_RUNNER_IMAGE_REFERENCE) - name: IMAGE_APPEND_PLATFORM value: "true" runAfter: - - prefetch-dependencies + - run-script taskRef: params: - name: name @@ -254,7 +298,7 @@ spec: - name: BINARY_IMAGE_DIGEST value: $(tasks.build-image-index.results.IMAGE_DIGEST) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) runAfter: @@ -355,7 +399,7 @@ spec: - name: image-url value: $(tasks.build-image-index.results.IMAGE_URL) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) - name: TARGET_DIRS @@ -430,7 +474,7 @@ spec: - name: BUILD_ARGS_FILE value: $(params.build-args-file) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) - name: TARGET_DIRS @@ -480,7 +524,7 @@ spec: - name: image-url value: $(tasks.build-image-index.results.IMAGE_URL) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) - name: TARGET_DIRS @@ -509,7 +553,7 @@ spec: - name: image-url value: $(tasks.build-image-index.results.IMAGE_URL) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) - name: CACHI2_ARTIFACT value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) - name: TARGET_DIRS @@ -559,7 +603,7 @@ spec: - name: CONTEXT value: $(params.path-context) - name: SOURCE_ARTIFACT - value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + value: $(tasks.run-script.results.SCRIPT_ARTIFACT) runAfter: - build-image-index taskRef: diff --git a/.tekton/single-arch-build-pipeline.yaml b/.tekton/single-arch-build-pipeline.yaml index 857903ee0..7822ca92f 100644 --- a/.tekton/single-arch-build-pipeline.yaml +++ b/.tekton/single-arch-build-pipeline.yaml @@ -191,7 +191,21 @@ spec: value: registry.access.redhat.com/ubi9/go-toolset:1.25 - name: SCRIPT value: | - export COMMIT_SHA="$(tasks.clone-repository.results.commit)" + COMMIT_MSG=$(git log -1 --format=%s) + + # Check if this commit requests clean version + if [[ "$COMMIT_MSG" =~ \[clean-version\] ]]; then + VERSION=$(grep -E "^VERSION \?=" Makefile | awk '{print $3}') + echo "Clean version requested: $VERSION" + export VERSION + else + # Normal commit - generate commit-based version + BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | awk '{print $3}') + COMMIT_SHA=$(tasks.clone-repository.results.commit) + export VERSION="${BASE_VERSION}+${COMMIT_SHA:0:7}" + echo "Generated commit-based version: $VERSION" + fi + exec ./hack/bump-version.sh - name: HERMETIC value: "true" diff --git a/.tekton/snapshot-validation-pipeline.yaml b/.tekton/snapshot-validation-pipeline.yaml new file mode 100644 index 000000000..bb07e82f8 --- /dev/null +++ b/.tekton/snapshot-validation-pipeline.yaml @@ -0,0 +1,248 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: snapshot-validation-pipeline + annotations: + description: | + Pre-release validation pipeline that checks snapshot consistency. + Validates that the operator image referenced in the bundle CSV matches + an image in the snapshot. +spec: + description: | + This pipeline runs before a release is created to validate snapshot integrity. + It extracts the bundle image from the snapshot, inspects the CSV, and verifies + that the internal operator image SHA matches one of the snapshot component images. + params: + - name: snapshot + description: JSON string of the Snapshot to validate + type: string + - name: release + description: JSON string of the Release (optional for pre-release) + type: string + default: "{}" + + results: + - name: VALIDATION_RESULT + description: Result of snapshot validation (PASS or FAIL) + value: $(tasks.validate-snapshot.results.VALIDATION_RESULT) + - name: INTERNAL_IMAGE + description: Internal operator image found in bundle CSV + value: $(tasks.validate-snapshot.results.INTERNAL_IMAGE) + + tasks: + - name: validate-snapshot + taskSpec: + params: + - name: snapshot + results: + - name: VALIDATION_RESULT + description: PASS or FAIL + - name: INTERNAL_IMAGE + description: Internal operator image + steps: + - name: extract-and-validate + image: quay.io/konflux-ci/appstudio-utils:latest + script: | + #!/bin/bash + set -euo pipefail + + echo "==========================================" + echo "Snapshot Validation Pipeline" + echo "==========================================" + + # Install tools to user directory + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + + echo "Installing jq (latest)..." + JQ_VERSION=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + echo "Latest jq version: $JQ_VERSION" + curl -L "https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64" -o /tmp/tools/jq + chmod +x /tmp/tools/jq + + echo "Installing yq (latest)..." + YQ_VERSION=$(curl -s https://api.github.com/repos/mikefarah/yq/releases/latest | jq -r '.tag_name') + echo "Latest yq version: $YQ_VERSION" + curl -L "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o /tmp/tools/yq + chmod +x /tmp/tools/yq + yq --version + + echo "Installing skopeo (latest)..." + SKOPEO_VERSION=$(curl -s https://api.github.com/repos/lework/skopeo-binary/releases/latest | jq -r '.tag_name') + echo "Latest skopeo version: $SKOPEO_VERSION" + curl -L "https://github.com/lework/skopeo-binary/releases/download/${SKOPEO_VERSION}/skopeo-linux-amd64" -o /tmp/tools/skopeo + chmod +x /tmp/tools/skopeo + skopeo --version + + # Fetch snapshot from Kubernetes + echo "Fetching snapshot..." + SNAPSHOT_REF='$(params.snapshot)' + echo "Snapshot reference: $SNAPSHOT_REF" + + # Parse namespace/name from reference + if [[ "$SNAPSHOT_REF" == *"/"* ]]; then + SNAPSHOT_NAMESPACE="${SNAPSHOT_REF%/*}" + SNAPSHOT_NAME="${SNAPSHOT_REF#*/}" + else + SNAPSHOT_NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + SNAPSHOT_NAME="$SNAPSHOT_REF" + fi + + echo "Fetching snapshot $SNAPSHOT_NAME from namespace $SNAPSHOT_NAMESPACE..." + SNAPSHOT=$(oc get snapshot "$SNAPSHOT_NAME" -n "$SNAPSHOT_NAMESPACE" -o json) + + echo "$SNAPSHOT" | jq '.' > /tmp/snapshot.json + echo "Validating snapshot: $SNAPSHOT_NAME" + + # Extract container images + echo "" + echo "Extracting container images from snapshot..." + IMAGES=$(echo "$SNAPSHOT" | jq -r '.spec.components[].containerImage') + + echo "Container images in snapshot:" + echo "$IMAGES" + + # Find bundle image by component name (any component with "bundle" in name) + echo "" + echo "Extracting bundle image from snapshot components..." + BUNDLE_IMAGE=$(echo "$SNAPSHOT" | jq -r '.spec.components[] | select(.name | contains("bundle")) | .containerImage' | head -n1) + + if [ -z "$BUNDLE_IMAGE" ] || [ "$BUNDLE_IMAGE" = "null" ]; then + echo "❌ ERROR: No bundle image found in snapshot components." + echo " Expected: component with 'bundle' in name" + echo " Available components:" + echo "$SNAPSHOT" | jq -r '.spec.components[].name' | sed 's/^/ - /' + echo "FAIL" > $(results.VALIDATION_RESULT.path) + echo "none" > $(results.INTERNAL_IMAGE.path) + exit 1 + fi + + echo "" + echo "Bundle image: $BUNDLE_IMAGE" + + # Copy the bundle image using skopeo (doesn't require privileged mode) + echo "" + echo "Copying bundle image..." + BUNDLE_OCI="/tmp/bundle_oci" + BUNDLE_DIR="/tmp/operator_bundle" + mkdir -p "$BUNDLE_OCI" "$BUNDLE_DIR" + + # Use skopeo to copy image to OCI format + skopeo copy "docker://$BUNDLE_IMAGE" "oci:$BUNDLE_OCI" + + echo "Bundle image copied to OCI format" + + # Extract bundle layers using skopeo and tar + echo "Extracting bundle filesystem..." + # Get the image digest from OCI layout + IMAGE_DIGEST=$(jq -r '.manifests[0].digest' "$BUNDLE_OCI/index.json" | cut -d: -f2) + + # Find and extract all layer blobs + MANIFEST_FILE="$BUNDLE_OCI/blobs/sha256/$IMAGE_DIGEST" + LAYER_DIGESTS=$(jq -r '.layers[].digest' "$MANIFEST_FILE" | cut -d: -f2) + + for digest in $LAYER_DIGESTS; do + echo " Extracting layer: $digest" + tar -xzf "$BUNDLE_OCI/blobs/sha256/$digest" -C "$BUNDLE_DIR" 2>/dev/null || \ + tar -xf "$BUNDLE_OCI/blobs/sha256/$digest" -C "$BUNDLE_DIR" 2>/dev/null || true + done + + # Find the CSV file + CSV_FILE=$(find "$BUNDLE_DIR" -name "*.clusterserviceversion.yaml" | head -n 1) + + if [ -z "$CSV_FILE" ]; then + echo "❌ ERROR: ClusterServiceVersion YAML not found in bundle." + echo "FAIL" > $(results.VALIDATION_RESULT.path) + echo "none" > $(results.INTERNAL_IMAGE.path) + exit 1 + fi + + echo "" + echo "Found CSV file: $CSV_FILE" + + # Extract internal image from CSV annotations + INTERNAL_IMAGE=$(yq '.spec.install.spec.deployments[0].spec.template.metadata.annotations."multiarch.openshift.io/image"' "$CSV_FILE") + + if [ -z "$INTERNAL_IMAGE" ] || [ "$INTERNAL_IMAGE" = "null" ]; then + echo "❌ ERROR: Could not extract internal image from CSV annotations." + echo "Expected annotation: multiarch.openshift.io/image" + echo "FAIL" > $(results.VALIDATION_RESULT.path) + echo "none" > $(results.INTERNAL_IMAGE.path) + exit 1 + fi + + echo "" + echo "Internal operator image from CSV:" + echo " $INTERNAL_IMAGE" + + # Extract SHA from internal image + INTERNAL_SHA=$(echo "$INTERNAL_IMAGE" | grep -oP 'sha256:\K[a-f0-9]{64}' || true) + + if [ -z "$INTERNAL_SHA" ]; then + echo "❌ ERROR: Could not extract SHA256 from internal image." + echo "Image format should be: registry/repo@sha256:..." + echo "FAIL" > $(results.VALIDATION_RESULT.path) + echo "$INTERNAL_IMAGE" > $(results.INTERNAL_IMAGE.path) + exit 1 + fi + + echo "Internal image SHA256: $INTERNAL_SHA" + + # Extract SHAs from all snapshot images + echo "" + echo "Extracting SHAs from snapshot images..." + SNAPSHOT_SHAS=$(echo "$IMAGES" | grep -oP 'sha256:\K[a-f0-9]{64}' || true) + + echo "Snapshot component SHAs:" + for sha in $SNAPSHOT_SHAS; do + echo " $sha" + done + + # Compare internal SHA to snapshot SHAs + echo "" + echo "Validating internal image SHA against snapshot..." + MATCH_FOUND=0 + for sha in $SNAPSHOT_SHAS; do + if [ "$sha" = "$INTERNAL_SHA" ]; then + MATCH_FOUND=1 + break + fi + done + + # Report results + echo "" + echo "==========================================" + if [ $MATCH_FOUND -eq 1 ]; then + echo "✅ VALIDATION PASSED" + echo "==========================================" + echo "The internal operator image SHA matches a snapshot component." + echo "Snapshot is consistent and ready for release." + echo "PASS" > $(results.VALIDATION_RESULT.path) + echo "$INTERNAL_IMAGE" > $(results.INTERNAL_IMAGE.path) + else + echo "❌ VALIDATION FAILED" + echo "==========================================" + echo "The internal operator image SHA does NOT match any snapshot component." + echo "" + echo "This means the bundle references an operator image that is not" + echo "included in this snapshot. The snapshot is inconsistent." + echo "" + echo "Expected SHA: $INTERNAL_SHA" + echo "Snapshot SHAs:" + for sha in $SNAPSHOT_SHAS; do + echo " $sha" + done + echo "" + echo "FAIL" > $(results.VALIDATION_RESULT.path) + echo "$INTERNAL_IMAGE" > $(results.INTERNAL_IMAGE.path) + exit 1 + fi + + # Cleanup + echo "" + echo "Cleaning up..." + rm -rf "$BUNDLE_OCI" "$BUNDLE_DIR" + echo "Done." + params: + - name: snapshot + value: $(params.snapshot) \ No newline at end of file diff --git a/.tekton/tag-release-commit-pipeline.yaml b/.tekton/tag-release-commit-pipeline.yaml new file mode 100644 index 000000000..598039ecf --- /dev/null +++ b/.tekton/tag-release-commit-pipeline.yaml @@ -0,0 +1,334 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: tag-release-commit-pipeline + annotations: + description: | + Pipeline to tag the release commit with the version from the release. + Extracts version from release name and creates/pushes a git tag. +spec: + description: | + This pipeline tags the source commit referenced in a release with the appropriate version tag. + It extracts the commit SHA from the release metadata and the version from the release name. + params: + - name: snapshot + description: Snapshot reference (namespace/name format) + type: string + - name: release + description: Release reference (namespace/name format) + type: string + - name: git-url + description: Git repository URL + type: string + default: "https://github.com/openshift/multiarch-tuning-operator.git" + - name: git-branch + description: Git branch that was built + type: string + default: "v1.x" + + results: + - name: TAG_NAME + description: The git tag that was created + value: $(tasks.tag-release-commit.results.TAG_NAME) + - name: COMMIT_SHA + description: The commit that was tagged + value: $(tasks.extract-release-commit.results.COMMIT_SHA) + + tasks: + - name: extract-release-commit + taskSpec: + params: + - name: release + results: + - name: COMMIT_SHA + description: The commit SHA from the release + - name: RELEASE_NAME + description: The release name + - name: GIT_BRANCH + description: The git branch from the release + steps: + - name: parse-commit + image: quay.io/konflux-ci/appstudio-utils:latest + script: | + #!/bin/bash + set -euo pipefail + + # Install jq + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + echo "Installing jq (latest)..." + JQ_VERSION=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + echo "Latest jq version: $JQ_VERSION" + curl -L "https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64" -o /tmp/tools/jq + chmod +x /tmp/tools/jq + + RELEASE_REF='$(params.release)' + echo "Release reference: $RELEASE_REF" + + # Parse namespace/name from reference (same as snapshot handling) + if [[ "$RELEASE_REF" == *"/"* ]]; then + RELEASE_NAMESPACE="${RELEASE_REF%/*}" + RELEASE_NAME="${RELEASE_REF#*/}" + else + RELEASE_NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + RELEASE_NAME="$RELEASE_REF" + fi + + echo "Fetching release $RELEASE_NAME from namespace $RELEASE_NAMESPACE..." + + # Fetch the release using oc + RELEASE=$(oc get release "$RELEASE_NAME" -n "$RELEASE_NAMESPACE" -o json) + + # Parse release JSON + COMMIT_SHA=$(echo "$RELEASE" | jq -r '.metadata.labels."pac.test.appstudio.openshift.io/sha"') + GIT_BRANCH=$(echo "$RELEASE" | jq -r '.metadata.annotations."pac.test.appstudio.openshift.io/source-branch" // empty' | sed 's|^refs/heads/||') + + # Default to git-branch parameter if not found in release + if [ -z "$GIT_BRANCH" ] || [ "$GIT_BRANCH" = "null" ]; then + GIT_BRANCH="v1.x" + fi + + echo "Release: $RELEASE_NAME" + echo "Commit SHA: $COMMIT_SHA" + echo "Git branch: $GIT_BRANCH" + + echo -n "$COMMIT_SHA" > $(results.COMMIT_SHA.path) + echo -n "$RELEASE_NAME" > $(results.RELEASE_NAME.path) + echo -n "$GIT_BRANCH" > $(results.GIT_BRANCH.path) + params: + - name: release + value: $(params.release) + + - name: extract-version + runAfter: + - extract-release-commit + taskSpec: + params: + - name: release-name + results: + - name: VERSION + description: Version extracted from release name + steps: + - name: parse-version + image: quay.io/konflux-ci/appstudio-utils:latest + script: | + #!/bin/bash + set -euo pipefail + + RELEASE_NAME='$(params.release-name)' + + # Extract version from release name (e.g., release-test-1-3-0-xyz -> v1.3.0) + VERSION=$(echo "$RELEASE_NAME" | grep -oP '\d+-\d+-\d+' | tr '-' '.' | sed 's/^/v/') + + if [ -z "$VERSION" ] || [ "$VERSION" = "v" ]; then + echo "❌ ERROR: Unable to extract version from release name: $RELEASE_NAME" + echo " Expected format: release-X-Y-Z-suffix" + exit 1 + fi + + # Append test suffix with timestamp to avoid conflicts + TIMESTAMP=$(date +%s) + VERSION="${VERSION}-test-${TIMESTAMP}" + + echo "Extracted version: $VERSION" + echo -n "$VERSION" > $(results.VERSION.path) + params: + - name: release-name + value: $(tasks.extract-release-commit.results.RELEASE_NAME) + + - name: tag-release-commit + runAfter: + - extract-version + taskSpec: + params: + - name: git-url + - name: git-branch + - name: commit-sha + - name: version + results: + - name: TAG_NAME + description: The tag that was created/pushed + steps: + - name: create-and-push-tag + image: quay.io/konflux-ci/appstudio-utils:latest + env: + - name: APP_ID + valueFrom: + secretKeyRef: + name: github-app-credentials + key: app-id + optional: true + - name: INSTALLATION_ID + valueFrom: + secretKeyRef: + name: github-app-credentials + key: installation-id + optional: true + - name: PRIVATE_KEY + valueFrom: + secretKeyRef: + name: github-app-credentials + key: private-key + optional: true + script: | + #!/bin/bash + set -euo pipefail + + GIT_URL="$(params.git-url)" + GIT_BRANCH="$(params.git-branch)" + COMMIT_SHA="$(params.commit-sha)" + VERSION="$(params.version)" + + echo "==========================================" + echo "Tagging Release Commit" + echo "==========================================" + echo "Repository: $GIT_URL" + echo "Branch: $GIT_BRANCH" + echo "Commit: $COMMIT_SHA" + echo "Version: $VERSION" + echo "" + + # Generate GitHub App installation token if credentials are available + if [ -n "${APP_ID:-}" ] && [ -n "${INSTALLATION_ID:-}" ] && [ -n "${PRIVATE_KEY:-}" ]; then + echo "Generating GitHub App installation token..." + + # Install required tools + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + + # Install jq if not already available + if ! command -v jq &> /dev/null; then + echo "Installing jq..." + JQ_VERSION=$(curl -s https://api.github.com/repos/jqlang/jq/releases/latest | grep -oP '"tag_name": "\K[^"]+') + curl -L "https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64" -o /tmp/tools/jq + chmod +x /tmp/tools/jq + fi + + # Generate JWT + # Header + HEADER='{"alg":"RS256","typ":"JWT"}' + HEADER_B64=$(echo -n "$HEADER" | base64 -w0 | tr '+/' '-_' | tr -d '=') + + # Payload (expires in 10 minutes) + NOW=$(date +%s) + EXPIRES=$((NOW + 600)) + PAYLOAD="{\"iat\":$NOW,\"exp\":$EXPIRES,\"iss\":\"$APP_ID\"}" + PAYLOAD_B64=$(echo -n "$PAYLOAD" | base64 -w0 | tr '+/' '-_' | tr -d '=') + + # Signature + SIGNATURE_INPUT="${HEADER_B64}.${PAYLOAD_B64}" + echo -n "$PRIVATE_KEY" > /tmp/private-key.pem + SIGNATURE=$(echo -n "$SIGNATURE_INPUT" | openssl dgst -sha256 -sign /tmp/private-key.pem | base64 -w0 | tr '+/' '-_' | tr -d '=') + rm -f /tmp/private-key.pem + + JWT="${SIGNATURE_INPUT}.${SIGNATURE}" + + # Exchange JWT for installation access token + echo "Exchanging JWT for installation token..." + RESPONSE=$(curl -s -X POST \ + -H "Authorization: Bearer $JWT" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/app/installations/$INSTALLATION_ID/access_tokens") + + GITHUB_TOKEN=$(echo "$RESPONSE" | jq -r '.token') + + if [ "$GITHUB_TOKEN" = "null" ] || [ -z "$GITHUB_TOKEN" ]; then + echo "❌ ERROR: Failed to generate installation token" + echo "Response: $RESPONSE" + exit 1 + fi + + echo "✅ Generated installation token successfully" + # Export for gh CLI (which uses GH_TOKEN instead of GITHUB_TOKEN) + export GH_TOKEN="$GITHUB_TOKEN" + else + echo "⚠️ GitHub App credentials not found, skipping token generation" + GITHUB_TOKEN="" + fi + + # Clone the repository + REPO_DIR="/tmp/repo" + echo "Cloning repository (branch: $GIT_BRANCH)..." + + if [ -n "${GITHUB_TOKEN:-}" ]; then + echo "Using authenticated clone..." + git clone --branch "$GIT_BRANCH" --single-branch \ + "https://oauth2:${GITHUB_TOKEN}@github.com/openshift/multiarch-tuning-operator.git" \ + "$REPO_DIR" + else + echo "⚠️ Warning: GITHUB_TOKEN not set, cloning without authentication..." + git clone --branch "$GIT_BRANCH" --single-branch \ + "$GIT_URL" \ + "$REPO_DIR" + fi + + cd "$REPO_DIR" + + # Configure git + git config user.name "Konflux Release Bot" + git config user.email "konflux-release@redhat.com" + + # Configure authentication for push if token available + if [ -n "${GITHUB_TOKEN:-}" ]; then + git remote set-url origin "https://oauth2:${GITHUB_TOKEN}@github.com/openshift/multiarch-tuning-operator.git" + fi + + # Check if tag already exists + if git rev-parse "$VERSION" >/dev/null 2>&1; then + EXISTING_COMMIT=$(git rev-parse "$VERSION^{commit}") + if [ "$EXISTING_COMMIT" = "$COMMIT_SHA" ]; then + echo "✅ Tag $VERSION already exists on correct commit" + echo -n "$VERSION" > $(results.TAG_NAME.path) + + # Still try to push in case tag exists locally but not on remote + if [ -n "${GITHUB_TOKEN:-}" ]; then + echo "Ensuring tag is pushed to remote..." + git push origin "$VERSION" 2>&1 || echo "Tag may already exist on remote" + fi + exit 0 + else + echo "⚠️ Warning: Tag $VERSION already exists but on different commit:" + echo " Existing: $EXISTING_COMMIT" + echo " Expected: $COMMIT_SHA" + echo " Skipping tag creation to avoid conflicts" + echo -n "$VERSION" > $(results.TAG_NAME.path) + exit 0 + fi + fi + + # Create the tag + echo "Creating tag $VERSION on commit $COMMIT_SHA..." + git tag -a "$VERSION" "$COMMIT_SHA" -m "Release $VERSION + + Automated tag created by Konflux release pipeline. + + Release commit: $COMMIT_SHA + Source branch: $GIT_BRANCH" + + echo "✅ Tag created successfully" + + # Push the tag + if [ -n "${GITHUB_TOKEN:-}" ]; then + echo "Pushing tag to remote..." + git push origin "$VERSION" + echo "✅ Tag pushed successfully" + echo -n "$VERSION" > $(results.TAG_NAME.path) + else + echo "❌ ERROR: GITHUB_TOKEN not set" + echo "Cannot push tag without GitHub authentication" + echo "" + echo "To fix this, create the github-token secret:" + echo " oc create secret generic github-token \\" + echo " --from-literal=token= \\" + echo " -n " + exit 1 + fi + params: + - name: git-url + value: $(params.git-url) + - name: git-branch + value: $(tasks.extract-release-commit.results.GIT_BRANCH) + - name: commit-sha + value: $(tasks.extract-release-commit.results.COMMIT_SHA) + - name: version + value: $(tasks.extract-version.results.VERSION) \ No newline at end of file diff --git a/hack/bump-version-manual.sh b/hack/bump-version-manual.sh new file mode 100644 index 000000000..b1a6f3c94 --- /dev/null +++ b/hack/bump-version-manual.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -e + +# Accept version as first argument, fall back to VERSION env var, or show usage +if [ -n "$1" ]; then + VERSION="$1" +elif [ -z "$VERSION" ]; then + echo "Usage: $0 " + echo " or set VERSION environment variable" + echo "Example: $0 1.2.1" + exit 1 +fi + +echo "Bumping version to: $VERSION" + +# Extract major.minor version for CPE label (e.g., 1.3.4 -> 1.3) +MAJOR_MINOR=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/') +echo "CPE version (major.minor): $MAJOR_MINOR" + +yq -i ".spec.version=\"${VERSION}\"" config/manifests/bases/multiarch-tuning-operator.clusterserviceversion.yaml +yq -i ".metadata.name=\"multiarch-tuning-operator.v${VERSION}\"" config/manifests/bases/multiarch-tuning-operator.clusterserviceversion.yaml +yq -i ".spec.startingCSV=\"multiarch-tuning-operator.v${VERSION}\"" deploy/base/operators.coreos.com/subscriptions/openshift-multiarch-tuning-operator/subscription.yaml +yq eval-all -i "(select(.schema==\"olm.channel\").entries[0].name)=\"multiarch-tuning-operator.v${VERSION}\"" index.base.yaml + + +if [[ "$(uname)" == "Darwin" ]]; then + # macOS BSD sed + sed -i '' "s/^LABEL release=.*/LABEL release=\"${VERSION}\"/" Dockerfile + sed -i '' "s/^LABEL version=.*/LABEL version=\"${VERSION}\"/" Dockerfile + sed -i '' "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" Dockerfile + sed -i '' "s/^LABEL release=.*/LABEL release=\"${VERSION}\"/" konflux.Dockerfile + sed -i '' "s/^LABEL version=.*/LABEL version=\"${VERSION}\"/" konflux.Dockerfile + sed -i '' "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" konflux.Dockerfile + sed -i '' "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" bundle.Dockerfile + sed -i '' "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" bundle.konflux.Dockerfile + sed -i '' "s/^VERSION ?= .*/VERSION ?= ${VERSION}/" Makefile +else + # Linux GNU sed + sed -i "s/^LABEL release=.*/LABEL release=\"${VERSION}\"/" Dockerfile + sed -i "s/^LABEL version=.*/LABEL version=\"${VERSION}\"/" Dockerfile + sed -i "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" Dockerfile + sed -i "s/^LABEL release=.*/LABEL release=\"${VERSION}\"/" konflux.Dockerfile + sed -i "s/^LABEL version=.*/LABEL version=\"${VERSION}\"/" konflux.Dockerfile + sed -i "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" konflux.Dockerfile + sed -i "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" bundle.Dockerfile + sed -i "s/^LABEL cpe=.*/LABEL cpe=\"cpe:\/a:redhat:multiarch_tuning_operator:${MAJOR_MINOR}::el9\"/" bundle.konflux.Dockerfile + sed -i "s/^VERSION ?= .*/VERSION ?= ${VERSION}/" Makefile +fi +echo "make bundle" +make bundle \ No newline at end of file diff --git a/hack/bump-version.sh b/hack/bump-version.sh index 669c2d779..0a51009ca 100755 --- a/hack/bump-version.sh +++ b/hack/bump-version.sh @@ -38,7 +38,7 @@ else # If we have a commit SHA, append it to the version if [ -n "$COMMIT_SHA_VALUE" ]; then COMMIT_SHORT="${COMMIT_SHA_VALUE:0:7}" - VERSION="${BASE_VERSION}-${COMMIT_SHORT}" + VERSION="${BASE_VERSION}+${COMMIT_SHORT}" echo "Generated version: $VERSION (from Makefile: $BASE_VERSION + commit: $COMMIT_SHORT)" else VERSION="$BASE_VERSION" @@ -48,7 +48,7 @@ fi echo "Bumping version to: $VERSION" -# Extract major.minor version for CPE label (e.g., 1.3.4 -> 1.3, 1.3.0-abc1234 -> 1.3) +# Extract major.minor version for CPE label (e.g., 1.3.4 -> 1.3, 1.3.0+abc1234 -> 1.3) MAJOR_MINOR=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/') echo "CPE version (major.minor): $MAJOR_MINOR" From 43693cdce8e07023fd801137b261c9157e6e3aa4 Mon Sep 17 00:00:00 2001 From: AnnaZivkovic Date: Tue, 19 May 2026 11:38:24 -0700 Subject: [PATCH 2/2] Update Cel in operator on-push to run every time the bundle on-push runs --- .tekton/multiarch-tuning-operator-pull-request.yaml | 2 +- .tekton/multiarch-tuning-operator-push.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.tekton/multiarch-tuning-operator-pull-request.yaml b/.tekton/multiarch-tuning-operator-pull-request.yaml index a3ad1d8d1..56f7af95b 100644 --- a/.tekton/multiarch-tuning-operator-pull-request.yaml +++ b/.tekton/multiarch-tuning-operator-pull-request.yaml @@ -8,7 +8,7 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/max-keep-runs: "3" pipelinesascode.tekton.dev/on-cel-expression: | - event == "pull_request" && target_branch == "main" && (".tekton/***".pathChanged() || "api/***".pathChanged() || "internal/controller/***".pathChanged() || "pkg/***".pathChanged() || "test/***".pathChanged() || "konflux.Dockerfile".pathChanged() || "go.mod".pathChanged() || "cmd/***".pathChanged() || "go.sum".pathChanged() || "trigger-konflux-builds.txt".pathChanged() ) + event == "pull_request" && target_branch == "main" && (".tekton/***".pathChanged() || "api/***".pathChanged() || "internal/***".pathChanged() || "pkg/***".pathChanged() || "test/***".pathChanged() || "konflux.Dockerfile".pathChanged() || "go.mod".pathChanged() || "cmd/***".pathChanged() || "go.sum".pathChanged() || "trigger-konflux-builds.txt".pathChanged() ) creationTimestamp: null labels: appstudio.openshift.io/application: multiarch-tuning-operator diff --git a/.tekton/multiarch-tuning-operator-push.yaml b/.tekton/multiarch-tuning-operator-push.yaml index dc1723bae..1bf05eca7 100644 --- a/.tekton/multiarch-tuning-operator-push.yaml +++ b/.tekton/multiarch-tuning-operator-push.yaml @@ -7,10 +7,10 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/max-keep-runs: "3" pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch - == "main" && (".tekton/***".pathChanged() || "api/***".pathChanged() || "internal/controller/***".pathChanged() + == "main" && (".tekton/***".pathChanged() || "api/***".pathChanged() || "internal/***".pathChanged() || "pkg/***".pathChanged() || "test/***".pathChanged() || "konflux.Dockerfile".pathChanged() - || "go.mod".pathChanged() || "cmd/***".pathChanged() || "go.sum".pathChanged() - || "trigger-konflux-builds.txt".pathChanged() ) + || "go.mod".pathChanged() || "cmd/***".pathChanged() || "go.sum".pathChanged() || "bundle/***".pathChanged() || + "bundle.konflux.Dockerfile".pathChanged() || "trigger-konflux-builds.txt".pathChanged() ) creationTimestamp: null labels: appstudio.openshift.io/application: multiarch-tuning-operator