mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
## What Follow-up to #7073. Turns the story scan into an accessibility gate: stories run axe in a real browser, and CI flags a change that adds a **new** violation. The app has plenty of existing a11y problems (mostly theme-level colour contrast), so rather than block everything on those, they're recorded in `.storybook/a11y-baseline.json` and grandfathered. The gate cares about three things: - a story breaking a rule it wasn't already breaking - a story that fails to render at all - a scan that didn't cover everything it was asked to Starting point: 839 stories carry a known violation, 1058 story-rule pairs. ## Where it runs - **Pull requests** scan only the stories the branch touches — usually seconds. A full sweep is ~30 minutes, too slow to sit in front of every merge, and the `frontend` path filter is broad enough that unrelated changes would pay for it. - **Nightly** scans every story, so a violation introduced somewhere other than the story itself — a shared component, a theme token — still surfaces within a day. - Both upload their scan reports as artifacts; the reports carry the offending selector and help text, without which a red run can only be understood by reproducing it locally. - **Advisory to start with.** It is deliberately not in `all-checks-passed`, so it reports without blocking. Worth promoting once a few weeks of runs show the pass/fail is stable. ## Using it - **Fixed some violations?** `task frontend:storybook:a11y:record` re-records so the gate locks the improvement in. - **Locally:** `task frontend:storybook:a11y:changed` for your branch, `task frontend:storybook:a11y` for everything. - **New component?** Its story is picked up automatically. ## Testing - Every story — 526 files, ~1,450 stories — runs in a real browser with no render failures, and the gate reports no regressions against the baseline. - Running the gate over a single changed story takes seconds, which is the pull-request path. - The gate's own behaviour is covered against synthetic scan reports: a new rule fails, the same rule on more nodes does not, a crashed story fails, an incomplete scan refuses to report, and re-recording refuses while anything is crashing. - Typecheck (all build variants), ESLint and Prettier pass. ## Notes for reviewers Some of this PR is making the mechanism trustworthy rather than adding features, so it's worth knowing what changed and why: - Rule ids come from the axe docs URL in each violation, not a hand-maintained list of rule names — the old list silently ignored 39 of axe's 104 rules, including `object-alt`, `target-size` and the table rules. - The baseline records **which** rules a story breaks, not how many nodes break them. Node counts drift between runs because stories fetch asynchronously and axe samples whatever has rendered, which made unrelated changes look like regressions. For the same reason the baseline is the union of repeated scans, so a run can only be a subset of it. - A story that fails for a non-a11y reason used to yield no rule id and was recorded as clean, which hid crashes and could mask real violations. Those now fail, and re-recording refuses to run while any story is crashing. - The scan writes a manifest of every story file it intends to cover and the check fails unless all of them reported, so a dropped batch can't read as "no violations". - Vite was pre-bundling the JSX runtime mid-run and reloading the page, which crashed whichever stories were loading; those deps are now named up front and the per-story timeout is above the 5s default. Colour contrast dominates the baseline and is theme-level, tracked separately from this.
513 lines
15 KiB
YAML
513 lines
15 KiB
YAML
version: '3'
|
|
|
|
# Tasks operate from the workspace root (frontend/). Editor commands pass
|
|
# `editor` as the vite project root (positional after `build` / before the
|
|
# mode flag) or use `--project editor/...` for tsc — so the editor lives
|
|
# under frontend/editor/ without each task needing a cd.
|
|
|
|
tasks:
|
|
install:
|
|
desc: "Install dependencies"
|
|
run: once
|
|
cmds:
|
|
- '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}'
|
|
sources:
|
|
- package-lock.json
|
|
- package.json
|
|
status:
|
|
- test -d node_modules
|
|
env:
|
|
CI: '{{ .CI | default "false" }}'
|
|
|
|
prepare:env:
|
|
internal: true
|
|
run: when_changed
|
|
deps: [install]
|
|
vars:
|
|
MODE: '{{.MODE | default ""}}'
|
|
cmds:
|
|
- npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}}
|
|
sources:
|
|
- editor/scripts/setup-env.mts
|
|
generates:
|
|
- editor/.env.local
|
|
- editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local
|
|
|
|
prepare:icons:
|
|
internal: true
|
|
run: once
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/generate-icons.js
|
|
|
|
prepare:og:
|
|
internal: true
|
|
run: when_changed
|
|
desc: "Regenerate OG/social-preview metadata from the tool registry"
|
|
cmds:
|
|
- node editor/scripts/generate-og-metadata.mjs
|
|
sources:
|
|
- editor/src/core/types/toolId.ts
|
|
- editor/src/core/utils/urlMapping.ts
|
|
- editor/src/core/data/useTranslatedToolRegistry.tsx
|
|
- editor/public/og_images/*.png
|
|
generates:
|
|
- editor/src/core/data/ogImageMap.json
|
|
- editor/public/og-metadata.json
|
|
|
|
prepare:
|
|
desc: "Set up dev environment"
|
|
run: when_changed
|
|
vars:
|
|
MODE: '{{.MODE | default ""}}'
|
|
deps:
|
|
- task: prepare:env
|
|
vars: { MODE: '{{.MODE}}' }
|
|
- prepare:icons
|
|
- prepare:og
|
|
|
|
# ============================================================
|
|
# Development
|
|
# ============================================================
|
|
|
|
dev:_run:
|
|
internal: true
|
|
ignore_error: true
|
|
vars:
|
|
MODE: '{{.MODE}}'
|
|
PORT: '{{.PORT | default "5173"}}'
|
|
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
|
OPEN: '{{.OPEN | default ""}}'
|
|
env:
|
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
|
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
|
|
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
|
|
# full path, hostname, or user. Consumed at dev-serve time by vite.config
|
|
# and dropped from production builds.
|
|
STIRLING_DEV_LABEL:
|
|
sh: >-
|
|
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
|
|
cmds:
|
|
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
|
|
|
dev:
|
|
desc: "Start frontend dev server"
|
|
cmds:
|
|
- task: dev:proprietary
|
|
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:core:
|
|
desc: "Start frontend dev server in core mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:proprietary:
|
|
desc: "Start frontend dev server in proprietary mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:saas:
|
|
desc: "Start frontend dev server in SaaS mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:desktop:
|
|
desc: "Start frontend dev server in desktop mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
dev:prototypes:
|
|
desc: "Start frontend dev server in prototypes mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: dev:_run
|
|
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
|
|
|
# ============================================================
|
|
# Build
|
|
# ============================================================
|
|
|
|
build:
|
|
desc: "Production build (default mode)"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor
|
|
|
|
build:core:
|
|
desc: "Build for core mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor --mode core
|
|
|
|
build:proprietary:
|
|
desc: "Build for proprietary mode"
|
|
deps: [prepare]
|
|
vars:
|
|
PREVIEW: '{{.PREVIEW | default ""}}'
|
|
cmds:
|
|
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary'
|
|
|
|
build:saas:
|
|
desc: "Build for SaaS mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
cmds:
|
|
- npx vite build editor --mode saas
|
|
|
|
build:desktop:
|
|
desc: "Build for desktop mode"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- npx vite build editor --mode desktop
|
|
|
|
build:prototypes:
|
|
desc: "Build for prototypes mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vite build editor --mode prototypes
|
|
|
|
|
|
storybook:
|
|
desc: "Start Storybook dev server"
|
|
deps: [install]
|
|
cmds:
|
|
- npx storybook dev -p 6006 {{.CLI_ARGS}}
|
|
|
|
storybook:build:
|
|
desc: "Build static Storybook"
|
|
deps: [install]
|
|
cmds:
|
|
- npx storybook build {{.CLI_ARGS}}
|
|
|
|
storybook:browser:
|
|
internal: true
|
|
desc: "Install the Chromium build the story scan runs in"
|
|
run: once
|
|
deps: [install]
|
|
cmds:
|
|
- npx playwright install chromium
|
|
|
|
storybook:test:
|
|
desc: "Scan every story in real Chromium: it must render and pass axe"
|
|
deps: [install, storybook:browser]
|
|
cmds:
|
|
# Runs each story as a browser test. Pass a filter through, e.g.
|
|
# task frontend:storybook:test -- Button
|
|
- npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}}
|
|
|
|
storybook:a11y:
|
|
desc: "a11y regression gate over every story: fail only on NEW axe violations"
|
|
deps: [install, storybook:browser]
|
|
cmds:
|
|
- bash .storybook/a11y-scan.sh
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
|
|
|
|
storybook:a11y:changed:
|
|
desc: "a11y gate over stories changed vs a base ref (default origin/main)"
|
|
summary: |
|
|
Scans only the stories this branch touches, which is what pull requests
|
|
run — a full scan takes ~30 minutes, far too long to sit in front of every
|
|
merge. The nightly job covers the rest of the suite.
|
|
|
|
Pass a base ref through CLI_ARGS, e.g.
|
|
task frontend:storybook:a11y:changed -- origin/release
|
|
deps: [install, storybook:browser]
|
|
vars:
|
|
BASE: '{{.CLI_ARGS | default "origin/main"}}'
|
|
# Stories touched by this branch, plus any not yet committed.
|
|
CHANGED:
|
|
sh: |
|
|
{ git diff --name-only --diff-filter=d {{.CLI_ARGS | default "origin/main"}}...HEAD -- '*.stories.ts' '*.stories.tsx';
|
|
git diff --name-only --diff-filter=d -- '*.stories.ts' '*.stories.tsx';
|
|
git ls-files --others --exclude-standard -- '*.stories.ts' '*.stories.tsx'; } \
|
|
| sed 's|^frontend/||' | sort -u | tr '\n' ' '
|
|
cmds:
|
|
- cmd: |
|
|
if [ -z "{{.CHANGED}}" ]; then
|
|
echo "a11y: no story files changed vs {{.BASE}} — nothing to check"
|
|
exit 0
|
|
fi
|
|
bash .storybook/a11y-scan.sh {{.CHANGED}}
|
|
node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
|
|
|
|
storybook:a11y:record:
|
|
desc: "Re-record the a11y baseline (run after intentionally fixing/adding violations)"
|
|
deps: [install, storybook:browser]
|
|
cmds:
|
|
- bash .storybook/a11y-scan.sh
|
|
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record
|
|
|
|
# ============================================================
|
|
# Code quality
|
|
# ============================================================
|
|
|
|
lint:
|
|
desc: "Run linting"
|
|
deps: [install]
|
|
cmds:
|
|
- task: lint:eslint
|
|
- task: lint:dpdm
|
|
- task: lint:colors
|
|
|
|
lint:colors:
|
|
desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components"
|
|
aliases: [lint:colours]
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/lint/theme-lint.mjs
|
|
- node editor/scripts/lint/theme-lint.mjs css-colors
|
|
- node editor/scripts/lint/theme-lint.mjs code-colors
|
|
- node editor/scripts/lint/theme-lint.mjs no-primitives
|
|
|
|
contrast:
|
|
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/lint/theme-lint.mjs contrast
|
|
|
|
lint:eslint:
|
|
desc: "Run ESLint linting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx eslint --max-warnings=0
|
|
|
|
lint:dpdm:
|
|
desc: "Run circular import linting"
|
|
deps: [install]
|
|
cmds:
|
|
# Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is
|
|
# shell-agnostic. Covers the whole editor tree, including the portal layer.
|
|
- npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
|
|
|
|
lint:fix:
|
|
desc: "Auto-fix lint issues"
|
|
deps: [install]
|
|
cmds:
|
|
- npx eslint --fix
|
|
|
|
format:
|
|
desc: "Auto-fix code formatting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx prettier --write .
|
|
|
|
format:check:
|
|
desc: "Check code formatting"
|
|
deps: [install]
|
|
cmds:
|
|
- npx prettier --check .
|
|
|
|
fix:
|
|
desc: "Auto-fix lint and format"
|
|
cmds:
|
|
- task: format
|
|
- task: lint:fix
|
|
|
|
typecheck:
|
|
desc: "Typecheck default build of the app"
|
|
cmds:
|
|
- task: typecheck:proprietary
|
|
|
|
typecheck:_run:
|
|
internal: true
|
|
cmds:
|
|
- 'npx tsc --noEmit --project {{.PROJECT}}'
|
|
|
|
typecheck:core:
|
|
desc: "Typecheck core build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/core/tsconfig.json }
|
|
|
|
typecheck:proprietary:
|
|
desc: "Typecheck proprietary build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/proprietary/tsconfig.json }
|
|
|
|
typecheck:saas:
|
|
desc: "Typecheck SaaS build variant"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: saas }
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/saas/tsconfig.json }
|
|
|
|
typecheck:desktop:
|
|
desc: "Typecheck desktop build variant"
|
|
deps:
|
|
- task: prepare
|
|
vars: { MODE: desktop }
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/desktop/tsconfig.json }
|
|
|
|
typecheck:cloud:
|
|
desc: "Typecheck cloud shared layer (standalone)"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/cloud/tsconfig.json }
|
|
|
|
typecheck:scripts:
|
|
desc: "Typecheck scripts"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/scripts/tsconfig.json }
|
|
|
|
typecheck:prototypes:
|
|
desc: "Typecheck prototypes build variant"
|
|
deps: [prepare]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/prototypes/tsconfig.json }
|
|
|
|
typecheck:portal:
|
|
desc: "Typecheck developer portal build variant"
|
|
deps: [install]
|
|
cmds:
|
|
- task: typecheck:_run
|
|
vars: { PROJECT: editor/src/portal/tsconfig.json }
|
|
|
|
typecheck:all:
|
|
desc: "Typecheck all build variants"
|
|
cmds:
|
|
- task: typecheck:core
|
|
- task: typecheck:proprietary
|
|
- task: typecheck:saas
|
|
- task: typecheck:desktop
|
|
- task: typecheck:cloud
|
|
- task: typecheck:scripts
|
|
- task: typecheck:prototypes
|
|
- task: typecheck:portal
|
|
|
|
# ============================================================
|
|
# Quality Gate
|
|
# ============================================================
|
|
|
|
check:
|
|
desc: "Quick quality gate for local development"
|
|
cmds:
|
|
- task: typecheck
|
|
- task: lint
|
|
- task: format:check
|
|
- task: test
|
|
|
|
og:check:
|
|
desc: "Fail if committed OG/social-preview metadata is out of date"
|
|
cmds:
|
|
- node editor/scripts/generate-og-metadata.mjs --check
|
|
|
|
check:all:
|
|
desc: "Full CI quality gate"
|
|
cmds:
|
|
# Runs first, before prepare regenerates: guards the committed og-metadata.json /
|
|
# ogImageMap.json that the Cloudflare Pages (plain `vite build`) deploy relies on.
|
|
- task: og:check
|
|
- task: typecheck:all
|
|
- task: lint
|
|
- task: format:check
|
|
- task: build
|
|
- task: test
|
|
- task: storybook:build
|
|
|
|
# ============================================================
|
|
# Test
|
|
# ============================================================
|
|
|
|
test:
|
|
desc: "Run tests"
|
|
cmds:
|
|
- task: test:editor
|
|
|
|
test:editor:
|
|
desc: "Run editor tests"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vitest run --root editor
|
|
|
|
test:watch:
|
|
desc: "Run tests in watch mode"
|
|
deps: [prepare]
|
|
cmds:
|
|
- npx vitest --watch --root editor
|
|
|
|
test:coverage:
|
|
desc: "Run tests with coverage (one-shot; CI-friendly)."
|
|
deps: [prepare]
|
|
cmds:
|
|
# `vitest run` makes this CI-safe (the bare `vitest` form enters watch
|
|
# mode). Explicit reporter list because v8 + json-summary is what the
|
|
# coverage-summary.py helper consumes; html/text are kept for humans.
|
|
#
|
|
# reportsDirectory is pinned to ./coverage relative to vitest's root
|
|
# (--root editor), so output lands at frontend/editor/coverage/. The
|
|
# CI upload step reads from that path. An earlier attempt with
|
|
# `./editor/coverage` double-nested into frontend/editor/editor/coverage;
|
|
# pinning future-proofs against vitest changing the default.
|
|
- >
|
|
npx vitest run --root editor --coverage
|
|
--coverage.provider=v8
|
|
--coverage.reporter=text-summary
|
|
--coverage.reporter=json-summary
|
|
--coverage.reporter=html
|
|
--coverage.reportsDirectory=./coverage
|
|
|
|
# ============================================================
|
|
# Code Generation
|
|
# ============================================================
|
|
|
|
tool-models:
|
|
desc: "Generate tool API types from the Java OpenAPI spec"
|
|
deps: [install, ":backend:swagger"]
|
|
cmds:
|
|
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts
|
|
sources:
|
|
- editor/scripts/generate-tool-api-types.mts
|
|
- ../SwaggerDoc.json
|
|
generates:
|
|
- editor/src/core/types/toolApiTypes.ts
|
|
|
|
tool-models:check:
|
|
desc: "Fail if committed tool API types are out of date"
|
|
deps: [install, ":backend:swagger"]
|
|
cmds:
|
|
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --check
|
|
|
|
licenses:generate:
|
|
desc: "Generate frontend license report"
|
|
deps: [install]
|
|
cmds:
|
|
- node editor/scripts/generate-licenses.js
|
|
|
|
# ============================================================
|
|
# Clean
|
|
# ============================================================
|
|
|
|
clean:
|
|
desc: "Clean build artifacts and caches"
|
|
cmds:
|
|
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist
|
|
platforms: [windows]
|
|
- cmd: rm -rf node_modules/.vite editor/dist dist
|
|
platforms: [linux, darwin]
|