tests(RHOAIENG-60178): Remove duplicated tests #6130
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Build Validation (Konflux Simulator) | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'frontend/**' | |
| - 'backend/**' | |
| - 'packages/**' | |
| - 'Dockerfile' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| - 'manifests/**' | |
| - '.github/workflows/pr-build-validation.yml' | |
| env: | |
| NODE_VERSION: '22' | |
| # Concurrency control: Cancel in-progress runs when new commits are pushed | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Gate job: Centralized skip condition check | |
| # All other jobs depend on this to avoid duplicating the skip logic | |
| check-skip: | |
| name: "Check Skip Condition" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-skip: ${{ steps.check.outputs.skip }} | |
| steps: | |
| - name: Check if validation should be skipped | |
| id: check | |
| run: | | |
| if [[ "${{ contains(github.event.pull_request.title, '[skip konflux-sim]') }}" == "true" ]] || \ | |
| [[ "${{ contains(github.event.pull_request.labels.*.name, 'skip-konflux-sim') }}" == "true" ]]; then | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ Validation will be skipped ([skip konflux-sim] marker found)" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| echo "✅ Validation will run" | |
| fi | |
| # Phase 0: Early Static Checks (runs first, fails fast) | |
| hermetic-preflight: | |
| name: "Phase 0: Hermetic Build Preflight" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| needs: check-skip | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Set up Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Free disk space | |
| uses: ./.github/actions/free-disk-space | |
| - name: Validate lockfile for Hermeto/Cachi2 compatibility | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Checking package-lock.json for unsupported protocols" | |
| # Check for protocols that Hermeto/Cachi2 cannot resolve | |
| UNSUPPORTED=$(grep -E '"resolved":\s*"(git\+|github:|file:)' package-lock.json || true) | |
| if [ -n "$UNSUPPORTED" ]; then | |
| echo "❌ FAIL: Found unsupported dependency protocols for hermetic builds:" | |
| echo "$UNSUPPORTED" | |
| echo "" | |
| echo "Hermeto/Cachi2 requires all dependencies to have HTTP/HTTPS URLs." | |
| echo "Replace git+, github:, and file: protocols with registry versions." | |
| exit 1 | |
| fi | |
| echo "✅ PASS: No unsupported protocols found" | |
| echo "::endgroup::" | |
| echo "::group::Verifying all dependencies have resolved URLs" | |
| # Check that all node_modules dependencies have resolved field | |
| # Skip root package ("") and workspace packages (don't start with "node_modules/") | |
| if command -v jq &> /dev/null; then | |
| MISSING_RESOLVED=$(jq -r '.packages | to_entries[] | select(.key | startswith("node_modules/")) | select(.value.resolved == null or .value.resolved == "") | .key' package-lock.json || true) | |
| if [ -n "$MISSING_RESOLVED" ]; then | |
| echo "❌ FAIL: Found dependencies without resolved URLs" | |
| echo "$MISSING_RESOLVED" | |
| exit 1 | |
| fi | |
| echo "✅ PASS: All dependencies have resolved URLs" | |
| else | |
| echo "⚠️ WARNING: jq not installed, skipping detailed lockfile validation" | |
| fi | |
| echo "::endgroup::" | |
| - name: Test hermetic npm install | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Testing hermetic npm install with network disabled" | |
| # Two-stage approach: | |
| # 1. Populate npm cache with network enabled (using npm ci) | |
| # 2. Test that npm ci --offline works (simulates hermetic environment) | |
| # Create Dockerfile that tests hermetic install capability | |
| cat > Dockerfile.hermetic-test <<'DOCKERFILE' | |
| FROM node:22-alpine AS cache-builder | |
| WORKDIR /cache | |
| COPY package.json package-lock.json ./ | |
| COPY packages ./packages | |
| # Populate cache - this validates lockfile is complete | |
| RUN npm ci --cache /npm-cache --prefer-offline | |
| FROM node:22-alpine AS hermetic-test | |
| WORKDIR /test | |
| # Copy populated cache from previous stage | |
| COPY --from=cache-builder /npm-cache /root/.npm | |
| COPY package.json package-lock.json ./ | |
| COPY packages ./packages | |
| # Install using ONLY cached deps (offline mode) | |
| # This simulates Konflux/Hermeto hermetic build | |
| RUN npm ci --offline --cache /root/.npm | |
| DOCKERFILE | |
| # Build the image - hermetic-test stage should work entirely from cache | |
| if ! docker build -f Dockerfile.hermetic-test -t hermetic-test --target hermetic-test . 2>&1 | tee /tmp/hermetic-build.log; then | |
| echo "❌ FAIL: Hermetic install failed" | |
| echo "" | |
| echo "The offline install failed, which means:" | |
| echo " - package-lock.json is incomplete or out of sync, OR" | |
| echo " - Dependencies have dynamic resolution that requires network access" | |
| echo "" | |
| echo "::group::Last 50 lines of build output" | |
| tail -50 /tmp/hermetic-build.log | |
| echo "::endgroup::" | |
| exit 1 | |
| fi | |
| # Cleanup | |
| docker rmi hermetic-test || true | |
| rm -f Dockerfile.hermetic-test | |
| echo "✅ PASS: Hermetic install succeeded (all dependencies resolved from lockfile cache)" | |
| echo "::endgroup::" | |
| - name: Validate workspace dependencies vs Dockerfile COPY | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Checking workspace dependencies" | |
| if command -v jq &> /dev/null; then | |
| # Only scan direct subdirectories of packages/ — top-level workspaces like | |
| # backend/ and frontend/ are not Konflux module builds and must not be checked. | |
| WORKSPACE_DIRS=$(find packages -mindepth 1 -maxdepth 1 -type d | sort) | |
| echo "Workspace package directories to check:" | |
| echo "$WORKSPACE_DIRS" | |
| # Track failures — missing COPY lines are a hard build failure in Konflux. | |
| # Lesson from #7855: adding a new shared package requires updating every | |
| # Dockerfile.workspace that imports it. | |
| WORKSPACE_COPY_FAILURES=0 | |
| # Only check Dockerfile.workspace files — those are what upstream Konflux | |
| # pipelines reference (verified via .tekton/*-pull-request.yaml). | |
| while IFS= read -r dockerfile; do | |
| [ -z "$dockerfile" ] && continue | |
| echo "" | |
| echo "Checking $dockerfile..." | |
| DOCKERFILE_DIR=$(dirname "$dockerfile") | |
| while IFS= read -r ws_dir; do | |
| if [ -z "$ws_dir" ]; then continue; fi | |
| # Read the actual scoped npm package name (e.g. @odh-dashboard/k8s-core) | |
| PKG_JSON="$ws_dir/package.json" | |
| [ ! -f "$PKG_JSON" ] && continue | |
| PKG_NAME=$(jq -r '.name // empty' "$PKG_JSON" 2>/dev/null || true) | |
| [ -z "$PKG_NAME" ] && continue | |
| # Match only exact scoped imports in production source files. | |
| # Exclude test files and directories — they don't run inside the | |
| # container and their imports (e.g. @odh-dashboard/contract-tests, | |
| # @odh-dashboard/jest-config) are dev-only and never need COPYing. | |
| IMPORTS=$(grep -r \ | |
| --include="*.ts" --include="*.tsx" \ | |
| --exclude="*.spec.ts" --exclude="*.spec.tsx" \ | |
| --exclude="*.test.ts" --exclude="*.test.tsx" \ | |
| --exclude="jest.config.*" \ | |
| --exclude-dir="__tests__" \ | |
| --exclude-dir="contract-tests" \ | |
| --exclude-dir="cypress" \ | |
| "from ['\"]${PKG_NAME}['\"]" "$DOCKERFILE_DIR" 2>/dev/null || true) | |
| if [ -n "$IMPORTS" ]; then | |
| COPY_FOUND=$(grep "COPY.*${ws_dir}/" "$dockerfile" || true) | |
| if [ -z "$COPY_FOUND" ]; then | |
| echo "❌ FAIL: $dockerfile imports '$PKG_NAME' but is missing:" | |
| echo " COPY --chown=default:root $ws_dir/ ./$ws_dir/" | |
| echo " Without this, Konflux builds will fail with module-not-found errors." | |
| echo " Example import found:" | |
| echo "$IMPORTS" | head -3 | sed 's/^/ /' | |
| WORKSPACE_COPY_FAILURES=$((WORKSPACE_COPY_FAILURES + 1)) | |
| fi | |
| fi | |
| done <<< "$WORKSPACE_DIRS" | |
| done < <(find . -name "Dockerfile.workspace" -not -path "*/node_modules/*") | |
| if [ "$WORKSPACE_COPY_FAILURES" -gt 0 ]; then | |
| echo "" | |
| echo "❌ TOTAL: $WORKSPACE_COPY_FAILURES Dockerfile(s) are missing COPY instructions for workspace packages they import." | |
| echo " These will cause build failures in Konflux and downstream Dockerfile.konflux.* variants." | |
| echo " Add the missing COPY lines to each Dockerfile listed above." | |
| echo "::endgroup::" | |
| exit 1 | |
| fi | |
| echo "✅ PASS: All Dockerfiles COPY the workspace packages they import" | |
| else | |
| echo "⚠️ WARNING: jq not installed, skipping workspace validation" | |
| fi | |
| echo "::endgroup::" | |
| - name: FIPS compliance check | |
| run: | | |
| echo "::group::Checking FIPS requirements" | |
| # Check if Dockerfile removes esbuild (required for FIPS) | |
| if ! grep -q "rm -rf.*esbuild" Dockerfile; then | |
| echo "⚠️ WARNING: Dockerfile should remove esbuild binaries for FIPS compliance" | |
| echo " Add: RUN rm -rf node_modules/esbuild node_modules/@esbuild node_modules/.bin/esbuild" | |
| else | |
| echo "✅ PASS: Dockerfile removes esbuild binaries" | |
| fi | |
| # Check for Go builds with FIPS tags (if Go present) | |
| if [ -f "go.mod" ]; then | |
| GO_BUILDS=$(grep -r "go build" . --include="Dockerfile*" || true) | |
| if [ -n "$GO_BUILDS" ]; then | |
| if ! echo "$GO_BUILDS" | grep -q "strictfipsruntime"; then | |
| echo "⚠️ WARNING: Go builds should use -tags strictfipsruntime for FIPS" | |
| else | |
| echo "✅ PASS: Go builds use strictfipsruntime" | |
| fi | |
| fi | |
| fi | |
| echo "::endgroup::" | |
| # Phase 1: Docker Build Validation | |
| docker-build-odh: | |
| name: "Phase 1: Docker Build (ODH mode)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: write # for upload-artifact | |
| needs: [check-skip, hermetic-preflight] | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Free disk space | |
| uses: ./.github/actions/free-disk-space | |
| - name: Build ODH image | |
| run: | | |
| set -euo pipefail | |
| # TODO: Pin BASE_IMAGE to specific digest for hermetic builds | |
| # Example: --build-arg BASE_IMAGE=registry.access.redhat.com/ubi9/nodejs-22@sha256:... | |
| docker build \ | |
| --build-arg BUILD_MODE=ODH \ | |
| --tag odh-dashboard:odh-test \ | |
| --file Dockerfile \ | |
| . | |
| - name: Save ODH image | |
| run: docker save odh-dashboard:odh-test | gzip > /tmp/odh-dashboard-odh.tar.gz | |
| - name: Upload ODH image artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.0 | |
| with: | |
| name: odh-dashboard-odh-image | |
| path: /tmp/odh-dashboard-odh.tar.gz | |
| retention-days: 1 | |
| docker-build-rhoai: | |
| name: "Phase 1: Docker Build (RHOAI mode)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: write # for upload-artifact | |
| needs: [check-skip, hermetic-preflight] | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Free disk space | |
| uses: ./.github/actions/free-disk-space | |
| - name: Build RHOAI image | |
| run: | | |
| set -euo pipefail | |
| # TODO: Pin BASE_IMAGE to specific digest for hermetic builds | |
| # Example: --build-arg BASE_IMAGE=registry.access.redhat.com/ubi9/nodejs-22@sha256:... | |
| docker build \ | |
| --build-arg BUILD_MODE=RHOAI \ | |
| --tag odh-dashboard:rhoai-test \ | |
| --file Dockerfile \ | |
| . | |
| - name: Save RHOAI image | |
| run: docker save odh-dashboard:rhoai-test | gzip > /tmp/odh-dashboard-rhoai.tar.gz | |
| - name: Upload RHOAI image artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.0 | |
| with: | |
| name: odh-dashboard-rhoai-image | |
| path: /tmp/odh-dashboard-rhoai.tar.gz | |
| retention-days: 1 | |
| # Phase 2 & 3: Runtime and Module Federation Validation | |
| runtime-validation-odh: | |
| name: "Phase 2-3: Runtime & Module Federation (ODH)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read # for download-artifact | |
| needs: [check-skip, docker-build-odh] | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Download ODH image | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.2.1 | |
| with: | |
| name: odh-dashboard-odh-image | |
| path: /tmp | |
| - name: Load ODH image | |
| run: docker load < /tmp/odh-dashboard-odh.tar.gz | |
| - name: Validate ODH branding in build artifacts | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Validating default ODH build branding" | |
| # Create temporary container to extract built files | |
| CONTAINER_ID=$(docker create odh-dashboard:odh-test) | |
| echo "Created temporary container: $CONTAINER_ID" | |
| # Extract the built HTML file | |
| docker cp "$CONTAINER_ID":/usr/src/app/frontend/public/index.html /tmp/odh-index.html | |
| # Extract dist directory for artifact checks | |
| docker cp "$CONTAINER_ID":/usr/src/app/frontend/public /tmp/odh-dist | |
| # Extract favicon and logo files | |
| docker cp "$CONTAINER_ID":/usr/src/app/frontend/public/images /tmp/odh-images || true | |
| # Clean up container | |
| docker rm "$CONTAINER_ID" | |
| FAILURES=0 | |
| echo "Checking HTML content for ODH branding..." | |
| HTML=$(cat /tmp/odh-index.html) | |
| # Check 1: ODH product name in title (default build) | |
| if ! echo "$HTML" | grep -q "Open Data Hub"; then | |
| echo "❌ FAIL: ODH product name not found in HTML title (default build)" | |
| echo "::group::HTML title tag" | |
| echo "$HTML" | grep -i "<title>" || echo "(no title tag found)" | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: ODH product name present in title (default build)" | |
| fi | |
| # Check 2: RHOAI branding should NOT be in default build | |
| if echo "$HTML" | grep -q "Red Hat OpenShift AI"; then | |
| echo "❌ FAIL: RHOAI branding found in default ODH build (BUILD_MODE contamination)" | |
| echo "::group::RHOAI references" | |
| echo "$HTML" | grep "Red Hat OpenShift AI" | head -3 | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: No RHOAI branding in default build" | |
| fi | |
| # Check 3: ODH favicon reference | |
| if ! echo "$HTML" | grep -q "odh-favicon"; then | |
| echo "❌ FAIL: ODH favicon not referenced (expected odh-favicon.svg)" | |
| echo "::group::Favicon link tags" | |
| echo "$HTML" | grep -i "favicon\|<link.*icon" || echo "(no favicon tags found)" | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: ODH favicon referenced" | |
| fi | |
| # Check 4: Verify ODH logo files exist in build | |
| if [ -d "/tmp/odh-images" ]; then | |
| if [ ! -f "/tmp/odh-images/odh-logo-light-theme.svg" ]; then | |
| echo "⚠️ WARNING: odh-logo-light-theme.svg not found in images/" | |
| else | |
| echo "✅ PASS: ODH logo file present in build" | |
| fi | |
| if [ ! -f "/tmp/odh-images/odh-favicon.svg" ]; then | |
| echo "⚠️ WARNING: odh-favicon.svg not found in images/" | |
| else | |
| echo "✅ PASS: ODH favicon file present in build" | |
| fi | |
| else | |
| echo "⚠️ WARNING: Could not extract images/ directory" | |
| fi | |
| echo "::endgroup::" | |
| # Validate Module Federation artifacts | |
| echo "::group::Validating Module Federation artifacts" | |
| # Check for main app bundle (always required) | |
| # Webpack outputs app.[contenthash].js, not app.bundle.js | |
| if ! ls /tmp/odh-dist/app.*.js >/dev/null 2>&1; then | |
| echo "❌ FAIL: app.*.js not found - build did not complete" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| APP_FILE=$(find /tmp/odh-dist -name 'app.*.js' | head -1) | |
| APP_SIZE=$(stat -c%s "$APP_FILE" 2>/dev/null || stat -f%z "$APP_FILE" 2>/dev/null) | |
| echo "✅ PASS: Main app bundle present ($(basename "$APP_FILE"), ${APP_SIZE} bytes)" | |
| fi | |
| # Check for remoteEntry.js (optional - only if federated modules exist) | |
| if [ -f "/tmp/odh-dist/remoteEntry.js" ]; then | |
| SIZE=$(stat -c%s /tmp/odh-dist/remoteEntry.js 2>/dev/null || stat -f%z /tmp/odh-dist/remoteEntry.js 2>/dev/null) | |
| if [ "${SIZE:-0}" -lt 100 ]; then | |
| echo "❌ FAIL: remoteEntry.js is suspiciously small (${SIZE:-0} bytes)" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: remoteEntry.js present and valid ($SIZE bytes)" | |
| fi | |
| # Check webpack chunks | |
| CHUNK_COUNT=$(find /tmp/odh-dist -name "*.js" -o -name "*.bundle.js" | wc -l | tr -d ' ') | |
| if [ "${CHUNK_COUNT:-0}" -lt 2 ]; then | |
| echo "⚠️ WARNING: Very few webpack chunks found (${CHUNK_COUNT:-0})" | |
| else | |
| echo "✅ PASS: Found ${CHUNK_COUNT:-0} webpack chunks" | |
| fi | |
| else | |
| echo "ℹ️ INFO: No remoteEntry.js found (no federated modules configured)" | |
| fi | |
| # Report dist size | |
| du -sh /tmp/odh-dist | |
| echo "::endgroup::" | |
| if [ $FAILURES -gt 0 ]; then | |
| echo "" | |
| echo "❌ TOTAL FAILURES: $FAILURES" | |
| echo "" | |
| echo "Default ODH build validation failed." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "✅ PASS: Default ODH build produces correct artifacts and branding" | |
| runtime-validation-rhoai: | |
| name: "Phase 2-3: Runtime & Module Federation (RHOAI)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read # for download-artifact | |
| needs: [check-skip, docker-build-rhoai] | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Download RHOAI image | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.2.1 | |
| with: | |
| name: odh-dashboard-rhoai-image | |
| path: /tmp | |
| - name: Load RHOAI image | |
| run: docker load < /tmp/odh-dashboard-rhoai.tar.gz | |
| - name: Validate RHOAI branding in build artifacts | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Validating BUILD_MODE=RHOAI applied branding" | |
| # Create temporary container to extract built files | |
| # (Don't need to run it - just create and extract) | |
| CONTAINER_ID=$(docker create odh-dashboard:rhoai-test) | |
| echo "Created temporary container: $CONTAINER_ID" | |
| # Extract the built HTML file | |
| docker cp "$CONTAINER_ID":/usr/src/app/frontend/public/index.html /tmp/rhoai-index.html | |
| # Extract favicon and logo files | |
| docker cp "$CONTAINER_ID":/usr/src/app/frontend/public/images /tmp/rhoai-images || true | |
| # Clean up container | |
| docker rm "$CONTAINER_ID" | |
| FAILURES=0 | |
| echo "Checking HTML content for RHOAI branding..." | |
| HTML=$(cat /tmp/rhoai-index.html) | |
| # Check 1: RHOAI product name in title | |
| if ! echo "$HTML" | grep -q "Red Hat OpenShift AI"; then | |
| echo "❌ FAIL: RHOAI product name not found in HTML title" | |
| echo "::group::HTML title tag" | |
| echo "$HTML" | grep -i "<title>" || echo "(no title tag found)" | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: RHOAI product name present in title" | |
| fi | |
| # Check 2: ODH branding should NOT be present | |
| if echo "$HTML" | grep -q "Open Data Hub"; then | |
| echo "❌ FAIL: ODH branding found in RHOAI build (BUILD_MODE not applied)" | |
| echo "::group::ODH references" | |
| echo "$HTML" | grep "Open Data Hub" | head -3 | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: No ODH branding (correctly overridden by BUILD_MODE)" | |
| fi | |
| # Check 3: RHOAI favicon reference | |
| if ! echo "$HTML" | grep -q "rhoai-favicon"; then | |
| echo "❌ FAIL: RHOAI favicon not referenced (expected rhoai-favicon.svg)" | |
| echo "::group::Favicon link tags" | |
| echo "$HTML" | grep -i "favicon\|<link.*icon" || echo "(no favicon tags found)" | |
| echo "::endgroup::" | |
| FAILURES=$((FAILURES + 1)) | |
| else | |
| echo "✅ PASS: RHOAI favicon referenced" | |
| fi | |
| # Check 4: Verify RHOAI logo files exist in build | |
| if [ -d "/tmp/rhoai-images" ]; then | |
| if [ ! -f "/tmp/rhoai-images/rhoai-logo.svg" ]; then | |
| echo "⚠️ WARNING: rhoai-logo.svg not found in images/" | |
| else | |
| echo "✅ PASS: RHOAI logo file present in build" | |
| fi | |
| if [ ! -f "/tmp/rhoai-images/rhoai-favicon.svg" ]; then | |
| echo "⚠️ WARNING: rhoai-favicon.svg not found in images/" | |
| else | |
| echo "✅ PASS: RHOAI favicon file present in build" | |
| fi | |
| else | |
| echo "⚠️ WARNING: Could not extract images/ directory" | |
| fi | |
| if [ $FAILURES -gt 0 ]; then | |
| echo "" | |
| echo "❌ TOTAL FAILURES: $FAILURES" | |
| echo "" | |
| echo "BUILD_MODE=RHOAI did not correctly apply RHOAI branding." | |
| echo "Check Dockerfile environment variable setup and webpack configuration." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "✅ PASS: BUILD_MODE=RHOAI correctly applied RHOAI branding" | |
| echo "::endgroup::" | |
| # Phase 4: Operator Integration (Kind cluster) | |
| operator-integration: | |
| name: "Phase 4: Operator Integration" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read # for download-artifact | |
| needs: [check-skip, runtime-validation-odh, runtime-validation-rhoai] | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Download ODH image | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.2.1 | |
| with: | |
| name: odh-dashboard-odh-image | |
| path: /tmp | |
| - name: Install Kind | |
| run: | | |
| set -euo pipefail | |
| # Download kind with checksum verification | |
| KIND_URL="https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64" | |
| KIND_SHA256="513a7213d6d3332dd9ef27c24dab35e5ef10a04fa27274fe1c14d8a246493ded" | |
| curl -sLo /tmp/kind "$KIND_URL" | |
| echo "${KIND_SHA256} /tmp/kind" | sha256sum --check --status | |
| chmod +x /tmp/kind | |
| sudo mv /tmp/kind /usr/local/bin/kind | |
| - name: Create Kind cluster | |
| run: | | |
| set -euo pipefail | |
| kind create cluster --name odh-test --wait 300s | |
| - name: Load image to Kind | |
| run: | | |
| set -euo pipefail | |
| docker load < /tmp/odh-dashboard-odh.tar.gz | |
| kind load docker-image odh-dashboard:odh-test --name odh-test | |
| - name: Create opendatahub namespace | |
| run: | | |
| kubectl create namespace opendatahub | |
| - name: Create mock OpenShift secrets | |
| run: | | |
| set -euo pipefail | |
| # Create dummy TLS secret for proxy | |
| openssl req -x509 -newkey rsa:2048 -nodes \ | |
| -keyout /tmp/tls.key -out /tmp/tls.crt \ | |
| -days 1 -subj "/CN=dashboard" 2>/dev/null | |
| kubectl create secret tls dashboard-proxy-tls \ | |
| --cert=/tmp/tls.crt --key=/tmp/tls.key \ | |
| -n opendatahub | |
| rm /tmp/tls.key /tmp/tls.crt | |
| # Create dummy CA bundle configmaps | |
| kubectl create configmap odh-ca-cert \ | |
| --from-literal=odh-ca-bundle.crt="# Dummy CA bundle for Kind testing" \ | |
| -n opendatahub | |
| kubectl create configmap odh-trusted-ca-cert \ | |
| --from-literal=odh-trusted-ca-bundle.crt="# Dummy trusted CA bundle for Kind testing" \ | |
| -n opendatahub | |
| echo "✅ Created mock OpenShift secrets and configmaps" | |
| - name: Create mock OpenShift ConfigMaps | |
| run: | | |
| set -euo pipefail | |
| # Dashboard pods expect OpenShift-specific ConfigMaps for volume mounts | |
| # Create empty ConfigMaps to satisfy those requirements in Kind | |
| echo "Creating mock openshift-service-ca.crt ConfigMap..." | |
| kubectl create configmap openshift-service-ca.crt \ | |
| --from-literal=service-ca.crt="" \ | |
| -n opendatahub | |
| echo "Creating mock kube-root-ca.crt ConfigMap (if needed)..." | |
| # kube-root-ca.crt may already exist (Kubernetes auto-creates it in some versions) | |
| kubectl create configmap kube-root-ca.crt \ | |
| --from-literal=ca.crt="" \ | |
| -n opendatahub 2>/dev/null || echo " (already exists, skipping)" | |
| echo "✅ Mock OpenShift ConfigMaps ready" | |
| - name: Apply manifests | |
| run: | | |
| set -euo pipefail | |
| # Apply base manifests to opendatahub namespace | |
| # Try overlay first, fall back to base, fail if both are invalid | |
| APPLY_EXIT=0 | |
| if [ -d "manifests/overlays/odh" ]; then | |
| echo "Applying manifests/overlays/odh..." | |
| if ! kubectl apply -k manifests/overlays/odh -n opendatahub 2>&1; then | |
| APPLY_EXIT=$? | |
| echo "::error::Manifest application failed with exit code $APPLY_EXIT" | |
| echo "::group::Recent cluster events" | |
| kubectl get events --sort-by='.lastTimestamp' -n opendatahub | tail -20 || true | |
| echo "::endgroup::" | |
| exit $APPLY_EXIT | |
| fi | |
| elif [ -d "manifests/odh" ]; then | |
| echo "Applying manifests/odh..." | |
| if ! kubectl apply -k manifests/odh -n opendatahub 2>&1; then | |
| APPLY_EXIT=$? | |
| echo "::error::Manifest application failed with exit code $APPLY_EXIT" | |
| echo "::group::Recent cluster events" | |
| kubectl get events --sort-by='.lastTimestamp' -n opendatahub | tail -20 || true | |
| echo "::endgroup::" | |
| exit $APPLY_EXIT | |
| fi | |
| else | |
| echo "❌ FAIL: No kustomize manifests found" | |
| exit 1 | |
| fi | |
| echo "✅ Manifests applied successfully" | |
| - name: Patch deployment for Kind resource constraints | |
| run: | | |
| set -euo pipefail | |
| # Apply image, imagePullPolicy, resource limits, and replica count in a single | |
| # JSON patch so only one rollout is triggered. | |
| # | |
| # Why image + imagePullPolicy: Never? | |
| # The manifests hardcode imagePullPolicy: Always, so Kubernetes would pull the | |
| # live quay.io:main image even if it's loaded into Kind. "Never" forces it to | |
| # use only what was loaded locally, guaranteeing we test the PR's built image. | |
| # | |
| # The full patch is attempted first. If it fails (e.g. resource paths differ | |
| # across manifest versions), we fall back to the essential image-only patch so | |
| # the job does not fail due to resource-constraint differences. | |
| echo "Applying combined patch (image + imagePullPolicy + resources + replicas)..." | |
| if kubectl patch deployment odh-dashboard -n opendatahub --type=json -p='[ | |
| {"op": "replace", "path": "/spec/replicas", "value": 1}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "odh-dashboard:odh-test"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "128Mi"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value": "50m"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/1/resources/requests/memory", "value": "64Mi"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/1/resources/requests/cpu", "value": "50m"} | |
| ]' 2>/dev/null; then | |
| echo "✅ Full patch applied — single rollout triggered" | |
| else | |
| echo "⚠️ Full patch failed (resource paths may differ); applying essential patch..." | |
| kubectl patch deployment odh-dashboard -n opendatahub --type=json -p='[ | |
| {"op": "replace", "path": "/spec/replicas", "value": 1}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "odh-dashboard:odh-test"}, | |
| {"op": "replace", "path": "/spec/template/spec/containers/0/imagePullPolicy", "value": "Never"} | |
| ]' | |
| echo "✅ Essential patch applied (image + imagePullPolicy + replicas)" | |
| fi | |
| - name: Wait for pods to attempt starting | |
| run: | | |
| set -euo pipefail | |
| echo "Waiting for pods to be created and attempt starting..." | |
| # Wait up to 2 minutes for pod creation | |
| for i in {1..24}; do | |
| POD_COUNT=$(kubectl get pods -n opendatahub -l app=odh-dashboard --no-headers 2>/dev/null | wc -l) | |
| if [ "$POD_COUNT" -gt 0 ]; then | |
| echo "✅ Pod(s) created" | |
| break | |
| fi | |
| echo "Waiting for pods... ($i/24)" | |
| sleep 5 | |
| done | |
| # Wait for the rollout to complete rather than a fixed sleep. | |
| # This correctly handles the single rollout triggered by the combined patch and | |
| # avoids the race condition where a fixed sleep may expire before the new pod | |
| # has fully replaced the old one. | |
| echo "Waiting for rollout to complete..." | |
| kubectl rollout status deployment/odh-dashboard -n opendatahub --timeout=120s | |
| # rollout status exits as soon as new pods are Ready, but old pods from the | |
| # previous revision can still be in Terminating state for their gracePeriod. | |
| # Wait up to 60s for them to fully disappear so the next step doesn't pick up | |
| # a Terminating pod as items[0] and produce a false health-check failure. | |
| echo "Waiting for any Terminating pods to clear..." | |
| for i in {1..12}; do | |
| TERM_COUNT=$(kubectl get pods -n opendatahub -l app=odh-dashboard \ | |
| -o jsonpath='{range .items[*]}{.metadata.deletionTimestamp}{"\n"}{end}' 2>/dev/null \ | |
| | grep -vc '^$' || echo "0") | |
| if [ "${TERM_COUNT:-0}" -eq 0 ]; then | |
| echo "✅ No Terminating pods" | |
| break | |
| fi | |
| echo " Waiting for ${TERM_COUNT} pod(s) to finish terminating... ($i/12)" | |
| sleep 5 | |
| done | |
| - name: Check container status and validate | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Pod status" | |
| kubectl get pods -n opendatahub -o wide | |
| echo "::endgroup::" | |
| echo "::group::Deployment status" | |
| kubectl describe deployment odh-dashboard -n opendatahub | |
| echo "::endgroup::" | |
| # Select the active (non-Terminating) pod. After a rolling update items[0] may | |
| # still be the old revision with a deletionTimestamp set. Filter it out via | |
| # custom-columns so health checks always target the current revision. | |
| POD=$(kubectl get pods -n opendatahub -l app=odh-dashboard \ | |
| -o custom-columns='NAME:.metadata.name,DEL:.metadata.deletionTimestamp' \ | |
| --no-headers 2>/dev/null | awk '$2 == "<none>"' | head -1 | awk '{print "pod/" $1}') | |
| if [ -z "$POD" ]; then | |
| echo "❌ FAIL: No non-Terminating pod found for app=odh-dashboard" | |
| kubectl get pods -n opendatahub -l app=odh-dashboard | |
| exit 1 | |
| fi | |
| echo "Active pod: $POD" | |
| # Get pod details | |
| echo "::group::Pod events and status" | |
| kubectl describe -n opendatahub "$POD" | |
| echo "::endgroup::" | |
| # Check if containers started (even if they fail later) | |
| echo "Validating container lifecycle..." | |
| POD_NAME="${POD#pod/}" | |
| CONTAINER_STATUSES=$(kubectl get pod -n opendatahub "$POD_NAME" \ | |
| -o jsonpath='{.status.containerStatuses[*].state}' 2>/dev/null || echo "") | |
| if [ -z "$CONTAINER_STATUSES" ]; then | |
| echo "❌ FAIL: No container statuses found - pods may not have started" | |
| exit 1 | |
| fi | |
| # Check for successful image pulls | |
| IMAGES_PULLED=$(kubectl get pod -n opendatahub "$POD_NAME" \ | |
| -o jsonpath='{.status.containerStatuses[*].imageID}' 2>/dev/null | wc -w) | |
| if [ "$IMAGES_PULLED" -eq 0 ]; then | |
| echo "❌ FAIL: No images were pulled successfully" | |
| exit 1 | |
| fi | |
| echo "✅ PASS: $IMAGES_PULLED image(s) pulled" | |
| echo "Container states: $CONTAINER_STATUSES" | |
| # Show logs from main container before the health check so they are visible | |
| # even when the container is crashing. | |
| echo "::group::Container logs (odh-dashboard)" | |
| kubectl logs -n opendatahub "$POD" -c odh-dashboard --tail=100 || echo "No logs available" | |
| echo "::endgroup::" | |
| # Assert that the PR-built odh-dashboard container is actually running and has | |
| # not restarted. A CrashLoopBackOff or any restart count > 0 means the server | |
| # failed to start and is a hard failure. | |
| echo "::group::Health check: odh-dashboard container" | |
| DASHBOARD_RUNNING=$(kubectl get pod -n opendatahub "$POD_NAME" \ | |
| -o jsonpath='{.status.containerStatuses[?(@.name=="odh-dashboard")].state.running}' \ | |
| 2>/dev/null || echo "") | |
| # Use -1 as sentinel: a non-negative integer means success, anything else | |
| # (including -1 or empty) means kubectl itself failed — pod not found, | |
| # API server timeout, etc. — which should be a hard failure, not a silent pass. | |
| DASHBOARD_RESTARTS=$(kubectl get pod -n opendatahub "$POD_NAME" \ | |
| -o jsonpath='{.status.containerStatuses[?(@.name=="odh-dashboard")].restartCount}' \ | |
| 2>/dev/null || echo "-1") | |
| if [ -z "$DASHBOARD_RUNNING" ]; then | |
| TERM_REASON=$(kubectl get pod -n opendatahub "$POD_NAME" \ | |
| -o jsonpath='{.status.containerStatuses[?(@.name=="odh-dashboard")].state.terminated.reason}' \ | |
| 2>/dev/null || echo "unknown") | |
| echo "❌ FAIL: odh-dashboard container is not running (reason: ${TERM_REASON})" | |
| echo " This usually means the server crashed at startup." | |
| echo " Check the container logs above for the root cause." | |
| exit 1 | |
| fi | |
| if ! [[ "$DASHBOARD_RESTARTS" =~ ^[0-9]+$ ]]; then | |
| echo "❌ FAIL: Could not determine restart count (kubectl may have failed — got: '${DASHBOARD_RESTARTS}')" | |
| echo " This may indicate the pod does not exist or the API server is unreachable." | |
| exit 1 | |
| fi | |
| if [ "$DASHBOARD_RESTARTS" -gt "0" ]; then | |
| echo "❌ FAIL: odh-dashboard container has restarted ${DASHBOARD_RESTARTS} time(s)" | |
| echo " The server is crashing on startup (CrashLoopBackOff)." | |
| echo " Check the container logs above for the root cause." | |
| exit 1 | |
| fi | |
| echo "✅ PASS: odh-dashboard container is running with 0 restarts" | |
| echo "::endgroup::" | |
| - name: Cleanup | |
| if: always() | |
| run: kind delete cluster --name odh-test | |
| # Phase 5 (Manifest Validation) is not yet included. | |
| # manifests/overlays/dev has a known kustomization path issue | |
| # (references ../common/crd but should be ../../common/crd). | |
| # Add manifest validation once the upstream path is fixed. | |
| # Sidecar Module Validation: Build and smoke-test packages/*/Dockerfile.workspace images | |
| # Catches startup regressions (e.g. Go protobuf panics) that only surface when the | |
| # sidecar binary runs, not when it compiles. | |
| detect-sidecar-changes: | |
| name: "Detect Sidecar Module Changes" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| needs: check-skip | |
| if: needs.check-skip.outputs.should-skip != 'true' | |
| outputs: | |
| matrix: ${{ steps.detect.outputs.matrix }} | |
| has-changes: ${{ steps.detect.outputs.has-changes }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changed sidecar modules | |
| id: detect | |
| run: | | |
| set -euo pipefail | |
| # Get files changed in this PR | |
| CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD) | |
| # Check if root build files changed (affects all sidecar builds) | |
| ROOT_BUILD_CHANGED=false | |
| if echo "$CHANGED_FILES" | grep -qE '^(package\.json|package-lock\.json)$'; then | |
| ROOT_BUILD_CHANGED=true | |
| echo "Root build files changed — all sidecar modules will be built" | |
| fi | |
| # Dynamically discover packages with Dockerfile.workspace | |
| MATRIX_ITEMS="[]" | |
| while IFS= read -r dockerfile; do | |
| [ -z "$dockerfile" ] && continue | |
| PKG_DIR=$(dirname "$dockerfile") | |
| MODULE_NAME=$(basename "$PKG_DIR") | |
| # Skip non-module scaffolding packages | |
| if [ "$MODULE_NAME" = "plugin-template" ]; then | |
| echo "Skipping $MODULE_NAME (scaffold template, not a deployable module)" | |
| continue | |
| fi | |
| # Check if this package has changed files, or root build files changed | |
| if [ "$ROOT_BUILD_CHANGED" = "true" ] || echo "$CHANGED_FILES" | grep -qF "${PKG_DIR}/"; then | |
| MATRIX_ITEMS=$(echo "$MATRIX_ITEMS" | jq -c --arg name "$MODULE_NAME" --arg dockerfile "$dockerfile" '. + [{"module": $name, "dockerfile": $dockerfile}]') | |
| echo "Will build: $MODULE_NAME ($dockerfile)" | |
| fi | |
| done < <(find packages -maxdepth 2 -name "Dockerfile.workspace" -not -path "*/node_modules/*" | sort) | |
| COUNT=$(echo "$MATRIX_ITEMS" | jq 'length') | |
| if [ "$COUNT" -gt 0 ]; then | |
| echo "has-changes=true" >> "$GITHUB_OUTPUT" | |
| echo "matrix={\"include\":$MATRIX_ITEMS}" >> "$GITHUB_OUTPUT" | |
| echo "Found $COUNT sidecar module(s) to build" | |
| else | |
| echo "has-changes=false" >> "$GITHUB_OUTPUT" | |
| echo "matrix={\"include\":[]}" >> "$GITHUB_OUTPUT" | |
| echo "No sidecar modules affected by this PR" | |
| fi | |
| sidecar-build-validation: | |
| name: "Sidecar Build: ${{ matrix.module }}" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| needs: [check-skip, detect-sidecar-changes] | |
| if: needs.check-skip.outputs.should-skip != 'true' && needs.detect-sidecar-changes.outputs.has-changes == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.detect-sidecar-changes.outputs.matrix) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 | |
| - name: Free disk space | |
| uses: ./.github/actions/free-disk-space | |
| - name: Build sidecar image | |
| env: | |
| MODULE: ${{ matrix.module }} | |
| DOCKERFILE: ${{ matrix.dockerfile }} | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Building ${MODULE} from ${DOCKERFILE}" | |
| docker build \ | |
| --no-cache \ | |
| --file "${DOCKERFILE}" \ | |
| --tag "sidecar-${MODULE}:test" \ | |
| . | |
| echo "::endgroup::" | |
| echo "Build completed for ${MODULE}" | |
| - name: Validate sidecar startup | |
| run: | | |
| set -euo pipefail | |
| MODULE="${{ matrix.module }}" | |
| CONTAINER_NAME="sidecar-${MODULE}-test" | |
| IMAGE="sidecar-${MODULE}:test" | |
| # Run the container with no special flags. BFFs will likely exit quickly | |
| # because there is no Kubernetes cluster in CI — that is expected. | |
| # We only care that the binary does not *crash* (panic, segfault, etc.). | |
| echo "::group::Starting $MODULE sidecar container" | |
| docker run -d --name "$CONTAINER_NAME" "$IMAGE" | |
| echo "::endgroup::" | |
| echo "Waiting 5 seconds for BFF startup..." | |
| sleep 5 | |
| echo "::group::Container status" | |
| RUNNING=$(docker inspect --format '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo "false") | |
| EXIT_CODE=$(docker inspect --format '{{.State.ExitCode}}' "$CONTAINER_NAME" 2>/dev/null || echo "-1") | |
| echo "Running: $RUNNING" | |
| echo "Exit code: $EXIT_CODE" | |
| echo "::endgroup::" | |
| echo "::group::Container logs" | |
| LOGS=$(docker logs "$CONTAINER_NAME" 2>&1 || echo "") | |
| echo "$LOGS" | |
| echo "::endgroup::" | |
| # Scan logs for Go runtime crash patterns only. | |
| # Clean exits (missing kubeconfig, no cluster, invalid config) are fine — | |
| # they prove the binary compiled, loaded, and ran its init logic. | |
| FAILURES=0 | |
| CRASH_PATTERNS=( | |
| "panic:" | |
| "fatal error:" | |
| "runtime error:" | |
| "signal: segmentation fault" | |
| "SIGSEGV" | |
| ) | |
| for pattern in "${CRASH_PATTERNS[@]}"; do | |
| if echo "$LOGS" | grep -q "$pattern"; then | |
| echo "::error::$MODULE sidecar logs contain crash indicator: $pattern" | |
| echo "$LOGS" | grep "$pattern" | head -5 | |
| FAILURES=$((FAILURES + 1)) | |
| fi | |
| done | |
| if [ "$RUNNING" = "true" ]; then | |
| echo "INFO: $MODULE sidecar is still running after 5s" | |
| else | |
| echo "INFO: $MODULE exited (exit code: $EXIT_CODE) — expected without a Kubernetes cluster" | |
| fi | |
| # Clean up | |
| docker stop "$CONTAINER_NAME" 2>/dev/null || true | |
| docker rm "$CONTAINER_NAME" 2>/dev/null || true | |
| docker rmi "$IMAGE" 2>/dev/null || true | |
| if [ "$FAILURES" -gt 0 ]; then | |
| echo "" | |
| echo "::error::$MODULE sidecar startup validation failed with $FAILURES issue(s)" | |
| echo "Check the container logs above for the root cause." | |
| exit 1 | |
| fi | |
| echo "PASS: $MODULE sidecar built and started without crashes" | |
| summary: | |
| name: "Build Validation Summary" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| needs: | |
| - check-skip | |
| - hermetic-preflight | |
| - docker-build-odh | |
| - docker-build-rhoai | |
| - runtime-validation-odh | |
| - runtime-validation-rhoai | |
| - operator-integration | |
| - detect-sidecar-changes | |
| - sidecar-build-validation | |
| # Run summary even when jobs are skipped, but not when workflow is skipped | |
| if: always() && needs.check-skip.outputs.should-skip != 'true' | |
| steps: | |
| - name: Check results | |
| run: | | |
| set -euo pipefail | |
| echo "## Konflux Build Simulation Results" | |
| echo "" | |
| echo "Phase 0 (Hermetic Preflight): ${{ needs.hermetic-preflight.result }}" | |
| echo "Phase 1 (Docker Build ODH): ${{ needs.docker-build-odh.result }}" | |
| echo "Phase 1 (Docker Build RHOAI): ${{ needs.docker-build-rhoai.result }}" | |
| echo "Phase 2-3 (Runtime ODH): ${{ needs.runtime-validation-odh.result }}" | |
| echo "Phase 2-3 (Runtime RHOAI): ${{ needs.runtime-validation-rhoai.result }}" | |
| echo "Phase 4 (Operator Integration): ${{ needs.operator-integration.result }}" | |
| echo "Sidecar Detection: ${{ needs.detect-sidecar-changes.result }}" | |
| echo "Sidecar Build Validation: ${{ needs.sidecar-build-validation.result }}" | |
| # Accept "success" and "skipped" as passing states | |
| # "skipped" occurs when upstream job was skipped (normal dependency chain) | |
| # Only "failure", "cancelled" are actual failures | |
| FAILED=false | |
| for result in "${{ needs.hermetic-preflight.result }}" \ | |
| "${{ needs.docker-build-odh.result }}" \ | |
| "${{ needs.docker-build-rhoai.result }}" \ | |
| "${{ needs.runtime-validation-odh.result }}" \ | |
| "${{ needs.runtime-validation-rhoai.result }}" \ | |
| "${{ needs.operator-integration.result }}" \ | |
| "${{ needs.detect-sidecar-changes.result }}" \ | |
| "${{ needs.sidecar-build-validation.result }}"; do | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| FAILED=true | |
| break | |
| fi | |
| done | |
| if [ "$FAILED" = "true" ]; then | |
| echo "" | |
| echo "❌ Some validations failed. Check individual job logs above." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "✅ All validations passed!" |