perf(ci): reduce E2E container setup time (#3163)

This commit is contained in:
Carlos Florêncio
2026-08-30 19:55:20 +00:00
committed by GitHub
parent b2764dae7a
commit ec1a7d567f
12 changed files with 875 additions and 20 deletions
@@ -122,6 +122,85 @@ class DesktopE2EWorkflowContractTest(unittest.TestCase):
self.assertIn('artifact.name === "e2e-timing-profile"', workflow)
self.assertIn("!artifact.expired", workflow)
# @covers AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.1
# @covers AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.2
def test_container_job_reuses_verified_browser_cache_with_fallback(self) -> None:
workflow = E2E_WORKFLOW.read_text(encoding="utf-8")
container_job = job_block(workflow, "e2e-containers", "desktop-e2e")
self.assertIn(
"uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9",
container_job,
)
self.assertIn(
"uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9",
container_job,
)
self.assertIn("id: playwright_image", container_job)
self.assertLess(
container_job.index("- name: Log in to GHCR"),
container_job.index("- name: Resolve immutable Playwright runtime image"),
)
self.assertIn(
"if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository\n uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f",
container_job,
)
self.assertIn(
"image=ghcr.io/kdlbs/kandev-ci:runtime-latest",
container_job,
)
self.assertIn(
'docker buildx imagetools inspect "$image"',
container_job,
)
self.assertIn("for attempt in 1 2 3; do", container_job)
self.assertIn(
'if candidate="$(docker buildx imagetools inspect "$image"',
container_job,
)
self.assertIn('sleep "$((attempt * 2))"', container_job)
self.assertIn("path: /tmp/ms-playwright", container_job)
self.assertIn(
"key: e2e-playwright-${{ runner.os }}-v1.61.1-noble-${{ steps.playwright_image.outputs.digest }}-${{ github.run_id }}-${{ github.run_attempt }}",
container_job,
)
self.assertIn(
"restore-keys: e2e-playwright-${{ runner.os }}-v1.61.1-noble-${{ steps.playwright_image.outputs.digest }}-",
container_job,
)
self.assertIn("id: playwright_cache", container_job)
self.assertIn("id: playwright_cache_verify", container_job)
self.assertIn("PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright", container_job)
self.assertIn(
"if: steps.playwright_cache.outputs.cache-hit != ''",
container_job,
)
self.assertIn(
"if: steps.playwright_cache.outputs.cache-hit == '' || steps.playwright_cache_verify.outcome != 'success'",
container_job,
)
self.assertIn(
"github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository",
container_job,
)
self.assertIn(
"PLAYWRIGHT_RUNTIME_REF: ${{ steps.playwright_image.outputs.ref }}",
container_job,
)
self.assertIn('docker pull "$PLAYWRIGHT_RUNTIME_REF"', container_job)
self.assertIn('"$PLAYWRIGHT_RUNTIME_REF" \\', container_job)
self.assertNotIn('docker pull "${{ steps.playwright_image.outputs.ref }}"', container_job)
self.assertIn(
"if: success() && (steps.playwright_cache.outputs.cache-hit == '' || steps.playwright_cache_verify.outcome != 'success')",
container_job,
)
self.assertIn("GITHUB_STEP_SUMMARY", container_job)
self.assertIn("id: browser_setup_timer", container_job)
self.assertIn('echo "started_at=$(date +%s)" >> "$GITHUB_OUTPUT"', container_job)
self.assertIn('setup_mode="cache-hit"', container_job)
self.assertIn('setup_mode="image-fallback"', container_job)
self.assertGreaterEqual(container_job.count("continue-on-error: true"), 2)
if __name__ == "__main__":
unittest.main()
+145 -16
View File
@@ -513,14 +513,20 @@ jobs:
working-directory: apps
run: pnpm install --frozen-lockfile
- name: Start Playwright browser setup timer
id: browser_setup_timer
run: echo "started_at=$(date +%s)" >> "$GITHUB_OUTPUT"
# The kandev-ci image is public, so fork PRs can pull it anonymously.
# Their read-only GITHUB_TOKEN can still be rejected by GHCR during an
# explicit login because it belongs to the base repository, not the
# contributor's fork. Keep authenticated pulls for trusted runs.
# contributor's fork. Keep authenticated metadata and image requests for
# trusted runs.
#
# A merge_group run has no `github.event.pull_request` at all, so the
# first operand decides it. That is the right answer: the queue branch
# lives in this repository and its token is a base-repository token.
# condition decides it is a base-repository run. That is the right
# answer: the queue branch lives in this repository and its token is a
# base-repository token.
- name: Log in to GHCR
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -529,27 +535,107 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Skip the Playwright CDN download (~7 min and an outage-prone path) by
# copying the prebaked chromium out of the kandev-ci image, which is
# `FROM mcr.microsoft.com/playwright:...` and already ships the matching
# browser at /ms-playwright. Pulling the `runtime` tag (matches the
# `e2e` / `e2e-report` jobs above) — the `build` tag has the same
# browser layer but is ~300 MB larger because of the Go toolchain.
- name: Extract Playwright browsers from kandev-ci image
# Resolve the mutable convenience tag before restoring the cache. The
# digest binds both the cache and the fallback extraction to one image
# identity, while avoiding a full image pull on a verified cache hit.
- name: Resolve immutable Playwright runtime image
id: playwright_image
run: |
set -euo pipefail
docker pull ghcr.io/kdlbs/kandev-ci:runtime-latest
image=ghcr.io/kdlbs/kandev-ci:runtime-latest
digest=""
for attempt in 1 2 3; do
candidate=""
if candidate="$(docker buildx imagetools inspect "$image" | awk '$1 == "Digest:" { print $2; exit }')" \
&& [[ "$candidate" =~ ^sha256:[0-9a-f]{64}$ ]]; then
digest="$candidate"
break
fi
if (( attempt < 3 )); then
echo "::warning::Could not resolve $image on attempt $attempt/3; retrying"
sleep "$((attempt * 2))"
fi
done
if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "::error::Could not resolve an immutable digest for $image"
exit 1
fi
{
echo "digest=$digest"
echo "ref=$image@$digest"
} >> "$GITHUB_OUTPUT"
# The runtime image remains the browser source of truth. The cache only
# avoids copying its already-installed browsers on warm host runners.
# Each run has a unique primary key so a repaired fallback can replace a
# cache entry whose verification failed. The restore prefix selects the
# newest compatible entry without allowing a rejected exact key to win.
- name: Restore Playwright browser cache
id: playwright_cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
continue-on-error: true
with:
path: /tmp/ms-playwright
key: e2e-playwright-${{ runner.os }}-v1.61.1-noble-${{ steps.playwright_image.outputs.digest }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: e2e-playwright-${{ runner.os }}-v1.61.1-noble-${{ steps.playwright_image.outputs.digest }}-
- name: Configure Playwright browser path
run: echo "PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright" >> "$GITHUB_ENV"
# A cache hit is usable only after Chromium launches from the restored
# directory. `cache-hit` is non-empty for both an exact hit and a
# restore-key match. A corrupted or incompatible entry takes the normal
# image extraction path below.
- name: Verify cached Playwright browser
id: playwright_cache_verify
if: steps.playwright_cache.outputs.cache-hit != ''
continue-on-error: true
working-directory: apps/web
run: |
set -euo pipefail
node <<'NODE'
const { chromium } = require("@playwright/test");
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto("data:text/html,ok");
await browser.close();
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
echo "Playwright Chromium cache entry verified."
# On a cache miss or failed cache verification, skip the Playwright CDN
# download (~7 min and an outage-prone path) by copying the prebaked
# browser out of the kandev-ci image. The runtime image is
# `FROM mcr.microsoft.com/playwright:...` and ships it at /ms-playwright.
# Pulling the resolved digest keeps the fallback tied to the image used
# for the cache key; the build image has the same browser layer but is
# ~300 MB larger.
- name: Extract Playwright browsers from kandev-ci image
id: playwright_extract
if: steps.playwright_cache.outputs.cache-hit == '' || steps.playwright_cache_verify.outcome != 'success'
env:
PLAYWRIGHT_RUNTIME_REF: ${{ steps.playwright_image.outputs.ref }}
run: |
set -euo pipefail
docker pull "$PLAYWRIGHT_RUNTIME_REF"
mkdir -p /tmp/ms-playwright
docker run --rm \
-v /tmp/ms-playwright:/dest \
ghcr.io/kdlbs/kandev-ci:runtime-latest \
"$PLAYWRIGHT_RUNTIME_REF" \
cp -a /ms-playwright/. /dest/
echo "PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright" >> "$GITHUB_ENV"
# Browser binaries come from the image above. GitHub's Ubuntu runners
# usually already have the shared libs Chromium needs, so smoke-test the
# copied browser first and only hit apt as a bounded fallback.
# Browser binaries come from the verified cache or the image above.
# GitHub's Ubuntu runners usually already have the shared libs Chromium
# needs, so smoke-test the copied browser and only hit apt as a bounded
# fallback.
- name: Verify Playwright browser runtime
id: playwright_runtime_verify
if: steps.playwright_cache.outputs.cache-hit == '' || steps.playwright_cache_verify.outcome != 'success'
working-directory: apps/web
run: |
set -euo pipefail
@@ -579,6 +665,49 @@ jobs:
timeout 180s npx playwright install-deps chromium
smoke_test
- name: Save Playwright browser cache
id: playwright_cache_save
if: success() && (steps.playwright_cache.outputs.cache-hit == '' || steps.playwright_cache_verify.outcome != 'success')
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
continue-on-error: true
with:
path: /tmp/ms-playwright
key: e2e-playwright-${{ runner.os }}-v1.61.1-noble-${{ steps.playwright_image.outputs.digest }}-${{ github.run_id }}-${{ github.run_attempt }}
- name: Summarize Playwright browser setup
if: always()
env:
CACHE_HIT: ${{ steps.playwright_cache.outputs.cache-hit }}
CACHE_RESTORE_OUTCOME: ${{ steps.playwright_cache.outcome }}
CACHE_VERIFY_OUTCOME: ${{ steps.playwright_cache_verify.outcome }}
CACHE_EXTRACT_OUTCOME: ${{ steps.playwright_extract.outcome }}
CACHE_SAVE_OUTCOME: ${{ steps.playwright_cache_save.outcome }}
IMAGE_DIGEST: ${{ steps.playwright_image.outputs.digest }}
SETUP_STARTED_AT: ${{ steps.browser_setup_timer.outputs.started_at }}
run: |
set -euo pipefail
started="${SETUP_STARTED_AT:-$(date +%s)}"
elapsed="$(( $(date +%s) - started ))"
cache_state="miss"
setup_mode="image-fallback"
if [[ -n "${CACHE_HIT:-}" ]]; then
cache_state="hit"
fi
if [[ -n "${CACHE_HIT:-}" && "${CACHE_VERIFY_OUTCOME:-}" == "success" ]]; then
setup_mode="cache-hit"
fi
{
echo "### Playwright browser setup"
echo "- image-digest: ${IMAGE_DIGEST:-unresolved}"
echo "- cache: ${cache_state}"
echo "- mode: ${setup_mode}"
echo "- restore: ${CACHE_RESTORE_OUTCOME:-not-run}"
echo "- verification: ${CACHE_VERIFY_OUTCOME:-not-run}"
echo "- extraction: ${CACHE_EXTRACT_OUTCOME:-not-run}"
echo "- save: ${CACHE_SAVE_OUTCOME:-not-run}"
echo "- elapsed: ${elapsed}s"
} >> "$GITHUB_STEP_SUMMARY"
- name: Download backend build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
+16
View File
@@ -182,6 +182,22 @@ Dispatch the workflow with `fail_on_flaky=true` to set
`failOnFlakyTests: true` for a diagnostic run. Normal PR runs retain the
existing two-retry policy while the summary makes retry groups visible.
Container-backed CI jobs also cache the browser directory used by the host
runner. Each job first resolves the `runtime-latest` convenience tag to a
sha256 image digest. The cache key includes the runner OS, the Playwright
`v1.61.1-noble` browser source, that digest, and a run-specific primary-key
suffix. Its stable restore prefix selects the newest compatible entry without
making a rejected cache entry win forever. A verified exact or prefix match
sets `PLAYWRIGHT_BROWSERS_PATH=/tmp/ms-playwright` and skips the GHCR pull and
browser copy. A miss, stale entry, unavailable cache, or failed Chromium
verification uses the digest-pinned runtime-image extraction path. A fallback
save uses the current run's primary key, so a repaired entry becomes the next
candidate. Cache operations are best-effort and cannot change the manifest or
block a healthy fallback. The container job summary reports the image digest,
cache state, verification, extraction, save status, and total setup time so
cache transfer cost can be compared with the 54-second browser provisioning
baseline.
### Flake rate and trend
CI retries hide flakes: with `retries: 2` and `failOnFlakyTests: false`, a test
@@ -17,6 +17,7 @@ const MOVED_PATH = "review-status-a-very-long-new-name-that-must-truncate.ts";
test.describe("Review file status", () => {
test.describe.configure({ timeout: 120_000 });
// @covers AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.4
test("shows every status, keeps the marker visible at minimum width, and explains a pure move", async ({
testPage,
apiClient,
@@ -224,9 +225,13 @@ test.describe("Review file status", () => {
.toBeGreaterThan(0);
await movedRow.click();
const movedHeader = dialog.locator(
`[data-testid="review-file-header"][data-file-path="${MOVED_PATH}"]`,
);
const movedSection = movedHeader.locator("..");
await expect(
dialog.getByText(`Moved from ${MOVED_FROM_PATH}; no textual changes`),
movedSection.getByText(`Moved from ${MOVED_FROM_PATH}; no textual changes`),
).toBeVisible();
await expect(dialog.getByText("Loading diff...")).toHaveCount(0);
await expect(movedSection.getByText("Loading diff...")).toHaveCount(0);
});
});
@@ -0,0 +1,74 @@
# ADR-2026-08-30: Cache host-runner E2E browser provisioning
**Status:** accepted
**Date:** 2026-08-30
**Area:** infra, workflow
## Context
The container-backed E2E jobs run on host runners because they need the Docker
daemon. Each job currently pulls the runtime image and copies its baked
Playwright browsers from `/ms-playwright` to `/tmp/ms-playwright`. Recent job
logs show about 54 seconds of browser provisioning per container shard. The
same setup is repeated across all six parallel jobs, even though the browser
source is identical.
The runtime image remains the reviewed source of the browser binaries. A cache
must reduce repeat work without allowing an unavailable or stale cache to
change test selection or block a correct E2E run.
## Decision
The host-runner container jobs use a GitHub Actions cache for the Playwright
browser directory. The workflow resolves the `runtime-latest` convenience tag
to a sha256 digest before it constructs the cache key. The key includes the
runner operating system, the Playwright browser source, the resolved image
digest, and a unique workflow run ID and attempt. A stable digest-scoped
restore prefix selects the newest compatible entry.
The job restores the cache before browser provisioning and verifies Chromium
from the restored path. A verified exact or prefix hit skips the image pull and
browser copy. A miss, stale key, cache-service error, or failed verification
uses the digest-pinned runtime-image extraction and smoke check. A successful
fallback populates a new run-specific cache key, so a failed cache entry cannot
remain the newest exact candidate forever. If the image digest cannot be
resolved, the job fails before using the mutable tag. Cache restore and save
are best-effort acceleration steps and are reported separately from test
correctness.
The cache contains browser binaries only. It does not contain source,
credentials, Docker state, or test artifacts.
## Consequences
- Warm container jobs avoid the repeated browser copy and usually start their
tests sooner.
- The first run after a browser-source change still pays the current setup
cost while creating a new cache entry.
- Cache service incidents do not block E2E when the pinned-image fallback is
healthy.
- Updating the runtime image digest invalidates the cache, including changes
that do not affect browsers. This is conservative and binds the cache to the
exact fallback image.
- The workflow must report cache state and provisioning duration so a cache
hit rate and actual wall-time reduction can be evaluated after merge.
## Alternatives considered
### Keep extracting browsers on every host runner
Rejected. It has no cache failure mode, but repeats a measured setup cost in
every container shard and leaves an avoidable critical-path expense.
### Upload browsers as a workflow artifact from the build job
Rejected for this change. It would add a producer and consumer artifact path
to every run, increase coupling to the build job, and still require transfer
before the container cohort starts. The cache can be warmed lazily and uses
the same explicit source key.
### Build a host image with browsers already installed
Deferred. Host-image ownership would couple the Docker executor to runner
provisioning and would not help arbitrary GitHub-hosted runners. Revisit only
if cache hit rates or cache transfer time do not produce a measured gain.
+1
View File
@@ -217,3 +217,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-08-27-mixed-git-change-facets | [Preserve mixed Git changes as path facets](2026-08-27-mixed-git-change-facets.md) | accepted | backend, agentctl, frontend, protocol | 2026-08-27 |
| 2026-08-28-bind-github-auto-merge-attempts-to-reviewed-head | [Bind GitHub Auto-Merge Attempts to the Reviewed Head](2026-08-28-bind-github-auto-merge-attempts-to-reviewed-head.md) | accepted | backend, frontend, protocol, security, GitHub | 2026-08-28 |
| 2026-08-30-context-reset-quiesces-active-turn | [Quiesce Active Turns Before Context Reset](2026-08-30-context-reset-quiesces-active-turn.md) | accepted | backend, workflow | 2026-08-30 |
| 2026-08-30-e2e-browser-cache | [Cache host-runner E2E browser provisioning](2026-08-30-e2e-browser-cache.md) | accepted | infra, workflow | 2026-08-30 |
+158
View File
@@ -0,0 +1,158 @@
---
spec: ../../specs/platform/requirements/e2e-duration-aware-sharding.md
created: 2026-08-30
status: in_progress
---
# Implementation Plan: E2E CI setup and flake efficiency
## Objective
Reduce the E2E critical path after the duration-aware planner has already
removed most test-work imbalance. This package reuses verified Playwright
browsers on host-runner container jobs and removes one recurring false flake
from the review-diff regression test. It preserves the fourteen normal shards,
the six container shards, one worker per shard, existing retries, and the
required aggregate gate.
## Evidence and diagnosis
The recent merged E2E stabilization changes are relevant inputs:
- [PR #3150](https://github.com/kdlbs/kandev/pull/3150) added a causal layout
wait for the PR status badge hover. [PR #3151](https://github.com/kdlbs/kandev/pull/3151)
then narrowed an automation-trigger notification assertion. Both are recent
examples of synchronization fixes that should be preferred over longer
waits.
- [PR #3133](https://github.com/kdlbs/kandev/pull/3133) removed shared-worker
cleanup races and reinforced task-owned test state.
- A fair successful-main run, [run 33250014029](https://github.com/kdlbs/kandev/actions/runs/33250014029),
had a roughly 16m28s normal-shard tail after the build completed. Its six
container jobs repeated browser provisioning.
- In [run 33308951784](https://github.com/kdlbs/kandev/actions/runs/33308951784),
the predicted-versus-actual normal shards stayed within the existing 1.25x
cohort-median target and the container cohort was also balanced. The
workflow's 75m44s end-to-end wall time was dominated by runner queue delay,
so queue capacity is tracked separately from code-level setup savings.
- Container job logs show about 54 seconds to pull/copy Playwright browsers
into `/tmp/ms-playwright` on each host runner.
- The same latest-main run recorded one passed-after-retry result in
`tests/review/review-file-status.spec.ts`. Its final assertion counted two
unrelated `Loading diff...` placeholders in lazy sections while checking a
moved file that had already rendered. The test has also appeared in earlier
flake history, so this is a reproducible synchronization defect rather than
a reason to increase retries.
## Scope
### In scope
- Best-effort browser-cache restore and save in the existing host-runner
container cohort, keyed to the exact checked-in browser source.
- A browser smoke verification that decides whether the restored path is safe;
the current pinned-image extraction remains the fallback.
- Step-summary diagnostics for cache state, browser verification, and setup
duration so future runs can measure the gain.
- A selection-scoped readiness assertion in the review-file-status E2E test.
- Workflow contract coverage, focused E2E coverage, and rollout evidence.
### Out of scope
- Changing the duration-aware planner, timing-profile schema, 14/6 matrix, or
`workers: 1` default.
- Splitting mobile Chrome into a separate matrix.
- Increasing retries or timeouts, adding fixed sleeps, or weakening the E2E
gate.
- Replacing the runtime image, moving browsers to an external service, or
changing application behavior.
- Treating GitHub runner queue time as a test or sharding defect.
## Technical approach
The container jobs resolve the `runtime-latest` convenience tag to an immutable
sha256 digest, then restore `/tmp/ms-playwright` before the digest-pinned image
pull and copy step. The cache key includes the runner OS, browser source,
resolved image digest, and a run-specific primary-key suffix with a stable
digest-scoped restore prefix. A verified exact or prefix hit skips the
pull/copy step. A miss, stale, cache-service failure, or failed Chromium
verification runs the existing digest-pinned extraction path and can populate
the cache under a new primary key after successful setup. Cache actions are
best-effort and cannot turn a healthy fallback into a failed E2E job.
The review test anchors the final moved-file assertions to the selected file's
existing review header/section boundary. Other lazy sections may still show a
loading marker and do not affect the selected file's readiness.
## Work orders
### Wave 1
- [ ] [task-01-cache-host-runner-browsers](task-01-cache-host-runner-browsers.md)
- [x] [task-02-scope-review-diff-readiness](task-02-scope-review-diff-readiness.md)
The slices are independently verifiable, but execution remains sequential in
the primary session so workflow and test changes can be reviewed together.
## Rollout and success criteria
1. Run the workflow contract and action-pinning checks locally.
2. Run the focused review E2E test repeatedly with retries disabled.
3. Merge the workflow change and compare at least three successful `main`
runs with the preceding setup and execution measurements.
4. Confirm warm container jobs report a verified cache hit and omit browser
extraction. Confirm a forced key miss or unavailable cache uses the pinned
image fallback and still passes.
5. Retain the change only if the container setup component decreases without
new browser-verification failures, retries, or test failures. Continue to
report queue delay separately.
The existing shard-tail target remains the acceptance threshold for test-work
balance. A cache change is not credited with a sharding improvement, and a
shorter queued workflow is not credited as a test-speed improvement.
## Verification strategy
The work orders own focused verification. The implementation handoff should
also run the repository specification lint after changing the design package:
```bash
python3 scripts/lint-spec-files.py --all
git diff --check
```
Remote rollout evidence belongs in this plan after the branch workflow runs.
Until then, the cache hit rate and setup timings remain open operational
measurements.
## Implementation results
- The container workflow now resolves an immutable runtime image digest,
restores and saves a Playwright browser cache keyed by runner OS, browser
source, digest, and a run-specific suffix, and uses a stable digest-scoped
restore prefix. Cache verification gates the digest-pinned GHCR fallback,
and setup state is written to the job summary. Cache errors remain
non-blocking.
- The review-file-status regression now scopes the moved-file assertions to its
selected diff section. A 30-repeat host run with retries disabled passed all
30 repetitions.
- PR run [#3163](https://github.com/kdlbs/kandev/pull/3163) passed all 20 E2E
shards and the full required check set. A container shard restored a
digest-scoped prefix cache, verified Chromium, skipped image extraction, and
reported about 9 seconds for browser setup. The preceding PR run exposed a
transient GHCR metadata lookup failure; the workflow now retries that lookup
three times before failing safely.
- Post-merge comparison across three successful `main` runs and a forced-miss
fallback measurement remain open rollout work.
## Risks
- GitHub Actions cache transfer can cost enough time to erase the browser-copy
saving. Step summaries must report restore and verification duration before
declaring success.
- Concurrent first-run cache saves can race. The fallback remains valid and
each run has a distinct primary key; the restore prefix selects the newest
compatible successful entry.
- The runtime image may change without a browser-version change. Including the
resolved image digest in the key is a conservative invalidation rule and
binds fallback extraction to the same image identity.
- Hosted-runner queue capacity can dominate total workflow wall time. The
rollout must compare job execution intervals and queue intervals separately.
@@ -0,0 +1,81 @@
---
id: "01-cache-host-runner-browsers"
title: "Cache host-runner Playwright browsers"
status: in_progress
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/platform/requirements/e2e-duration-aware-sharding.md"
---
# Task 01: Cache host-runner Playwright browsers
## Outcome
Remove repeat browser provisioning from warm container-backed E2E jobs while
preserving the current pinned-image path as a correctness-safe fallback.
## Requirements and design
- `REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-002`
- `AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.1`
- `AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.2`
- `docs/specs/platform/system-design/e2e-duration-aware-sharding.md`,
Browser cache contract and Observability sections
- `docs/decisions/2026-08-30-e2e-browser-cache.md`
## Acceptance
- The existing host-runner container jobs resolve the runtime image to an
immutable digest and restore `/tmp/ms-playwright` using a digest-scoped
restore prefix plus a run-specific primary key. A verified exact or prefix
hit skips the Docker pull/copy step.
- A cache miss, stale or unavailable entry, cache action error, or failed
browser verification runs the digest-pinned image extraction and smoke
check. A successful fallback uses a new primary key so a rejected cache
entry is not repeatedly selected. Cache operations cannot fail a healthy
fallback or change the test manifest, retries, timeout, or worker count.
- Each container job reports cache state, browser verification, and setup
timing. Workflow contract tests protect the key, conditional fallback, and
existing container setup.
## Likely files
- `.github/workflows/e2e-tests.yml`
- `.github/scripts/e2e-tests-workflow-contract_test.py`
- `apps/web/e2e/README.md` when the local/CI setup documentation needs the
cache and fallback behavior recorded
## Verification
```bash
python3 .github/scripts/e2e-tests-workflow-contract_test.py
python3 .github/scripts/lint-action-pinning_test.py
python3 .github/scripts/lint-action-pinning.py
git diff --check
```
After the branch workflow is available, inspect one cache-hit and one forced
miss run. The hit must omit the browser pull/copy step. The miss must complete
the existing extraction and E2E smoke path. Record restore, verification,
fallback, and test-start durations in the parent plan.
## Dependencies and parallelism
No implementation dependency. Execute sequentially in the primary session so
the workflow contract and fallback behavior are reviewed as one change.
## Exclusions
Do not change the runtime image's browser source, the normal E2E job, the
matrix sizes, or Docker resource ownership. Do not make cache availability a
required external service.
## Results
The workflow contract, action-pinning tests, targeted workflow security scan,
YAML parse, and diff checks pass locally. PR #3163 passed all container shards;
run 33325045980 restored a digest-scoped prefix cache, verified Chromium, and
skipped image extraction in a reported setup interval of about 9 seconds. The
workflow also retries transient immutable-image metadata lookup failures three
times. A forced-miss fallback run and the post-merge three-run comparison remain
pending for rollout validation.
@@ -0,0 +1,68 @@
---
id: "02-scope-review-diff-readiness"
title: "Scope review diff readiness assertions"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/platform/requirements/e2e-duration-aware-sharding.md"
---
# Task 02: Scope review diff readiness assertions
## Outcome
Remove the recurring false flake in the moved-file review regression test by
making its loading assertion apply only to the selected file section.
## Requirements and design
- `REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-002`
- `AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.3`
- `AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.4`
- `docs/specs/platform/system-design/e2e-duration-aware-sharding.md`,
Reliability and flake contract section
## Acceptance
- `review-file-status.spec.ts` anchors the moved-file explanation and loading
assertions to the selected moved-file diff section, using existing stable
review markup. Unselected lazy sections cannot fail the selected-file check.
- The test uses causal Playwright assertions and no fixed sleep, timeout
inflation, or retry-dependent success path.
- The focused test passes repeatedly with retries disabled and the existing
E2E lint and sleep-ratchet checks remain clean.
## Likely files
- `apps/web/e2e/tests/review/review-file-status.spec.ts`
## Verification
```bash
cd apps/web
pnpm e2e:run --host --no-build tests/review/review-file-status.spec.ts -- --repeat-each=30 --workers=1 --retries=0
pnpm exec eslint --max-warnings 0 e2e/tests/review/review-file-status.spec.ts
pnpm run e2e:sleep-ratchet
```
The focused run must complete without retries or the observed global
`Loading diff...` count failure. If the selected section has a new stable
boundary, keep the selector scoped to that boundary and record the reason in
the test.
## Dependencies and parallelism
No implementation dependency. Execute sequentially in the primary session.
## Exclusions
Do not modify the recently merged PR-status hover or automation-notification
tests. Do not change the review product component unless the existing section
boundary cannot be addressed from the test.
## Results
The pre-fix stress run exposed a separate existing workspace-update fixture
race on one repetition. The targeted selector fix then passed once and passed
all 30 repetitions with retries disabled. ESLint and the E2E sleep ratchet also
pass.
+1
View File
@@ -79,6 +79,7 @@ localization, feature toggles, health, and shared session recovery services.
- [Diagnostic logging System Design Part 1](system-design/diagnostic-logging-01.md)
- [Diagnostic logging System Design Part 2](system-design/diagnostic-logging-02.md)
- [Expected runtime log severity](system-design/expected-runtime-log-severity.md)
- [Duration-aware E2E sharding and CI reliability](system-design/e2e-duration-aware-sharding.md)
- [Health Endpoint — Surface the Running Version](system-design/health-endpoint-version.md)
- [Internationalization (i18n)](system-design/i18n.md)
- [LSP File Intelligence System Design Part 1](system-design/lsp-file-intelligence-01.md)
@@ -2,7 +2,7 @@
status: active
system: platform
created: 2026-08-10
updated: 2026-08-23
updated: 2026-08-30
owners:
- kandev
---
@@ -23,6 +23,20 @@ The E2E workflow currently uses Playwright's ordinal test-level sharding. It kee
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-001.1:** **Normal:** the existing fourteen-shard Chromium and mobile-Chrome matrix.
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-001.2:** **Containers:** the existing six-shard Docker and SSH matrix.
### REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-002: Efficient setup and actionable flake feedback
**Intent:** Once test work is balanced, repeated setup and false-positive
assertions become the next avoidable sources of CI time and lost diagnosis.
Setup acceleration must preserve the existing fallback, and a green result
must not hide a test that only passed after a retry.
#### Acceptance criteria
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.1:** Container-backed E2E jobs shall resolve the runtime image to an immutable digest, reuse browser binaries from a matching digest-scoped cache when available, and use a per-run save key with a stable digest-scoped restore prefix. A miss, stale entry, cache-service failure, or incompatible entry shall use the digest-pinned image extraction path and shall not change test selection or make the cache a correctness dependency. If the image digest cannot be resolved, the job shall fail before using an unpinned image reference.
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.2:** E2E reporting shall separate test execution time from material setup costs, including dependency installation, runtime-image startup, and browser provisioning, and shall expose whether the timing plan used a profile or a fallback.
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.3:** E2E reporting shall identify each passed-after-retry result by stable test key, attempt status, error category, and available diagnostic artifacts. The explicit diagnostic lane shall be able to fail when a test passes only after retry.
- **AC-PLATFORM-E2E-DURATION-AWARE-SHARDING-002.4:** When an E2E test selects one lazy-loaded item, readiness assertions shall be scoped to that selected item. Unrelated loading placeholders shall not determine the selected item's result.
## Migrated source detail
## Why
@@ -240,4 +254,7 @@ manifests, rolling profile, retry diagnostics, and fail-closed runner have
unit, type, and targeted E2E coverage. The prebuilt desktop image amendment is
implemented and was proven by the branch image publish and pull-request smoke
run recorded in `docs/plans/desktop-e2e-prebuilt-image/plan.md`. The balance
target remains an operational check after merge.
target remains an operational check after merge. The setup-reuse and
selection-scoped readiness follow-up is implemented on the current branch.
Remote cache hit/miss measurements and post-merge rollout evidence remain
pending.
@@ -0,0 +1,226 @@
---
status: current
system: platform
requirements:
- REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-001
- REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-002
created: 2026-08-30
updated: 2026-08-30
owners:
- kandev
---
# Duration-aware E2E sharding and CI reliability system design
## Purpose and boundaries
This design defines the operational boundary for Kandev's Playwright E2E
workflow. It keeps test work balanced, removes repeatable setup work from the
container-backed cohort where safe, and makes retry and timing evidence
actionable.
The design covers GitHub Actions workflow orchestration, E2E planning and
reporting, browser provisioning, and the synchronization contract of the
affected regression test. It does not change application behavior, the
worker-scoped backend fixture model, the required E2E gate, or the normal
matrix shape.
## Requirement mapping
| Requirement | Design sections |
| --- | --- |
| `REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-001` | [Execution flow](#execution-flow), [Existing planning contract](#existing-planning-contract) |
| `REQ-PLATFORM-E2E-DURATION-AWARE-SHARDING-002` | [Browser cache contract](#browser-cache-contract), [Reliability and flake contract](#reliability-and-flake-contract), [Observability](#observability) |
## Design invariants
- The normal cohort remains fourteen shards covering Chromium and mobile
Chrome, and the container cohort remains six shards covering Docker and SSH.
- Each shard remains single-worker until a measured concurrency change passes
the existing isolation and retry criteria.
- A timing profile or browser cache accelerates planning or setup only. A
missing, stale, or inaccessible optimization input has a deterministic
fallback and cannot change the selected test set.
- A passing-after-retry result remains visible, even when the normal pull
request lane is allowed to finish green.
- CI time analysis separates runner queue time from job setup and test
execution time. Queue time is capacity evidence, not a test-synchronization
signal.
## Components and responsibilities
| Component | Responsibility |
| --- | --- |
| `.github/workflows/e2e-tests.yml` | Detect relevant changes, build artifacts, obtain timing data, run the two E2E cohorts, and publish reports. |
| `apps/web/e2e/scripts/e2e-timings.ts` | Maintain the bounded successful-`main` timing profile and classify profile fallbacks. |
| `apps/web/e2e/scripts/plan-shards.ts` | Discover the cohort catalog, create deterministic duration-aware manifests, and validate coverage. |
| `apps/web/e2e/scripts/run-planned-shard.ts` | Validate and execute one explicit manifest selection while preserving Playwright reporting and retry settings. |
| `apps/web/e2e/scripts/retry-summary.ts` and `flake-report.ts` | Group attempts by stable test key and publish retry and historical flake evidence. |
| `.github/docker/ci-base/Dockerfile` | Define the pinned Playwright browser source baked into the runtime image. |
| `apps/web/e2e/tests/review/review-file-status.spec.ts` | Verify the selected review file's status using a selection-scoped readiness assertion. |
## Existing planning contract
The build job remains the only producer of run-scoped normal and container
manifests. It obtains the latest eligible successful-`main` profile, discovers
the catalog from the authoritative Playwright configuration, and produces
explicit file selections. The planner continues to use longest-processing-time
bin packing at project/file granularity with deterministic tie-breaking.
The normal and container matrix jobs download only their cohort manifest. They
do not add ordinal Playwright sharding as a hidden fallback. An unavailable
profile uses the existing count-based plan and reports that mode. An invalid
manifest fails before test execution.
The report job remains responsible for merged reports, the next timing-profile
candidate, retry summaries, and predicted-versus-actual shard diagnostics.
The aggregate gate retains its current cancellation and failure semantics.
## Execution flow
1. The change-detection job decides whether the E2E workflow is required.
2. The build job installs dependencies, builds the backend and web artifacts,
packages the plugin fixture, resolves the eligible `main` profile, and
creates validated manifests for both cohorts.
3. Normal matrix jobs download artifacts and run their explicit manifest
selection in the existing runtime image.
4. Container matrix jobs restore the browser cache described below, use the
pinned image extraction fallback when needed, then run the explicit
Docker/SSH manifest selection on the host runner.
5. The report job merges blobs, records setup and execution diagnostics,
creates the retry and flake summaries, and publishes a new profile only
when the run is an eligible successful `main` result.
6. The required gate evaluates the final job results.
## Browser cache contract
The container-backed jobs currently copy `/ms-playwright` from the pinned
runtime image to `/tmp/ms-playwright` on every host runner. The cache removes
that repeated copy after the first successful warm-up without replacing the
image as the source of truth.
The existing host job uses the following sequence:
1. Resolve the runtime image manifest to a sha256 digest. If resolution fails,
stop before using a mutable image reference.
2. Restore `/tmp/ms-playwright` from the GitHub Actions cache before the
digest-pinned Docker pull and copy step.
3. Use the runner operating system, Playwright browser source, resolved image
digest, workflow run ID, and attempt number in the primary key. Supply a
stable prefix ending before the run-specific suffix as the restore key. A
browser-source or image change therefore cannot reuse an incompatible
entry, while a failed entry cannot win an exact-key lookup forever.
4. On a matching exact or prefix hit, set `PLAYWRIGHT_BROWSERS_PATH` to the
restored path, verify that Chromium is usable, and skip the image pull and
copy.
5. On a miss, stale entry, cache-service error, or failed verification, pull
the resolved digest, run the existing image extraction and browser smoke
check, and save a successful fallback under the current run's new primary
key. Cache restore and save failures are reported as setup diagnostics and
do not fail a test whose digest-pinned fallback succeeds.
The cache contains browser binaries only. It does not contain repository
source, credentials, test results, or generated application artifacts. A
cache hit cannot alter the manifest, project filters, retries, timeout, or
worker count. The first run after a key change is expected to pay the current
provisioning cost.
## Reliability and flake contract
The retry summary continues to use the stable project, repository-relative
file, and full-title key. It records attempt count, each attempt's status,
error category, and links to available trace, screenshot, or error-context
artifacts. The historical report retains recent successful-main entries and
cross-checks repeat offenders against the Playwright result data.
The explicit diagnostic lane sets `failOnFlakyTests` so a test that passes only
after retry fails that lane. The normal pull request lane may retain retries,
but its report must still expose the passed-after-retry result.
For the review-file-status regression, the selected moved-file header is the
readiness anchor. Assertions about the moved-file explanation and the absence
of its own loading marker are scoped to that file's section. Loading markers
for other lazy-loaded files in the dialog are not evidence that the selected
file is unready and must not fail the test.
This synchronization uses existing DOM state and causal Playwright assertions.
It does not add a fixed sleep or enlarge a timeout to mask the race.
## Timing and setup data
The report preserves these distinct measurements:
- runner queue and job start delay, derived from workflow and job timestamps;
- dependency installation;
- runtime image startup and browser provisioning;
- Playwright test execution per shard;
- report merge and aggregate-gate delay.
Predicted-versus-actual shard data remains keyed by cohort and shard number.
It includes the planning mode, unknown and warm counts, target duration, and
actual duration. The container summary emits the resolved image digest,
logical cache state, setup mode, restore/verification/extraction/save outcomes,
and total browser setup elapsed time. These values let setup savings be
compared with the test and queue budgets without claiming per-step durations
that the workflow does not measure.
## Failure modes and recovery
| Condition | Required behavior |
| --- | --- |
| Timing profile is absent or unusable | Use the validated count-based plan and report the fallback. |
| Manifest is missing, malformed, incomplete, or overlapping | Fail the planning or shard job before test execution. |
| Runtime image digest cannot be resolved | Fail before cache restore or image use; never fall back to the mutable convenience tag. |
| Browser cache hit | Verify the restored browser path, then skip image extraction only when verification succeeds. |
| Browser cache miss, stale key, cache outage, or failed verification | Use the digest-pinned runtime image extraction and smoke check, then save under a new run-specific key when possible. Test selection and correctness remain unchanged. |
| Cache save is unavailable | Finish the successful test run and report that the next run will need the fallback path. |
| Test passes after retry | Preserve the final pass, attempt details, error category, and diagnostic artifacts; fail the explicit diagnostic lane. |
| Selected review file is loaded while other lazy sections still show placeholders | Pass the selection-scoped assertion; unrelated placeholders are ignored. |
| Runner queue is long | Preserve job timestamps and execution diagnostics so capacity delay is not mistaken for shard imbalance. |
## Security and workflow constraints
- Keep all workflow actions pinned to their existing reviewed commit SHAs.
- Use the existing read-only package and artifact permissions. The browser
cache must not require a new secret or broaden pull-request credentials.
- Resolve `runtime-latest` to a sha256 digest and use the digest-pinned image
reference for fallback extraction. Restore only the exact browser path used
by the host job and use the digest-scoped cache prefix as a compatibility
boundary. Do not restore arbitrary files into the workspace or Docker state.
- Preserve the repository's workflow rules for untrusted pull requests,
checkout credentials, and container cleanup.
## Observability
The workflow retains the existing `e2e-timing-diagnostics`,
`e2e-retry-summary`, `e2e-flake-history`, timing-profile, manifest, blob, and
merged-report artifacts. The container jobs add a short step summary with
cache state and setup timings. The report continues to make the following
questions answerable from one run:
- Did the run use a timing profile or count fallback?
- Was the slowest shard caused by test execution or setup?
- Which immutable image digest and cache state did the run use, and did the
cache hit remove the extraction step?
- Did any test pass only after retry, and is it a repeat offender?
- Was the total wall time dominated by runner queue capacity?
Three successful `main` runs remain the rollout sample for the shard-tail
target. A cache change is retained only if it reduces the container setup
component without increasing retries, failed browser verification, or test
failures.
## Out of scope
- changing the fourteen normal or six container shards;
- enabling `workers: 2` by default;
- splitting the normal matrix into separate mobile and desktop cohorts;
- changing the Playwright retry count, test timeout, or required gate;
- replacing the rolling profile with a service or checked-in shard lists;
- hiding retry groups by increasing waits or weakening assertions;
- changing application code solely to accommodate this CI optimization.
## Related decisions and delivery plan
- [Duration-aware E2E sharding uses rolling `main` timings](../../../decisions/2026-08-10-duration-aware-e2e-sharding.md)
- [Browser cache for host-runner E2E provisioning](../../../decisions/2026-08-30-e2e-browser-cache.md)
- [E2E CI efficiency plan](../../../plans/e2e-ci-efficiency/plan.md)