mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
462 lines
28 KiB
YAML
462 lines
28 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 merge commit SHA tied to
|
|
# the event's head SHA (the head SHA itself if there is none)
|
|
# then POLL and mirror pass/fail. Only a lost test runner is re-run, up to twice, 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:
|
|
# Opens, reopens, pushes, base changes and the gate's own labels run it; any other label or edit only mirrors its current result.
|
|
pull_request:
|
|
types: [opened, reopened, synchronize, edited, labeled, unlabeled, ready_for_review, converted_to_draft]
|
|
# 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 or gate label supersedes the previous gate attempt for the same PR.
|
|
concurrency:
|
|
# Other labels (auto-requeue-* and the like) and title/body edits only mirror the gate's result, so they must not cancel a running gate; the newest mirror is enough.
|
|
group: playwright-e2e-crosscheck-${{ github.event.pull_request.number || github.ref }}${{ (((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'ready-for-ci' && github.event.label.name != 'skip-ent-e2e') || (github.event.action == 'edited' && !github.event.changes.base)) && '-mirror' || '' }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
playwright-e2e-crosscheck:
|
|
# Without ready-for-ci, or on a draft, the required ci-gate status stays pending, so skipping here cannot let a PR merge.
|
|
if: ${{ github.event_name == 'merge_group' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false) }}
|
|
name: Playwright E2E Crosscheck
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
checks: read
|
|
actions: read
|
|
# Starting (5) + the first run's budget (165) + two lost-runner re-runs (80 each, plus 1 to see the new attempt), from the poll budgets below.
|
|
timeout-minutes: 340
|
|
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 }}
|
|
MIRROR: ${{ ((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'ready-for-ci' && github.event.label.name != 'skip-ent-e2e') || (github.event.action == 'edited' && !github.event.changes.base) }}
|
|
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
|
|
# A fork's branch is not in this repo and its runs get no PAT, so the engine cannot check it out.
|
|
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')
|
|
CHANGED=$(gh api "repos/${{ github.repository }}/pulls/$PR" -q .changed_files)
|
|
# The files API lists at most 3000 files, so a list it cut short cannot prove the PR is irrelevant.
|
|
if [ "$CHANGED" -ge 3000 ] || [ "$CHANGED" -gt "$(grep -c . <<<"$FILES")" ]; then
|
|
RELEVANT=true
|
|
# A here-string, not a pipe: grep -q exits early and the writer's SIGPIPE would fail the pipeline under pipefail.
|
|
elif grep -qE '^(web/|tests/ui-testing/)' <<<"$FILES"; 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"
|
|
elif [ "$MIRROR" = "true" ]; then
|
|
echo "mode=mirror" >> "$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: Mirror — report the gate's current result for this head
|
|
if: steps.triage.outputs.mode == 'mirror'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
RUN_ID: ${{ github.run_id }}
|
|
GATE_ON: ${{ contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false }}
|
|
run: |
|
|
set -euo pipefail
|
|
CHECKS="repos/${{ github.repository }}/commits/$HEAD_SHA/check-runs?check_name=Playwright%20E2E%20Crosscheck&filter=all&per_page=100"
|
|
OWN=""
|
|
for _ in 1 2 3 4 5; do
|
|
OWN=$(gh api --paginate "$CHECKS" | jq -r --arg u "/actions/runs/$RUN_ID/" '.check_runs[] | select(.details_url | contains($u)) | .id' | sort -n | tail -n 1) && [ -n "$OWN" ] && break
|
|
sleep 10
|
|
done
|
|
[ -n "$OWN" ] || { echo "::error::Could not find this job's own check run."; exit 1; }
|
|
# Only older checks count: the newest check of this name is the required one, so a later gate run overrides this mirror anyway.
|
|
ERRORS=0; IGNORE="[]"
|
|
api_error() {
|
|
ERRORS=$((ERRORS + 1))
|
|
if [ "$ERRORS" -ge 10 ]; then echo "::error::The GitHub API kept failing — cannot report the gate's result."; exit 1; fi
|
|
sleep 30
|
|
}
|
|
while :; do
|
|
# An API error must not read as "no result", which would pass.
|
|
if ! LIST=$(gh api --paginate "$CHECKS" | jq -r --argjson own "$OWN" --argjson ignore "$IGNORE" \
|
|
'.check_runs[] | select(.id < $own and .conclusion != "skipped" and (.id as $i | $ignore | index($i) | not)) | "\(.id) \(.status) \(.conclusion // "")"'); then
|
|
api_error; continue
|
|
fi
|
|
read -r ID STATUS CONCL <<<"$(sort -n <<<"$LIST" | tail -n 1)"
|
|
if [ -z "$ID" ]; then
|
|
# This check is the newest, so passing here would make the required check green without the gate ever running.
|
|
if [ "$GATE_ON" = "true" ]; then
|
|
echo "::error::No Playwright E2E Crosscheck run exists for $HEAD_SHA, so there is no result to report. Re-apply the 'ready-for-ci' label or push a commit to run the gate."
|
|
exit 1
|
|
fi
|
|
echo "::notice::No gate result on $HEAD_SHA yet — nothing to mirror."
|
|
exit 0
|
|
fi
|
|
# Mirrors and failed triages share this check name, so only a check whose Run step started is a gate result; an unfinished one may still start it.
|
|
if ! RAN=$(gh api "repos/${{ github.repository }}/actions/jobs/$ID" -q '[.steps[]? | select(.status == "in_progress" or (.conclusion != null and .conclusion != "skipped")) | .name | split(" — ")[0]] | join(" ")'); then
|
|
api_error; continue
|
|
fi
|
|
if [[ " $RAN " == *" Run "* ]]; then
|
|
if [ "$STATUS" = "completed" ]; then break; fi
|
|
elif [[ " $RAN " == *" Mirror "* ]] || [ "$STATUS" = "completed" ]; then
|
|
IGNORE=$(jq -c --argjson i "$ID" '. + [$i]' <<<"$IGNORE"); continue
|
|
fi
|
|
# This check is newer than the running gate, so it must end with the gate's result, not before it.
|
|
ERRORS=0; sleep 30
|
|
done
|
|
echo "::notice::Mirroring gate check $ID on $HEAD_SHA: $CONCL."
|
|
[ "$CONCL" = "success" ] || { echo "::error::The gate's current result on this head is '$CONCL'."; exit 1; }
|
|
|
|
- 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 -->"
|
|
GATE_LOGIN=$(gh api user -q .login 2>/dev/null || echo "")
|
|
|
|
# One display-only status comment per PR; --paginate prints one array per page, so take the first matching id.
|
|
sticky() {
|
|
STICKY_ID=${STICKY_ID:-$(gh api "repos/openobserve/openobserve/issues/$PR/comments" --paginate 2>/dev/null \
|
|
| jq -r --arg login "$GATE_LOGIN" --arg marker "$MARKER" '.[] | select(.user.login == $login and (.body | contains($marker))) | .id' | head -n 1 || true)}
|
|
if [ -n "$STICKY_ID" ]; then
|
|
gh api -X PATCH "repos/openobserve/openobserve/issues/comments/$STICKY_ID" -f body="$1" >/dev/null 2>&1 || true
|
|
else
|
|
STICKY_ID=$(gh api -X POST "repos/openobserve/openobserve/issues/$PR/comments" -f body="$1" -q .id 2>/dev/null || echo "")
|
|
fi
|
|
}
|
|
cannot_start() {
|
|
sticky "### 🚫 Playwright E2E Crosscheck — could not start
|
|
|
|
$MARKER
|
|
$1 The gate stays red until it runs — re-push or re-label to retry.
|
|
|
|
${SISTER_NOTE:-}"
|
|
echo "::error::$1"
|
|
exit 1
|
|
}
|
|
|
|
# An open same-named ENT PR runs these specs itself, so the gate defers to its checks.
|
|
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
|
|
|
|
# A release-line PR needs the same-named ENT branch, and only plain names may reach API paths ("#", "?", "%" retarget them).
|
|
if [[ "$HEAD_REF" =~ ^[A-Za-z0-9._/-]+$ ]] && 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" ] && [[ "$BASE_REF" =~ ^[A-Za-z0-9._/-]+$ ]] && 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
|
|
|
|
# The run title names the exact ENT code and engine commits, so the gate can tell its own run from any other.
|
|
sha_of() {
|
|
local sha
|
|
for _ in 1 2 3; do
|
|
sha=$(gh api "repos/$REPO/commits/$1" -q .sha 2>/dev/null || true)
|
|
if [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then echo "$sha"; return 0; fi
|
|
sleep 5
|
|
done
|
|
}
|
|
ENT_SHA=$(sha_of "$ENT_CODE_REF")
|
|
[ -n "$ENT_SHA" ] || cannot_start "Could not resolve ENT \`$ENT_CODE_REF\` to a commit."
|
|
|
|
# Against ENT main, test the PR merged into OSS main: a stale branch would miss OSS code ENT main already uses.
|
|
TESTED_SHA="$HEAD_SHA"
|
|
if [ "$ENT_CODE_REF" = "main" ] && [ "$BASE_REF" = "main" ]; then
|
|
MERGEABLE=""; MERGE_SHA=""; PR_HEAD=""
|
|
for _ in 1 2 3 4 5; do
|
|
read -r MERGEABLE MERGE_SHA PR_HEAD < <(gh api "repos/openobserve/openobserve/pulls/$PR" -q '"\(.mergeable) \(.merge_commit_sha // "-") \(.head.sha)"' 2>/dev/null || echo "")
|
|
if [ "$MERGEABLE" = "true" ] || [ "$MERGEABLE" = "false" ]; then break; fi
|
|
sleep 6
|
|
done
|
|
# The merge commit belongs to this push only when the same response reports this head.
|
|
if [ "$MERGEABLE" = "true" ] && [[ "$MERGE_SHA" =~ ^[0-9a-f]{40}$ ]] && [ "$PR_HEAD" = "$HEAD_SHA" ]; then
|
|
TESTED_SHA="$MERGE_SHA"
|
|
SISTER_NOTE="$SISTER_NOTE Tested as this PR merged into OSS \`main\`."
|
|
else
|
|
echo "::warning::PR #$PR has no merge commit for head $HEAD_SHA (mergeable=$MERGEABLE, head=$PR_HEAD) — testing the branch head as-is."
|
|
fi
|
|
fi
|
|
|
|
# The engine checks out oss_branch, so a commit SHA there makes it test exactly what its title names.
|
|
MATCH="Playwright E2E Crosscheck — OSS PR #$PR ("
|
|
# The engine commit is whatever ENT_REF points at when the run starts, so it is read from the title, not predicted.
|
|
TITLE="${MATCH}$TESTED_SHA) oss=$HEAD_SHA ent=$ENT_SHA engine="
|
|
newest_run() {
|
|
gh run list --repo "$REPO" --workflow "$WF" --event workflow_dispatch -L 40 --json databaseId,displayTitle,createdAt,event 2>/dev/null \
|
|
| jq -r --arg t "$TITLE" --arg old "${MATCH}$TESTED_SHA)" --argjson min "$1" \
|
|
'[.[] | select(.event == "workflow_dispatch" and ((.displayTitle | startswith($t) and (ltrimstr($t) | test("^[0-9a-f]{40}$"))) or .displayTitle == $old) and (.createdAt | fromdateiso8601) >= $min)] | sort_by(.databaseId) | last | .databaseId // empty' 2>/dev/null || true
|
|
}
|
|
DISPATCH_TS=$(date -u +%s)
|
|
echo "::notice::Dispatching ENT engine ($REPO@$ENT_REF, code=$ENT_CODE_REF@$ENT_SHA, oss=$TESTED_SHA) for OSS PR #$PR ($HEAD_REF)…"
|
|
gh workflow run "$WF" --repo "$REPO" --ref "$ENT_REF" \
|
|
-f oss_branch="$TESTED_SHA" \
|
|
-f oss_head_ref="$HEAD_REF" \
|
|
-f oss_pr="$PR" \
|
|
-f oss_sha="$HEAD_SHA" \
|
|
-f enterprise_ref="$ENT_SHA"
|
|
RUN_ID=""
|
|
sleep 8
|
|
for _ in $(seq 1 30); do
|
|
RUN_ID=$(newest_run "$((DISPATCH_TS - 60))")
|
|
if [[ "$RUN_ID" =~ ^[0-9]+$ ]]; then break; fi
|
|
RUN_ID=""; sleep 10
|
|
done
|
|
[ -n "$RUN_ID" ] || cannot_start "Dispatching the enterprise e2e engine failed or the run never appeared within 5 min (check \`ORG_ADMIN_TOKEN\` / \`ENT_GATE_REF=$ENT_REF\`)."
|
|
|
|
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
|
|
echo "::notice::Tracking ENT run: $RUN_URL"
|
|
|
|
report_pass() {
|
|
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."
|
|
}
|
|
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 only the lost ENT jobs, up to 3 attempts of the run in all.
|
|
MAX_ATTEMPTS=3; RETRIES=0
|
|
# Poll budgets follow the engine's job timeouts: subset 10 + build 75 + shard 45 + aggregate 10 + report 10 min, plus 15 min of runner queueing.
|
|
SHARD_MIN=45; RUN_BUDGET_MIN=$((10 + 75 + SHARD_MIN + 10 + 10 + 15)); RERUN_BUDGET_MIN=$((SHARD_MIN + 10 + 10 + 15))
|
|
DEADLINE=$((SECONDS + RUN_BUDGET_MIN * 60))
|
|
while :; do
|
|
STATUS=""; CONCL=""
|
|
while :; do
|
|
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" ] || [ "$SECONDS" -ge "$DEADLINE" ]; 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 report_pass; exit 0; fi
|
|
|
|
# A run of this same test cancels ours (engine concurrency), so the gate follows it; anything else never finished the specs.
|
|
if [ "$CONCL" = "cancelled" ] || [ "$CONCL" = "skipped" ] || [ -z "$CONCL" ]; then
|
|
NEXT=$(newest_run "$((DISPATCH_TS - 60))")
|
|
if [[ "$NEXT" =~ ^[0-9]+$ ]] && [ "$NEXT" -gt "$RUN_ID" ]; then
|
|
echo "::notice::ENT run $RUN_ID was cancelled by run $NEXT of this same test — following it."
|
|
RUN_ID="$NEXT"; RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"; DEADLINE=$((SECONDS + RUN_BUDGET_MIN * 60))
|
|
continue
|
|
fi
|
|
sticky "### 🚫 Playwright E2E Crosscheck — enterprise run was cancelled, re-run needed
|
|
|
|
$MARKER
|
|
The [enterprise e2e run]($RUN_URL) was cancelled (conclusion: \`${CONCL:-none}\`) and no newer run of this same test replaced it, so the specs did not finish. Re-run the failed job of this check.
|
|
|
|
$SISTER_NOTE"
|
|
echo "::error::ENT run concluded '${CONCL:-none}' with no newer run of this same test — failing the gate (unverified)."
|
|
exit 1
|
|
fi
|
|
|
|
# A build failure and spec failures need different fixes, so the report tells them apart.
|
|
BUILD_CONCL=$(gh run view "$RUN_ID" --repo "$REPO" --json jobs -q '.jobs[]|select(.name=="build_binary")|.conclusion' 2>/dev/null || echo "")
|
|
# Single-job re-runs spread shard results over attempts, so each test job counts by its latest attempt; an aggregate counts only without shards.
|
|
TEST_JOBS=$(gh api --paginate "repos/$REPO/actions/runs/$RUN_ID/jobs?filter=all&per_page=100" --jq '.jobs[]' 2>/dev/null | jq -s '[.[]|select(.name=="ent_gate_tests" or (.name|startswith("ent_gate_tests (")))] | group_by(.name) | map(max_by(.run_attempt)) | (map(select(.name|startswith("ent_gate_tests (")))) as $s | if ($s|length) > 0 then $s else . end' 2>/dev/null || echo '[]')
|
|
TEST_CONCL=$(jq -r '[.[].conclusion]|if index("failure") then "failure" elif length > 0 then .[0] // "" else "" end' <<<"$TEST_JOBS")
|
|
# A lost runner leaves its step running: no failed or cancelled step, and it ended before the job timeout, which cancels the step.
|
|
FAILED_JOBS=$(jq -c --argjson limit "$SHARD_MIN" '[.[] | select(.conclusion == "failure") | {id, lost: (([.steps[]? | select(.conclusion == "failure" or .conclusion == "cancelled")] | length) == 0 and .started_at != null and .completed_at != null and ((.completed_at | fromdateiso8601) - (.started_at | fromdateiso8601)) < ($limit * 60))}]' <<<"$TEST_JOBS" 2>/dev/null || echo '[]')
|
|
LOST_IDS=$(jq -r '.[] | select(.lost) | .id' <<<"$FAILED_JOBS")
|
|
TEST_LOST=$(jq '[.[] | select(.lost)] | length' <<<"$FAILED_JOBS")
|
|
TEST_SPEC_FAILED=$(jq '[.[] | select(.lost | not)] | length' <<<"$FAILED_JOBS")
|
|
TEST_LOST=${TEST_LOST:-0}; TEST_SPEC_FAILED=${TEST_SPEC_FAILED:-0}
|
|
RUN_ATTEMPT=$(gh api "repos/$REPO/actions/runs/$RUN_ID" -q '.run_attempt' 2>/dev/null || echo "$MAX_ATTEMPTS")
|
|
|
|
# A re-run would repeat the jobs with spec failures too, so only a lost-runner-only failure is retried.
|
|
if [ "$TEST_CONCL" = "failure" ] && [ "$TEST_LOST" != "0" ] && [ "$TEST_SPEC_FAILED" = "0" ] \
|
|
&& [ "$RUN_ATTEMPT" -lt "$MAX_ATTEMPTS" ] && [ "$RETRIES" -lt $((MAX_ATTEMPTS - 1)) ]; then
|
|
RETRIES=$((RETRIES + 1))
|
|
echo "::warning::ENT test runner lost in $TEST_LOST job(s) — re-running them (attempt $((RUN_ATTEMPT + 1)) of $MAX_ATTEMPTS)."
|
|
# The jobs API re-runs one job; a run can't take a second job re-run while the first is going, and with no spec failures --failed holds only lost jobs.
|
|
if [ "$TEST_LOST" = "1" ]; then
|
|
RERUN=(gh api -X POST "repos/$REPO/actions/jobs/$LOST_IDS/rerun")
|
|
else
|
|
RERUN=(gh run rerun "$RUN_ID" --repo "$REPO" --failed)
|
|
fi
|
|
if "${RERUN[@]}" >/dev/null; 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 lost job is being re-run automatically (attempt $((RUN_ATTEMPT + 1)) of $MAX_ATTEMPTS).
|
|
$SISTER_NOTE
|
|
|
|
[ENT run]($RUN_URL)"
|
|
# The poll must not read the finished attempt as the re-run's result.
|
|
for _ in $(seq 1 12); do
|
|
NOW_ATTEMPT=$(gh api "repos/$REPO/actions/runs/$RUN_ID" -q '.run_attempt' 2>/dev/null || echo "$RUN_ATTEMPT")
|
|
if [ "$NOW_ATTEMPT" -gt "$RUN_ATTEMPT" ]; then break; fi
|
|
sleep 5
|
|
done
|
|
DEADLINE=$((SECONDS + RERUN_BUDGET_MIN * 60))
|
|
continue
|
|
fi
|
|
echo "::warning::Could not re-run the lost ENT job(s) — 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 "")
|
|
|
|
rm -rf /tmp/fs; SPEC_LIST=""
|
|
if [ "$TEST_CONCL" = "failure" ] && gh run download "$RUN_ID" --repo "$REPO" -n failed-specs -D /tmp/fs >/dev/null 2>&1 && [ -f /tmp/fs/failed-specs.txt ]; then
|
|
# A comment body is capped at 65536 characters, so a huge list would make the update fail.
|
|
SPEC_LIST=$(awk '!/^[[:space:]]*$/ && n < 50 { n++; print "- `" substr($0, 1, 200) "`" }' /tmp/fs/failed-specs.txt)
|
|
SPEC_N=$(grep -cv '^[[:space:]]*$' /tmp/fs/failed-specs.txt || true)
|
|
if [ "${SPEC_N:-0}" -gt 50 ]; then SPEC_LIST="$SPEC_LIST"$'\n'"- … $((SPEC_N - 50)) more"; fi
|
|
fi
|
|
|
|
# Spec failures in another shard outrank a lost runner; the artifact only lists their files.
|
|
if [ "$TEST_CONCL" = "failure" ] && [ "$TEST_LOST" != "0" ] && [ "$TEST_SPEC_FAILED" = "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-runs didn't get past it. Re-run the failed job of this check.
|
|
|
|
$SISTER_NOTE · [ENT run]($RUN_URL)"
|
|
elif [ "$TEST_CONCL" = "failure" ]; then
|
|
[ -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 lost_jobs=$TEST_LOST)."
|
|
exit 1
|