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: