-
-
Notifications
You must be signed in to change notification settings - Fork 9.2k
462 lines (421 loc) · 19.6 KB
/
Copy pathe2e.yml
File metadata and controls
462 lines (421 loc) · 19.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
name: E2E
on:
push:
branches:
- develop
- master
- release/**
pull_request:
merge_group:
workflow_dispatch:
inputs: &workflow_inputs
group_argos_images:
description: Composite screenshots into batched sheets before Argos upload
type: boolean
default: true
spec_pattern:
description: Cypress spec glob (empty = full suite)
type: string
default: ''
workflow_call:
inputs: *workflow_inputs
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
env:
# For PRs and MergeQueues, the target commit is used, and for push events to non-develop branches, github.event.previous is used if available. Otherwise, 'develop' is used.
targetHash: >-
${{
github.event_name == 'workflow_dispatch' && 'develop' ||
github.event.pull_request.base.sha ||
github.event.merge_group.base_sha ||
(
(
(github.event_name == 'push' && github.ref == 'refs/heads/develop') ||
github.event.before == '0000000000000000000000000000000000000000'
) && 'develop'
) ||
github.event.before
}}
RUN_VISUAL_TEST: >-
${{
github.repository == 'mermaid-js/mermaid' && (
github.event_name == 'workflow_dispatch' ||
(github.event_name != 'pull_request' || !startsWith(github.head_ref, 'renovate/'))
)
}}
# Automatic runs always batch; manual runs can upload per-screenshot instead.
# `inputs.group_argos_images` is `null` for non-manual runs, so treat it like `true`.
GROUP_ARGOS_IMAGES: >-
${{ format('{0}', inputs.group_argos_images) == 'false' && 'false' || 'true' }}
# On a push to the baseline branch (develop), upload BOTH the grouped sheets
# and the individual per-test screenshots in a single Argos build, so the
# default build's baseline is a superset. Scoped grouped PRs and manual
# per-screenshot runs then both diff cleanly against it (each marked --subset).
ARGOS_UPLOAD_BOTH: >-
${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
jobs:
cache:
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.16.0-chrome-127.0.6533.88-1-ff-128.0.3-edge-127.0.2651.74-1
options: --user 1001
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
# If a snapshot for a given Hash is not found, we checkout that commit, run the tests and cache the snapshots.
- name: Switch to base branch
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ env.targetHash }}
- name: Install dependencies
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true' }}
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
# just perform install
runTests: false
- name: Calculate bundle size
if: ${{ steps.cache-snapshot.outputs.cache-hit != 'true'}}
run: |
pnpm run build:viz
mkdir -p cypress/snapshots/stats/base
mv stats cypress/snapshots/stats/base
# Detect which diagram(s) the PR touches and determine the minimal set of
# Cypress specs to run.
detect-scope:
runs-on: ubuntu-latest
outputs:
spec_pattern: ${{ steps.scope.outputs.spec_pattern }}
matrix: ${{ steps.scope.outputs.matrix }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Need enough history to diff against the base commit.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
- name: Detect e2e scope
id: scope
run: |
if [ "$SPEC" != 'null' ]; then
SPEC=$(node -e '
// `SPEC` should be a JSON string, so unwrap it
const assert = require("node:assert");
const parsed = JSON.parse(process.env.SPEC);
assert.strictEqual(typeof parsed, "string", "SPEC must be a JSON string");
console.log(parsed);
')
if [ -n "$SPEC" ]; then
echo "spec_pattern=${SPEC}" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Manual run — scoped to: ${SPEC}"
else
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Manual run — full suite."
fi
exit 0
fi
# Feature flag off → always use full matrix
if [ "${E2E_SCOPE_BY_DIAGRAM}" != "true" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Feature flag disabled — full suite."
exit 0
fi
BASE="${{ env.targetHash }}"
# For direct pushes to the main development branch, always run the
# full suite (no PR diff to scope against).
if [ "$BASE" = "develop" ] || [ "$BASE" = "master" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Direct push to ${BASE} — full suite."
exit 0
fi
# Fetch the target branch so we can find the true merge-base.
git fetch origin develop --depth=1 2>/dev/null || true
MERGE_BASE=$(git merge-base HEAD origin/develop 2>/dev/null || echo "")
if [ -z "$MERGE_BASE" ]; then
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Could not determine merge-base — full suite."
exit 0
fi
echo "[detect-scope] merge-base: ${MERGE_BASE}"
# Get changed files and run the detection script.
SPEC=$(git diff --name-only "$MERGE_BASE" HEAD 2>/dev/null \
| node scripts/e2e-diagram-scope.mjs \
|| echo "")
# Check and set required no of containers to run
if [ "$SPEC" = "SKIP" ]; then
echo "spec_pattern=SKIP" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Only docs/ignorable files changed — e2e can be skipped."
elif [ -n "$SPEC" ]; then
echo "spec_pattern=${SPEC}" >> "$GITHUB_OUTPUT"
echo "matrix=[1]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Scoped to: ${SPEC}"
else
echo "spec_pattern=" >> "$GITHUB_OUTPUT"
echo "matrix=[1,2,3,4,5,6,7,8]" >> "$GITHUB_OUTPUT"
echo "[detect-scope] Cannot scope — full suite."
fi
echo "[detect-scope] matrix output: $(grep '^matrix=' "$GITHUB_OUTPUT" | tail -1)"
env:
# Toggle diagram-scoped e2e: set this repository variable to 'true'
# (GitHub → Settings → Secrets and variables → Actions → Variables)
E2E_SCOPE_BY_DIAGRAM: "${{ vars.E2E_SCOPE_BY_DIAGRAM == 'true' }}"
# Either `null` (no inputs), or a JSON string to the spec_pattern.
# Empty JSON string (i.e. `""`) means run the full suite.
SPEC: ${{ toJSON( inputs.spec_pattern ) }}
e2e:
# Skip the entire e2e job when only docs/ignorable files changed.
if: needs.detect-scope.outputs.spec_pattern != 'SKIP'
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-20.16.0-chrome-127.0.6533.88-1-ff-128.0.3-edge-127.0.2651.74-1
options: --user 1001
needs: [cache, detect-scope]
strategy:
fail-fast: false
matrix:
containers: ${{ fromJSON(needs.detect-scope.outputs.matrix) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# uses version from "packageManager" field in package.json
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
# These cached snapshots are downloaded, providing the reference snapshots.
- name: Cache snapshots
id: cache-snapshot
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: ./cypress/snapshots
key: ${{ runner.os }}-snapshots-${{ env.targetHash }}
- name: Install dependencies
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
with:
runTests: false
- name: Output size diff
if: ${{ matrix.containers == 1 }}
run: |
pnpm run build:viz
mv stats cypress/snapshots/stats/head
echo '## Bundle size difference' >> "$GITHUB_STEP_SUMMARY"
echo '' >> "$GITHUB_STEP_SUMMARY"
npx tsx scripts/size.ts >> "$GITHUB_STEP_SUMMARY"
# Rebuild the served bundle with inline source maps so the Cypress run can
# collect native V8 coverage and map it back to source (no instrumentation).
# Gated to runs whose coverage is uploaded.
- name: Build coverage bundle for e2e coverage
if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/develop' }}
run: pnpm build:coverage
# Install NPM dependencies, cache them correctly
# and run all Cypress tests (or a scoped subset when detect-scope fires).
- name: Cypress run
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16
id: cypress
with:
install: false
start: pnpm run dev:coverage
wait-on: 'http://localhost:9000'
browser: chrome
# When detect-scope produced a pattern, run only those specs.
spec: ${{ needs.detect-scope.outputs.spec_pattern }}
# Disable recording if we don't have an API key
# e.g. if this action was run from a fork
record: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.CYPRESS_RECORD_KEY != '' }}
env:
CYPRESS_COMMIT: ${{ github.sha }}
CYPRESS_RECORD_KEY: ${{ env.RUN_VISUAL_TEST == 'true' && secrets.CYPRESS_RECORD_KEY || ''}}
SPLIT: ${{ strategy.job-total }}
SPLIT_INDEX: ${{ strategy.job-index }}
SPLIT_FILE: 'cypress/timings.json'
VITEST_COVERAGE: true
# Stash each shard's lcov; the host-based `codecov-upload` job uploads them.
- name: Upload e2e coverage artifact
if: ${{ steps.cypress.conclusion == 'success' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: e2e-coverage-${{ matrix.containers }}
path: coverage/cypress/lcov.info
if-no-files-found: ignore
retention-days: 1
- name: Upload raw screenshots for Argos batching
if: ${{ env.RUN_VISUAL_TEST == 'true' && !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-${{ matrix.containers }}
path: cypress/screenshots
if-no-files-found: ignore
retention-days: 1
# Upload coverage from the host, where curl + gpg are available; the
# cypress/browsers container lacks them and can't install them (non-root).
codecov-upload:
needs: e2e
if: ${{ !cancelled() && (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download e2e coverage artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: e2e-coverage-*
path: coverage-artifacts
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
with:
directory: coverage-artifacts
flags: e2e
name: mermaid-codecov
fail_ci_if_error: false
verbose: true
token: 6845cc80-77ee-4e17-85a1-026cd95e0766
# Batch all per-container screenshots into folder-wise composite sheets and
# upload them to Argos in a single build. PR / merge-queue runs upload only the
# sheets; a push to develop additionally uploads the individual screenshots so
# the baseline is a superset (see ARGOS_UPLOAD_BOTH).
#
# Only runs when the e2e matrix fully succeeded: a failed/partial e2e run would
# otherwise upload an incomplete or wrong set of screenshots — and on develop
# that set becomes the baseline. `needs.e2e.result == 'success'` requires every
# shard to pass (the matrix is fail-fast: false, so its result is failure if any
# shard failed).
argos-batch:
if: >-
${{
github.repository == 'mermaid-js/mermaid' &&
(github.event_name == 'workflow_dispatch' ||
(github.event_name != 'pull_request' || !startsWith(github.head_ref, 'renovate/'))) &&
needs.detect-scope.outputs.spec_pattern != 'SKIP' &&
needs.e2e.result == 'success'
}}
needs: [detect-scope, e2e]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version-file: '.node-version'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download screenshot artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
pattern: argos-screenshots-*
merge-multiple: true
path: cypress/screenshots
# cypress-split gives each shard a different spec subset, so Cypress names
# screenshots relative to that shard's spec common-ancestor (the rendering/
# prefix is kept or stripped depending on the mix). Re-root every screenshot
# under its spec's true cypress/integration path so names are deterministic
# across runs — otherwise the same diagram churns the Argos baseline.
- name: Canonicalize screenshot paths
run: pnpm run argos:canonicalize
- name: Build composite sheets
if: env.GROUP_ARGOS_IMAGES == 'true'
run: pnpm run argos:batch
# Baseline branch (develop): merge the grouped sheets and the individual
# screenshots into one tree and upload them as a SINGLE Argos build. The CLI
# uploads one directory and names each screenshot relative to it, so merging
# preserves both naming schemes — sheet names (<group>/<group>-NNN.png) and
# individual names (<group>/<spec>.spec.js/<name>.png) never collide and match
# exactly what grouped PR / manual runs upload. The default build thus becomes
# a superset baseline both modes can diff against (each marked --subset).
# No --subset here: a develop push runs the full suite and uploads both
# representations, so this is the complete, authoritative baseline.
#
# Sent as a single Argos build, but split into K parallel uploads sharing one
# nonce. Argos's PUT /builds/{buildId} finalize body inlines metadata for every
# screenshot, so uploading the whole superset in one request exceeds the API
# body limit ("request entity too large", HTTP 413). Each parallel index PUTs
# only its slice; --files globs stay relative to the same root so names are
# preserved, and Argos combines the indices into one build once all K report.
- name: Upload images to Argos
env:
ARGOS_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
mkdir -p cypress/argos-baseline
if [ "$ARGOS_UPLOAD_BOTH" = 'true' ]; then
cp -r cypress/argos-sheets/. cypress/argos-baseline/
cp -r cypress/screenshots/. cypress/argos-baseline/
elif [ "$GROUP_ARGOS_IMAGES" = 'true' ]; then
# PR / merge-queue / manual grouped runs: upload only the composite sheets.
cp -r cypress/argos-sheets/. cypress/argos-baseline/
export ARGOS_SUBSET=true
else
# Manual per-screenshot runs (workflow_dispatch with group_argos_images=false).
cp -r cypress/screenshots/. cypress/argos-baseline/
export ARGOS_SUBSET=true
fi
# Round-robin the leaf directories (those that directly hold images)
# into K buckets so each upload's finalize body carries only ~1/K of the
# suite. Bucketing by top-level folder instead put all of rendering/
# (~60% of the suite) into one bucket whose finalize body exceeded
# Argos's limit (HTTP 413) regardless of K; balancing at the leaf level
# keeps the biggest bucket to ~1/K.
mapfile -t groups < <(
cd cypress/argos-baseline &&
find . -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) |
sed 's#/[^/]*$##' | sort -u | sed 's#^\./##'
)
K=5
for i in $(seq 1 "$K"); do
globs=()
for j in "${!groups[@]}"; do
# Glob each leaf dir non-recursively (`/*` not `/**`) so the buckets
# stay a disjoint partition, and match only images: a `/**` glob would
# also pick up the `<name>.png.argos.json` metadata sidecars, which
# Argos treats as screenshots and whose doubled `<sidecar>.argos.json`
# path overflows the 255-byte filename limit (ENAMETOOLONG, surfaced
# as "Failed to read metadata").
if [ $(( j % K + 1 )) -eq "$i" ]; then globs+=( "${groups[$j]}/*.{png,jpg,jpeg}" ); fi
done
[ ${#globs[@]} -eq 0 ] && continue
npx argos upload cypress/argos-baseline \
--files "${globs[@]}" \
--parallel --parallel-total "$K" --parallel-index "$i"
done
e2e-required:
if: always()
needs: [detect-scope, e2e]
runs-on: ubuntu-latest
steps:
- name: Verify e2e outcome
env:
E2E_RESULT: ${{ needs.e2e.result }}
SCOPE_RESULT: ${{ needs.detect-scope.result }}
SPEC_PATTERN: ${{ needs.detect-scope.outputs.spec_pattern }}
run: |
echo "detect-scope result: ${SCOPE_RESULT}"
echo "detect-scope spec_pattern: ${SPEC_PATTERN}"
echo "e2e result: ${E2E_RESULT}"
case "${E2E_RESULT}" in
success|skipped)
echo "OK — required check satisfied."
;;
*)
echo "FAIL — e2e matrix did not complete successfully."
exit 1
;;
esac