mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 01:06:14 +00:00
Values coming from `${{ }}` expressions were interpolated directly into
`run:` scripts, so GitHub substituted them into the shell source before
bash parsed it. A commit title containing a double quote broke the
"Write step summary" step of the bundle build with a syntax error, and
the same pattern allowed arbitrary command execution on the
self-hosted runners.
Pass every expression used inside `run:` through step/job `env:` and
reference it as a quoted shell variable instead. Use the runner's
default variables (GITHUB_RUN_ID, GITHUB_REPOSITORY, ...) where the
value comes from the `github` context.
Also validate `plugin_name` in plugins-deploy-package.yml against
`^[a-z0-9][a-z0-9-]*$`, since it is free-form and reaches paths,
worker names, GITHUB_ENV and action inputs.
Affected workflows: build-bundle, build-docker,
build-docker-admin-console, plugins-deploy-package,
plugins-deploy-api-doc, plugins-deploy-styles-doc, release, tests-e2e.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
421 lines
13 KiB
YAML
421 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 }}
|
|
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@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
|