Files
openobserve/.github/workflows/api-testing.yml
T

747 lines
32 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: API Integration Test
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 'ready-for-ci' starts or resets CI; drafts never run paid CI.
types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review, converted_to_draft]
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/
ZO_BASE_URL_SC: http://localhost:5080/
WS_ZO_BASE_URL: ws://localhost:5080/
ZO_USAGE_PUBLISH_INTERVAL: 5
ZO_ALERT_SCHEDULE_INTERVAL: 3
ZO_SMTP_ENABLED: true
ZO_CREATE_ORG_THROUGH_INGESTION: true
ZO_INGEST_ALLOWED_UPTO: 48 # Allow backdated data ingestion for backfill tests
ZO_SSRF_ALLOW_LOOPBACK: true # Allow localhost destinations in API tests
jobs:
check_changes:
# Runs only on PRs carrying the 'ready-for-ci' label; the merge queue runs unit tests only (see docs/ci).
if: ${{ github.event_name != 'merge_group' && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false)) }}
name: check_changes
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
# Debounce: a push inside this window cancels the run (concurrency) before anything paid starts.
- if: github.event_name == 'pull_request'
run: sleep 60
- uses: actions/checkout@v5
with:
fetch-depth: 0
# One shared classification for every suite (see .github/actions/classify-changes); a change to
# this workflow file re-tests itself on top of it.
- uses: ./.github/actions/classify-changes
id: classify
- id: check
env:
HAS_CHANGES: ${{ steps.classify.outputs.run_api }}
CHANGED_WORKFLOWS: ${{ steps.classify.outputs.changed_workflows }}
run: |
if [ "$HAS_CHANGES" = "true" ] || echo " $CHANGED_WORKFLOWS " | grep -q ' api-testing.yml '; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
build_api_binary:
name: build_api_binary
needs: [check_changes]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
env:
# CI executes a stripped binary; omitting dependency debuginfo keeps the
# shared compile cache small while preserving dev-profile debug assertions.
CARGO_PROFILE_DEV_DEBUG: "0"
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: Checkout git repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: ./.github/actions/github-auth
- name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # master
with:
toolchain: nightly-2026-05-20
- name: Setup Rust Cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# Share one dependency build cache with the DB main-branch writer. Do
# not cache workspace crates: a measured warm cache still rebuilt all
# 42 workspace crates while increasing the archive from ~1.1 to 3.1 GiB.
shared-key: backend-debug0-mimalloc-deps-v1
add-job-id-key: false
# The DB workflow's successful push:main build is the only writer.
save-if: false
- name: Install Protoc
run: bash ./.github/protoc.sh
- name: Build OpenObserve debug binary
run: cargo build --features mimalloc
# The consumers only execute this binary, so remove debug symbols from the
# distribution copy before compressing and uploading it once.
- name: Package OpenObserve debug binary
run: |
strip target/debug/openobserve
tar -C target/debug -czf openobserve-api-debug.tar.gz openobserve
- name: Upload OpenObserve debug binary
uses: actions/upload-artifact@v4
with:
name: openobserve-api-debug-${{ github.sha }}
path: openobserve-api-debug.tar.gz
if-no-files-found: error
retention-days: 3
# The payload is already gzip-compressed; avoid a second compression pass.
compression-level: 0
api_integration_tests:
name: api_integration_tests
needs: [check_changes, build_api_binary]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
permissions:
actions: read
contents: read # only need to checkout the repo
steps:
- name: Checkout git repo
uses: actions/checkout@v5
- name: Download OpenObserve debug binary
uses: actions/download-artifact@v5
with:
name: openobserve-api-debug-${{ github.sha }}
path: api-binary-artifact
- name: Extract OpenObserve debug binary
run: |
mkdir -p target/debug
tar -xzf api-binary-artifact/openobserve-api-debug.tar.gz -C target/debug
chmod +x target/debug/openobserve
- name: Setup Python
uses: actions/setup-python@v7
id: setup-python
with:
python-version: "3.11.6"
- name: Setup rye
uses: eifinger/setup-rye@c694239a43768373e87d0103d7f547027a23f3c8 # v4.2.9
env:
# Reuse setup-python's interpreter so `rye self install` skips the flaky toolchain download.
RYE_TOOLCHAIN: ${{ steps.setup-python.outputs.python-path }}
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync
run: rye sync
working-directory: tests/api-testing/
- name: Run default-set tests
# Phase 6.b: regression matrix tests moved to their own
# api_regression_tests job below — they now run in parallel
# with this default-set job instead of sequentially within it.
# Cuts ~8 min off CI wall time at the cost of more runner minutes.
# NB: --force-sugar is intentionally omitted. pytest-sugar is already
# auto-loaded via the entry point; passing --force-sugar registers it
# a second time, which inflates the progress counter (CI displayed
# ~3x the real test count). Letting the plugin load via discovery
# alone keeps the counter honest.
run: |
${{ github.workspace }}/target/debug/openobserve > o2_default.log 2>&1 &
SERVER_PID_DEFAULT=$!
echo "OpenObserve PID for default tests: $SERVER_PID_DEFAULT"
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then
echo "Server ready after ${i}s"; break
fi
sleep 1
done
# test_pending_silence.py is carved into api_alerts_pending_tests
# (runs -n 4 in parallel) — those 4 wall-clock tests were ~85% of the
# alerts runtime when run serially here.
rye run pytest --ignore=tests/regression --ignore=tests/query_agent --ignore=tests/vortex --ignore=tests/alerts/test_pending_silence.py --junitxml=junit-integration.xml
kill $SERVER_PID_DEFAULT || true
working-directory: tests/api-testing/
# Test-count metrics sidecar: ship the junit so report_to_openobserve can
# tally API active/skipped counts (suite=api). always() so counts emit even
# when pytest fails (junit is written before pytest's non-zero exit).
- name: Upload API junit (integration)
if: always()
uses: actions/upload-artifact@v4
with:
name: api-junit__api_integration_tests__main
path: tests/api-testing/junit-integration.xml
if-no-files-found: warn
retention-days: 7
- name: Upload OpenObserve server logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openobserve-server-logs
path: tests/api-testing/o2_*.log
if-no-files-found: warn
retention-days: 14
# Dedicated query agent SQL regression suite. Runs in parallel with
# the default-set and regression jobs so the 310-case (155 queries ×
# 2 phases: memtable + parquet) suite doesn't extend CI wall time.
api_query_agent_tests:
name: "api_query_agent / ${{ matrix.config.label }}"
needs: [check_changes, build_api_binary]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
# Run the full SQL regression suite with the single-node physical
# optimizer both enabled (default) and disabled. The non-optimized
# RemoteScanRewriter path triggers known upstream DataFusion bugs on
# a handful of queries — those are tagged skip_without_single_node_opt
# and filtered by load_all_queries() when SINGLE_NODE_OPT_MODE=false.
strategy:
fail-fast: false
matrix:
config:
- label: single_node_opt_false
env: "ZO_FEATURE_SINGLE_NODE_OPTIMIZE_ENABLED=false"
mode: "false"
- label: single_node_opt_true
env: "ZO_FEATURE_SINGLE_NODE_OPTIMIZE_ENABLED=true"
mode: "true"
- label: remove_filter_with_index_false
env: "ZO_FEATURE_QUERY_REMOVE_FILTER_WITH_INDEX=false"
mode: "true"
permissions:
actions: read
contents: read
steps:
- name: Checkout git repo
uses: actions/checkout@v5
- name: Download OpenObserve debug binary
uses: actions/download-artifact@v5
with:
name: openobserve-api-debug-${{ github.sha }}
path: api-binary-artifact
- name: Extract OpenObserve debug binary
run: |
mkdir -p target/debug
tar -xzf api-binary-artifact/openobserve-api-debug.tar.gz -C target/debug
chmod +x target/debug/openobserve
- name: Setup Python
uses: actions/setup-python@v7
id: setup-python
with:
python-version: "3.11.6"
- name: Setup rye
uses: eifinger/setup-rye@c694239a43768373e87d0103d7f547027a23f3c8 # v4.2.9
env:
# Reuse setup-python's interpreter so `rye self install` skips the flaky toolchain download.
RYE_TOOLCHAIN: ${{ steps.setup-python.outputs.python-path }}
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync
run: rye sync
working-directory: tests/api-testing/
- name: Regenerate query-agent expected values
run: |
cd ${{ github.workspace }}/tests/test-data/query-agent
${{ github.workspace }}/tests/api-testing/.venv/bin/python compute_counts.py
- name: Run query agent tests
run: |
ZO_MAX_FILE_RETENTION_TIME=1 ${{ matrix.config.env }} ${{ github.workspace }}/target/debug/openobserve > o2_query_agent_${{ matrix.config.label }}.log 2>&1 &
SERVER_PID=$!
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then
echo "Server ready after ${i}s"; break
fi
sleep 1
done
# SINGLE_NODE_OPT_MODE controls which queries load_all_queries() includes
SINGLE_NODE_OPT_MODE=${{ matrix.config.mode }} rye run pytest tests/query_agent/ --ignore=tests/query_agent/test_3_vortex.py -v --junitxml=junit-queryagent-${{ matrix.config.label }}.xml
kill $SERVER_PID || true
working-directory: tests/api-testing/
# Test-count metrics sidecar (see integration job). Parser dedups the two
# opt-mode legs by test identity so the same suite isn't counted twice.
- name: Upload API junit (query_agent)
if: always()
uses: actions/upload-artifact@v4
with:
name: api-junit__query_agent_tests__${{ matrix.config.label }}
path: tests/api-testing/junit-queryagent-${{ matrix.config.label }}.xml
if-no-files-found: warn
retention-days: 7
- name: Upload server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openobserve-query-agent-${{ matrix.config.label }}-log
path: tests/api-testing/o2_query_agent_${{ matrix.config.label }}.log
if-no-files-found: warn
retention-days: 14
# Phase 6.b: regression matrix split. The 4 env-flag-sensitive test
# configurations now run as 4 parallel jobs instead of 4 sequential
# restarts in one job. Each job downloads the run-scoped OO binary and
# runs its single configured pytest target.
api_regression_tests:
name: "api_regression / ${{ matrix.config.label }}"
needs: [check_changes, build_api_binary]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
permissions:
actions: read
contents: read # only need to checkout the repo
strategy:
fail-fast: false
matrix:
config:
- label: utf8_view_false
env: "ZO_FEATURE_JOIN_MATCH_ONE_ENABLED=true ZO_UTF8_VIEW_ENABLED=false"
target: tests/regression/test_subquery_join_match.py
- label: utf8_view_true
env: "ZO_FEATURE_JOIN_MATCH_ONE_ENABLED=true ZO_UTF8_VIEW_ENABLED=true"
target: tests/regression/test_subquery_join_match.py
- label: join_match_one_true
env: "ZO_FEATURE_JOIN_MATCH_ONE_ENABLED=true"
target: tests/regression/test_logs_to_logs_join.py
- label: join_match_one_false
env: "ZO_FEATURE_JOIN_MATCH_ONE_ENABLED=false"
target: tests/regression/test_logs_to_logs_join.py
# A free-space requirement no runner meets keeps the disk circuit breaker tripped.
- label: otlp_grpc_backpressure
env: "ZO_DISK_CIRCUIT_BREAKER_ENABLED=true ZO_DISK_CIRCUIT_BREAKER_THRESHOLD=900000000"
target: tests/regression/test_otlp_grpc_backpressure.py
# Spawns and restarts its own server from target/debug/openobserve.
- label: sqlite_meta_invalid_utf8
env: ""
target: tests/regression/test_sqlite_invalid_utf8_meta_startup.py
steps:
- uses: actions/checkout@v5
- name: Download OpenObserve debug binary
uses: actions/download-artifact@v5
with:
name: openobserve-api-debug-${{ github.sha }}
path: api-binary-artifact
- name: Extract OpenObserve debug binary
run: |
mkdir -p target/debug
tar -xzf api-binary-artifact/openobserve-api-debug.tar.gz -C target/debug
chmod +x target/debug/openobserve
- uses: actions/setup-python@v7
id: setup-python
with: { python-version: "3.11.6" }
- name: Setup rye
uses: eifinger/setup-rye@c694239a43768373e87d0103d7f547027a23f3c8 # v4.2.9
env:
# Reuse setup-python's interpreter so `rye self install` skips the flaky toolchain download.
RYE_TOOLCHAIN: ${{ steps.setup-python.outputs.python-path }}
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync
run: rye sync
working-directory: tests/api-testing/
- name: "Run ${{ matrix.config.label }} regression test"
# Server binary lives at the repo root (target/debug/openobserve);
# this step uses working-directory: tests/api-testing/, so we must
# invoke it via ${{ github.workspace }} or the shell will fail with
# "No such file or directory". --force-sugar omitted on purpose —
# pytest-sugar auto-loads via entry point, and passing the flag
# registers it twice and inflates the progress counter ~3x.
run: |
${{ matrix.config.env }} ${{ github.workspace }}/target/debug/openobserve > "o2_${{ matrix.config.label }}.log" 2>&1 &
SERVER_PID=$!
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then
echo "Server ready after ${i}s"; break
fi
sleep 1
done
rye run pytest ${{ matrix.config.target }} -v --junitxml=junit-regression-${{ matrix.config.label }}.xml
kill $SERVER_PID || true
working-directory: tests/api-testing/
# Test-count metrics sidecar (see integration job). Parser dedups the
# flag-combo legs that re-run the same target files.
- name: Upload API junit (regression)
if: always()
uses: actions/upload-artifact@v4
with:
name: api-junit__api_integration_tests__regression_${{ matrix.config.label }}
path: tests/api-testing/junit-regression-${{ matrix.config.label }}.xml
if-no-files-found: warn
retention-days: 7
- name: Upload server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: "openobserve-regression-${{ matrix.config.label }}-log"
path: "tests/api-testing/o2_${{ matrix.config.label }}.log"
if-no-files-found: warn
retention-days: 14
# Carved out of the serial default-set job: the 4 pending/silence alert
# tests are wall-clock bound (each needs 2-3 alert eval cycles at the 60s
# frequency floor + a real pending window), so serially they were ~85% of
# the alerts runtime (~10 min). Run here with -n 4 so their waits overlap
# and this job lands at ~the single longest test (~3 min), in parallel with
# the other API jobs.
api_alerts_pending_tests:
name: api_alerts_pending
needs: [check_changes, build_api_binary]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
permissions:
actions: read
contents: read # only need to checkout the repo
steps:
- uses: actions/checkout@v5
- name: Download OpenObserve debug binary
uses: actions/download-artifact@v5
with:
name: openobserve-api-debug-${{ github.sha }}
path: api-binary-artifact
- name: Extract OpenObserve debug binary
run: |
mkdir -p target/debug
tar -xzf api-binary-artifact/openobserve-api-debug.tar.gz -C target/debug
chmod +x target/debug/openobserve
- uses: actions/setup-python@v7
id: setup-python
with: { python-version: "3.11.6" }
- name: Setup rye
uses: eifinger/setup-rye@c694239a43768373e87d0103d7f547027a23f3c8 # v4.2.9
env:
# Reuse setup-python's interpreter so `rye self install` skips the flaky toolchain download.
RYE_TOOLCHAIN: ${{ steps.setup-python.outputs.python-path }}
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync
run: rye sync
working-directory: tests/api-testing/
- name: Run pending/silence alert tests (parallel)
# Server binary lives at the repo root (target/debug/openobserve);
# this step uses working-directory: tests/api-testing/, so invoke it
# via ${{ github.workspace }}. The 4 tests use uniq() names/streams and
# a non-destructive shared org, so -n 4 is safe. --force-sugar omitted
# on purpose (see integration job — double-registers, ~3x count).
run: |
${{ github.workspace }}/target/debug/openobserve > o2_alerts_pending.log 2>&1 &
SERVER_PID=$!
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then
echo "Server ready after ${i}s"; break
fi
sleep 1
done
rye run pytest tests/alerts/test_pending_silence.py -n 4 -v --junitxml=junit-alerts-pending.xml
kill $SERVER_PID || true
working-directory: tests/api-testing/
# Test-count metrics sidecar (see integration job). Category dir name is
# api_integration_tests so the parser folds it into the integration total.
- name: Upload API junit (alerts pending)
if: always()
uses: actions/upload-artifact@v4
with:
name: api-junit__api_integration_tests__alerts_pending
path: tests/api-testing/junit-alerts-pending.xml
if-no-files-found: warn
retention-days: 7
- name: Upload server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openobserve-alerts-pending-log
path: tests/api-testing/o2_alerts_pending.log
if-no-files-found: warn
retention-days: 14
# Phase 5: schemathesis fuzz job — informational (continue-on-error),
# surfaces 5xx panics across the 150 OpenAPI endpoints. Decoupled from
# api_integration_tests so its findings don't block merges yet; teams
# can promote it to blocking after the noise floor is understood.
api_fuzz_tests:
name: api_fuzz_tests
needs: [check_changes, build_api_binary]
if: needs.check_changes.outputs.has_changes == 'true'
runs-on: ubicloud-standard-4
continue-on-error: true
permissions:
actions: read
contents: read # only need to checkout the repo
steps:
- name: Checkout git repo
uses: actions/checkout@v5
- name: Download OpenObserve debug binary
uses: actions/download-artifact@v5
with:
name: openobserve-api-debug-${{ github.sha }}
path: api-binary-artifact
- name: Extract OpenObserve debug binary
run: |
mkdir -p target/debug
tar -xzf api-binary-artifact/openobserve-api-debug.tar.gz -C target/debug
chmod +x target/debug/openobserve
- name: Setup Python
uses: actions/setup-python@v7
id: setup-python
with:
python-version: "3.11.6"
- name: Setup rye
uses: eifinger/setup-rye@c694239a43768373e87d0103d7f547027a23f3c8 # v4.2.9
env:
# Reuse setup-python's interpreter so `rye self install` skips the flaky toolchain download.
RYE_TOOLCHAIN: ${{ steps.setup-python.outputs.python-path }}
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync (schemathesis dep installed)
run: rye sync
working-directory: tests/api-testing/
- name: Start OpenObserve
run: target/debug/openobserve > o2_fuzz.log 2>&1 &
- name: Wait for /healthz
run: |
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then exit 0; fi
sleep 1
done
exit 1
- name: Run schemathesis fuzz tests
# Schemathesis 4.x CLI flag renames vs the 3.x originals:
# --base-url -> --url / -u
# --auth-type basic -> removed (USER:PASS implies basic)
# --hypothesis-max-examples -> --max-examples / -n
# --hypothesis-deadline -> no equivalent; use --request-timeout
# (seconds, not ms) to bound per-request
# --data-generation-method -> --mode / -m
# The two slack/oauth paths are excluded because CI configures no Slack
# credentials, so they correctly answer 503 "not configured" — which the
# not_a_server_error check cannot tell apart from a real 5xx panic.
# `--checks not_a_server_error` keeps focus on 5xx panics (actual
# backend bugs, not spec-conformance nits). `-n 20` bounds runtime;
# `--mode positive` keeps inputs realistic (negative inputs are a
# separate exercise once the noise floor here is understood).
# run_fuzz.py prevents Hypothesis' auto-loaded CI profile from
# overriding --suppress-health-check. Keep CI's too_slow suppression
# alongside filter_too_much; the 5xx response check is unchanged.
run: |
rye run python run_fuzz.py run \
-u http://localhost:5080 \
--auth "$ZO_ROOT_USER_EMAIL:$ZO_ROOT_USER_PASSWORD" \
--checks not_a_server_error \
-n 20 \
--request-timeout 5 \
-m positive \
--suppress-health-check=filter_too_much,too_slow \
--exclude-path "/api/{org_id}/v1/logs" \
--exclude-path "/api/{org_id}/v1/metrics" \
--exclude-path "/api/{org_id}/v1/traces" \
--exclude-path "/api/{org_id}/loki/api/v1/push" \
--exclude-path "/api/{org_id}/prometheus/api/v1/write" \
--exclude-path "/api/{org_id}/dashboards" \
--exclude-path "/api/{org_id}/dashboards/{dashboard_id}" \
--exclude-path "/api/{org_id}/dashboards/{dashboard_id}/panels" \
--exclude-path "/api/{org_id}/dashboards/{dashboard_id}/panels/{panel_id}" \
--exclude-path "/api/{org_id}/pipelines/history" \
--exclude-path "/api/{org_id}/organizations/assume_service_account" \
--exclude-path "/api/{org_id}/alerts/destinations/slack/oauth/start" \
--exclude-path "/api/{org_id}/alerts/destinations/slack/oauth/exchange" \
http://localhost:5080/api-doc/openapi.json
working-directory: tests/api-testing/
- name: Upload OpenObserve server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openobserve-fuzz-log
path: o2_fuzz.log
if-no-files-found: warn
retention-days: 14
api_tests_summary:
runs-on: ubuntu-latest
permissions: {}
needs: [check_changes, build_api_binary, api_integration_tests, api_query_agent_tests, api_regression_tests, api_alerts_pending_tests]
if: always() && github.event_name != 'merge_group' && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false))
steps:
- name: Check test results
run: |
# 1. check_changes must have succeeded.
if [ "${{ needs.check_changes.result }}" != "success" ]; then
echo "check_changes job did not succeed: ${{ needs.check_changes.result }}"
exit 1
fi
# 2. Nothing API-relevant changed → nothing ran → allow.
if [ "${{ needs.check_changes.outputs.has_changes }}" != "true" ]; then
echo "✅ No API-relevant changes — suite not required."
exit 0
fi
# 3. Suite was expected to run — the run-scoped binary and all gating
# test jobs must have succeeded.
if [ "${{ needs.build_api_binary.result }}" == "success" ] && \
[ "${{ needs.api_integration_tests.result }}" == "success" ] && \
[ "${{ needs.api_query_agent_tests.result }}" == "success" ] && \
[ "${{ needs.api_regression_tests.result }}" == "success" ] && \
[ "${{ needs.api_alerts_pending_tests.result }}" == "success" ]; then
echo "All API test jobs passed"
exit 0
else
echo "API tests failed: build=${{ needs.build_api_binary.result }}, integration=${{ needs.api_integration_tests.result }}, query_agent=${{ needs.api_query_agent_tests.result }}, regression=${{ needs.api_regression_tests.result }}, alerts_pending=${{ needs.api_alerts_pending_tests.result }}"
exit 1
fi
# Non-fatal metrics sidecar: one retry-aware run-summary doc to OpenObserve (stream ci_test_runs,
# suite=api). Re-runs append one doc per attempt; the dashboard keeps the latest attempt per run_id.
report_to_openobserve:
name: Report run metrics to OpenObserve
needs: [check_changes, api_integration_tests, api_query_agent_tests, api_regression_tests, api_alerts_pending_tests]
if: always() && github.event_name != 'merge_group' && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false))
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Clone the current repo
uses: actions/checkout@v5
# Pull every per-shard junit. NO merge-multiple: each artifact lands in its
# own subdir (api-junit__<category>__<leg>) so the parser can read the test
# CATEGORY from the dir name. Missing artifacts are non-fatal.
- name: Download API junit reports
if: always()
continue-on-error: true
uses: actions/download-artifact@v4
with:
pattern: api-junit__*
path: junit-xml
- name: Report run metrics
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 }}
INTEG: ${{ needs.api_integration_tests.result }}
AGENT: ${{ needs.api_query_agent_tests.result }}
REG: ${{ needs.api_regression_tests.result }}
PEND: ${{ needs.api_alerts_pending_tests.result }}
run: |
if [ "$INTEG" = "skipped" ] && [ "$AGENT" = "skipped" ] && [ "$REG" = "skipped" ] && [ "$PEND" = "skipped" ]; then
echo "::notice::no API tests ran (no relevant changes) — skipping metrics"; exit 0
fi
if [ "$INTEG" = "failure" ] || [ "$AGENT" = "failure" ] || [ "$REG" = "failure" ] || [ "$PEND" = "failure" ]; then CONCLUSION=failure
elif [ "$INTEG" = "cancelled" ] || [ "$AGENT" = "cancelled" ] || [ "$REG" = "cancelled" ] || [ "$PEND" = "cancelled" ]; then CONCLUSION=cancelled
else CONCLUSION=success; fi
export CONCLUSION STREAM=ci_test_runs SUITE=api
# Parse junit into per-category + total counts. The TOTAL folds into the
# run doc (EXTRA_JSON) so suite=api shows up in the same KPI/pie panels
# as UI; the per-category rows go to ci_test_counts for the per-shard
# breakdown (the API analog of UI's ci_test_modules). All best-effort.
if [ -d junit-xml ]; then
COUNTS=$(python3 .github/scripts/parse-api-junit.py junit-xml 2>/dev/null || echo "")
if [ -n "$COUNTS" ] && printf '%s' "$COUNTS" | jq -e . >/dev/null 2>&1; then
echo "API counts: $COUNTS"
EXTRA_JSON=$(printf '%s' "$COUNTS" | jq -c .total); export EXTRA_JSON
printf '%s' "$COUNTS" | jq -c --arg run_id "$GITHUB_RUN_ID" --arg repo "$GITHUB_REPOSITORY" \
'[.by_category[] | {run_id:$run_id, repo:$repo, suite:"api", workflow:"test", ingest_source:"live",
module:.module, tests_total, tests_passed, tests_failed, tests_flaky, tests_skipped}]' > api_counts_rows.json
if [ "$(jq 'length' api_counts_rows.json 2>/dev/null || echo 0)" -gt 0 ]; then
bash .github/scripts/o2-report.sh ci_test_counts api_counts_rows.json
fi
fi
fi
bash .github/scripts/o2-run-report.sh