penpot/.serena/memories/frontend/e2e-ci-workflow.md
Andrey Antukh 5c22f5bfb7
⚡ Build the frontend bundle once for all E2E suites (#11792)
* ⚡ Build the frontend bundle once for all E2E suites

Merge tests-integration, tests-composable-suite and tests-plugin-api-suite
into one "CI: E2E" workflow. Each of the three ran its own full
frontend/scripts/build on every PR, so one PR paid the build three times.

The new build-bundle job restores actions/cache key frontend-bundle-<sha>,
runs frontend/scripts/build only on a miss and saves the key before the
job ends. The integration shards, the composable suite and the mocked
Plugin API suite now all need build-bundle and restore the same key with
fail-on-cache-miss, so none of them builds. A workflow re-run of the same
SHA reuses the cached bundle instead of rebuilding it.

Triggers become the union of the previous paths (frontend, common,
render-wasm, plugins): the bundle embeds the built plugins, so a plugins
change runs the whole set. workflow_dispatch keeps running the
integration job only, as before.

Job names are kept identical on purpose: they are the GitHub check
contexts and branch protection may match them by name.

Docs: new mem:frontend/e2e-ci-workflow records the build-once contract,
referenced from mem:frontend/core and mem:frontend/testing; the composable
memory and both suite READMEs are updated.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Fix mocked plugin suites crashing without frontend deps

The mocked CI drivers shelled out to frontend/scripts/e2e-server.js,
which imports express from frontend/node_modules. CI jobs install
only plugins/ deps, so the import failed with ERR_MODULE_NOT_FOUND
and the run timed out waiting for localhost:3000.

Serve the prebuilt bundle with a zero-dependency static server
built into each driver (ci/static-server.ts, kept in sync in both
suites) plus node:test coverage for it.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-22 10:23:15 +02:00

2.2 KiB

E2E CI workflow (build-once frontend bundle)

.github/workflows/tests-e2e.yml ("CI: E2E") is the single workflow for every suite that drives a real frontend bundle:

  • Integration Tests — Playwright specs under frontend/playwright (sharded).
  • Run composable test suite (mocked backend) — mem:frontend/composable-component-tests.
  • Run Plugin API Test Suite (mocked) — plugins/apps/plugin-api-test-suite.

Triggers: PR/push touching frontend/**, common/**, render-wasm/**, plugins/** (or the workflow file), plus workflow_dispatch (integration only). A plugins/** change runs the whole set on purpose: the bundle embeds the built plugins.

Invariants

  • ONE frontend/scripts/build per SHA. The build-bundle job restores actions/cache key frontend-bundle-<sha>, builds only on a miss, and saves the key before the job ends. A re-run of the same SHA reuses the cache.
  • Consumer jobs (needs: build-bundle) restore the same key with fail-on-cache-miss: true and NEVER run frontend/scripts/build.
  • The bundle is frontend/resources/public. The integration specs serve it with frontend/scripts/e2e-server.js; each mocked plugin driver serves it with its own zero-dependency ci/static-server.ts (duplicated in both suites — keep the copies in sync).
  • Mocked plugin jobs install only plugins/ deps, so their drivers must not import anything from frontend/node_modules at runtime (e.g. no frontend/scripts/e2e-server.js, which needs express).
  • Cache key comes from git rev-parse HEAD (the checked-out ref), not github.sha, because workflow_dispatch can target a different ref.
  • Job name: values are the GitHub check contexts. Keep them stable: branch protection may match them by name. Renaming the workflow file/name is safe.

Adding a bundle-consuming suite

Add a job with needs: build-bundle, a Restore Cache step (actions/cache/restore@v5, key needs.build-bundle.outputs.bundle_key, fail-on-cache-miss: true), then that suite's own deps. Never add a build step.

Scope

Distinct from Bundles Builder (.github/workflows/build-bundle.yml), the release path that zips the bundle (manage.sh build-bundle) and uploads it to S3.