mirror of
https://github.com/penpot/penpot.git
synced 2026-09-24 04:46:14 +00:00
* ⚡ 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
419 lines
13 KiB
YAML
419 lines
13 KiB
YAML
name: "CI: E2E"
|
|
|
|
# Single entry point for every suite that drives a real frontend bundle.
|
|
# The bundle is built ONCE by `build-bundle` and restored by every consumer,
|
|
# so adding a suite no longer adds another `frontend/scripts/build` run.
|
|
#
|
|
# Suites and what they need from the bundle:
|
|
#
|
|
# - Integration Tests: Playwright specs, backend faked with frontend mocks.
|
|
# - Composable test suite: Plugin runtime, backend faked with Playwright RPC
|
|
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
|
|
# - Plugin API Test Suite (mocked): Plugin runtime, backend faked with
|
|
# Playwright RPC fixtures and MOCK_BACKEND=1.
|
|
# See plugins/apps/plugin-api-test-suite/README.md.
|
|
#
|
|
# Hand-off: `build-bundle` restores `actions/cache` keyed by the checked-out
|
|
# SHA, builds only on a miss, and saves it; every consumer restores the same
|
|
# key after `needs: build-bundle` completes. A re-run of the same SHA reuses
|
|
# the cached bundle instead of rebuilding it.
|
|
#
|
|
# NOTE: the job `name:` values are the GitHub check contexts, so they are kept
|
|
# stable on purpose (branch protection may require them by name). Only the
|
|
# workflow file/name changed.
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
gh_ref:
|
|
description: 'Name of the branch or ref'
|
|
type: string
|
|
required: true
|
|
default: 'develop'
|
|
|
|
shards:
|
|
description: 'Shard layout (JSON array)'
|
|
type: choice
|
|
required: true
|
|
default: '[1, 2, 3, 4]'
|
|
options:
|
|
- '[1, 2, 3, 4]'
|
|
- '[1, 2, 3, 4, 5, 6]'
|
|
- '[1, 2]'
|
|
- '[1]'
|
|
|
|
workers:
|
|
description: 'Playwright workers per shard'
|
|
type: string
|
|
required: true
|
|
default: '2'
|
|
|
|
base_url:
|
|
description: 'Penpot base URL for the (disabled) live Plugin API suite'
|
|
required: false
|
|
default: 'https://localhost:3449'
|
|
|
|
pull_request:
|
|
paths:
|
|
- 'frontend/**'
|
|
- 'common/**'
|
|
- 'render-wasm/**'
|
|
- 'plugins/**'
|
|
- '.github/workflows/tests-e2e.yml'
|
|
|
|
types:
|
|
- opened
|
|
- synchronize
|
|
- ready_for_review
|
|
|
|
push:
|
|
branches:
|
|
- develop
|
|
- staging
|
|
|
|
paths:
|
|
- 'frontend/**'
|
|
- 'common/**'
|
|
- 'render-wasm/**'
|
|
- 'plugins/**'
|
|
- '.github/workflows/tests-e2e.yml'
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ── 1. Build the frontend bundle once ──────────────────────────────────
|
|
build-bundle:
|
|
if: ${{ !github.event.pull_request.draft }}
|
|
name: "Build Integration Bundle"
|
|
runs-on: penpot-extended-runner
|
|
timeout-minutes: 30
|
|
|
|
container:
|
|
image: penpotapp/devenv:latest
|
|
volumes:
|
|
- /var/cache/github-runner/m2:/root/.m2
|
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
|
|
|
outputs:
|
|
bundle_key: ${{ steps.vars.outputs.bundle_key }}
|
|
|
|
steps:
|
|
# An empty `ref` makes checkout fall back to its default (the PR merge
|
|
# ref on pull_request, the pushed ref on push).
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.gh_ref }}
|
|
|
|
# The cache key must come from the SHA actually checked out: on a manual
|
|
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
|
|
- name: Extract cache key
|
|
id: vars
|
|
run: |
|
|
echo "bundle_key=frontend-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
|
|
|
- name: Restore Bundle Cache
|
|
id: restore
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
key: ${{ steps.vars.outputs.bundle_key }}
|
|
path: frontend/resources/public
|
|
|
|
- name: Build Bundle
|
|
if: steps.restore.outputs.cache-hit != 'true'
|
|
working-directory: ./frontend
|
|
run: |
|
|
./scripts/build
|
|
|
|
- name: Store Bundle Cache
|
|
if: steps.restore.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
key: ${{ steps.vars.outputs.bundle_key }}
|
|
path: frontend/resources/public
|
|
|
|
# ── 2. Consumers: restore the bundle, never rebuild it ─────────────────
|
|
test-integration:
|
|
if: ${{ !github.event.pull_request.draft }}
|
|
name: "Integration Tests (${{ matrix.shard }})"
|
|
runs-on: penpot-extended-runner
|
|
timeout-minutes: 40
|
|
|
|
needs: build-bundle
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: ${{ fromJSON(inputs.shards || '[1, 2, 3, 4]') }}
|
|
|
|
container:
|
|
image: penpotapp/devenv:latest
|
|
volumes:
|
|
- /var/cache/github-runner/m2:/root/.m2
|
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
|
- /var/cache/github-runner/ms-playwright:/ms-playwright
|
|
env:
|
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
|
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.gh_ref }}
|
|
|
|
- name: Restore Cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
key: ${{ needs.build-bundle.outputs.bundle_key }}
|
|
path: frontend/resources/public
|
|
fail-on-cache-miss: true
|
|
|
|
- name: Install deps
|
|
working-directory: ./frontend
|
|
run: |
|
|
pnpm install --frozen-lockfile;
|
|
|
|
# No-op once the shared volume is warm; keeps the first run working.
|
|
- name: Install Playwright Chromium
|
|
working-directory: ./frontend
|
|
run: pnpm exec playwright install chromium
|
|
|
|
# `strategy.job-total` is the matrix size, so the shard denominator
|
|
# follows the `shards` input without being hardcoded.
|
|
- name: Run Tests
|
|
working-directory: ./frontend
|
|
env:
|
|
WORKERS: ${{ inputs.workers }}
|
|
run: |
|
|
WORKERS=${WORKERS:-2}
|
|
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
|
|
pnpm exec playwright test --project default \
|
|
--workers="$WORKERS" \
|
|
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
|
|
--reporter=blob
|
|
|
|
- name: Upload blob report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: integration-blob-report-${{ matrix.shard }}
|
|
path: frontend/blob-report/
|
|
overwrite: true
|
|
retention-days: 3
|
|
|
|
- name: Upload test result
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: integration-tests-result-${{ matrix.shard }}
|
|
path: frontend/test-results/
|
|
overwrite: true
|
|
if-no-files-found: ignore
|
|
retention-days: 3
|
|
|
|
composable-test-suite:
|
|
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
|
name: "Run composable test suite (mocked backend)"
|
|
runs-on: penpot-extended-runner
|
|
timeout-minutes: 30
|
|
|
|
needs: build-bundle
|
|
|
|
container:
|
|
image: penpotapp/devenv:latest
|
|
volumes:
|
|
- /var/cache/github-runner/m2:/root/.m2
|
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
|
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
# The driver serves the prebuilt bundle from frontend/resources/public.
|
|
- name: Restore Cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
key: ${{ needs.build-bundle.outputs.bundle_key }}
|
|
path: frontend/resources/public
|
|
fail-on-cache-miss: true
|
|
|
|
- name: Install deps
|
|
working-directory: ./plugins
|
|
run: |
|
|
pnpm install;
|
|
|
|
- name: Install Playwright Chromium
|
|
working-directory: ./plugins
|
|
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
|
|
|
|
- name: Run composable test suite (mocked)
|
|
working-directory: ./plugins
|
|
run: pnpm --filter composable-test-suite run test:ci
|
|
|
|
api-test-suite-mocked:
|
|
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
|
name: "Run Plugin API Test Suite (mocked)"
|
|
runs-on: penpot-extended-runner
|
|
timeout-minutes: 30
|
|
|
|
needs: build-bundle
|
|
|
|
container:
|
|
image: penpotapp/devenv:latest
|
|
volumes:
|
|
- /var/cache/github-runner/m2:/root/.m2
|
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
|
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
|
|
- name: Restore Cache
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
key: ${{ needs.build-bundle.outputs.bundle_key }}
|
|
path: frontend/resources/public
|
|
fail-on-cache-miss: true
|
|
|
|
- name: Install deps
|
|
working-directory: ./plugins
|
|
run: |
|
|
pnpm install;
|
|
|
|
- name: Install Playwright Chromium
|
|
working-directory: ./plugins
|
|
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
|
|
|
- name: Generate API surface
|
|
working-directory: ./plugins
|
|
run: pnpm --filter plugin-api-test-suite run gen:api
|
|
|
|
- name: Run API test suite (mocked)
|
|
working-directory: ./plugins
|
|
env:
|
|
MOCK_BACKEND: "1"
|
|
run: pnpm --filter plugin-api-test-suite run test:ci
|
|
|
|
# ── 3. Merge the per-shard integration reports ─────────────────────────
|
|
merge-reports:
|
|
if: ${{ !cancelled() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
|
|
name: "Merge Integration Reports"
|
|
runs-on: penpot-extended-runner
|
|
timeout-minutes: 15
|
|
|
|
needs: test-integration
|
|
|
|
container:
|
|
image: penpotapp/devenv:latest
|
|
volumes:
|
|
- /var/cache/github-runner/m2:/root/.m2
|
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
|
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.gh_ref }}
|
|
|
|
- name: Install deps
|
|
working-directory: ./frontend
|
|
run: |
|
|
pnpm install --frozen-lockfile;
|
|
|
|
- name: Download blob reports
|
|
uses: actions/download-artifact@v7
|
|
with:
|
|
path: frontend/all-blob-reports
|
|
pattern: integration-blob-report-*
|
|
merge-multiple: true
|
|
|
|
- name: Merge into HTML report
|
|
working-directory: ./frontend
|
|
env:
|
|
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
|
|
run: |
|
|
pnpm exec playwright merge-reports \
|
|
--reporter=html,json,list ./all-blob-reports
|
|
|
|
- name: Test summary
|
|
if: always()
|
|
continue-on-error: true
|
|
working-directory: ./frontend
|
|
run: |
|
|
if [ ! -f report.json ]; then
|
|
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
|
|
exit 0
|
|
fi
|
|
jq -r -f ../.github/scripts/playwright-summary.jq report.json \
|
|
>> "$GITHUB_STEP_SUMMARY" \
|
|
|| echo "Summary generation failed; see the HTML report artifact." \
|
|
>> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Upload JSON report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: integration-json-report
|
|
path: frontend/report.json
|
|
overwrite: true
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
- name: Upload HTML report
|
|
uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: integration-html-report
|
|
path: frontend/playwright-report/
|
|
overwrite: true
|
|
retention-days: 7
|
|
|
|
# ── 4. Live Plugin API suite (disabled) ────────────────────────────────
|
|
#
|
|
# True end-to-end against a LIVE instance: point PENPOT_BASE_URL at a
|
|
# reachable instance and provide login credentials via repo secrets. Manual
|
|
# because the CI runner has no Docker to stand up a full stack.
|
|
#
|
|
# api-test-suite-live:
|
|
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
|
# name: Run Plugin API Test Suite (live)
|
|
# runs-on: penpot-extended-runner
|
|
# container:
|
|
# image: penpotapp/devenv:latest
|
|
#
|
|
# env:
|
|
# PENPOT_BASE_URL: ${{ inputs.base_url }}
|
|
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
|
|
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
|
|
#
|
|
# steps:
|
|
# - uses: actions/checkout@v6
|
|
#
|
|
# - name: Setup Node
|
|
# uses: actions/setup-node@v6
|
|
# with:
|
|
# node-version-file: .nvmrc
|
|
#
|
|
# - name: Install deps
|
|
# working-directory: ./plugins
|
|
# run: |
|
|
# pnpm install;
|
|
#
|
|
# - name: Install Playwright Chromium
|
|
# working-directory: ./plugins
|
|
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
|
#
|
|
# - name: Generate API surface
|
|
# working-directory: ./plugins
|
|
# run: pnpm --filter plugin-api-test-suite run gen:api
|
|
#
|
|
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
|
|
# - name: Run API test suite
|
|
# working-directory: ./plugins
|
|
# run: pnpm --filter plugin-api-test-suite run test:ci
|