perf(ci): reduce E2E container setup time (#3163)
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user