-
Notifications
You must be signed in to change notification settings - Fork 7
431 lines (389 loc) · 17.7 KB
/
Copy pathci.yml
File metadata and controls
431 lines (389 loc) · 17.7 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
# ci.yml
# PR gate -- runs on every pull request to main.
# Jobs are path-filtered so only relevant checks run for each PR.
#
# Every external action is pinned to a full commit SHA; the trailing
# comment records the human-readable version that SHA corresponds to.
# Dependabot (.github/dependabot.yml, github-actions ecosystem, weekly)
# maintains both -- it bumps the SHA and rewrites the comment, so updates
# still arrive as reviewable PRs. Dependabot does NOT convert tags to
# SHAs; it preserves whichever reference style it finds. Keep new actions
# SHA-pinned when you add them, or they stay on a mutable tag forever.
# Local composite actions (uses: ./.github/actions/*) have no SHA to pin.
#
# Artifact strategy: the build job uploads dist/ as a single artifact. Jobs
# that need dist/ on their own runner (test, chromatic) download it instead
# of rebuilding. build-storybook is the exception -- it also triggers on
# mdx/storybook-config changes where build doesn't run, so it always does
# its own full build via the shared composite.
#
# Visual-regression gate: detect-css-change gates Chromatic on whether the
# PR actually moved compiled CSS. New stories added without a CSS change
# won't be auto-baselined, so a maintainer must do an npm run test:visual.
name: CI
on:
pull_request:
# ready_for_review is required so draft-gated jobs run when a draft is
# marked ready (it is not one of the default activity types).
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
# Cancel a PR's previous in-flight run when a new commit is pushed. Keyed on
# the PR number (falls back to ref for non-PR runs) so unrelated PRs don't
# cancel each other. Frees runner slots instead of building superseded commits.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
# -----------------------------------------------------------------------
# Filter -- determines which jobs (and which lint/build steps) run based
# on what changed.
#
# `output_affecting` gates the build job and the contract checks inside it.
# Use it -- not `scss` -- for anything that validates the published
# artifact. A build-pipeline change (postcss plugin, Style Dictionary
# config) or a package.json `files`/`exports` change moves what ships
# without touching a .scss file, and gating those checks on `scss` alone
# let PR #175 through with every output check reported as skipped.
# -----------------------------------------------------------------------
filter:
name: Detect changed paths
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
scss: ${{ steps.filter.outputs.scss }}
js: ${{ steps.filter.outputs.js }}
json: ${{ steps.filter.outputs.json }}
md: ${{ steps.filter.outputs.md }}
mdx: ${{ steps.filter.outputs.mdx }}
stories: ${{ steps.filter.outputs.stories }}
pkg: ${{ steps.filter.outputs.pkg }}
storybook_config: ${{ steps.filter.outputs.storybook_config }}
tokens: ${{ steps.filter.outputs.tokens }}
output_affecting: ${{ steps.filter.outputs.output_affecting }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
# Filter names use underscores, not hyphens: a hyphenated output is
# read as subtraction in `${{ ... }}` expressions and never matches.
filters: |
scss:
- 'src/scss/**'
- 'tokens.json'
js:
- '**/*.js'
- '**/*.mjs'
- '**/*.cjs'
json:
- '**/*.json'
- '!package-lock.json'
md:
- '**/*.md'
mdx:
- 'stories/**/*.mdx'
stories:
- '**/*.stories.js'
pkg:
- 'package.json'
storybook_config:
- '.storybook/**'
tokens:
- 'tokens.json'
- '.config/sd.config.js'
# Anything that can change the published artifact -- compiled CSS,
# the asset tree, or the packed tarball. Gates the build job, the
# contract checks inside it (bundle size, public API snapshot), and
# the detect-css-change VR trigger. Deliberately wider than `scss`:
# the build pipeline itself (postcss config and plugins, Style
# Dictionary config, the sprite config) moves output without a
# single .scss file changing, and package.json `files`/`exports`
# move what ships without moving CSS at all. src/assets feeds
# dist/ (copy:hds + sprite) without touching .scss, and scripts/
# holds the check gates themselves -- an edit to any of those must
# run the build and contract checks it controls.
output_affecting:
- 'src/scss/**'
- 'src/assets/**'
- 'tokens.json'
- 'package.json'
- 'postcss.config.*'
- '.config/**'
- 'scripts/**'
# -----------------------------------------------------------------------
# Lint -- one runner for every linter. The lockfile check always runs
# (fast supply-chain gate); the rest run only when their file types
# changed. Each conditional step is guarded with !cancelled() so one
# linter failing doesn't hide the others -- you still get every linter's
# result in a single pass. Token drift lives here too: it needs
# node_modules (Style Dictionary), not the compiled dist/.
#
# The token drift step keeps the scss filter (not just tokens) so a
# hand-edit directly to a generated file -- which wouldn't touch
# tokens.json/.config/sd.config.js -- still gets caught.
# -----------------------------------------------------------------------
lint:
name: Lint
runs-on: ubuntu-latest
needs: filter
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: Lockfile lint
run: npm run lint:lockfile
- name: Stylelint
if: ${{ !cancelled() && needs.filter.outputs.scss == 'true' }}
run: npm run lint:scss
- name: ESLint
if: ${{ !cancelled() && (needs.filter.outputs.js == 'true' || needs.filter.outputs.json == 'true') }}
run: npm run lint:js
- name: remark-lint (Markdown)
if: ${{ !cancelled() && needs.filter.outputs.md == 'true' }}
run: npm run lint:md
- name: remark-lint (MDX)
if: ${{ !cancelled() && needs.filter.outputs.mdx == 'true' }}
run: npm run lint:mdx
- name: Prettier format check
if: ${{ !cancelled() && (needs.filter.outputs.md == 'true' || needs.filter.outputs.mdx == 'true' || needs.filter.outputs.js == 'true' || needs.filter.outputs.json == 'true') }}
run: npm run format
- name: Token generation drift
if: ${{ !cancelled() && (needs.filter.outputs.tokens == 'true' || needs.filter.outputs.scss == 'true') }}
run: npm run check:tokens
# -----------------------------------------------------------------------
# Build -- compiles CSS/assets, then runs the checks that only need the
# freshly built dist/ (bundle size, public API snapshot, CSS-output hash)
# as in-job steps, so they don't each spin a separate runner and
# re-download an artifact. Uploads dist/ for the jobs that genuinely run
# on their own runner (test, chromatic).
#
# Coupling note: because these checks run here, a failure in any of them
# fails this job and skips the downstream jobs that need dist/. That is
# intentional -- a public-API, bundle, or CSS-baseline regression is
# merge-blocking anyway -- but it is the one behavioral change from the
# previous per-check-job layout.
# -----------------------------------------------------------------------
build:
name: Build
runs-on: ubuntu-latest
needs: filter
timeout-minutes: 15
if: |
needs.filter.outputs.output_affecting == 'true' ||
needs.filter.outputs.stories == 'true' ||
needs.filter.outputs.js == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: Build
run: npm run build
- name: Check bundle size
if: ${{ !cancelled() && needs.filter.outputs.output_affecting == 'true' }}
run: bash scripts/check-bundle-size.sh
- name: Check public API snapshot
if: ${{ !cancelled() && needs.filter.outputs.output_affecting == 'true' }}
run: npm run check:api-snapshot
- name: Upload dist
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
retention-days: 1
# -----------------------------------------------------------------------
# Test -- downloads the dist artifact instead of rebuilding. Skips draft
# PRs (heavy: Playwright browser install + browser-mode a11y run). The
# browser binary is cached so most runs skip the ~100MB download.
# -----------------------------------------------------------------------
test:
name: Test
runs-on: ubuntu-latest
needs: [filter, build]
timeout-minutes: 20
if: |
github.event.pull_request.draft == false &&
(needs.filter.outputs.output_affecting == 'true' ||
needs.filter.outputs.stories == 'true' ||
needs.filter.outputs.js == 'true')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Download dist
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
- name: Unit tests
run: npx vitest run --project unit --reporter=dot
- name: A11y tests
run: npx vitest run --project storybook --reporter=dot
# -----------------------------------------------------------------------
# Build Storybook -- the build proves it compiles; the docs-render crawl
# proves MDX pages don't throw at runtime (a page can compile but throw,
# e.g. a <Canvas> referencing a deleted story). The crawl skips scss: a
# style change can't throw a render error (that's Chromatic's lane), and
# stories are already rendered by the vitest `storybook` project.
#
# Self-contained: doesn't reuse the build artifact, since it also runs on
# mdx/storybook-config changes where the build job is skipped.
# -----------------------------------------------------------------------
build-storybook:
name: Build / Storybook
runs-on: ubuntu-latest
needs: filter
timeout-minutes: 15
if: |
github.event.pull_request.draft == false &&
(needs.filter.outputs.mdx == 'true' ||
needs.filter.outputs.stories == 'true' ||
needs.filter.outputs.scss == 'true' ||
needs.filter.outputs.storybook_config == 'true')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Shared with the deploy-storybook workflow so the build sequence is
# defined once. See .github/actions/build-storybook.
- name: Build Storybook
uses: ./.github/actions/build-storybook
- name: Cache Playwright browsers
if: ${{ needs.filter.outputs.mdx == 'true' || needs.filter.outputs.stories == 'true' || needs.filter.outputs.storybook_config == 'true' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Playwright browser
if: ${{ needs.filter.outputs.mdx == 'true' || needs.filter.outputs.stories == 'true' || needs.filter.outputs.storybook_config == 'true' }}
run: npx playwright install --with-deps chromium
- name: Check docs render
if: ${{ needs.filter.outputs.mdx == 'true' || needs.filter.outputs.stories == 'true' || needs.filter.outputs.storybook_config == 'true' }}
run: npm run check:docs-render
# -----------------------------------------------------------------------
# Detect CSS change -- the Chromatic trigger. Compares freshly built base
# vs PR CSS rather than a committed baseline, so it stays correct no matter
# when the PR branched or whether main moved since (a committed hash goes
# stale against the PR's merge-base; this recomputes on every re-run).
# -----------------------------------------------------------------------
detect-css-change:
name: Detect CSS output change
runs-on: ubuntu-latest
needs: filter
timeout-minutes: 15
if: needs.filter.outputs.output_affecting == 'true'
outputs:
css_output: ${{ steps.compare.outputs.css_output }}
steps:
- name: Check out PR
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: pr
- name: Check out base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
path: base
- name: Build + hash base CSS
id: base
working-directory: base
run: |
npm ci
npm run build
echo "hash=$(bash scripts/css-output-hash.sh)" >> "$GITHUB_OUTPUT"
- name: Build + hash PR CSS
id: pr
working-directory: pr
run: |
npm ci
npm run build
echo "hash=$(bash scripts/css-output-hash.sh)" >> "$GITHUB_OUTPUT"
- name: Compare
id: compare
run: |
if [ "${{ steps.base.outputs.hash }}" = "${{ steps.pr.outputs.hash }}" ]; then
echo "Compiled CSS unchanged (${{ steps.pr.outputs.hash }}). Chromatic will skip."
echo "css_output=false" >> "$GITHUB_OUTPUT"
else
echo "Compiled CSS changed."
echo " base: ${{ steps.base.outputs.hash }}"
echo " pr: ${{ steps.pr.outputs.hash }}"
echo "css_output=true" >> "$GITHUB_OUTPUT"
{
echo "### Compiled CSS output changed"
echo "Chromatic visual regression will run for this PR (fork PRs run once a maintainer approves the workflow)."
} >> "$GITHUB_STEP_SUMMARY"
fi
# -----------------------------------------------------------------------
# Chromatic. Fork PRs run this too, but the token only reaches them after a
# maintainer approves the run -- enforced by the repo Setting "Require
# approval for all outside collaborators"). DO NOT remove that Setting.
# The token can only upload snapshots (not accept baselines or touch account
# settings), so risk is low. Can be reset from the Chromatic Configure page.
# -----------------------------------------------------------------------
chromatic:
name: Chromatic visual regression
runs-on: ubuntu-latest
needs: [filter, build, detect-css-change]
timeout-minutes: 15
if: |
github.event.pull_request.draft == false &&
needs.detect-css-change.outputs.css_output == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# fetch-depth: 0 required for Chromatic TurboSnap baseline detection.
fetch-depth: 0
- uses: ./.github/actions/setup
- name: Download dist
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
- name: Run Chromatic
uses: chromaui/action@69be7d88fc9479eedbe5b2861f847eb48f97cabc # v18.6.1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true
# The CLI only auto-detects chromatic.config.json at the repo
# root; without this input the .config/ file is silently ignored
# and the externals (src/scss/**, src/assets/**) are lost, so
# TurboSnap skips every story on a CSS-only change.
configFile: .config/chromatic.config.json
# -----------------------------------------------------------------------
# USWDS integrity -- runs when package.json changes (potential USWDS bump)
# check:uswds-core architectural gate: @use 'uswds-core' must emit no CSS
# check:uswds component tracker: USWDS Sass source unchanged
# If either fails, do not merge until resolved per CONTRIBUTING.md.
# -----------------------------------------------------------------------
check-uswds:
name: USWDS integrity
runs-on: ubuntu-latest
needs: filter
timeout-minutes: 10
if: needs.filter.outputs.pkg == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: Verify uswds-core emits no CSS
run: npm run check:uswds-core
- name: Verify USWDS packages unchanged
run: npm run check:uswds