Skip to content

Cut the e2e pipeline cost: environments by trigger, weekly bootstrap sweep, less setup per leg - #129

Merged
sscarduzio merged 28 commits into
masterfrom
perf/e2e-cost-reduction
Sep 15, 2026
Merged

sscarduzio merged 28 commits into
masterfrom
perf/e2e-cost-reduction

Conversation

@sscarduzio

@sscarduzio sscarduzio commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Makes the e2e pipeline cheaper to run. No assertion is weakened, and no ELK version stops being tested. The e2e legs run on the environments each trigger needs, the bootstrap sweep moves to its own scheduled workflow, and every leg carries less fixed setup.

What each trigger runs

The scheduling model @coutoPL asked for in review.

all-e2e-tests.yml

trigger images environments legs
daily cron, manual dispatch prod and dev (built from develop) docker, eck-2.16.1, eck-3.5.0 12 + 12
push to master prod docker, eck-3.5.0 8
push to develop dev docker, eck-3.5.0 8
pull request to master prod docker, eck-3.5.0 8
pull request to develop dev docker, eck-3.5.0 8
draft pull request as the ready one docker 4

ready_for_review is a trigger, so a draft that becomes a pull request gets its ECK leg. Bootstrap tests do not run on pushes or pull requests.

bootstrap-tests.yml (new)

trigger images environment legs
Sunday cron prod docker 34
Wednesday cron dev (built from develop) docker 34
manual dispatch both docker 68

Before this PR: 80 legs on every PR to master, every push to master and every night; 13 on a PR to develop.

Coverage: what still runs

Change Coverage after
ECK legs by trigger The daily cron runs every ELK version on all three environments, against both image sets. Pushes and pull requests run docker and eck-3.5.0; a draft runs docker.
Bootstrap sweep on a schedule, in its own workflow All 34 versions still swept, on docker, against prod and dev images. It no longer gates on prod-e2e-tests, so a flaky suite leg cannot skip it (it ran on 14 of the last 30 nightlies).
One describe.skip spec deleted Nothing. Kibana-config has never executed an assertion in this repository and cannot run in either environment here; the ROR KBN copy is untouched.
Tenancy tests moved out of runTests() The same assertions run. Two of them read neither callback, so passes 2 and 3 were byte-identical copies. The third (share link) keeps its assertions; the first test in runTests() already covers the variants it dropped.
Malformed-settings step removed from Settings.cy.ts Nothing real. Its only live assertion was expect(statusCode).to.eq(200) on saving a broken config.
cy.wait(200) between ESC keydown and keyup Nothing. Same events, same order, no gap. Now KibanaNavigation.closeOpenOverlays().
Disk cleanup, yarn/Cypress caches, paths-ignore Nothing. Runner setup only.

Net effect on the suite: same specs, same assertions, 13 test bodies instead of 17 in Tenancy.cy.ts.

The changes

Pick the e2e environments by trigger — a plan job decides the images (prod, dev) and the environments for the trigger, and the test jobs only ask it. ELK_VERSIONS, ECK_3X and ECK_2X are workflow env vars at the top, one line each to bump. The earlier ECK "pairing" and the --flip trick are gone: the daily cron covers every (version, environment) cell, so a PR does not have to.

Move the bootstrap sweep to its own workflow--run bootstrap runs no tests; the only assertion is that the stack came up. Its input is the plugin images, which a PR to this repo cannot change. Yield over 75 nightlies: 7 failures in 3,317 executed legs, 6 of them the 7.10.0 already removed from the list. The dev sweep dispatches the plugin pre-build workflows for all 34 versions from develop; the wait timeouts are 4 hours per plugin, which is not measured, because this run has never existed.

Move the yarn and Cypress caches into their own action.github/cypress-cache, used by run-e2e-tests. Measured over 10 legs of nightly 34547123638: 30s for the install and 5-22s for the Cypress binary on every leg.

Skip the disk cleanup when the runner already has room — deleting the preinstalled toolchains costs about 80s. A bootstrap leg asks for 8 GB, a suite leg for 25. Each rm -rf runs in the background and is waited on by pid, so a failed deletion fails the step.

Delete the spec that runs in no environmentKibana-config.cy.ts is describe.skip on the whole file: two replicas disagree about the active config on docker, and kibana.yml is read-only on ECK. Four unreferenced kibana.yml fixtures go with it. ror-config.cy.ts stays: #133 kept it with the reason on the skip (the suite lifts it when #116 merges), together with the two fixtures it reads.

