name: Playwright UI Tests on: push: branches: - "main" pull_request: branches: - "**" merge_group: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.merge_group.head_ref || github.ref }} cancel-in-progress: true env: COLUMNS: 150 ZO_ROOT_USER_EMAIL: root@example.com ZO_ROOT_USER_PASSWORD: Complexpass#123 ZO_BASE_URL: http://localhost:5080 WS_ZO_BASE_URL: ws://localhost:5080 ZO_BASE_URL_SC: http://localhost:5080 ZO_BASE_URL_SC_UI: http://localhost:5080 INGESTION_URL: http://localhost:5080 ORGNAME: default ZO_WEB_URL: "http://localhost:5080" ZO_QUICK_MODE_NUM_FIELDS: 100 ZO_QUICK_MODE_STRATEGY: first ZO_ALLOW_USER_DEFINED_SCHEMAS: true ZO_INGEST_ALLOWED_UPTO: 5 ZO_FEATURE_QUERY_EXCLUDE_ALL: false ZO_USAGE_BATCH_SIZE: 200 ZO_USAGE_PUBLISH_INTERVAL: 2 ZO_USAGE_REPORTING_ENABLED: true ZO_MIN_AUTO_REFRESH_INTERVAL: 5 ZO_STREAMING_ENABLED: true ZO_COLS_PER_RECORD_LIMIT: "80000" ZO_SMTP_ENABLED: true ZO_FORMAT_STREAM_NAME_TO_LOWERCASE: false ZO_CREATE_ORG_THROUGH_INGESTION: true ZO_UTF8_VIEW_ENABLED: false ZO_ENABLE_CROSS_LINKING: true # Timechart (visualize) tab is hidden by default; enable it so visualize E2E tests can reach it. ZO_TIMECHART_ENABLED: true # Allow alert destinations to target localhost so the test validation stream # (http://localhost:5080/api/.../..._json) is not blocked by the SSRF guard. ZO_SSRF_ALLOW_LOOPBACK: true ZO_MODEL_PRICING_ENABLED: true UPLOAD_TO_TESTDINO: ${{ vars.UPLOAD_TO_TESTDINO || 'false' }} jobs: check_changes: timeout-minutes: 5 name: check_changes runs-on: ubuntu-latest permissions: contents: read outputs: has_changes: ${{ steps.check.outputs.has_changes }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - id: check run: | CHANGED=$(git diff --name-only origin/${{ github.base_ref || 'main' }} ${{ github.sha }}) if [ -z "$CHANGED" ]; then echo "has_changes=true" >> $GITHUB_OUTPUT elif echo "$CHANGED" | grep -qvE '(\.md$|^docs/)'; then echo "has_changes=true" >> $GITHUB_OUTPUT else echo "has_changes=false" >> $GITHUB_OUTPUT fi build_binary: timeout-minutes: 45 name: build_binary needs: [check_changes] if: needs.check_changes.outputs.has_changes == 'true' runs-on: labels: ubicloud-standard-16 permissions: contents: read steps: - name: Remove unused tools run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Clone the current repo uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup Rust Toolchain uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2026-05-20 targets: x86_64-unknown-linux-gnu - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true shared-key: playwright-release-ci save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install Protoc run: | bash ./.github/protoc.sh - uses: actions/setup-node@v5 with: node-version: 22 - name: Lint frontend code run: cd web && npm ci && npm run lint:ci - name: Build binary and frontend env: NODE_OPTIONS: "--max-old-space-size=8192" GIT_HASH_OVERRIDE: ${{ github.sha }} run: | bash -c "cd web && npm run build && cp -r ./dist /tmp/dist" & sed -i 's|folder = "web/dist/"|folder = "/tmp/dist/"|' src/handler/http/router/ui.rs cargo build --profile release-ci --target x86_64-unknown-linux-gnu wait - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: release-ci-binary path: target/x86_64-unknown-linux-gnu/release-ci/openobserve retention-days: 1 - name: Upload frontend artifact uses: actions/upload-artifact@v4 with: name: frontend-dist path: /tmp/dist retention-days: 1 if-no-files-found: error ui_integration_tests: timeout-minutes: 45 name: e2e / ${{ matrix.testfolder }} needs: [build_binary] if: ${{ !cancelled() && needs.build_binary.result == 'success' }} runs-on: labels: ubicloud-standard-8 permissions: contents: read actions: read # container: # image: mcr.microsoft.com/playwright:v1.50.0-jammy # options: --user root strategy: fail-fast: false matrix: include: - testfolder: "GeneralTests" browser: "chrome" run_files: [ "sanity.spec.js", "changeOrg.spec.js", "enrichment.spec.js", "schema.spec.js", "schemaload.spec.js", "serviceAccount.spec.js", "usersOrg.spec.js", "org.spec.js", "theme-management.spec.js", "ingestion-config.spec.js", "ingestionTokens.spec.js", "landingPage.spec.js", "languageTranslation.spec.js", "model-pricing.spec.js", "edition-features.spec.js", "logsqueries.sql-comments.spec.js", "iam-form-validation.spec.js", "regex-patterns-form-validation.spec.js", "cipher-keys-form-validation.spec.js", "settings-form-validation.spec.js", "ai-toolsets-form-validation.spec.js", "shared-components-form-validation.spec.js", "onboarding-form-validation.spec.js", "rum-form-validation.spec.js", ] - testfolder: "Logs-Builder-Basic" browser: "chrome" run_files: [ "logsQueryBuilder-chart.spec.js", "logsQueryBuilder-filters-basic.spec.js", "pagination.spec.js", "unflattened.spec.js", ] - testfolder: "Logs-Builder-Advanced" browser: "chrome" run_files: [ "logsQueryBuilder-editor.spec.js", "logsQueryBuilder-filters-advanced.spec.js", "logsquickmode.spec.js", "logshistogram.spec.js", ] - testfolder: "Logs-Core" browser: "chrome" run_files: [ "logspage.spec.js", "logstable.spec.js", "logsqueries.spec.js", "join.spec.js", "shareLink.spec.js", "logsAnalyzeDimensions.spec.js", "monaco-query-prefill.spec.js", "logsqueries.matchall.spec.js", ] - testfolder: "Logs-Features" browser: "chrome" run_files: [ "logs-autocomplete-suggestions.spec.js", # "searchJobInspector.spec.js", # TODO: update these tests for new correlation feature "logs-sql-autocomplete.spec.js", "logsqueries.cte.spec.js", "logsDownloads.spec.js", "secondsPrecisionAdded.spec.js", "searchpartition.spec.js", "indexquery.spec.js", "region.spec.js", ] - testfolder: "Alerts" browser: "chrome" run_files: [ # "alerts-e2e-flow.spec.js", "alerts-ui-operations.spec.js", "alerts-import.spec.js", # "alerts-advanced.spec.js", # "alerts-scheduled-features.spec.js", "alerts-metrics-notification.spec.js", "alerts-destinations-prebuilt.spec.js", "alerts-vrl-encoding.spec.js", "alerts-form-validation.spec.js", "anomaly-detection-form-validation.spec.js", ] - testfolder: "Dashboards-Core" browser: "chrome" run_files: [ "dashboard.spec.js", "dashboard2.spec.js", "dashboardtype.spec.js", "dashboard-folder.spec.js", "dashboard-import.spec.js", "maxquery.spec.js", "dashboard-chartJson.spec.js", "dashboard-promql-suggestions.spec.js", "dashboard-sql-autocomplete.spec.js", "dashboard-share-link.spec.js", "crossLinking.spec.js", "dashboard-multi-sql.spec.js", "crossLinkMultiStream.spec.js", "dashboards-form-validation.spec.js", ] - testfolder: "Dashboards-Settings" browser: "chrome" run_files: [ "dashboard-filter.spec.js", "dashboard-general-setting.spec.js", "dashboard-tabs-setting.spec.js", "dashboard-transpose.spec.js", "dashboard-join.spec.js", "dashboard-nested-functions.spec.js", "dashboard-legends-copy.spec.js", "dashboard-raw-query.spec.js", ] - testfolder: "Dashboards-Charts" browser: "chrome" run_files: [ "custom-charts.spec.js", "dashboard-geoMap.spec.js", "dashboard-maps.spec.js", "dashboard-multi-y-axis.spec.js", "dashboard-html-chart.spec.js", "visualize.spec.js", "visualize-vrl.spec.js", "dashboard-create-alert.spec.js", "dashboard-sankey.spec.js", "dashboard-pie-donut.spec.js", "dashboard-table-chart.spec.js", "dashboard-table-copy-cell.spec.js", "dashboard-table-csv-download.spec.js", "dashboard-metric-camelcase.spec.js", ] - testfolder: "Dashboards-Streaming" browser: "chrome" run_files: ["dashboard-streaming.spec.js"] - testfolder: "Pipelines" browser: "chrome" run_files: [ "pipeline-conditions.spec.js", "pipeline-conditions-validation.spec.js", "pipelines.spec.js", "pipeline-dynamic.spec.js", "pipeline-core.spec.js", "pipeline-traces.spec.js", "pipeline-metrics.spec.js", "pipeline-backfill.spec.js", "pipeline-history.spec.js", "scheduled-pipeline-query-builder.spec.js", # "remotepipeline.spec.js" "pipelines-form-validation.spec.js", ] - testfolder: "Functions" browser: "chrome" run_files: [ "js-transform-type.spec.js", "row-expansion.spec.js", "enrichment-table-url.spec.js", "functions-form-validation.spec.js", "action-scripts-form-validation.spec.js", ] - testfolder: "Reports" browser: "chrome" run_files: [ "reportsScheduleNow.spec.js", "reportsScheduleLater.spec.js", "reportFolders.spec.js", "reports-form-validation.spec.js", ] - testfolder: "Streams" browser: "chrome" run_files: [ "multiselect-stream.spec.js", "streamname.spec.js", "streaming.spec.js", "stream-settings.spec.js", "streams-form-validation.spec.js", ] - testfolder: "Traces" browser: "chrome" run_files: [ "service-catalog.spec.js", "tracesSearch.spec.js", "traceQueryEditor.spec.js", "traceErrorFilter.spec.js", "traceDetails.spec.js", "traceAdvancedFiltering.spec.js", "tracesAnalyzeDimensions.spec.js", "traces-autocomplete-suggestions.spec.js", ] - testfolder: "Metrics" browser: "chrome" run_files: [ "metrics.spec.js", "metrics-queries.spec.js", "metrics-aggregations.spec.js", "metrics-advanced.spec.js", "metrics-config-tabs.spec.js", "metrics-visualizations.spec.js", "metrics-config.spec.js", "metrics-promql-query-persistence.spec.js", "metrics-table-column-order.spec.js", "metrics-promql-builder.spec.js", "promqlAutocomplete.spec.js", "metrics-share-deep-link.spec.js", ] - testfolder: "Dashboards-Variables" browser: "chrome" run_files: [ "dashboard-variables-setting.spec.js", "dashboard-variables-global.spec.js", "dashboard-variables-tab-level.spec.js", "dashboard-variables-panel-level.spec.js", "dashboard-variables-dependency.spec.js", "dashboard-variables-refresh.spec.js", "dashboard-variables-url-sync.spec.js", "dashboard-variables-creation-scopes.spec.js", "dashboard-variables-default-values-chain.spec.js", "dashboard-variables-custom-parents.spec.js", "dashboard-variables-stream-field.spec.js", "dashboard-mustache-variables.spec.js", ] - testfolder: "Dashboard-Table-Pagination" browser: "chrome" run_files: ["dashboard-table-pagination.spec.js"] - testfolder: "Dashboard-Pivot-Table" browser: "chrome" run_files: ["dashboard-pivot-table.spec.js"] - testfolder: "Dashboards-Panel-Level-DateTime-Config" browser: "chrome" run_files: [ "dashboard-panel-time-config-behavior.spec.js", "dashboard-panel-time-url-priority.spec.js", "dashboard-panel-time-advanced-edge-cases.spec.js", "dashboard-panel-time-apply-behavior.spec.js", "dashboard-panel-time-variables-behavior.spec.js", ] - testfolder: "Dashboard-Config-Settings" browser: "chrome" run_files: [ "dashboard-series-color-multiwindow.spec.js", "dashboard-config-advanced.spec.js", "dashboard-config-axis.spec.js", "dashboard-config-gauge-maps.spec.js", "dashboard-config-general.spec.js", "dashboard-config-legends.spec.js", "dashboard-config-line-style.spec.js", "dashboard-config-panel-time.spec.js", "dashboard-config-table.spec.js", "dashboard-config-trellis.spec.js", "dashboard-config-drilldown.spec.js", "dashboard-config-promql.spec.js", "dashboard-config-markline.spec.js", ] steps: - name: Kill background apt processes run: | sudo systemctl stop apt-daily.service apt-daily-upgrade.service unattended-upgrades.service 2>/dev/null || true sudo systemctl stop apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true sudo systemctl disable apt-daily.service apt-daily-upgrade.service unattended-upgrades.service 2>/dev/null || true sudo flock --wait 30 /var/lib/dpkg/lock-frontend true 2>/dev/null || true - name: Clone the current repo uses: actions/checkout@v5 with: fetch-depth: 0 - name: Download binary artifact uses: actions/download-artifact@v5 with: name: release-ci-binary path: release-ci-binary - name: Download frontend artifact uses: actions/download-artifact@v5 with: name: frontend-dist path: /tmp/dist - name: Start OpenObserve run: chmod +x ./release-ci-binary/openobserve && ./release-ci-binary/openobserve > o2.log 2>&1 & - name: Wait for start run: | for i in $(seq 1 120); do if curl -sf http://localhost:5080/web/login > /dev/null 2>&1; then echo "Server ready after ${i}s" exit 0 fi sleep 1 done echo "Server failed to start in 120s" exit 1 - name: Ensure we are getting a reply from the server run: curl http://localhost:5080/web/login - uses: actions/setup-node@v5 with: node-version: 22 - name: Cache Playwright browser id: playwright-cache uses: actions/cache@v5 with: path: ~/.cache/ms-playwright key: playwright-chromium-${{ runner.os }}-${{ hashFiles('tests/ui-testing/package-lock.json') }} restore-keys: | playwright-chromium-${{ runner.os }}- - name: Check if this is a rerun id: check_rerun env: GH_TOKEN: ${{ github.token }} run: | echo "::notice::GitHub run attempt: ${{ github.run_attempt }}" if [ "${{ github.run_attempt }}" -gt "1" ]; then # This is a rerun - always try TestDino optimization echo "is_rerun=true" >> $GITHUB_OUTPUT echo "::notice::This is a rerun (attempt ${{ github.run_attempt }}). Will attempt to run only failed tests." # Additionally detect if this is "re-run all" vs "re-run failed" for better handling echo "::group::Detecting rerun type (all jobs vs failed only)" CURRENT_ATTEMPT=${{ github.run_attempt }} # Get all jobs from the run # Fallback to empty jobs array on network/rate-limit errors so the step # never fails due to a transient GitHub API issue. ALL_JOBS_RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs?filter=all&per_page=100" \ || echo '{"jobs":[]}') # Count jobs that are NOT completed (actually running in this attempt) # Fall back to "0" if jq fails (e.g. API returned an error body instead of JSON). CURRENT_ATTEMPT_JOBS=$(echo "$ALL_JOBS_RESPONSE" | jq -r "[.jobs[] | select(.name | startswith(\"e2e /\")) | select(.run_attempt == $CURRENT_ATTEMPT) | select(.status != \"completed\")] | length" 2>/dev/null || echo "0") # Count total unique matrix job names (from attempt 1) TOTAL_MATRIX_JOBS=$(echo "$ALL_JOBS_RESPONSE" | jq -r "[.jobs[] | select(.name | startswith(\"e2e /\")) | select(.run_attempt == 1) | .name] | unique | length" 2>/dev/null || echo "0") echo "::notice::Total unique matrix job names: $TOTAL_MATRIX_JOBS" echo "::notice::Jobs running in current attempt $CURRENT_ATTEMPT: $CURRENT_ATTEMPT_JOBS" # Store rerun type for potential use. # Guard against non-numeric jq output (e.g. "null") to prevent bash # "integer expression expected" errors under set -eo pipefail. if [ "${CURRENT_ATTEMPT_JOBS:-0}" -eq "${TOTAL_MATRIX_JOBS:-0}" ] 2>/dev/null; then echo "::notice::✅ DETECTED: Re-run ALL jobs (all $TOTAL_MATRIX_JOBS matrix jobs running)" echo "rerun_type=all" >> $GITHUB_OUTPUT else echo "::notice::✅ DETECTED: Re-run FAILED jobs only ($CURRENT_ATTEMPT_JOBS out of $TOTAL_MATRIX_JOBS jobs)" echo "rerun_type=failed" >> $GITHUB_OUTPUT fi echo "::endgroup::" else echo "is_rerun=false" >> $GITHUB_OUTPUT echo "rerun_type=none" >> $GITHUB_OUTPUT echo "::notice::This is the first run. Will run all tests in matrix." fi - name: Install dependencies and run ui-tests env: TESTDINO_TOKEN: ${{ secrets.TESTDINO_API_TOKEN }} GH_TOKEN: ${{ github.token }} run: | touch .env echo "ZO_ROOT_USER_EMAIL=${ZO_ROOT_USER_EMAIL}" >> .env echo "ZO_ROOT_USER_PASSWORD=${ZO_ROOT_USER_PASSWORD}" >> .env echo "ZO_BASE_URL=${ZO_BASE_URL}" >> .env echo "WS_ZO_BASE_URL=${WS_ZO_BASE_URL}" >> .env echo "ZO_BASE_URL_SC=${ZO_BASE_URL_SC}" >> .env echo "ZO_BASE_URL_SC_UI=${ZO_BASE_URL_SC_UI}" >> .env echo "INGESTION_URL=${INGESTION_URL}" >> .env echo "ORGNAME=${ORGNAME}" >> .env echo "ZO_SMTP_ENABLED=${ZO_SMTP_ENABLED}" >> .env mv .env tests/ui-testing cd tests/ui-testing && npm ci # Install apt system dependencies (always needed) npx playwright install-deps chromium # Get revision from playwright's own metadata CHROMIUM_REV=$(node -pe " require('./node_modules/playwright-core/browsers.json') .browsers.find(b => b.name === 'chromium').revision ") install_browser() { local NAME="$1" local BINARY="$2" local ZIP_PATH="$3" local DIR="$HOME/.cache/ms-playwright/${NAME}-${CHROMIUM_REV}" if [ -f "${DIR}/INSTALLATION_COMPLETE" ]; then echo "${NAME} already installed (cache hit)"; return 0 fi rm -rf "${DIR}" echo "Cache miss — downloading ${NAME} r${CHROMIUM_REV} via curl + system unzip" curl -fL --progress-bar \ "https://cdn.playwright.dev/dbazure/download/playwright/builds/${ZIP_PATH}" \ -o "/tmp/${NAME}.zip" mkdir -p "${DIR}" unzip -q "/tmp/${NAME}.zip" -d "${DIR}" rm -f "/tmp/${NAME}.zip" find "${DIR}" -name "${BINARY}" -type f -exec chmod 755 {} \; find "${DIR}" -name 'chrome_sandbox' -type f -exec chmod 4755 {} \; 2>/dev/null || true touch "${DIR}/INSTALLATION_COMPLETE" echo "${NAME} installed: $(ls ${DIR}/)" } install_browser "chromium" "chrome" \ "chromium/${CHROMIUM_REV}/chromium-linux.zip" install_browser "chromium_headless_shell" "headless_shell" \ "chromium/${CHROMIUM_REV}/chromium-headless-shell-linux.zip" # ffmpeg is required for video recording (video: retain-on-failure in playwright.config.js) # The custom install_browser uses CHROMIUM_REV which doesn't match ffmpeg's own revision, # so use the local playwright binary (pinned via package-lock.json) to install ffmpeg only. ./node_modules/.bin/playwright install ffmpeg # Check if this is a rerun IS_RERUN="${{ steps.check_rerun.outputs.is_rerun }}" RERUN_TYPE="${{ steps.check_rerun.outputs.rerun_type }}" if [ "$IS_RERUN" = "true" ]; then # Check if this is "re-run all" - if so, skip TestDino and run all tests if [ "$RERUN_TYPE" = "all" ]; then echo "::notice::🔄 Re-run ALL jobs detected - skipping TestDino optimization" echo "::notice::Will run ALL tests in this shard regardless of previous failures" RERUN_MODE="normal" EXTRA_PW_FLAGS="" else # This is "re-run failed" - use TestDino optimization echo "::group::Fetching last failed tests from TestDino" # Use custom cache ID that includes run_id to distinguish separate workflow runs # Format: gh_openobserve_main_12345678 (for main) or gh_openobserve_feature-branch_12345678 (for PRs) # Sanitize branch name by replacing slashes with underscores BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//_/g') CACHE_ID="gh_openobserve_${BRANCH_NAME}_${{ github.run_id }}" echo "::notice::Using cache ID: $CACHE_ID" # Try to get last failed tests from TestDino # Use a temp file to avoid shell variable truncation for large output TEMP_OUTPUT="/tmp/testdino_output_$$.txt" if npx tdpw last-failed --cache-id "$CACHE_ID" > "$TEMP_OUTPUT" 2>&1; then # Filter out ALL lines that don't match test file pattern # Valid format: "FolderName/testfile.spec.js" or "FolderName/testfile.spec.js -g test name" # This filters out debug lines, empty lines, and anything that's not a test path LAST_FAILED_FLAGS="$(grep -E '^[A-Za-z_-]+/[a-zA-Z0-9_-]+\.spec\.js' "$TEMP_OUTPUT" | tail -1)" rm -f "$TEMP_OUTPUT" if [ -n "$LAST_FAILED_FLAGS" ] && [ "$LAST_FAILED_FLAGS" != "No failed tests found" ]; then echo "::notice::✅ Found failed tests from previous run." echo "::notice::TestDino flags: $LAST_FAILED_FLAGS" RERUN_MODE="optimized" EXTRA_PW_FLAGS="$LAST_FAILED_FLAGS" else echo "::warning::TestDino returned no failed tests. Falling back to running all tests in this matrix job." echo "::warning::This means you'll be billed for all tests. Please investigate why last-failed returned empty." RERUN_MODE="fallback" EXTRA_PW_FLAGS="" fi else echo "::error::Failed to fetch last failed tests from TestDino (exit code: $?)." echo "::warning::Falling back to running all tests in this matrix job." echo "::warning::This means you'll be billed for all tests. Please check TestDino integration." RERUN_MODE="fallback" EXTRA_PW_FLAGS="" fi echo "::endgroup::" fi else RERUN_MODE="normal" EXTRA_PW_FLAGS="" fi # Get list of files to run using join function FILE_LIST="${{ join(matrix.run_files, ' ') }}" echo "DEBUG: FILE_LIST = $FILE_LIST" echo "DEBUG: matrix.testfolder = ${{ matrix.testfolder }}" if [ -n "$FILE_LIST" ]; then # Map logical folder names to actual directories case "${{ matrix.testfolder }}" in "Logs-Builder-Basic"|"Logs-Builder-Advanced"|"Logs-Core"|"Logs-Features") ACTUAL_FOLDER="Logs" ;; "Dashboards-Core"|"Dashboards-Settings"|"Dashboards-Charts"|"Dashboards-Streaming"|"Dashboards-Variables"|"Dashboard-Table-Pagination"|"Dashboard-Pivot-Table"|"Dashboards-Panel-Level-DateTime-Config"|"Dashboard-Config-Settings") ACTUAL_FOLDER="Dashboards" ;; *) ACTUAL_FOLDER="${{ matrix.testfolder }}" ;; esac echo "DEBUG: ACTUAL_FOLDER = $ACTUAL_FOLDER" # Build file paths FILE_PATHS="" for file in $FILE_LIST; do FILE_PATH="./playwright-tests/$ACTUAL_FOLDER/$file" echo "DEBUG: Will run file: $FILE_PATH" FILE_PATHS="$FILE_PATHS $FILE_PATH" done # Determine what to run based on rerun mode if [ "$RERUN_MODE" = "optimized" ]; then # TestDino returns ALL failed tests from the previous run # Format examples: # Single: "SDR/test.spec.js -g test name" # Multiple: "Logs/test1.spec.js -g name1 Pipelines/test2.spec.js -g name2" echo "DEBUG: Full TestDino output (all failed tests): $EXTRA_PW_FLAGS" echo "DEBUG: Current shard folder: $ACTUAL_FOLDER" # Strategy: Extract only tests that start with "$ACTUAL_FOLDER/" # Use grep to find lines matching our shard folder SHARD_TESTS="" # Check if the TestDino output contains tests for this shard (case-insensitive) # Look for pattern: $ACTUAL_FOLDER/something.spec.js if echo "$EXTRA_PW_FLAGS" | grep -iq "$ACTUAL_FOLDER/.*\.spec\.js"; then echo "DEBUG: Found test(s) for this shard in TestDino output" # IMPORTANT: Based on observation, TestDino returns ONE test at a time # Format: "Folder/file.spec.js -g test name" # Simply check if it starts with our folder and pass it through # Extract file path (everything before -g or end of string) - case-insensitive TEST_FILE=$(echo "$EXTRA_PW_FLAGS" | grep -ioE "$ACTUAL_FOLDER/[^ ]+\.spec\.js" | head -1) if [ -n "$TEST_FILE" ]; then # Check if file actually starts with our folder (case-insensitive) if [[ "${TEST_FILE,,}" == "${ACTUAL_FOLDER,,}/"* ]]; then # Extract just the filename from the test file path TEST_FILENAME=$(basename "$TEST_FILE") # Check if this file is actually in the current shard's run_files list if echo "$FILE_LIST" | grep -qw "$TEST_FILENAME"; then # Parse out the file path and test name separately for proper quoting # Extract file path (before -g) TEST_FILE_PATH=$(echo "$EXTRA_PW_FLAGS" | sed "s/ -g.*//" | sed "s|^|./playwright-tests/|") # Check if there's a -g flag if echo "$EXTRA_PW_FLAGS" | grep -q " -g "; then # Extract test name (after -g) and strip any surrounding quotes TEST_NAME=$(echo "$EXTRA_PW_FLAGS" | sed 's/.*-g //' | sed 's/^"\(.*\)"$/\1/') echo "DEBUG: File path: $TEST_FILE_PATH" echo "DEBUG: Test name (unquoted): $TEST_NAME" echo "::notice::🎯 OPTIMIZED RERUN: Running failed test from this shard" echo "DEBUG: Final command: npx playwright test \"$TEST_FILE_PATH\" -g \"$TEST_NAME\"" # Properly quote the test name to handle spaces and special chars npx playwright test "$TEST_FILE_PATH" -g "$TEST_NAME" else # No -g flag, just run the file echo "DEBUG: File path: $TEST_FILE_PATH (no -g flag)" echo "::notice::🎯 OPTIMIZED RERUN: Running failed test file from this shard" echo "DEBUG: Final command: npx playwright test \"$TEST_FILE_PATH\"" npx playwright test "$TEST_FILE_PATH" fi else echo "::notice::⏭️ Test file '$TEST_FILENAME' not in this shard's run_files list." echo "::notice::Checking if this shard was cancelled/abruptly ended in previous attempt..." # Check the status of this matrix job in the previous attempt PREV_ATTEMPT=$((${{ github.run_attempt }} - 1)) if [ "$PREV_ATTEMPT" -ge 1 ]; then JOB_NAME="e2e / ${{ matrix.testfolder }}" API_RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/$PREV_ATTEMPT/jobs" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$API_RESPONSE" ]; then PREV_JOB_STATUS=$(echo "$API_RESPONSE" | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .conclusion" 2>/dev/null) if [ "$PREV_JOB_STATUS" = "cancelled" ] || [ "$PREV_JOB_STATUS" = "null" ] || [ -z "$PREV_JOB_STATUS" ]; then echo "::warning::⚠️ Previous shard was cancelled/abruptly ended - running FULL test suite" npx playwright test $FILE_PATHS exit $? elif [ "$PREV_JOB_STATUS" = "success" ]; then echo "::notice::✅ Previous shard completed successfully - no tests to rerun" exit 0 elif [ "$PREV_JOB_STATUS" = "failure" ]; then echo "::warning::⚠️ Previous shard failed but TestDino has no failures recorded - running FULL test suite" npx playwright test $FILE_PATHS exit $? else echo "::warning::⚠️ Unknown job status - running FULL test suite for safety" npx playwright test $FILE_PATHS exit $? fi else echo "::error::Failed to fetch job status from GitHub API - running FULL test suite" npx playwright test $FILE_PATHS exit $? fi else echo "::notice::✅ This is the first run, no previous attempt to check" echo "::notice::No failed tests to rerun. Exiting successfully." exit 0 fi fi else echo "::notice::⏭️ Test file doesn't match shard folder exactly. Skipping." exit 0 fi else echo "::warning::Failed to extract test file path. Falling back to skip." exit 0 fi else echo "::notice::⏭️ OPTIMIZED RERUN: No failed tests found for '$ACTUAL_FOLDER' shard in TestDino output" echo "::notice::Checking if this shard was cancelled/abruptly ended in previous attempt..." # Check the status of this matrix job in the previous attempt # Rule: If shard was cancelled/abruptly ended → treat as failed → run full test suite PREV_ATTEMPT=$((${{ github.run_attempt }} - 1)) if [ "$PREV_ATTEMPT" -ge 1 ]; then # Job name format: "e2e / {matrix.testfolder}" JOB_NAME="e2e / ${{ matrix.testfolder }}" echo "DEBUG: Checking previous attempt #$PREV_ATTEMPT for job: $JOB_NAME" # Use GitHub REST API to get job status from specific attempt # This is guaranteed to work and provides attempt-specific data API_RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/$PREV_ATTEMPT/jobs" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$API_RESPONSE" ]; then # Successfully got API response, parse it PREV_JOB_STATUS=$(echo "$API_RESPONSE" | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .conclusion" 2>/dev/null) echo "DEBUG: Previous job status: $PREV_JOB_STATUS" if [ "$PREV_JOB_STATUS" = "cancelled" ] || [ "$PREV_JOB_STATUS" = "null" ] || [ -z "$PREV_JOB_STATUS" ]; then echo "::warning::⚠️ Previous shard was cancelled/abruptly ended (status: ${PREV_JOB_STATUS:-not found})" echo "::warning::Treating as FAILED - running FULL test suite for this shard" echo "DEBUG: Running all matrix tests: npx playwright test $FILE_PATHS" npx playwright test $FILE_PATHS exit $? elif [ "$PREV_JOB_STATUS" = "success" ]; then echo "::notice::✅ Previous shard completed successfully - all tests passed" echo "::notice::No failed tests to rerun. Exiting successfully." exit 0 elif [ "$PREV_JOB_STATUS" = "failure" ]; then echo "::warning::⚠️ Previous shard failed but TestDino has no failures recorded" echo "::warning::This shouldn't happen - running FULL test suite for safety" echo "DEBUG: Running all matrix tests: npx playwright test $FILE_PATHS" npx playwright test $FILE_PATHS exit $? else echo "::warning::⚠️ Unknown job status: $PREV_JOB_STATUS" echo "::warning::Running FULL test suite for safety" echo "DEBUG: Running all matrix tests: npx playwright test $FILE_PATHS" npx playwright test $FILE_PATHS exit $? fi else echo "::error::Failed to fetch job status from GitHub API" echo "::warning::Falling back to running FULL test suite for safety" echo "DEBUG: Running all matrix tests: npx playwright test $FILE_PATHS" npx playwright test $FILE_PATHS exit $? fi else echo "::notice::✅ This is the first run, no previous attempt to check" echo "::notice::No failed tests to rerun. Exiting successfully." exit 0 fi fi else # Normal run or fallback if [ "$RERUN_MODE" = "fallback" ]; then echo "::warning::⚠️ FALLBACK RERUN: Running all tests in matrix (TestDino optimization failed)" fi echo "DEBUG: Final command: npx playwright test $FILE_PATHS" npx playwright test $FILE_PATHS fi else echo "No files specified to run for ${{ matrix.testfolder }} folder" fi - name: Upload blob report to GitHub Actions Artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.testfolder }}-attempt-${{ github.run_attempt }} path: tests/ui-testing/blob-report retention-days: 1 - name: Check OpenObserve logs if: always() run: cat o2.log merge_and_upload_reports: timeout-minutes: 15 name: Merge Reports and Upload to TestDino needs: [ui_integration_tests] if: ${{ !cancelled() && needs.ui_integration_tests.result != 'skipped' }} runs-on: labels: ubicloud-standard-4 permissions: contents: read actions: read steps: - name: Clone the current repo uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: "22" - name: Install project dependencies run: cd tests/ui-testing && npm ci - name: Download blob reports from GitHub Actions Artifacts uses: actions/download-artifact@v4 continue-on-error: true id: download_blobs with: path: tests/ui-testing/all-blob-reports pattern: blob-report-*-attempt-${{ github.run_attempt }} merge-multiple: true - name: Check if any artifacts were downloaded run: | if [ -d "tests/ui-testing/all-blob-reports" ] && [ "$(ls -A tests/ui-testing/all-blob-reports)" ]; then echo "✅ Artifacts downloaded successfully" ls -la tests/ui-testing/all-blob-reports else echo "⚠️ No artifacts found or download failed completely" # Don't fail here - let the Merge Reports step handle this fi - name: Merge Reports env: GH_TOKEN: ${{ github.token }} run: | cd tests/ui-testing # Check if directory exists and contains reports if [ ! -d "all-blob-reports" ] || [ -z "$(ls -A all-blob-reports 2>/dev/null)" ]; then echo "::warning::No blob reports found to merge." # Investigate: Check if all test jobs succeeded (meaning they skipped intentionally) # vs test jobs failed (meaning blob reporter might have failed) echo "::group::Investigating why no blob reports exist" JOBS_RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}/jobs") # Count test matrix jobs by conclusion SUCCEEDED=$(echo "$JOBS_RESPONSE" | jq -r '[.jobs[] | select(.name | startswith("e2e /")) | select(.conclusion == "success")] | length') FAILED=$(echo "$JOBS_RESPONSE" | jq -r '[.jobs[] | select(.name | startswith("e2e /")) | select(.conclusion == "failure")] | length') TOTAL=$(echo "$JOBS_RESPONSE" | jq -r '[.jobs[] | select(.name | startswith("e2e /"))] | length') echo "::notice::Test job results: $SUCCEEDED succeeded, $FAILED failed, $TOTAL total" if [ "$SUCCEEDED" -gt 0 ] && [ "$FAILED" -eq 0 ]; then echo "::notice::✅ All test jobs succeeded - they likely skipped because tests already passed" echo "::notice::This is VALID behavior for optimized reruns" echo "::notice::Creating empty report structure for downstream jobs" mkdir -p playwright-results/html-report # Add stats object to make report compatible with TestDino # startTime must be ISO 8601 string (not Unix timestamp number) for TestDino compatibility START_TIME=$(date -u +%Y-%m-%dT%H:%M:%S.000Z) echo '{"config":{},"suites":[],"errors":[],"stats":{"startTime":"'"$START_TIME"'","duration":0,"expected":0,"unexpected":0,"flaky":0,"skipped":0}}' > playwright-results/report.json echo "

