docs(SDK-1129): keep alpha/gated events out of stable components' @ev… #12572
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| # Exclude the merge queue's ephemeral branches: the merge_group event | |
| # below already runs CI on the exact commit being merged, so letting the | |
| # branch push trigger a second run doubles load and stamps a duplicate | |
| # `test` status on the merge commit — a flake in that redundant run evicts | |
| # the PR from the queue even when the merge_group run passed. | |
| branches-ignore: | |
| - 'gh-readonly-queue/**' | |
| merge_group: | |
| workflow_dispatch: | |
| # Limit the permissions of the GITHUB_TOKEN | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Setup job: Install dependencies and cache them for parallel jobs | |
| setup: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| cache-key: ${{ steps.cache-key.outputs.key }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Generate cache key | |
| id: cache-key | |
| run: echo "key=node-modules-${{ hashFiles('package-lock.json') }}" >> $GITHUB_OUTPUT | |
| - name: Cache node_modules | |
| id: cache-node-modules | |
| uses: actions/cache@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ steps.cache-key.outputs.key }} | |
| - name: Install dependencies | |
| if: steps.cache-node-modules.outputs.cache-hit != 'true' | |
| run: npm ci | |
| # derive-models: Generate the API models barrel (src/models/external.ts) exactly once. | |
| # Both `build` (→ dist → API report) and `derive-check-src` (→ TypeDoc) consume this single | |
| # artifact, so every derived output in a run reflects the same barrel and derive-commit | |
| # pushes a self-consistent set. On main/merge_group/dependabot a stale barrel fails here. | |
| derive-models: | |
| needs: [setup] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| stale: ${{ steps.check.outputs.stale }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Generate API models barrel | |
| run: npm run models:derive | |
| - name: Check for stale barrel | |
| id: check | |
| run: | | |
| git add src/models/external.ts | |
| if git diff --staged --quiet; then | |
| echo "stale=false" >> $GITHUB_OUTPUT | |
| else | |
| if [[ "$GITHUB_REF" == "refs/heads/main" ]] || [[ "$GITHUB_ACTOR" == "dependabot[bot]" ]] || [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then | |
| echo "::error::src/models/external.ts is stale — run 'npm run derive' locally and commit the result." | |
| exit 1 | |
| fi | |
| echo "stale=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload derived models barrel | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: derived-models | |
| path: src/models/external.ts | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # derive-check-i18n: Generate the i18n types declaration (src/i18n/types.d.ts) exactly once. | |
| # `derive-check-src` (→ TypeDoc, which reads this file for the Resources reference) consumes | |
| # this single artifact so the docs reflect the same translations as the checked-in declaration | |
| # and derive-commit pushes a self-consistent set. On main/merge_group/dependabot a stale | |
| # declaration fails here. | |
| derive-check-i18n: | |
| needs: [setup] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| stale: ${{ steps.check.outputs.stale }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Generate i18n types | |
| run: npm run i18n:generate | |
| - name: Check for stale i18n types | |
| id: check | |
| run: | | |
| git add src/i18n/types.d.ts | |
| if git diff --staged --quiet; then | |
| echo "stale=false" >> $GITHUB_OUTPUT | |
| else | |
| if [[ "$GITHUB_REF" == "refs/heads/main" ]] || [[ "$GITHUB_ACTOR" == "dependabot[bot]" ]] || [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then | |
| echo "::error::src/i18n/types.d.ts is stale — run 'npm run derive' locally and commit the result." | |
| exit 1 | |
| fi | |
| echo "stale=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload derived i18n types | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: derived-i18n | |
| path: src/i18n/types.d.ts | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # Build job: Build the project and cache artifacts | |
| build: | |
| needs: [setup, derive-models] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Cache build artifacts | |
| id: cache-build | |
| uses: actions/cache@v5 | |
| with: | |
| path: dist | |
| key: build-${{ github.sha }} | |
| # Compile dist from the single canonical barrel so the API report (derived from | |
| # dist) reflects the same external.ts as the docs. | |
| - name: Download API models barrel | |
| if: steps.cache-build.outputs.cache-hit != 'true' | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-models | |
| path: src/models | |
| - name: Generate i18n translations | |
| if: steps.cache-build.outputs.cache-hit != 'true' | |
| run: npm run i18n:generate | |
| - name: Build | |
| if: steps.cache-build.outputs.cache-hit != 'true' | |
| run: npx vite build | |
| # Lint job: Run ESLint (parallel with other checks) | |
| lint: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Lint | |
| run: npm run lint:check | |
| # Format job: Run Prettier check (parallel with other checks) | |
| format: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Format check | |
| run: npm run format:check | |
| # TypeScript job: Run type checking (parallel with other checks) | |
| typecheck: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: TypeScript check | |
| run: npm run tsc | |
| # derive-check-src: Run endpoint inventory and TypeDoc checks on a standard runner. | |
| # Neither reads from dist/, so this job runs in parallel with `build`. | |
| # On regular branches: outputs stale=true if files changed (derive-commit handles the push). | |
| # On main, merge_group, and dependabot: fails immediately if files are stale. | |
| derive-check-src: | |
| needs: [setup, derive-models, derive-check-i18n] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| stale: ${{ steps.check.outputs.stale }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Install docs-site dependencies | |
| run: npm ci | |
| working-directory: docs-site | |
| # TypeDoc reads src/models/external.ts; use the single canonical barrel from | |
| # derive-models so the docs reflect the same external.ts as the API report. | |
| - name: Download API models barrel | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-models | |
| path: src/models | |
| # TypeDoc reads src/i18n/types.d.ts for the Resources reference; use the freshly | |
| # generated declaration from derive-check-i18n so the docs reflect the same | |
| # translations that job verified. | |
| - name: Download i18n types | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-i18n | |
| path: src/i18n | |
| # endpoints:derive must finish first: the TypeDoc theme reads the generated | |
| # endpoint-inventory.json to render the Endpoints section on each page, so | |
| # docs:api:generate depends on it and the two can no longer run in parallel. | |
| - name: Derive endpoint inventory, then TypeDoc | |
| run: | | |
| npm run endpoints:derive | |
| npm run docs:api:generate | |
| - name: Check for stale derived files | |
| id: check | |
| run: | | |
| git add docs/guides | |
| git add docs/reference | |
| if git diff --staged --quiet; then | |
| echo "stale=false" >> $GITHUB_OUTPUT | |
| else | |
| if [[ "$GITHUB_REF" == "refs/heads/main" ]] || [[ "$GITHUB_ACTOR" == "dependabot[bot]" ]] || [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then | |
| echo "::error::Derived files are stale — run 'npm run derive' locally and commit the result." | |
| exit 1 | |
| fi | |
| echo "stale=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload derived src files | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: derived-src | |
| path: | | |
| docs/guides | |
| docs/reference | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # derive-check-dist: Run API report check on a standard runner. | |
| # api-extractor reads dist/index.d.ts, so this job runs after `build`. | |
| # On regular branches: outputs stale=true if the report changed (derive-commit handles the push). | |
| # On main, merge_group, and dependabot: fails immediately if the report is stale. | |
| derive-check-dist: | |
| needs: [setup, build] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| stale: ${{ steps.check.outputs.stale }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Restore build artifacts | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: dist | |
| key: build-${{ github.sha }} | |
| - name: Derive API report | |
| run: npm run api-report:derive | |
| - name: Check for stale API report | |
| id: check | |
| run: | | |
| git add .reports | |
| if git diff --staged --quiet; then | |
| echo "stale=false" >> $GITHUB_OUTPUT | |
| else | |
| if [[ "$GITHUB_REF" == "refs/heads/main" ]] || [[ "$GITHUB_ACTOR" == "dependabot[bot]" ]] || [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then | |
| echo "::error::API report is stale — run 'npm run derive' locally and commit the result." | |
| exit 1 | |
| fi | |
| echo "stale=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload derived dist files | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: derived-dist | |
| path: .reports | |
| include-hidden-files: true | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # derive-commit: Re-derive and commit any stale files back to the branch. | |
| # Only runs when a check job detected staleness — the expensive self-hosted runner | |
| # (needed for the org IP allow list to push back to the repo) is never used otherwise. | |
| # Main, merge_group, and dependabot are gated out at the check layer (they fail on | |
| # staleness rather than setting stale=true), so derive-commit never runs for them. | |
| derive-commit: | |
| needs: [derive-models, derive-check-i18n, derive-check-src, derive-check-dist] | |
| if: needs.derive-models.outputs.stale == 'true' || needs.derive-check-i18n.outputs.stale == 'true' || needs.derive-check-src.outputs.stale == 'true' || needs.derive-check-dist.outputs.stale == 'true' | |
| # Must use the self-hosted runner group — GitHub-hosted runners are blocked | |
| # by the org IP allow list and cannot push back to the repository. | |
| runs-on: | |
| group: gusto-ubuntu-default | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Mint GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_SECRET }} | |
| - uses: actions/checkout@v6 | |
| with: | |
| token: ${{ steps.app-token.outputs.token }} | |
| # Clear the derived directories before applying the artifacts. The | |
| # checkout restores every committed file, including any that the | |
| # generators no longer produce (e.g. a page whose path changed). The | |
| # artifacts only contain freshly-generated files and download-artifact | |
| # overlays rather than replaces, so without this `git add` below would | |
| # never see the orphans as deleted and they'd linger forever. Wiping | |
| # first means the post-download tree is exactly the generator output, so | |
| # `git add` stages the deletions (git rm) alongside the additions. | |
| - name: Clear derived directories before applying artifacts | |
| run: rm -rf docs/guides docs/reference | |
| - name: Download derived src files | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-src | |
| path: docs | |
| - name: Download derived dist files | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-dist | |
| path: .reports | |
| - name: Download derived models barrel | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-models | |
| path: src/models | |
| - name: Download derived i18n types | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: derived-i18n | |
| path: src/i18n | |
| - name: Commit and push derived files | |
| run: | | |
| git add docs/guides | |
| git add docs/reference | |
| git add .reports | |
| git add src/models/external.ts | |
| git add src/i18n/types.d.ts | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| if ! git diff --staged --quiet; then | |
| git commit -m "chore: update derived files" | |
| git push | |
| fi | |
| # derive: Single required check that branch protection can gate on. | |
| # Aggregates derive-models, derive-check-src and derive-check-dist. derive-commit is | |
| # excluded intentionally — it only runs when files are stale and is not a health signal. | |
| # Fails when any check job reported stale=true so the gate stays red until | |
| # derive-commit pushes the fix and CI passes on the new commit. | |
| derive: | |
| needs: [derive-models, derive-check-i18n, derive-check-src, derive-check-dist] | |
| if: ${{ !cancelled() }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify derive checks passed | |
| env: | |
| MODELS: ${{ needs.derive-models.result }} | |
| I18N: ${{ needs.derive-check-i18n.result }} | |
| SRC: ${{ needs.derive-check-src.result }} | |
| DIST: ${{ needs.derive-check-dist.result }} | |
| MODELS_STALE: ${{ needs.derive-models.outputs.stale }} | |
| I18N_STALE: ${{ needs.derive-check-i18n.outputs.stale }} | |
| SRC_STALE: ${{ needs.derive-check-src.outputs.stale }} | |
| DIST_STALE: ${{ needs.derive-check-dist.outputs.stale }} | |
| run: | | |
| if [[ "$MODELS" != "success" && "$MODELS" != "skipped" ]]; then exit 1; fi | |
| if [[ "$I18N" != "success" && "$I18N" != "skipped" ]]; then exit 1; fi | |
| if [[ "$SRC" != "success" && "$SRC" != "skipped" ]]; then exit 1; fi | |
| if [[ "$DIST" != "success" && "$DIST" != "skipped" ]]; then exit 1; fi | |
| if [[ "$MODELS_STALE" == "true" || "$I18N_STALE" == "true" || "$SRC_STALE" == "true" || "$DIST_STALE" == "true" ]]; then | |
| echo "::error::Derived files were stale — an auto-fix commit may be in progress. Re-run CI after it lands, or run 'npm run derive' locally." | |
| exit 1 | |
| fi | |
| # Storybook build job: validates all stories compile and index successfully | |
| storybook-build: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Build Storybook | |
| run: npm run build-storybook | |
| # Docs build job: validates the Docusaurus site compiles, catching broken | |
| # links/anchors (onBrokenLinks: 'throw') at PR time rather than at release | |
| # time. Uses its own node_modules under docs-site/ — not the root cache. | |
| docs-build: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Validate docs front matter | |
| run: npm run docs:lint | |
| - name: Lint markdown | |
| run: npm run docs:lint:markdown | |
| - name: Spell check docs | |
| run: npm run docs:lint:spell | |
| - name: Install docs dependencies | |
| working-directory: docs-site | |
| run: npm ci | |
| - name: Build docs | |
| run: npm run docs:build | |
| # SDK app build job: validates the dev app's vite config and entry compile | |
| sdk-app-build: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Build SDK app | |
| run: npm run sdk-app:build | |
| # Test job: Run tests with coverage (parallel with other checks) | |
| test: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Test with coverage | |
| run: npm run test:ci | |
| # Scenarios job: Validate scenario JSON against schema and run scenario unit tests. | |
| # E2E jobs depend on this so a broken scenario fails fast (cheap) before Playwright runs (expensive). | |
| scenarios: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| - name: Validate scenario JSON | |
| run: npm run scenarios:validate | |
| - name: Run scenario unit tests | |
| run: npm run test:scenarios | |
| # E2E discover: Decide which domain shards should run for this event. | |
| # | |
| # Domains are discovered dynamically from `e2e/tests/<domain>/`. The | |
| # naming contract is `e2e/tests/<kebab>` ↔ `src/components/<Pascal>` | |
| # (e.g. time-off ↔ TimeOff). Adding or removing a domain folder is the | |
| # only thing required to add or remove a shard. | |
| # | |
| # The selection is a two-stage filter: | |
| # | |
| # 1. Drop files whose extension can't affect runtime or test | |
| # behavior (.md, LICENSE, .gitignore, images, etc.). A docs-only | |
| # PR produces an empty filtered diff and skips e2e entirely. | |
| # 2. Of the surviving files, if any sit OUTSIDE every known domain's | |
| # surface area (`src/components/<Pascal>/` or `e2e/tests/<kebab>/`), | |
| # treat the change as cross-cutting and run every shard. | |
| # Otherwise run only the domains whose surface was actually touched. | |
| # | |
| # Stage 2 is safe by default: new code in unfamiliar locations (e.g. a | |
| # fresh top-level directory like `src/middleware/`) fans out to all | |
| # shards rather than silently producing an empty matrix. | |
| # | |
| # On pushes to main and manual dispatches we run every domain | |
| # unconditionally — main is the canonical "all green" signal regardless | |
| # of which paths landed in the merge commit. | |
| # | |
| # Output is a JSON array of domain folder names that downstream | |
| # `e2e-setup` and `e2e` consume via fromJson() for matrix expansion. An | |
| # empty array is a valid result and short-circuits both downstream jobs. | |
| e2e-discover: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| outputs: | |
| domains: ${{ steps.resolve.outputs.domains }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| # Need merge-base history to diff PR branches against base. Default | |
| # depth=1 has no shared history with origin/<base>, which would | |
| # make `git diff base...HEAD` empty and silently skip every shard. | |
| fetch-depth: 0 | |
| - id: resolve | |
| name: Resolve domains JSON | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF: ${{ github.ref }} | |
| BASE_REF: ${{ github.base_ref }} | |
| run: bash .github/scripts/e2e-discover-domains.sh | |
| # E2E setup: Provision demo companies once per CI run and share state across | |
| # shards. E2E_DISABLE_CACHE ensures fresh provisioning for each CI run | |
| # (ignores stale .e2e-state.json from previous runs), but the artifact sharing | |
| # across shards within this run avoids duplicate provisioning load on the | |
| # demo backend. The set of domains to run is computed by `e2e-discover` | |
| # and passed through unchanged so downstream `e2e` consumers have a | |
| # single source of truth. Skipped entirely when `e2e-discover` produced | |
| # an empty array (a PR with no e2e-affecting changes). | |
| e2e-setup: | |
| needs: [setup, scenarios, e2e-discover] | |
| if: needs.e2e-discover.outputs.domains != '[]' | |
| runs-on: ubuntu-latest | |
| # Wallclock ceiling for provisioning all demo companies (primary + | |
| # dismissal + every shared scenario in e2e/scenarios/shared/). Each | |
| # scenario gets up to 180s of patient polling for the demo backend's | |
| # background seeding to complete. With 2 shared scenarios + 2 legacy | |
| # companies, worst case is ~12 min; 15 leaves a small margin. If this | |
| # is consistently hitting the cap, the demo factory is degraded — | |
| # surface that here rather than smearing the failure across every | |
| # downstream shard. | |
| timeout-minutes: 15 | |
| outputs: | |
| domains: ${{ needs.e2e-discover.outputs.domains }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| # Pin the Playwright browser cache key to the actual playwright version | |
| # (not the lockfile hash) so unrelated dependency churn doesn't bust the | |
| # ~250MB chromium download. Cache only invalidates on real Playwright | |
| # upgrades. | |
| - name: Resolve Playwright version | |
| id: playwright-version | |
| run: | | |
| version=$(node -p "require('./node_modules/playwright/package.json').version") | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} | |
| restore-keys: | | |
| playwright-${{ runner.os }}- | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: npx playwright install --with-deps chromium | |
| - name: Install Playwright system dependencies only | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: npx playwright install-deps chromium | |
| - name: Provision demo companies | |
| env: | |
| E2E_USE_REAL_BACKEND: 'true' | |
| E2E_DISABLE_CACHE: 'true' | |
| run: npm run e2e:setup | |
| - name: Upload e2e state | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: e2e-state | |
| # Includes both the legacy primary/dismissal state (used by | |
| # dismissal.spec.ts) and the shared scenarios artifact (used by | |
| # all scenario-annotated tests via the localTestFixture). | |
| # Provision-once / share-many model: every scenario is created and | |
| # validated here, every shard just downloads and reads. | |
| path: | | |
| e2e/.e2e-state.json | |
| e2e/.e2e-scenarios.json | |
| include-hidden-files: true | |
| retention-days: 1 | |
| if-no-files-found: error | |
| # E2E job: Run Playwright e2e tests against the live demo environment. | |
| # This is the sole Playwright gate. A previous MSW-mode e2e job was removed | |
| # because its mock fixtures could not keep pace with real demo backend | |
| # behavior, and the duplicate run added CI minutes for shallow coverage | |
| # already provided by Storybook + unit tests. This job exercises the | |
| # actual API contract, scenario provisioning, and state machine terminal | |
| # states. | |
| # | |
| # Sharded by domain so each domain runs in parallel and one domain's | |
| # failure doesn't block feedback on the others. The matrix is built | |
| # dynamically by the `e2e-setup` job from the subfolders under | |
| # `e2e/tests/`, so adding a new domain folder is sufficient to add a | |
| # shard. The `domain` filter is a Playwright path-substring match against | |
| # specs under `e2e/tests/<domain>/**/*.spec.ts`. | |
| e2e: | |
| needs: [setup, scenarios, e2e-setup] | |
| # Skip cleanly when no domain folders are selected — fromJson on an empty | |
| # array would otherwise fail matrix expansion. The selection itself | |
| # (including the "always run all on main" rule) is computed in | |
| # `e2e-discover`; we just honor whatever it produced. | |
| if: needs.e2e-setup.outputs.domains != '[]' | |
| runs-on: ubuntu-latest | |
| # Wallclock ceiling per shard. Scenario provisioning is now done in | |
| # e2e-setup, so shards just download the artifact and run tests against | |
| # pre-provisioned companies — no demo creation latency during the test | |
| # run. 20m is comfortable for the largest expected shard (the canaries | |
| # shard, which runs all 17 canary specs serially across 5 domains on | |
| # one shared company) with margin for slow demo backend. Tight enough | |
| # that a stuck run releases CI minutes quickly. | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| # Concurrent shard ceiling against flows.gusto-demo.com. We only have 3 | |
| # domains today (employee/time-off/payroll), so 4 effectively means "no | |
| # throttle — let every shard run in parallel". The matrix is generated | |
| # dynamically from e2e/tests/<domain>/, so this gives headroom for one | |
| # more domain without needing another bump. Each shard still runs with | |
| # workers: 1 in playwright.demo.config.ts, so per-shard backend load is | |
| # unchanged. | |
| max-parallel: 4 | |
| matrix: | |
| domain: ${{ fromJson(needs.e2e-setup.outputs.domains) }} | |
| name: e2e (${{ matrix.domain }}) | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Restore node_modules cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: node_modules | |
| key: ${{ needs.setup.outputs.cache-key }} | |
| # Pin the Playwright browser cache key to the actual playwright version | |
| # (not the lockfile hash) so unrelated dependency churn doesn't bust the | |
| # ~250MB chromium download. Cache only invalidates on real Playwright | |
| # upgrades. | |
| - name: Resolve Playwright version | |
| id: playwright-version | |
| run: | | |
| version=$(node -p "require('./node_modules/playwright/package.json').version") | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} | |
| restore-keys: | | |
| playwright-${{ runner.os }}- | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: npx playwright install --with-deps chromium | |
| - name: Install Playwright system dependencies only | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: npx playwright install-deps chromium | |
| - name: Download e2e state | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: e2e-state | |
| path: e2e/ | |
| - name: Run e2e tests | |
| env: | |
| E2E_USE_REAL_BACKEND: 'true' | |
| # Path-bounded grep so each shard runs only the specs directly under | |
| # its own top-level directory. Bare `${{ matrix.domain }}` would | |
| # substring-match any path containing that word — so the `company` | |
| # shard would also pick up `canaries/company/`. Playwright matches | |
| # the positional regex against absolute file paths; `tests/<domain>/` | |
| # is the smallest disambiguating substring that anchors to the | |
| # top-level directory without leaking into nested duplicates. | |
| run: npm run test:e2e:demo -- --pass-with-no-tests "tests/${{ matrix.domain }}/" | |
| - name: Upload test results | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: playwright-report-${{ matrix.domain }} | |
| path: playwright-report/ | |
| retention-days: 7 | |
| - name: Upload scenario reports | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: e2e-scenario-report-${{ matrix.domain }} | |
| path: e2e/reports/ | |
| retention-days: 7 | |
| # E2E success: Single required check that branch protection can gate on. | |
| # | |
| # Aggregates the result of every job that collectively constitutes "e2e | |
| # passed" — scenarios validation, the provisioning job, and the matrix | |
| # itself (whose `result` is the rolled-up status across every shard). | |
| # Skipped counts as success so a PR that only touches paths outside any | |
| # domain (and therefore produced an empty matrix) still satisfies the | |
| # gate. Failure or cancellation of any contributing job fails this check. | |
| # | |
| # `if: !cancelled()` is required: without it, a failed upstream short- | |
| # circuits this job to `skipped`, which branch protection treats as | |
| # passing — exactly the bug we're trying to avoid. | |
| e2e-success: | |
| needs: [scenarios, e2e-discover, e2e-setup, e2e] | |
| if: ${{ !cancelled() }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Verify required e2e jobs succeeded | |
| env: | |
| SCENARIOS: ${{ needs.scenarios.result }} | |
| E2E_DISCOVER: ${{ needs.e2e-discover.result }} | |
| E2E_SETUP: ${{ needs.e2e-setup.result }} | |
| E2E: ${{ needs.e2e.result }} | |
| run: bash .github/scripts/e2e-verify-success.sh |