diff --git a/.tekton/fbc-update-final-pipeline.yaml b/.tekton/fbc-update-final-pipeline.yaml new file mode 100644 index 000000000..4ead27651 --- /dev/null +++ b/.tekton/fbc-update-final-pipeline.yaml @@ -0,0 +1,555 @@ +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) + + # Try to extract version from image tag or component + VERSION=$(echo "$BUNDLE_IMAGE" | grep -oP 'v\d+\.\d+\.\d+' || echo "") + if [ -z "$VERSION" ]; then + # Try from snapshot component source + VERSION=$(echo "$SNAPSHOT" | jq -r '.spec.components[] | select(.name | contains("bundle")) | .source.git.revision' | head -n1 | grep -oP 'v\d+\.\d+\.\d+' || echo "") + fi + if [ -z "$VERSION" ]; then + # Try from release name (format: release--X-Y-Z-) + echo "Fetching release for version extraction..." + RELEASE_REF='$(params.release)' + 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 + RELEASE=$(oc get release "$RELEASE_NAME" -n "$RELEASE_NAMESPACE" -o json) + RELEASE_NAME=$(echo "$RELEASE" | jq -r '.metadata.name') + # Extract X-Y-Z from release name and convert to vX.Y.Z + VERSION=$(echo "$RELEASE_NAME" | grep -oP '\d+-\d+-\d+' | tr '-' '.' | sed 's/^/v/') + fi + if [ -z "$VERSION" ]; then + echo "❌ ERROR: Unable to determine release version from bundle image, snapshot, or release name" >&2 + echo " Bundle image: $BUNDLE_IMAGE" >&2 + echo " Snapshot source: $(echo "$SNAPSHOT" | jq -r '.spec.components[] | select(.name | contains("bundle")) | .source.git.revision // "none"' | head -n1)" >&2 + echo " Release name: $RELEASE_NAME" >&2 + exit 1 + fi + echo "Version: $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 "Will skip channel graph update but will update bundle metadata" + touch /workspace/skip-channel-update + else + echo "Version $ENTRY_NAME not found in channel graph - will add it" + fi + + # Step 3: Remove existing bundle metadata for this version + - name: remove-existing-bundle + image: quay.io/konflux-ci/appstudio-utils:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + VERSION="$(params.VERSION)" + VERSION_NO_V="${VERSION#v}" + BUNDLE_NAME="multiarch-tuning-operator.v${VERSION_NO_V}" + + echo "Removing existing bundle metadata for $BUNDLE_NAME (if it exists)..." + + # Install yq for YAML manipulation + mkdir -p /tmp/tools + export PATH="/tmp/tools:$PATH" + 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 /tmp/tools/yq + chmod +x /tmp/tools/yq + + # Remove bundle entries from each catalog + for catalog_file in fbc-v*/catalog/multiarch-tuning-operator/index.yaml; do + echo "Processing $catalog_file..." + + # Remove the olm.bundle entry for this version + yq eval "del(.[] | select(.schema == \"olm.bundle\" and .name == \"$BUNDLE_NAME\"))" -i "$catalog_file" + + echo "Removed bundle entry from $catalog_file (if it existed)" + done + + echo "Bundle cleanup complete" + + # 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 + + BUNDLE_IMAGE="$(params.BUNDLE_IMAGE)" + + # 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 opm (latest version)..." + # Get latest opm version from GitHub releases + OPM_VERSION=$(curl -s https://api.github.com/repos/operator-framework/operator-registry/releases/latest | jq -r '.tag_name') + echo "Latest opm version: $OPM_VERSION" + + # Download and install opm + curl -L "https://github.com/operator-framework/operator-registry/releases/download/${OPM_VERSION}/linux-amd64-opm" -o /tmp/tools/opm + chmod +x /tmp/tools/opm + + # Verify opm is installed + 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: Run update-graph.sh to update channel graph + - name: update-graph + image: registry.access.redhat.com/ubi9/ubi:latest + workingDir: /workspace/fbc-repo + script: | + #!/bin/bash + set -euo pipefail + + # Check if we should skip channel update + if [ -f /workspace/skip-channel-update ]; then + echo "Skipping update-graph - version already exists in channel graph" + exit 0 + fi + + VERSION="$(params.VERSION)" + + # Remove 'v' prefix if present for the script + VERSION_NO_V="${VERSION#v}" + + echo "Running update-graph.sh with version: $VERSION_NO_V" + + # Make script executable + chmod +x hack/update-graph.sh + + # Run the script + hack/update-graph.sh "$VERSION_NO_V" + + echo "Channel graph updated successfully" + + # 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/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