No tests executed - all shards skipped

All test shards exited early because tests had already passed in a previous attempt.

" > playwright-results/html-report/index.html echo "::endgroup::" exit 0 else echo "::error::❌ Some/all test jobs failed but no blob reports were found" echo "::error::This indicates a problem with test execution or blob reporter" echo "::error::Failed jobs: $FAILED, Succeeded jobs: $SUCCEEDED" echo "::endgroup::" exit 1 fi fi # Unset CI to use non-CI reporter config (html + json instead of blob) unset CI npx playwright merge-reports --config playwright.config.js ./all-blob-reports echo "Contents of playwright-results:" ls -la playwright-results/ echo "Contents of html-report:" ls -la playwright-results/html-report/ echo "Checking report.json:" ls -lh playwright-results/report.json - name: Cache test failures to TestDino if: ${{ needs.ui_integration_tests.result == 'failure' }} env: TESTDINO_TOKEN: ${{ secrets.TESTDINO_API_TOKEN }} run: | cd tests/ui-testing echo "::group::Caching test failure metadata to TestDino" if [ -f "playwright-results/report.json" ]; then echo "::notice::JSON report found at playwright-results/report.json" ls -lh playwright-results/report.json # Copy report.json to repo root to ensure correct git metadata mkdir -p ../../playwright-results cp playwright-results/report.json ../../playwright-results/ # Run tdpw cache from repo root directory (for correct git metadata) cd ../../playwright-results # Use custom cache ID that includes run_id to distinguish separate workflow runs # Format: gh_openobserve_main_12345678 (for main) or gh_openobserve_feature-branch_12345678 (for PRs) # Sanitize branch name by replacing slashes with underscores BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//_/g') CACHE_ID="gh_openobserve_${BRANCH_NAME}_${{ github.run_id }}" echo "::notice::Using cache ID: $CACHE_ID" # Suppress verbose debug output from TestDino, only show result if CACHE_OUTPUT=$(npx --yes tdpw cache --cache-id "$CACHE_ID" 2>&1); then # Extract only the success message, filter out debug spam SUCCESS_MSG=$(echo "$CACHE_OUTPUT" | grep "Cache data submitted successfully" || echo "") if [ -n "$SUCCESS_MSG" ]; then echo "::notice::✅ Successfully cached test failure metadata to TestDino cloud" else echo "::notice::✅ Cache command completed" fi else echo "::warning::⚠️ Failed to cache test metadata to TestDino" echo "::warning::Rerun optimization may not work properly" # Show output only on failure for debugging echo "$CACHE_OUTPUT" fi else echo "::notice::Skipping cache - no report.json found (tests may have been skipped due to rerun optimization)" fi echo "::endgroup::" - name: Upload to TestDino if: ${{ needs.ui_integration_tests.result == 'failure' && env.UPLOAD_TO_TESTDINO == 'true' }} run: | cd tests/ui-testing # Check if report.json exists (it won't exist if all tests were skipped due to rerun optimization) if [ ! -f "playwright-results/report.json" ]; then echo "::notice::Skipping TestDino upload - no report.json found (tests may have been skipped due to rerun optimization)" exit 0 fi # Fix startTime format: convert Unix timestamp (number) to ISO 8601 string for TestDino compatibility # Check if startTime is a number and convert it START_TIME=$(jq -r '.stats.startTime // empty' playwright-results/report.json) if [ -n "$START_TIME" ] && [[ "$START_TIME" =~ ^[0-9]+$ ]]; then echo "Converting startTime from Unix timestamp to ISO 8601 string" jq '.stats.startTime = (.stats.startTime / 1000 | strftime("%Y-%m-%dT%H:%M:%S.000Z"))' playwright-results/report.json > playwright-results/report.json.tmp mv playwright-results/report.json.tmp playwright-results/report.json fi # Filter report to only include failed tests for TestDino upload. # Retries with identical status+error are stripped to the final attempt only, # reducing quota usage. Retries with differing status or error are kept in full. node -e " const fs = require('fs'); const report = JSON.parse(fs.readFileSync('playwright-results/report.json', 'utf8')); let strippedRetries = 0; // accumulated once per test via single-visit tree traversal function getErrorFingerprint(r) { // First line of error.message is stable across retries (stack/timing in later lines varies). // Distinguish no-error-object (safe to strip on status alone) from error-without-message // (unknown content — treat as unique so we never falsely deduplicate). if (r.error?.message) return r.error.message.split('\n')[0].trim(); return r.status + ':' + (r.error ? 'error-no-msg' : 'no-error'); } function filterSuite(s) { const specs = (s.specs||[]) .filter(sp=>sp.tests&&sp.tests.some(t=>t.status==='unexpected')) .map(sp=>({ ...sp, tests: sp.tests.map(t=>{ const results = t.results||[]; if (!results.length) return {...t}; const first = results[0]; const allIdentical = results.length === 1 || results.every(r=> r.status === first.status && getErrorFingerprint(r) === getErrorFingerprint(first) ); if (allIdentical && results.length > 1) strippedRetries += results.length - 1; return { ...t, results: allIdentical ? [results[results.length-1]] : results }; }) })); const suites = (s.suites||[]).map(filterSuite).filter(sub=>(sub.specs&&sub.specs.length)||(sub.suites&&sub.suites.length)); return {...s,specs,suites}; } const filtered = { ...report, suites: report.suites.map(filterSuite).filter(s=>(s.specs&&s.specs.length)||(s.suites&&s.suites.length)), stats: {...report.stats,expected:0,flaky:0,skipped:0} }; fs.writeFileSync('playwright-results/report-failures-only.json',JSON.stringify(filtered)); const total=(report.stats.expected||0)+(report.stats.unexpected||0)+(report.stats.skipped||0)+(report.stats.flaky||0); console.log('Uploading '+(report.stats.unexpected||0)+' failed tests out of '+total+' total to TestDino (stripped '+strippedRetries+' identical retry results)'); " echo "::notice::Report filtered to failures only" npx --yes tdpw@latest upload playwright-results \ --json-report playwright-results/report-failures-only.json \ --html-report playwright-results/html-report \ --upload-html \ --verbose \ --token="${{ secrets.TESTDINO_API_TOKEN }}" playwright_summary: timeout-minutes: 5 runs-on: labels: ubicloud-standard-4 permissions: {} needs: [ check_changes, build_binary, ui_integration_tests, ] if: always() steps: - name: Check test results run: | # check_changes must have succeeded — if it failed, all downstream jobs skipped # and we would otherwise incorrectly report success. if [ "${{ needs.check_changes.result }}" != "success" ]; then echo "check_changes job did not succeed: ${{ needs.check_changes.result }}" exit 1 fi BUILD_OK=false UI_OK=false if [ "${{ needs.build_binary.result }}" == "success" ] || [ "${{ needs.build_binary.result }}" == "skipped" ]; then BUILD_OK=true fi if [ "${{ needs.ui_integration_tests.result }}" == "success" ] || [ "${{ needs.ui_integration_tests.result }}" == "skipped" ]; then UI_OK=true fi if [ "$BUILD_OK" == "true" ] && [ "$UI_OK" == "true" ]; then echo "All Playwright tests completed successfully" exit 0 else echo "Playwright tests failed:" echo " build_binary: ${{ needs.build_binary.result }}" echo " ui_integration_tests: ${{ needs.ui_integration_tests.result }}" exit 1 fi