mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
## Test Coverage — Dashboard Table Column Formatting & Filtering (PR #12531) ### Column Formatting | # | Test | Coverage | |---|------|----------| | 1 | Field type override | Auto-detected numeric column shows Value Formatting + Conditional Styling sections; forcing "text" hides them; forcing "num" restores them | | 2 | Custom unit | Custom unit input appears when unit = Custom; value persists after save + reopen | | 3 | Text/background color | Configured text & background color render correctly on the table cell | | 4 | Conditional styling | Two rules with non-overlapping thresholds prove genuine per-row, order-sensitive, last-match-wins rule evaluation | | 5 | Alignment | Changing alignment (Center) updates the cell's content justification | | 6 | Auto/unique color | Distinct column values render with distinct auto-assigned background colors | ### Column Filtering | # | Test | Coverage | |---|------|----------| | 7 | Filtering toggle | Enabling "Filtering" shows a filter icon in every column header | | 8 | Filter by value | Selecting a value narrows the table to matching rows; filter icon shows active state | | 9 | Clear filter | Clearing restores all rows and resets the filter icon | | 10 | Filter search | Typing in the search box narrows the value list to a "no matches" placeholder | | 11 | Filtering persistence | `table_filtering` toggle state survives save + reopen | ### Additional Changes - Extended `dashboard-panel-configs.js` / `dashboard-table-helpers.js` with reusable page-object methods for column formatting and filtering - Added `Dashboards-Charts` CI matrix entry in `.github/workflows/playwright.yml`
1349 lines
63 KiB
YAML
1349 lines
63 KiB
YAML
name: Playwright UI Tests
|
|
|
|
on:
|
|
# No `push: main` trigger: the merge_group (merge-queue) run already builds and
|
|
# tests the exact commit that lands on main, so a post-merge push run would be a
|
|
# full-suite duplicate. The merge_group run is now the trusted main-ward build and
|
|
# also warms the shared Rust cache (see save-if on the rust-cache step below).
|
|
pull_request:
|
|
branches:
|
|
- "**"
|
|
# labeled/unlabeled so adding or removing the 'e2e' label re-fires the run and
|
|
# re-evaluates the gate in playwright_summary.
|
|
types: [opened, reopened, synchronize, labeled, unlabeled]
|
|
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: |
|
|
# Skip when a PR touches only docs, images, or unrelated workflow files (e.g. a
|
|
# dependabot bump to another workflow) — none can affect the app. Source, web/, and
|
|
# this workflow (playwright.yml) still run the suite.
|
|
IGNORE_RE='(\.md$|^docs/|\.(png|jpe?g|gif|svg|webp)$|^\.github/workflows/)'
|
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref || 'main' }} ${{ github.sha }})
|
|
# Files not matching IGNORE_RE. Captured (not `grep -qv`) so the check doesn't depend
|
|
# on grep -qv exit semantics, which differ across grep builds; the `|| true` keeps
|
|
# `set -e` from aborting when grep matches nothing (exit 1). The `grep -qE` below is a
|
|
# loop-safe `if`-condition term (`set -e` doesn't fire there), so it needs no `|| true`.
|
|
RELEVANT=$(printf '%s' "$CHANGED" | grep -vE "$IGNORE_RE" || true)
|
|
if [ -z "$CHANGED" ] || [ -n "$RELEVANT" ] || printf '%s' "$CHANGED" | grep -qE '^\.github/workflows/playwright\.yml$'; 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]
|
|
# Run when there are e2e-relevant changes AND either this is not a pull_request
|
|
# (push to main / merge_group always run) or the PR carries the 'e2e' label.
|
|
if: >-
|
|
(github.event_name == 'pull_request' &&
|
|
contains(github.event.pull_request.labels.*.name, 'e2e')) ||
|
|
(github.event_name != 'pull_request' &&
|
|
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 the shared cache on merge_group runs (the main-ward build now that
|
|
# push:main is gone); keep the main-ref check for any non-PR fallback.
|
|
save-if: ${{ github.event_name == 'merge_group' || 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: eks-openobserve-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",
|
|
"ftsDefaultColumn.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-history.spec.js",
|
|
"alerts-template-prebuilt-guard.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-table-column-formatting.spec.js",
|
|
"dashboard-table-column-filtering.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-bulk-operations.spec.js",
|
|
"reports-form-validation.spec.js",
|
|
"reportCsvMediaType.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",
|
|
]
|
|
# The RUM ingest/token/sourcemaps routes are part of the standard
|
|
# (OSS) router (src/handler/http/router/mod.rs), so these specs run
|
|
# against the OSS binary built by this workflow. Ingestion-auth
|
|
# negative cases live in the API suite (tests/api-testing/tests/rum).
|
|
- testfolder: "RUM"
|
|
browser: "chrome"
|
|
run_files:
|
|
[
|
|
"rum-cdn-dataflow.spec.js",
|
|
"rum-npm-dataflow.spec.js",
|
|
"rum-page-dataflow.spec.js",
|
|
"rum-onboarding-snippets.spec.js",
|
|
"sourcemap-ui.spec.js",
|
|
"sourcemap-upload-pretty.spec.js",
|
|
]
|
|
# rum-token.spec.js RESETS the org RUM token; running it beside the
|
|
# dataflow specs (workers run files in parallel) would 401 their
|
|
# in-flight SDK beacons. Own shard = own isolated instance.
|
|
- testfolder: "RUM-Token"
|
|
browser: "chrome"
|
|
run_files: ["rum-token.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@v6
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('tests/ui-testing/package-lock.json') }}
|
|
|
|
- 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"
|
|
;;
|
|
"RUM-Token")
|
|
ACTUAL_FOLDER="RUM"
|
|
;;
|
|
*)
|
|
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_reports:
|
|
timeout-minutes: 15
|
|
name: Merge Reports
|
|
needs: [ui_integration_tests]
|
|
if: ${{ !cancelled() && needs.ui_integration_tests.result != 'skipped' }}
|
|
# eks runner: `playwright merge-reports` silently stops mid-merge (exits 0, ~3 of N blobs, no
|
|
# output) on the ubicloud and github-hosted runners — but works on the eks runners (same as ENT).
|
|
runs-on:
|
|
labels: eks-openobserve-standard-4
|
|
permissions:
|
|
contents: read
|
|
actions: read # the no-blobs investigation reads run/job state from the Actions API
|
|
outputs:
|
|
# Compact Playwright stats (expected/unexpected/flaky/skipped) from the merged report.json,
|
|
# consumed by report_to_openobserve to record per-test counts in ci_test_runs.
|
|
stats: ${{ steps.report_stats.outputs.stats }}
|
|
# Flaky + failed tests for the ci_test_results stream, consumed by
|
|
# report_to_openobserve. JSON array: [{module,title,file,status}] where
|
|
# status is "flaky"|"failed" (see .github/scripts/extract-flaky.js). "[]" if none.
|
|
flaky: ${{ steps.report_flaky.outputs.flaky }}
|
|
|
|
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 "<html><body><h1>No tests executed - all shards skipped</h1><p>All test shards exited early because tests had already passed in a previous attempt.</p></body></html>" > 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
|
|
|
|
# Force the html + json reporters explicitly with their output paths instead of
|
|
# relying on `unset CI` to flip playwright.config.js's `process.env.CI ? blob : html/json`
|
|
# ternary. The runner exports CI=true and `unset CI` does not reliably reach the
|
|
# merge subprocess, so the config would intermittently re-select the `blob` reporter —
|
|
# which writes a merged blob-report/ and NOTHING to playwright-results/, making the
|
|
# downstream `ls playwright-results/html-report/` fail with a cryptic exit 2.
|
|
mkdir -p playwright-results
|
|
# These env vars override playwright.config.js's reporter settings:
|
|
# PLAYWRIGHT_HTML_OUTPUT_DIR - html reporter output directory
|
|
# PLAYWRIGHT_HTML_OPEN=never - never auto-open the report (required for CI)
|
|
# PLAYWRIGHT_JSON_OUTPUT_NAME - json reporter output file path
|
|
PLAYWRIGHT_HTML_OUTPUT_DIR=playwright-results/html-report \
|
|
PLAYWRIGHT_HTML_OPEN=never \
|
|
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-results/report.json \
|
|
npx playwright merge-reports --reporter html,json ./all-blob-reports
|
|
|
|
echo "Contents of playwright-results:"
|
|
ls -la playwright-results/ || true
|
|
if [ ! -f playwright-results/html-report/index.html ] || [ ! -f playwright-results/report.json ]; then
|
|
echo "::error::merge-reports did not produce the expected html-report/index.html and report.json"
|
|
echo "::error::all-blob-reports contents:"
|
|
ls -la all-blob-reports/ || true
|
|
exit 1
|
|
fi
|
|
# Diagnostics only — never fail the step on these (the guard above already
|
|
# confirmed the outputs exist).
|
|
echo "Contents of html-report:"
|
|
ls -la playwright-results/html-report/ || true
|
|
echo "Checking report.json:"
|
|
ls -lh playwright-results/report.json || true
|
|
|
|
- name: Extract report stats for OpenObserve
|
|
id: report_stats
|
|
if: always()
|
|
run: |
|
|
cd tests/ui-testing
|
|
if [ -f playwright-results/report.json ]; then
|
|
STATS=$(jq -c '.stats // {}' playwright-results/report.json 2>/dev/null || echo '{}')
|
|
else
|
|
STATS='{}'
|
|
fi
|
|
echo "stats=$STATS" >> "$GITHUB_OUTPUT"
|
|
echo "report stats: $STATS"
|
|
|
|
- name: Extract flaky tests for OpenObserve
|
|
id: report_flaky
|
|
if: always()
|
|
run: |
|
|
# Flaky + failed tests from the merged report, as one JSON array, for the
|
|
# ci_test_results stream. Logic + schema live in extract-flaky.js (it
|
|
# handles a missing/malformed report by printing [] non-fatally, and is
|
|
# commented to explain status="flaky" vs "failed" and the sp.title choice).
|
|
FLAKY=$(node .github/scripts/extract-flaky.js tests/ui-testing/playwright-results/report.json)
|
|
echo "flaky=$FLAKY" >> "$GITHUB_OUTPUT"
|
|
# Log a count only — avoid dumping test file paths/titles into public CI logs.
|
|
echo "flaky/failed test count: $(printf '%s' "$FLAKY" | jq 'length' 2>/dev/null || echo '?')"
|
|
|
|
- name: Upload merged report
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: merged-report-${{ github.run_attempt }}
|
|
path: tests/ui-testing/playwright-results
|
|
retention-days: 1
|
|
if-no-files-found: warn
|
|
|
|
# OpenObserve metrics — its own job so playwright_summary never depends on it. Emits one
|
|
# retry-aware run-summary doc (ci_test_runs, suite=ui) + per-test flaky/failed rows
|
|
# (ci_test_results). The step is continue-on-error and the poster scripts never fail.
|
|
report_openobserve:
|
|
timeout-minutes: 10
|
|
name: Report to OpenObserve
|
|
needs: [ui_integration_tests, merge_reports]
|
|
if: ${{ !cancelled() && needs.ui_integration_tests.result != 'skipped' }}
|
|
runs-on:
|
|
labels: eks-openobserve-standard-4
|
|
permissions:
|
|
contents: read
|
|
actions: read # reads run/job timings from the Actions API
|
|
steps:
|
|
- name: Clone the current repo
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Report run metrics to OpenObserve
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
O2_REPORTING_INGEST_BASE: ${{ secrets.O2_REPORTING_INGEST_BASE }}
|
|
O2_REPORTING_AUTH: ${{ secrets.O2_REPORTING_AUTH }}
|
|
O2_REPORTING_INSECURE: ${{ vars.O2_REPORTING_INSECURE }}
|
|
TESTS: ${{ needs.ui_integration_tests.result }}
|
|
MERGE_STATS: ${{ needs.merge_reports.outputs.stats }}
|
|
MERGE_FLAKY: ${{ needs.merge_reports.outputs.flaky }}
|
|
# Branch + PR for per-test rows so the dashboard needs no cross-stream JOIN.
|
|
# head_ref is set on pull_request (the source branch); ref_name covers push +
|
|
# merge_group (gh-readonly-queue/main/pr-<n>-<sha>). PR empty on merge_group/push.
|
|
REPORT_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
REPORT_PR: ${{ github.event.pull_request.number }}
|
|
run: |
|
|
# This job runs only when tests actually ran (merge `if:` gates on !cancelled &&
|
|
# ui != skipped), so TESTS is success|failure here.
|
|
if [ "$TESTS" = "failure" ]; then CONCLUSION=failure; else CONCLUSION=success; fi
|
|
# Per-test counts from the merged report.json (tests_total includes skipped).
|
|
if [ -n "${MERGE_STATS:-}" ] && [ "$MERGE_STATS" != "null" ] && [ "$MERGE_STATS" != "{}" ]; then
|
|
EXTRA_JSON=$(printf '%s' "$MERGE_STATS" | jq -c '{tests_total:((.expected//0)+(.unexpected//0)+(.flaky//0)+(.skipped//0)), tests_passed:(.expected//0), tests_failed:(.unexpected//0), tests_flaky:(.flaky//0), tests_skipped:(.skipped//0)}' 2>/dev/null || echo "")
|
|
[ -n "$EXTRA_JSON" ] && export EXTRA_JSON
|
|
fi
|
|
export CONCLUSION STREAM=ci_test_runs SUITE=ui
|
|
bash .github/scripts/o2-run-report.sh
|
|
# One row per flaky-or-failed test -> ci_test_results (status flaky|failed; suite/
|
|
# workflow hardcoded UI-only). o2-report.sh takes the stream as arg 1. Non-fatal.
|
|
FLAKY="${MERGE_FLAKY:-[]}"
|
|
if printf '%s' "$FLAKY" | jq -e 'type=="array" and length>0' >/dev/null 2>&1; then
|
|
FLAKY_FILE=$(mktemp "${TMPDIR:-/tmp}/flaky.XXXXXX")
|
|
trap 'rm -f "$FLAKY_FILE"' EXIT
|
|
printf '%s' "$FLAKY" | jq -c --arg run_id "$GITHUB_RUN_ID" --arg repo "$GITHUB_REPOSITORY" \
|
|
--arg branch "${REPORT_BRANCH:-}" --arg pr "${REPORT_PR:-}" \
|
|
'[.[] | {run_id:$run_id, repo:$repo, suite:"ui", workflow:"test", ingest_source:"live", status:.status, module:.module, test_title:.title, test_file:.file, branch:(if $branch=="" then null else $branch end), pr_number:(if $pr=="" then null else ($pr|tonumber) end)}]' \
|
|
> "$FLAKY_FILE" 2>/dev/null || echo '[]' > "$FLAKY_FILE"
|
|
if [ "$(jq 'length' "$FLAKY_FILE" 2>/dev/null || echo 0)" -gt 0 ]; then
|
|
bash .github/scripts/o2-report.sh ci_test_results "$FLAKY_FILE"
|
|
fi
|
|
fi
|
|
|
|
# TestDino upload — its own job so it never gates playwright_summary. Only runs when tests
|
|
# failed (and uploads enabled); consumes the merged report via artifact.
|
|
upload_testdino:
|
|
timeout-minutes: 15
|
|
name: Upload to TestDino
|
|
needs: [ui_integration_tests, merge_reports]
|
|
if: ${{ !cancelled() && needs.ui_integration_tests.result == 'failure' && needs.merge_reports.result == 'success' }}
|
|
runs-on:
|
|
labels: eks-openobserve-standard-4
|
|
permissions:
|
|
contents: read
|
|
actions: read # download-artifact fetches the merged-report artifact
|
|
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 merged report
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: merged-report-${{ github.run_attempt }}
|
|
path: tests/ui-testing/playwright-results
|
|
|
|
- name: Cache test failures to TestDino
|
|
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: ${{ 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: eks-openobserve-standard-4
|
|
permissions: {}
|
|
needs:
|
|
[
|
|
check_changes,
|
|
build_binary,
|
|
ui_integration_tests,
|
|
]
|
|
if: always()
|
|
steps:
|
|
- name: Check test results
|
|
run: |
|
|
# 1. check_changes must have succeeded — if it failed, downstream jobs skipped
|
|
# and we must not report a false success.
|
|
if [ "${{ needs.check_changes.result }}" != "success" ]; then
|
|
echo "check_changes job did not succeed: ${{ needs.check_changes.result }}"
|
|
exit 1
|
|
fi
|
|
|
|
# 2. On a pull_request WITHOUT the 'e2e' label the suite did not run: block (fail)
|
|
# when the change is Playwright-relevant so the author opts in, else bypass (pass).
|
|
# Adding the 'e2e' label always runs the suite (see build_binary's if).
|
|
if [ "${{ github.event_name }}" == "pull_request" ] && \
|
|
[ "${{ contains(github.event.pull_request.labels.*.name, 'e2e') }}" != "true" ]; then
|
|
if [ "${{ needs.check_changes.outputs.has_changes }}" == "true" ]; then
|
|
echo "::error::This PR changes Playwright-relevant files but has no 'e2e' label, so the suite did not run."
|
|
echo "::error::Add the 'e2e' label to the PR to run the full Playwright suite before merging."
|
|
exit 1
|
|
fi
|
|
echo "✅ No Playwright-relevant changes and no 'e2e' label — suite not required for this PR."
|
|
exit 0
|
|
fi
|
|
|
|
# 3. Non-PR event (push / merge queue) with nothing relevant → nothing ran → allow.
|
|
if [ "${{ github.event_name }}" != "pull_request" ] && \
|
|
[ "${{ needs.check_changes.outputs.has_changes }}" != "true" ]; then
|
|
echo "✅ No Playwright-relevant changes — suite not required."
|
|
exit 0
|
|
fi
|
|
|
|
# 4. Suite was expected to run — pass only if build + tests actually succeeded.
|
|
if [ "${{ needs.build_binary.result }}" == "success" ] && \
|
|
[ "${{ needs.ui_integration_tests.result }}" == "success" ]; 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
|