penpot/.github/workflows/tests-e2e.yml
David Barragán Merino fa3e01f7c7 👷 Disable checkout credential persistence
Set persist-credentials: false on every actions/checkout step, so the
job token is not left in .git/config for the rest of the job. No step
after checkout pushes or fetches with it. The only authenticated operation,
gh release in release.yml, uses GH_TOKEN.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-30 17:01:58 +02:00

428 lines
14 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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
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@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
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@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
ref: ${{ inputs.gh_ref }}
- name: Restore Cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
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 }}
SHARD: ${{ matrix.shard }}
SHARD_TOTAL: ${{ strategy.job-total }}
run: |
WORKERS=${WORKERS:-2}
echo "Running shard ${SHARD}/${SHARD_TOTAL} with $WORKERS workers"
pnpm exec playwright test --project default \
--workers="$WORKERS" \
--shard="${SHARD}/${SHARD_TOTAL}" \
--reporter=blob
- name: Upload blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Restore Cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
- name: Restore Cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
ref: ${{ inputs.gh_ref }}
- name: Install deps
working-directory: ./frontend
run: |
pnpm install --frozen-lockfile;
- name: Download blob reports
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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