mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
ci(playwright): single-source ALL shard matrices via ci-matrix/ manifests (#13606)
## Why
The OSS and ENT Playwright shard matrices (`run_files` per `testfolder`)
were hand-maintained inline in each `playwright.yml` and had to be
synced on every spec add — error-prone and drift-prone (ENT was even
missing `dashboard-favorites`). This makes the shard list a **single
source of truth** for **every** Playwright workflow.
## What (OSS side)
All matrices are generated at run time from JSON manifests in
`tests/ui-testing/ci-matrix/`:
- **`ci-matrix/ci_matrix.json`** — PR-gate base (24 shards). The only
place shared shards live.
- **`ci-matrix/ci_matrix_regression.json`** — regression base (10
shards); `RegressionSet/<Feature>` mapping now carried in
`actual_folder`.
- **`.github/scripts/build-ci-matrix.js`** — merges base (+ optional ENT
overlay) → GitHub matrix; validates uniqueness / no dup specs / no spec
both active+disabled. Reused by ENT from its OSS checkout.
- **`ci-matrix/README.md`** — manifest index + how to add/move/disable a
spec.
- Workflows `playwright.yml` + `playwright_regression.yml`: new
`generate_matrix` job → `strategy.matrix: fromJSON(...)`; `case` blocks
→ `${{ matrix.actual_folder }}`.
Disabling a spec (JSON has no `//`): move it into that shard's
`disabled: [{file, reason}]` array — never emitted (won't run), record
survives. Every shard ships a `"disabled": []` placeholder.
## Safety (verified)
- **OSS `playwright.yml` and `playwright_regression.yml` matrices are
byte-identical to before** — zero behavior change.
- Companion ENT PR (same branch slug
`test/playwright-matrix-single-source`) derives its matrices from these
manifests + overlays.
## Validate
Add the `e2e` label; confirm `generate_matrix` prints the shard count
and the `e2e /` jobs fan out.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-ci-matrix.js — single source of truth for the Playwright UI shard matrix.
|
||||
*
|
||||
* Prints a GitHub Actions matrix object: {"include":[ {testfolder,actual_folder,browser,run_files}, ... ]}
|
||||
* consumed by `strategy.matrix: ${{ fromJSON(needs.generate_matrix.outputs.matrix) }}`.
|
||||
*
|
||||
* Usage:
|
||||
* node build-ci-matrix.js <base.json> # OSS: base manifest verbatim
|
||||
* node build-ci-matrix.js <base.json> <overlay.json> # ENT: base + enterprise overlay
|
||||
*
|
||||
* The base (OSS tests/ui-testing/ci-matrix/ci_matrix.json) is the ONLY place shared shards are
|
||||
* listed. ENT never re-lists shared specs — its overlay (ci_matrix.ent.json) carries
|
||||
* only the delta, so a spec added to OSS flows into ENT automatically:
|
||||
* { "append": { "<testfolder>": ["extra.spec.js", ...] }, // add ENT-only specs to a shared shard
|
||||
* "shards": [ {testfolder,actual_folder,browser,run_files} ], // whole ENT-only shards (Workflows, SDR…)
|
||||
* "disabled": { "<testfolder>": [{file,reason}, ...] } } // ENT-only specs turned off (doc only)
|
||||
*
|
||||
* DISABLING A SPEC (JSON has no // comments): don't delete it — move it into a
|
||||
* shard's "disabled": [{ "file": "x.spec.js", "reason": "why" }] array. Disabled
|
||||
* entries are documentation only: this script never emits them, so they don't run,
|
||||
* but the record + reason survive. "_comment" (or any _-prefixed key) is also ignored.
|
||||
*
|
||||
* This lives in OSS so ENT can reuse it from its tree-merged OSS checkout.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
|
||||
function die(msg) {
|
||||
process.stderr.write(`build-ci-matrix: ${msg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
} catch (e) {
|
||||
die(`cannot read/parse ${path}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [basePath, overlayPath] = process.argv.slice(2);
|
||||
if (!basePath) die("usage: build-ci-matrix.js <base.json> [overlay.json]");
|
||||
|
||||
const base = readJson(basePath);
|
||||
if (!Array.isArray(base)) die(`base ${basePath} must be a JSON array of shards`);
|
||||
|
||||
// Deep-copy base so we never mutate the parsed source objects.
|
||||
const include = base.map((s) => ({ ...s, run_files: [...(s.run_files || [])] }));
|
||||
const byFolder = new Map(include.map((s) => [s.testfolder, s]));
|
||||
|
||||
if (overlayPath) {
|
||||
const overlay = readJson(overlayPath);
|
||||
|
||||
// append: add enterprise-only specs to an EXISTING shared shard.
|
||||
for (const [folder, specs] of Object.entries(overlay.append || {})) {
|
||||
const shard = byFolder.get(folder);
|
||||
if (!shard) {
|
||||
die(`overlay append targets unknown folder "${folder}" — it must exist in the base manifest`);
|
||||
}
|
||||
for (const spec of specs) {
|
||||
if (shard.run_files.includes(spec)) {
|
||||
die(`overlay append: "${spec}" already in base shard "${folder}" — remove it from the overlay`);
|
||||
}
|
||||
shard.run_files.push(spec);
|
||||
}
|
||||
}
|
||||
|
||||
// shards: whole enterprise-only shards (Workflows, SDR-Logs, SDR-Traces, …).
|
||||
for (const shard of overlay.shards || []) {
|
||||
if (byFolder.has(shard.testfolder)) {
|
||||
die(`overlay shard "${shard.testfolder}" collides with a base shard — use "append" instead`);
|
||||
}
|
||||
const copy = { browser: "chrome", ...shard, run_files: [...(shard.run_files || [])] };
|
||||
include.push(copy);
|
||||
byFolder.set(copy.testfolder, copy);
|
||||
}
|
||||
|
||||
// disabled: enterprise-only turned-off specs, recorded against a shard for docs only.
|
||||
for (const [folder, entries] of Object.entries(overlay.disabled || {})) {
|
||||
const shard = byFolder.get(folder);
|
||||
if (!shard) die(`overlay disabled targets unknown folder "${folder}"`);
|
||||
shard.disabled = [...(shard.disabled || []), ...entries];
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity: unique testfolders, no empty shards, no dup specs, and no spec both
|
||||
// active (run_files) and disabled in the same shard.
|
||||
const seen = new Set();
|
||||
for (const s of include) {
|
||||
if (!s.testfolder) die(`shard missing testfolder: ${JSON.stringify(s)}`);
|
||||
if (seen.has(s.testfolder)) die(`duplicate testfolder "${s.testfolder}"`);
|
||||
seen.add(s.testfolder);
|
||||
if (!s.actual_folder) s.actual_folder = s.testfolder;
|
||||
if (!s.run_files || s.run_files.length === 0) die(`shard "${s.testfolder}" has no run_files`);
|
||||
if (new Set(s.run_files).size !== s.run_files.length) {
|
||||
die(`shard "${s.testfolder}" has duplicate run_files`);
|
||||
}
|
||||
for (const d of s.disabled || []) {
|
||||
if (s.run_files.includes(d.file)) {
|
||||
die(`shard "${s.testfolder}": "${d.file}" is in both run_files and disabled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit ONLY the fields the CI matrix consumes — disabled/_comment/notes are stripped,
|
||||
// so turned-off specs never reach `npx playwright test`.
|
||||
const matrix = include.map((s) => ({
|
||||
testfolder: s.testfolder,
|
||||
actual_folder: s.actual_folder,
|
||||
browser: s.browser || "chrome",
|
||||
run_files: s.run_files,
|
||||
}));
|
||||
|
||||
process.stdout.write(JSON.stringify({ include: matrix }));
|
||||
Reference in New Issue
Block a user