Drop an assertion that could only fail if ROR got it rightSettings.cy.ts saved readonlyrest: and asserted a 200.

Run Tenancy's two callback-free tests once, not three times — and the share-link test once, because the tenancy-survives-the-variant property is already asserted by the first test in runTests().

Name the retried tests on the run summary — a retried test that passed leaves no trace in the run output. The after:spec handler lists them on the GitHub run summary. Cypress keeps one handler per event name, so the report shares the handler with the video discard.

Numbers

Measured before the review rework and still true: the per-spec timings, the bootstrap yield, the cache savings, and the 80 legs per PR baseline. Not re-measured: the "about 60%" saving of the first version. The legs per trigger above are simulated against the if: expressions; the daily cron costs more than before (24 suite legs plus two plugin builds, against 12), pushes and pull requests cost less.

Checked before pushing

  • yarn tsCheck clean and yarn lint at 0 errors.
  • actionlint clean on both workflows.
  • Every YAML parsed with a duplicate-key-rejecting loader.
  • The plan step run locally for the seven trigger shapes (cron, dispatch, push master/develop, PR to master/develop, draft, fork) gives the expected images and matrix.
  • Every if: expression simulated for the nine trigger shapes, with needs propagation: the leg tables above.

Deliberately left alone

  • Test-settings.cy.ts and Settings.cy.ts test 1 (~53s). Their page objects assert real responses, so removing them drops the "the button is wired to the endpoint" coverage.
  • RorMenu.ts:49's 300ms settle. cy.get().should() retries the assertion but never re-runs the click.
  • Sharding. The account pool peaks at 20 concurrent jobs; sharding adds runner-minutes per leg and lengthens every queue.

Related pull requests

PR Repo What it does
sscarduzio/readonlyrest_kbn#1039 KBN plugin Mirrors the spec changes here into automatic-tests/.
#130 this repo Gives ro_strict real coverage: roStrictSettings.yaml is a whitespace-only copy of roSettings.yaml.
#131 this repo Moves two API-only specs out of Cypress into a plain HTTP suite.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • CI Improvements

    • Improved end-to-end test workflow selection, cancellation, retries, caching, and disk-space handling.
    • Added reporting for tests that pass after retries.
    • Reduced duplicate tenancy test executions.
    • Updated scheduled bootstrap testing across supported ELK versions.
  • Test Maintenance

    • Removed obsolete disabled Kibana configuration coverage and unused fixtures.
    • Simplified outdated test scenarios and navigation handling.
  • Documentation

    • Clarified that master and develop test suites run on daily schedules.

sscarduzio and others added 11 commits September 12, 2026 13:45
The docker env runs two Kibana replicas behind kbn-proxy. The upstream block had no connect
timeout and no failover, so while one replica restarted - which Kibana-config.cy.ts makes it do,
and which `restart: always` on kbn-ror exists for - half of every request went to a container that
was not listening.

A stopped container drops the packets rather than refusing them, so those requests hung instead of
failing. nginx waited its default 60s connect timeout, which is longer than every Cypress timeout,
so the browser gave up first, the failure never counted against the peer, and the peer stayed in
the rotation. Cypress retries did not help: all three attempts hit the same alternation.

