Files
openobserve/.github/workflows/ent-e2e-gate.yml
T

377 lines
20 KiB
YAML

name: Playwright E2E Crosscheck
# Pre-merge GUARD (runs in openobserve) that stops an OSS PR from silently breaking the
# enterprise-only e2e specs. web/tests live in OSS; the ENT e2e *triggers* live in o2-enterprise,
# so today an OSS frontend/test change only surfaces as an ENT breakage in some later ENT PR.
#
# This job IS the required status check. Make `Playwright E2E Crosscheck` a required check on main in OSS
# branch protection to actually block merges (one-time admin toggle).
#
# Behaviour (A+A+C design):
# - PR does NOT touch web/** or tests/ui-testing/** → pass instantly (never blocks).
# - Touches them + `skip-ent-e2e` label → pass (explicit, auditable opt-out).
# - Touches them (the e2e label gates the whole job, like every paid workflow):
# · open sister ENT PR (same-named branch) → DEFER to that PR's checks (link it, don't build here)
# · sister ENT branch only (no PR) → build the enterprise binary against that branch
# · PR base is a release line (not main) with a same-named ENT branch → build against THAT branch
# · no ENT counterpart → build against ENT main, testing the PR merged into
# OSS main (refs/pull/N/merge; branch head if not mergeable)
# then POLL and mirror pass/fail. A lost test runner is re-run once before it is reported.
#
# A single STICKY comment per PR (found by a hidden marker) reports running → passed / failed, lists the
# failing specs, and links the sister ENT PR/branch + the ENT run. A build failure is attributed by the
# engine's baseline step: the PR broke it, or the enterprise ref is broken against OSS main (still red).
#
# Pure CI — no AI/LLM. Deterministic dispatch + poll of o2-enterprise/oss-pr-ent-gate.yml.
#
# Secrets/vars:
# secrets.ORG_ADMIN_TOKEN — PAT: o2-enterprise Actions R/W (dispatch+poll) + openobserve PRs R/W (comment as human).
# vars.ENT_GATE_REF — o2-enterprise ref that carries oss-pr-ent-gate.yml (default 'main';
# only override to test the engine itself from a branch).
# vars.ENT_E2E_GATE_DISABLED — 'true' kill switch: gate passes without dispatching.
on:
pull_request:
types: [synchronize, labeled, unlabeled, ready_for_review, converted_to_draft]
# Runs once a labelled PR is approved, then for pushes/labels while the approval stands.
pull_request_review:
types: [submitted]
# A required check must also report on merge-queue commits; the gate already passed on the PR head.
merge_group:
# PR-scoped: a new push/label supersedes the previous gate attempt for the same PR.
concurrency:
# A review that is not an approval does nothing, so it must not cancel a running gate.
group: playwright-e2e-crosscheck-${{ github.event.pull_request.number || github.ref }}${{ github.event_name == 'pull_request_review' && github.event.review.state != 'approved' && format('-{0}', github.run_id) || '' }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
approval:
# reviewDecision of a stacked PR is judged by the stack's merge rules, so it is what the merge will require.
if: ${{ github.event_name != 'merge_group' && (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false) && (github.event_name != 'pull_request_review' || github.event.review.state == 'approved') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: read
outputs:
approved: ${{ steps.check.outputs.approved }}
steps:
- id: check
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
DECISION=$(gh pr view "$PR" --json reviewDecision --jq .reviewDecision)
echo "reviewDecision: $DECISION"
APPROVED=false
if [ "$DECISION" = "APPROVED" ]; then APPROVED=true; fi
echo "approved=$APPROVED" >> "$GITHUB_OUTPUT"
playwright-e2e-crosscheck:
needs: approval
# A pre-merge gate: runs once a labelled PR is approved, not on every push.
if: ${{ !cancelled() && (github.event_name == 'merge_group' || needs.approval.outputs.approved == 'true') }}
name: Playwright E2E Crosscheck
runs-on: ubuntu-latest
# ~5 min to find the ENT run + up to ~75 min to poll. Generous on purpose: normal is ~20 min,
# but a cold Rust cache can push the ENT build+run toward its own timeouts — don't false-fail
# the gate ("timed out") while ENT is still legitimately running. A lost runner adds one re-run.
timeout-minutes: 150
steps:
- name: Triage — relevant paths? opted out?
id: triage
env:
GH_TOKEN: ${{ github.token }} # OSS is same-repo/public → default token can read PR files
PR: ${{ github.event.pull_request.number }}
HAS_SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'skip-ent-e2e') }}
DISABLED: ${{ vars.ENT_E2E_GATE_DISABLED }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "merge_group" ]; then
echo "::notice::merge queue commit — gate already passed on the PR head."
echo "mode=pass" >> "$GITHUB_OUTPUT"; exit 0
fi
if [ "$DISABLED" = "true" ]; then
echo "::notice::ENT_E2E_GATE_DISABLED=true — passing without dispatch."
echo "mode=pass" >> "$GITHUB_OUTPUT"; exit 0
fi
# Fork PRs: head branch isn't in openobserve/openobserve and the PAT isn't available to
# forked-PR runs, so the ENT engine can't check it out. Pass with a notice; a maintainer
# re-runs from an internal branch if enterprise validation is needed.
if [ "$IS_FORK" = "true" ]; then
echo "::notice::Fork PR — Playwright E2E Crosscheck can't run (no PAT / branch not in main repo). Passing."
echo "mode=pass" >> "$GITHUB_OUTPUT"; exit 0
fi
# Relevant = any changed file under web/ or tests/ui-testing/
FILES=$(gh api "repos/${{ github.repository }}/pulls/$PR/files" --paginate -q '.[].filename')
if printf '%s\n' "$FILES" | grep -qE '^(web/|tests/ui-testing/)'; then
RELEVANT=true
else
RELEVANT=false
fi
echo "::notice::Relevant (web/** or tests/ui-testing/**) changes: $RELEVANT"
if [ "$RELEVANT" != "true" ]; then
echo "mode=pass" >> "$GITHUB_OUTPUT"
elif [ "$HAS_SKIP" = "true" ]; then
echo "::warning::PR carries 'skip-ent-e2e' — enterprise gate explicitly opted out (auditable)."
echo "mode=skip" >> "$GITHUB_OUTPUT"
else
echo "mode=run" >> "$GITHUB_OUTPUT"
fi
- name: Pass — no enterprise-relevant changes (or opted out / disabled)
if: steps.triage.outputs.mode == 'pass' || steps.triage.outputs.mode == 'skip'
run: echo "✅ Playwright E2E Crosscheck not required for this PR."
- name: Run — dispatch ENT engine, poll, mirror result
if: steps.triage.outputs.mode == 'run'
env:
GH_TOKEN: ${{ secrets.ORG_ADMIN_TOKEN }} # PAT: dispatch+poll o2-enterprise, comment as human
PR: ${{ github.event.pull_request.number }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
ENT_REF: ${{ vars.ENT_GATE_REF || 'main' }}
run: |
set -euo pipefail
REPO="openobserve/o2-enterprise"; WF="oss-pr-ent-gate.yml"
MARKER="<!-- playwright-e2e-crosscheck -->"
# Upsert ONE sticky gate comment per PR (located by the hidden marker) so status updates in
# place — pass AND fail — instead of spamming a fresh comment each run.
sticky() {
local body="$1" cid
cid=$(gh api "repos/openobserve/openobserve/issues/$PR/comments" --paginate \
-q "[.[] | select(.body|contains(\"$MARKER\")) | .id][0]" 2>/dev/null || echo "")
if [ -n "$cid" ] && [ "$cid" != "null" ]; then
gh api -X PATCH "repos/openobserve/openobserve/issues/comments/$cid" -f body="$body" >/dev/null 2>&1 || true
else
gh api -X POST "repos/openobserve/openobserve/issues/$PR/comments" -f body="$body" >/dev/null 2>&1 || true
fi
}
# Sister ENT resolution. An OPEN same-named ENT PR is the live counterpart — let ITS checks
# own enterprise validation; we DEFER (don't build here), just link. Only a branch (no PR) →
# build against that branch. Neither → the PR's base-line ENT branch if it exists, else main.
SISTER_PR=$(gh pr list --repo "$REPO" --head "$HEAD_REF" --state open --json number -q '.[0].number' 2>/dev/null || echo "")
if [ -n "$SISTER_PR" ] && [ "$SISTER_PR" != "null" ]; then
sticky "### ⏭️ Playwright E2E Crosscheck — handled by the sister ENT PR
$MARKER
This OSS PR has a matching enterprise PR on branch \`$HEAD_REF\`: **openobserve/o2-enterprise#$SISTER_PR**. The enterprise-only e2e specs run there, so this gate defers to that PR's checks and does not build here."
echo "::notice::Sister ENT PR #$SISTER_PR is open — deferring, not dispatching."
exit 0
fi
# No sister branch/PR → build against the PR's base line, not blindly main. A PR onto a
# release branch (e.g. branch-v1.0.0) must build against the ENT branch of the same name;
# ENT main would fail to compile the release-line OSS code. Fall back to main only when
# the base is main or has no ENT counterpart.
if gh api "repos/$REPO/branches/$HEAD_REF" >/dev/null 2>&1; then
ENT_CODE_REF="$HEAD_REF"
SISTER_NOTE="Built against the matching ENT branch \`$HEAD_REF\` (no ENT PR yet)."
echo "::notice::ENT branch '$HEAD_REF' exists (no PR) — building against it."
elif [ "$BASE_REF" != "main" ] && [ "$BASE_REF" != "master" ] && gh api "repos/$REPO/branches/$BASE_REF" >/dev/null 2>&1; then
ENT_CODE_REF="$BASE_REF"
SISTER_NOTE="No same-named ENT branch — built against the PR's base line \`$BASE_REF\` on ENT."
echo "::notice::No ENT branch '$HEAD_REF'; PR targets '$BASE_REF' which exists on ENT — building against it."
else
ENT_CODE_REF=main
SISTER_NOTE="No ENT counterpart — built against ENT \`main\`."
echo "::notice::No ENT counterpart for '$HEAD_REF' (base '$BASE_REF') — building against main."
fi
# Against ENT main, test the PR merged into OSS main: a stale branch would miss OSS code ENT main already uses.
OSS_REF="$HEAD_REF"
if [ "$ENT_CODE_REF" = "main" ] && [ "$BASE_REF" = "main" ]; then
MERGEABLE=""
for _ in 1 2 3 4 5; do
MERGEABLE=$(gh api "repos/openobserve/openobserve/pulls/$PR" -q '.mergeable' 2>/dev/null || echo "")
if [ "$MERGEABLE" = "true" ] || [ "$MERGEABLE" = "false" ]; then break; fi
sleep 6
done
if [ "$MERGEABLE" = "true" ]; then
OSS_REF="refs/pull/$PR/merge"
SISTER_NOTE="$SISTER_NOTE Tested as this PR merged into OSS \`main\`."
else
echo "::warning::PR #$PR is not cleanly mergeable into main (mergeable=$MERGEABLE) — testing the branch head as-is."
fi
fi
# Stamp the time BEFORE dispatch so correlation can reject stale prior runs for this PR.
DISPATCH_TS=$(date -u +%s)
echo "::notice::Dispatching ENT engine ($REPO@$ENT_REF, code=$ENT_CODE_REF, oss=$OSS_REF) for OSS PR #$PR ($HEAD_REF)…"
gh workflow run "$WF" --repo "$REPO" --ref "$ENT_REF" \
-f oss_branch="$OSS_REF" \
-f oss_pr="$PR" \
-f oss_sha="$HEAD_SHA" \
-f enterprise_ref="$ENT_CODE_REF"
# Correlate by PR-scoped run-name ("Playwright E2E Crosscheck — OSS PR #<PR> ("; the "(" boundary keeps
# #12 from matching #123) AND createdAt >= dispatch time (minus 60s clock-skew), so a stale
# prior run for this PR is never picked. Take the NEWEST qualifying run.
MATCH="OSS PR #$PR ("
MIN_TS=$((DISPATCH_TS - 60))
RUN_ID=""
sleep 8 # let the dispatched run register before the first lookup
for i in $(seq 1 30); do # ~5 min to appear
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WF" -L 40 \
--json databaseId,displayTitle,createdAt \
-q "[.[] | select(.displayTitle | contains(\"$MATCH\")) | select((.createdAt|fromdateiso8601) >= $MIN_TS)] | sort_by(.createdAt) | last | .databaseId" 2>/dev/null || echo "")
if [ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ]; then break; fi
sleep 10
done
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
sticky "### 🚫 Playwright E2E Crosscheck — could not start
$MARKER
Dispatching the enterprise e2e engine failed or the run never appeared (check \`ORG_ADMIN_TOKEN\` / \`ENT_GATE_REF=$ENT_REF\`). The gate stays red until it runs — re-push or re-label to retry.
$SISTER_NOTE"
echo "::error::Could not locate the dispatched ENT run within 5 min — failing the gate (unverified)."
exit 1
fi
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
echo "::notice::Tracking ENT run: $RUN_URL"
sticky "### 🔄 Playwright E2E Crosscheck — running
$MARKER
Building the enterprise binary and running the enterprise-only e2e specs against this PR.
$SISTER_NOTE
[ENT run]($RUN_URL)"
# A lost runner is infra, not the PR: re-run the failed ENT job once before reporting it.
ATTEMPT=1
while :; do
# Poll to completion (~75 min budget — covers a cold-cache ENT build+run worst case).
STATUS=""; CONCL=""
for i in $(seq 1 300); do # 300 * 15s = 75 min
read -r STATUS CONCL < <(gh run view "$RUN_ID" --repo "$REPO" \
--json status,conclusion -q '.status + " " + (.conclusion // "")' 2>/dev/null || echo "unknown ")
if [ "$STATUS" = "completed" ]; then break; fi
sleep 15
done
if [ "$STATUS" != "completed" ]; then
sticky "### ⏳ Playwright E2E Crosscheck — timed out
$MARKER
The [enterprise e2e run]($RUN_URL) didn't finish within the poll window. The gate stays red — re-push to retry.
$SISTER_NOTE"
echo "::error::ENT run did not finish within the poll window."
exit 1
fi
if [ "$CONCL" = "success" ]; then
sticky "### ✅ Playwright E2E Crosscheck — passed
$MARKER
The enterprise-only e2e specs passed against this PR. $SISTER_NOTE
[ENT run]($RUN_URL)"
echo "::notice::✅ Enterprise e2e specs passed against this PR."
exit 0
fi
# cancelled/skipped/empty ≈ a newer push superseded this run (engine cancel-in-progress).
# NOT a failure — the newer run is authoritative and updates the sticky itself. Defer.
if [ "$CONCL" = "cancelled" ] || [ "$CONCL" = "skipped" ] || [ -z "$CONCL" ]; then
echo "::notice::ENT run concluded '$CONCL' (likely superseded by a newer push) — deferring, not failing."
exit 0
fi
# Genuine failure — distinguish an enterprise BUILD failure (compile) from SPEC failures,
# and for specs list the failing files from the engine's failed-specs artifact.
BUILD_CONCL=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs -q '.jobs[]|select(.name=="build_binary")|.conclusion' 2>/dev/null || echo "")
TEST_CONCL=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs -q '.jobs[]|select(.name=="ent_gate_tests")|.conclusion' 2>/dev/null || echo "")
# A test job that failed with no failed step was killed from outside (runner lost), not by a spec.
TEST_FAILED_STEPS=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs -q '[.jobs[]|select(.name=="ent_gate_tests")|.steps[]|select(.conclusion=="failure")]|length' 2>/dev/null || echo "")
if [ "$TEST_CONCL" = "failure" ] && [ "$TEST_FAILED_STEPS" = "0" ] && [ "$ATTEMPT" = "1" ]; then
ATTEMPT=2
echo "::warning::ENT test runner was lost — re-running the failed ENT job once."
if gh run rerun "$RUN_ID" --repo "$REPO" --failed; then
sticky "### 🔄 Playwright E2E Crosscheck — runner lost, re-running
$MARKER
The enterprise test runner stopped before the specs finished (infra, not this PR), so the failed job is being re-run automatically.
$SISTER_NOTE
[ENT run]($RUN_URL)"
sleep 20
continue
fi
echo "::warning::Could not re-run the ENT job — reporting the lost runner."
fi
break
done
# The engine's baseline step builds the same ENT ref against OSS main after a failed build.
BASELINE_CONCL=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs -q '.jobs[]|select(.name=="build_binary")|.steps[]|select(.name|startswith("Baseline"))|.conclusion' 2>/dev/null || echo "")
if [ "$TEST_CONCL" = "failure" ] && [ "$TEST_FAILED_STEPS" = "0" ]; then
sticky "### ⚠️ Playwright E2E Crosscheck — runner lost, re-run needed
$MARKER
The enterprise test runner stopped before the specs finished (infra, not this PR), and the automatic re-run didn't get past it. Re-run the failed job of this check.
$SISTER_NOTE · [ENT run]($RUN_URL)"
elif [ "$TEST_CONCL" = "failure" ]; then
rm -rf /tmp/fs; SPEC_LIST=""
if gh run download "$RUN_ID" --repo "$REPO" -n failed-specs -D /tmp/fs >/dev/null 2>&1 && [ -f /tmp/fs/failed-specs.txt ]; then
SPEC_LIST=$(sed '/^[[:space:]]*$/d; s/^/- `/; s/$/`/' /tmp/fs/failed-specs.txt)
fi
[ -n "$SPEC_LIST" ] || SPEC_LIST="_(couldn't read the failed-spec list — open the run for details)_"
sticky "### 🚫 Playwright E2E Crosscheck — Needs Review
$MARKER
The **enterprise-only** e2e spec(s) below failed against this PR. They run only against the enterprise binary, so this breakage wouldn't show up in OSS CI:
$SPEC_LIST
$SISTER_NOTE · [ENT run]($RUN_URL)
Fix and re-push, or add \`skip-ent-e2e\` if this is a known-safe change."
elif [ "$BUILD_CONCL" = "failure" ] && [ "$BASELINE_CONCL" = "failure" ]; then
sticky "### 🚫 Playwright E2E Crosscheck — enterprise \`$ENT_CODE_REF\` is broken (not this PR)
$MARKER
Enterprise \`$ENT_CODE_REF\` doesn't compile against OSS \`main\` either, so this build failure is not caused by this PR. Usually a paired OSS + enterprise change where the enterprise half merged first. The check stays red: re-run it once the OSS half is on \`main\`.
$SISTER_NOTE · [ENT run]($RUN_URL)"
elif [ "$BUILD_CONCL" = "failure" ] && [ "$BASELINE_CONCL" = "success" ]; then
sticky "### 🚫 Playwright E2E Crosscheck — this PR breaks the enterprise build
$MARKER
Enterprise compiles against OSS \`main\` but not with this PR's changes, so the specs never ran. Check the build log for OSS code the enterprise side still uses.
$SISTER_NOTE · [ENT run]($RUN_URL)"
elif [ "$BUILD_CONCL" = "failure" ]; then
sticky "### 🚫 Playwright E2E Crosscheck — enterprise build failed
$MARKER
The enterprise binary didn't compile against this PR, so the specs never ran. This usually means the OSS code references enterprise code not present on the ref it was built against.
$SISTER_NOTE · [ENT run]($RUN_URL)"
else
sticky "### 🚫 Playwright E2E Crosscheck — failed
$MARKER
The [enterprise e2e run]($RUN_URL) failed (conclusion: \`$CONCL\`).
$SISTER_NOTE"
fi
echo "::error::Enterprise gate failed (conclusion=$CONCL build=$BUILD_CONCL baseline=$BASELINE_CONCL test=$TEST_CONCL failed_steps=$TEST_FAILED_STEPS)."
exit 1