Dashboard E2E Tests #9
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: Dashboard E2E Tests | |
| on: | |
| workflow_run: | |
| workflows: ["JS"] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| image_tag: | |
| description: "MLflow image tag to test (alphanumeric, dots, hyphens, underscores only)" | |
| required: true | |
| default: "odh-stable" | |
| type: string | |
| concurrency: | |
| group: e2e-${{ github.event.workflow_run.head_repository.full_name || github.repository }}-${{ github.event.workflow_run.head_branch || github.ref }} | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| gate: | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.conclusion == 'success') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| issues: read | |
| pull-requests: read | |
| outputs: | |
| should-run: ${{ github.event_name == 'workflow_dispatch' || steps.check.outputs.should-run == 'true' }} | |
| pr-number: ${{ steps.pr.outputs.pr_number }} | |
| head-sha: ${{ steps.pr.outputs.head_sha }} | |
| steps: | |
| - name: Resolve PR number | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| WF_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| FALLBACK_SHA: ${{ github.sha }} | |
| run: | | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| echo "pr_number=" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=$FALLBACK_SHA" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # workflow_run.pull_requests is empty for fork PRs | |
| PR_NUMBER="$WF_PR_NUMBER" | |
| if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then | |
| echo "pull_requests array is empty (likely a fork PR), resolving via API..." | |
| PR_NUMBER=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/pulls" \ | |
| --jq '[.[] | select(.state == "open")][0].number' 2>/dev/null || echo "") | |
| fi | |
| if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then | |
| echo "::error::Could not resolve PR number for head SHA ${HEAD_SHA}" | |
| exit 1 | |
| fi | |
| echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" | |
| echo "Resolved PR #$PR_NUMBER for SHA $HEAD_SHA" | |
| - name: Check for ok-to-test label | |
| if: github.event_name != 'workflow_dispatch' | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| run: | | |
| set -euo pipefail | |
| HAS_LABEL="$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" \ | |
| --jq 'map(.name) | any(. == "ok-to-test")')" | |
| if [[ "$HAS_LABEL" == "true" ]]; then | |
| echo "Label 'ok-to-test' found, proceeding with e2e tests." | |
| echo "should-run=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::Skipping e2e: PR #${PR_NUMBER} is missing the 'ok-to-test' label. A maintainer must add it to run e2e tests." | |
| fi | |
| e2e: | |
| needs: gate | |
| if: needs.gate.outputs.should-run == 'true' | |
| permissions: | |
| contents: read | |
| timeout-minutes: 45 | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: mlflow/server/js | |
| env: | |
| MLFLOW_E2E_BASE_URL: http://localhost:5000 | |
| MLFLOW_IMAGE: quay.io/opendatahub/mlflow:${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || format('odh-pr-{0}', needs.gate.outputs.pr-number) }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.gate.outputs.head-sha }} | |
| persist-credentials: false | |
| - name: Validate workflow_dispatch inputs | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| if [[ ! "$MLFLOW_IMAGE" =~ ^quay\.io/opendatahub/mlflow:[A-Za-z0-9._-]+$ ]]; then | |
| echo "::error::Invalid image_tag input. Only [A-Za-z0-9._-] is allowed." | |
| exit 1 | |
| fi | |
| - name: Log image tag | |
| run: echo "Using image $MLFLOW_IMAGE" | |
| - uses: ./.github/actions/setup-node | |
| - name: Install dependencies | |
| run: yarn install --immutable | |
| - name: Get Playwright version | |
| id: pw-version | |
| run: echo "version=$(npx playwright --version)" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-${{ hashFiles('mlflow/server/js/yarn.lock') }} | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| - name: Wait for Konflux build to complete | |
| if: github.event_name != 'workflow_dispatch' | |
| timeout-minutes: 30 | |
| working-directory: . | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| COMMIT_SHA: ${{ needs.gate.outputs.head-sha }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| echo "Waiting for Konflux build to complete for commit: $COMMIT_SHA" | |
| STATUS="pending" | |
| CONCLUSION="" | |
| for i in $(seq 1 40); do | |
| if ! RESPONSE=$(timeout 15 gh api "repos/${REPO}/commits/${COMMIT_SHA}/check-runs" 2>/dev/null); then | |
| echo "API call failed, retrying in 30s... (attempt $i/40)" | |
| sleep 30 | |
| continue | |
| fi | |
| RESULT=$(echo "$RESPONSE" | jq -c ' | |
| [.check_runs[] | select(.name | startswith("mlflow-on-pull-request"))] | |
| | sort_by(.started_at) | |
| | last // {} | |
| ') | |
| STATUS=$(echo "$RESULT" | jq -r '.status // "pending"') | |
| CONCLUSION=$(echo "$RESULT" | jq -r '.conclusion // ""') | |
| if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then | |
| echo "Konflux build succeeded after $((i * 30))s" | |
| break | |
| elif [ "$STATUS" = "completed" ] && [ "$CONCLUSION" != "success" ]; then | |
| echo "Konflux build failed with conclusion: $CONCLUSION" | |
| exit 1 | |
| fi | |
| echo "Konflux build: status=$STATUS, retrying in 30s... (attempt $i/40)" | |
| sleep 30 | |
| done | |
| if [ "$STATUS" != "completed" ]; then | |
| ELAPSED_MIN=$(( i * 30 / 60 )) | |
| echo "Konflux build did not complete after ${ELAPSED_MIN} minutes" | |
| exit 1 | |
| fi | |
| - name: Pull Konflux PR image | |
| working-directory: . | |
| run: | | |
| echo "Pulling image: $MLFLOW_IMAGE" | |
| for attempt in $(seq 1 5); do | |
| if docker pull "$MLFLOW_IMAGE"; then | |
| echo "Image pulled successfully on attempt $attempt" | |
| exit 0 | |
| fi | |
| echo "Pull failed, retrying in 15s... (attempt $attempt/5)" | |
| sleep 15 | |
| done | |
| echo "::error::Failed to pull $MLFLOW_IMAGE after 5 attempts" | |
| exit 1 | |
| - name: Start MLflow stack | |
| working-directory: docker-compose | |
| env: | |
| MLFLOW_EXTRA_ARGS: "--enable-workspaces" | |
| run: | | |
| cp .env.dev.example .env | |
| docker compose up -d --wait --wait-timeout 120 | |
| - name: Verify MLflow is serving | |
| run: | | |
| curl --retry 5 --retry-delay 5 --retry-all-errors --fail \ | |
| "$MLFLOW_E2E_BASE_URL/api/2.0/mlflow/experiments/search" | |
| - name: Run e2e tests | |
| run: yarn test:e2e | |
| - name: Upload test report | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: e2e-test-report | |
| path: mlflow/server/js/e2e/test-report/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: failure() | |
| with: | |
| name: e2e-test-results | |
| path: mlflow/server/js/e2e/test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Dump docker compose logs | |
| if: failure() | |
| working-directory: docker-compose | |
| run: docker compose logs --no-color --tail=500 2>&1 | tee docker-compose-logs.txt | |
| - name: Upload docker compose logs | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: failure() | |
| with: | |
| name: docker-compose-logs | |
| path: docker-compose/docker-compose-logs.txt | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Stop MLflow stack | |
| if: always() | |
| continue-on-error: true | |
| working-directory: docker-compose | |
| run: docker compose down -v --remove-orphans | |
| report-status: | |
| if: always() && github.event_name != 'workflow_dispatch' && needs.gate.result != 'skipped' | |
| needs: | |
| - gate | |
| - e2e | |
| permissions: | |
| statuses: write | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Set status on PR | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| STATUS_SHA: ${{ needs.gate.outputs.head-sha }} | |
| PR_NUMBER: ${{ needs.gate.outputs.pr-number }} | |
| GATE_RESULT: ${{ needs.gate.result }} | |
| GATE_SHOULD_RUN: ${{ needs.gate.outputs.should-run }} | |
| E2E_RESULT: ${{ needs.e2e.result }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$GATE_RESULT" != "success" ]]; then | |
| STATE="failure" | |
| DESC="E2E gate failed" | |
| elif [[ "$GATE_SHOULD_RUN" != "true" ]]; then | |
| STATE="pending" | |
| DESC="Waiting for ok-to-test label on PR #${PR_NUMBER}" | |
| elif [[ "$E2E_RESULT" == "success" ]]; then | |
| STATE="success" | |
| DESC="E2E tests passed" | |
| elif [[ "$E2E_RESULT" == "skipped" ]]; then | |
| STATE="pending" | |
| DESC="E2E tests were skipped" | |
| elif [[ "$E2E_RESULT" == "cancelled" ]]; then | |
| STATE="pending" | |
| DESC="E2E tests were cancelled" | |
| else | |
| STATE="failure" | |
| DESC="E2E tests failed" | |
| fi | |
| gh api "repos/${REPO}/statuses/${STATUS_SHA}" \ | |
| -f state="$STATE" \ | |
| -f context="E2E Tests" \ | |
| -f description="$DESC" \ | |
| -f target_url="${RUN_URL}" |