Measured on a two-replica reproduction with one replica stopped.

  before: 000 200 000 200 000 200 000 200 000 200      (50% lost, each hanging to the client's limit)
  after:  200/2.00s 200/0.0005s 200/0.0005s ...        (one failover, then the peer is out)

Sustained for 30s with the peer down: 110 requests, 0 failures, 3 over 0.5s (the 10s re-probe),
worst 2.0s - inside every Cypress timeout. The replica returns to the rotation on its own when it
answers again.

This fits the evidence: the failures only ever appear on the `docker` legs, never on `eck`, and
eck runs a single Kibana (kind-cluster/ror/base/kbn.yml, count: 1) with no proxy in front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The matrix is 34 released versions x 2 envs, so it is 68 jobs.

docs/dev/branching.md case 4 sends every workflow change to master.
Three of them moved there today, and each queued its own 68 bootstrap
jobs behind itself: approved, no failures, unmergeable for hours, while
sweeping plugins that were released weeks ago.

Every version in that matrix tests a plugin that is already out, so
nothing a pull request changes can change the result. The nightly and a
push to master still run it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coutoPL's point on the review: a stale run can be cancelled instead. That is true and worth
having, so it is here. It covers a different case, though. Cancelling helps when the same pull
request is pushed twice. It does nothing for three open pull requests, which still queue their
own bootstrap sweep each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting /usr/share/dotnet and /usr/local/lib/android costs 79s on average and 184s at worst,
measured over 10 jobs of nightly 34547123638. Every leg pays it, including the 68 bootstrap legs
that boot a stack and stop. That is 32% of a bootstrap leg spent making room nothing asks for.

The step now takes the space the job needs and returns early when df already reports it. Bootstrap
passes 8G; a suite leg keeps the 25G default, so it still cleans. The deletes that do run are
independent trees, so they run in parallel instead of in sequence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fixed costs every leg pays, measured over 10 legs of nightly 34547123638: 30s for
`yarn --frozen-lockfile install` and 5-22s for the Cypress binary download. Both caches key on
e2e-tests/yarn.lock, which is the file that decides their content, so a stale entry cannot be
served. The Cypress version is pinned in that lockfile too, so one key covers both.

paths-ignore keeps a prose change off the matrix. PR #125 was five markdown files and cost 1391
runner-minutes. The list holds only files nothing reads at runtime — e2e-tests/, .github/ and
environments/ all still trigger the full run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prod-e2e and dev-e2e ran 4 ELK versions against 3 environments. Over 75 nightly runs the two ECK
operator versions disagreed on 8 of 300 paired observations, every one a singleton that did not
repeat the next night. The operator version is orthogonal to the ELK version, so the third column
bought a third more legs and no information.

docker is not like that. It disagreed with ECK 23 times, 19 of them in the same direction, because
it runs two Kibana replicas behind a proxy where ECK runs one. Every version keeps its docker leg.

12 legs become 8. Every ELK version still meets both operators, because the nightly asks for the
flipped pairing, so no cell goes more than 24 hours unseen. Pull requests always get the same
pairing, so their check names do not move between runs.

ci/e2e-matrix.sh is now the one place the version list lives. It was in the workflow twice before,
once for the prod matrix and once for ELK_VERSIONS, under a comment asking the reader to keep them
in sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--run bootstrap` runs no tests. runner.sh skips run-tests.sh entirely and the only assertion is
that the stack came up. What it can catch is a newly published <elk>-ror-latest image that does
not start on an old ELK version.

Those tags move when ROR releases, about twice a month, and nothing in this repository can change
them. So the sweep now runs on a repository_dispatch from the release pipelines, on a weekly cron
as the safety net, and on a push to master, where the version list itself can change.

The yield agrees with the cadence. Over 75 nightly runs, 3317 executed legs produced 7 failures,
6 of them the 7.10.0 that is already out of the list.

It also no longer gates on prod-e2e-tests. That coupling meant one flaky suite leg skipped all 68
legs, and the sweep ran on 14 of the last 30 nightlies.

The eck column goes: over those 75 nightlies docker and eck failed on exactly the same versions,
and this job starts a stack rather than exercising the proxy, which is where the two differ.

Per-trigger legs, before -> after: PR to master 80 -> 9, nightly 80 -> 9, push master 80 -> 43,
plus 35 weekly and 35 per release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kibana-config.cy.ts is describe.skip on the whole file, and its own header explains why it cannot
be otherwise: it rewrites kibana.yml and restarts Kibana, which the docker env cannot do because
two replicas disagree about the active config, and the eck envs cannot do because kibana.yml is
mounted read-only from a ConfigMap. ror-config.cy.ts is describe.skip behind a TODO waiting on a
feature.

Neither has executed an assertion in this repository. A skipped spec file still costs Cypress 3-6
seconds to load, on every leg, to run nothing.

Kibana-config belongs on the kbn repo's IT stack, which is a single Kibana node with a writable
kibana.yml — the one place it could actually run. The copy there is untouched by this commit.

The six kibana.yml fixtures go with them. Nothing else in the repository reads any of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings.cy.ts saved the config `readonlyrest:` and its only live assertion was clickSaveButton's
`expect(statusCode).to.eq(200)`. So it asserted that saving malformed settings SUCCEEDS. It would
have stayed green if ROR silently swallowed a broken config, and it would have gone red the day
ROR started rejecting one. The toast assertion that carried the real meaning has been commented
out since the step was written. Rejecting malformed config is covered by JsonSchemaValidator and
rorApi tests in the ROR KBN repo.

Test-settings.cy.ts keeps its live assertions and loses 28 lines of commented-out steps that were
parked behind an ES plugin fix. The file now says what it tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runTests() is invoked three times, and the three passes differ only in the callbacks they hand it:
nothing, a history-back, and a second tab. Two of its five tests read neither callback, so the
second and third passes executed byte-identical copies of them.

They move to the top level of the describe. 17 test bodies become 13, and each one carries a full
Login.initialization, which is about 13 seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
openKibanaNavigation sends ESC as a keydown and a keyup with cy.wait(200) between them. They are
one key press; nothing has to happen in the middle. openPage routes every navigation in the suite
through this method, so the 200ms was paid about fifty times a run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sscarduzio
sscarduzio requested a review from Dzuming September 12, 2026 14:36
@coderabbitai

This comment was marked as spam.

sscarduzio and others added 2 commits September 12, 2026 14:37
retries.runMode is 2, so a test can fail twice, pass on the third attempt, and leave a green leg
with nothing in the output to say so. Job 103514840079 hid one: Sanity-check-ro-kibana-access ran
134s against its twin's 62s for identical work, and the difference was an invisible retry.

after:spec already receives the per-attempt data. This writes one row per retried test to the run
summary, so a flaky test can be named instead of inferred from a duration gap.

It reports and never acts: GITHUB_STEP_SUMMARY is unset outside Actions, and a failure to write
the report cannot fail a suite that passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The copy-link test is the most expensive in the suite: it installs the ecommerce sample dataset,
and runTests ran it three times. Unlike the two tests moved in the previous commit, it does read
both callbacks, so the three passes were not identical — they asked whether the copied link still
carries the tenancy after a history-back and after a cross-tab switch.

That property is the URL tenancy surviving the variant, and the first test in runTests already
asserts it under all three passes, through checkTenancyNameInBadge and
verifyKibanaNavigationLinkItemHref. What is left here is the share panel, which the variants do
not touch.

It keeps its own afterEach, because outside runTests it no longer inherits the sample-data cleanup.

Tenancy now runs 9 test bodies where it ran 17.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sscarduzio and others added 2 commits September 12, 2026 15:46
Registering a second `on('after:spec')` does not add a listener, it replaces the first one.
EventRegistrar holds one function per event name:

  private _registeredEvents: Record<string, Function> = {}
  registerEvent (event, callback) { this._registeredEvents[event] = callback }

and executeNodeEvent looks the handler up by name and calls exactly that one
(packages/data-context/src/data/EventRegistrar.ts at v15.20.1). Only `task` is merged across
registrations, in run_plugins.ts.

So the retry report added in the previous commit never ran: the video-discard handler below it
registered second and won. Both jobs now live in the one handler.

Found while mirroring these changes into the ROR KBN repo's copy of the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sscarduzio

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

sscarduzio and others added 2 commits September 12, 2026 21:15
A bare `wait` returns 0 whatever the background jobs did, so parallelising the deletions removed
the error propagation the sequential version had from `set -e`:

  $ bash -c 'set -e; (exit 7) & (exit 0) & wait; echo continued'
  continued

A deletion that failed would have left the disk full and let the suite run into it later, with an
error that points anywhere but at this step. Each job's pid is now kept and waited on by name.

Raised by CodeRabbit on this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into perf/e2e-cost-reduction"

This reverts commit 8aaed17, reversing
changes made to 86e9e4c.
@sscarduzio

This comment was marked as outdated.

Comment thread .github/cleanup-disk-space/action.yml
@sscarduzio

This comment was marked as outdated.

@10hexdev

This comment was marked as outdated.

@sscarduzio
sscarduzio requested a review from coutoPL September 13, 2026 13:23
10hexdev[bot]

This comment was marked as outdated.

@@ -1,221 +0,0 @@
import * as semver from 'semver';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we do with this reload Kibana config logic? Currently, it's blocked because:

  • It's not working in the case of multiple Kibana instances
  • It's not working in the case of ECK

So, it should be diagnosed and fixed on the ReadonlyREST Kbn plugin side. We need a follow-up task to decide if we will fix it or drop it because of the cost of changes in this logic, which is only for testing purposes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec is deleted here because it cannot run in either environment of this repo: two replicas behind the proxy on docker, read-only kibana.yml on ECK. The decision to fix the reload logic on the plugin side or drop it belongs in a follow-up ticket. Can you open it, or shall I?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, create a Jira task for it

Comment thread e2e-tests/cypress/support/page-objects/KibanaNavigation.ts Outdated
Comment thread .github/cleanup-disk-space/action.yml Outdated
Comment thread .github/run-e2e-tests/action.yml Outdated
Comment thread .github/workflows/all-e2e-tests.yml
sscarduzio and others added 7 commits September 14, 2026 13:18
dev-e2e-tests reads its matrix from the setup job. Nothing reads this output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comments this branch added told the story of the lines they replaced.
That story is in the commit messages. Each comment now says only what a
reader cannot get from the code, or is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A named method replaces the comment that explained the two events.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run-e2e-tests uses .github/cypress-cache, so the cache steps sit next to
the other composite actions and do not crowd the retry wrapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bootstrap-tests.yml runs the sweep on docker for all 34 ELK versions:
against the released plugins on the Sunday cron, against dev images built
from develop on the Wednesday cron, and both on a manual dispatch. The
version list lives in one setup job and feeds both sweeps and the dev
image pre-build.

all-e2e-tests.yml keeps the e2e legs only. The repository_dispatch trigger
goes: the plugin release pipelines do not send it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  daily cron, manual dispatch  prod + dev   docker, eck-2.16.1, eck-3.5.0
  push to master / develop     prod / dev   docker, eck-3.5.0
  pull request                 prod / dev   docker, eck-3.5.0
  draft pull request           prod / dev   docker

The daily cron now runs the dev legs too, against plugins built from
develop. ready_for_review is a trigger, so a draft that becomes a pull
request gets its ECK leg.

ci/e2e-matrix.sh builds a plain cross product of the version list and the
environments the setup job names. The --flip pairing goes with it: the
daily cron covers every (version, environment) cell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sscarduzio sscarduzio changed the title Cut the e2e pipeline cost by about 60% Cut the e2e pipeline cost: environments by trigger, weekly bootstrap sweep, less setup per leg Sep 14, 2026
coderabbitai[bot]

This comment was marked as outdated.

Keeps ror-config.cy.ts and its two fixtures, which #133 retained with the
reason on the skip: the suite lifts it when #116 merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No step in the workflow writes through GITHUB_TOKEN; the pre-build
dispatch uses ROR_GH_TOKEN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/all-e2e-tests.yml Outdated
Comment thread .github/workflows/all-e2e-tests.yml Outdated
Comment thread .github/workflows/all-e2e-tests.yml Outdated
Comment thread .github/workflows/all-e2e-tests.yml Outdated
Comment thread ci/e2e-matrix.sh Outdated
Comment thread .github/workflows/bootstrap-tests.yml Outdated
Comment thread .github/workflows/bootstrap-tests.yml Outdated
Comment thread .github/workflows/bootstrap-tests.yml Outdated
Comment thread .github/workflows/bootstrap-tests.yml Outdated
Comment thread CLAUDE.md Outdated
… workflow

The plan job now decides both the images (prod, dev) and the environments for every trigger,
and the test jobs only ask it. The comment table at the top described the same thing in prose
and would rot. ELK_VERSIONS, ECK_3X and ECK_2X are workflow env vars, so a bump touches one line.
ci/e2e-matrix.sh is gone: with the list in the workflow, the matrix is one jq call.

bootstrap-tests.yml gets the same plan job, and the cron-to-images choice moves out of the job
conditions into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sscarduzio
sscarduzio requested a review from coutoPL September 15, 2026 09:37
Comment thread .github/workflows/all-e2e-tests.yml Outdated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sscarduzio
sscarduzio requested a review from coutoPL September 15, 2026 10:30

@coutoPL coutoPL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sscarduzio
sscarduzio requested a review from coutoPL September 15, 2026 12:17

@coutoPL coutoPL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sscarduzio
sscarduzio merged commit 3849a62 into master Sep 15, 2026
13 checks passed
@coutoPL
coutoPL deleted the perf/e2e-cost-reduction branch September 15, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants