ci(scorecard): resolve visibility through the API and pin v2.4.4 (#45) #106
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 | |
| # Jobs run in a strict sequential chain so no two test suites ever share a runner | |
| # (the linked library is reloaded into every worker, so concurrent suites multiply | |
| # memory): `ci` (static gates + unit + build) gates `e2e` (the in-process HTTP, SSE | |
| # and WebSocket suites, backed by a Redis service), which gates `playwright` (the | |
| # browser journeys against a live api + web). The multi-instance `e2e-cluster` suite | |
| # is the heaviest and least proven on hosted runners, so it is gated to a manual | |
| # `workflow_dispatch` run rather than every push until runners prove it stable. | |
| # | |
| # The four job ids (`ci`, `e2e`, `playwright`, `e2e-cluster`) and their `name:` | |
| # fields are CONTRACTUAL: they are the branch-protection required-status-check names | |
| # for this repository (see docs/NPM_SWITCH.md). Do not rename or remove them; add | |
| # steps within `ci` rather than splitting new jobs out of it. | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| ci: | |
| name: install, typecheck, lint, test, build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| cache: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 24 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Typecheck | |
| run: pnpm typecheck | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Format check | |
| run: pnpm format:check | |
| # Unit suites run sequentially, one app at a time, per the workspace's | |
| # memory-safety rule: two Jest/Vitest processes must never share a runner. | |
| # Each config already pins maxWorkers to 50%; the heap cap is a further | |
| # guard because the linked library is reloaded into every worker. | |
| - name: Unit tests (api) | |
| run: pnpm --filter @nest-realtime-example/api test | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| - name: Unit tests (web) | |
| run: pnpm --filter @nest-realtime-example/web test | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| - name: Build | |
| run: pnpm build | |
| # Asserts every public export of the linked library is referenced somewhere in | |
| # the example (or carries a spec-sanctioned justified exception), so the | |
| # coverage matrix cannot silently drift from the library surface. | |
| - name: Export-usage audit | |
| run: pnpm audit:exports | |
| # Empirically proves the SSE-only build never ships socket.io-client in a | |
| # statically-required chunk, and that it exists only behind the chat/both | |
| # labs' dynamic import. | |
| - name: Bundle honesty check (web) | |
| run: pnpm --filter @nest-realtime-example/web run assert:bundle | |
| e2e: | |
| name: e2e (http, sse, ws) | |
| needs: ci | |
| runs-on: ubuntu-latest | |
| # The single-instance e2e suites boot the app in-process and back the offline | |
| # queue, tickets, presence and revocation with a real Redis. | |
| services: | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| cache: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 24 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # Runs the route-inventory, HTTP-route, SSE-flow and WebSocket-flow suites as | |
| # one Jest project on a single worker; the config already pins maxWorkers to 1. | |
| - name: E2E (HTTP, SSE, WebSocket) | |
| run: pnpm --filter @nest-realtime-example/api run test:e2e | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| REDIS_URL: redis://localhost:6379 | |
| playwright: | |
| name: playwright journeys | |
| needs: e2e | |
| runs-on: ubuntu-latest | |
| # The journeys drive the live api (tickets and presence need Redis) plus the | |
| # web app; Playwright's webServer boots both and waits for their health. | |
| services: | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| cache: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 24 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Install Playwright Chromium | |
| run: pnpm --filter @nest-realtime-example/web exec playwright install --with-deps chromium | |
| - name: Playwright journeys | |
| run: pnpm --filter @nest-realtime-example/web run test:e2e | |
| env: | |
| REDIS_URL: redis://localhost:6379 | |
| e2e-cluster: | |
| name: e2e cluster (manual) | |
| needs: ci | |
| # The multi-instance suite drives a docker-compose stack (two api instances + | |
| # nginx + Redis) and is the heaviest, least proven suite on hosted runners, so | |
| # it runs only on a deliberate manual dispatch, never on every push. | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| cache: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 24 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| # Each script brings its compose stack up, runs the suite alone, and always | |
| # tears it down; the SSE and WebSocket cluster runs never overlap. | |
| - name: Cluster e2e (SSE) | |
| run: pnpm run test:e2e:cluster | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| - name: Cluster e2e (WebSocket) | |
| run: pnpm run test:e2e:cluster:ws | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| # Single check the org ruleset can require, in every repo, under one name. | |
| # | |
| # A required status check is matched by literal name, and this repo's jobs are | |
| # named nothing like the next repo's — so there is no list of contexts that | |
| # works org-wide. One aggregate per repo, always spelled `CI passed`, is what | |
| # makes a single rule at the organisation level possible, and what makes every | |
| # repository created from now on inherit the gate without being configured. | |
| # | |
| # `always()` is what makes this a gate rather than decoration: without it the | |
| # job is skipped the moment a dependency fails, and a skipped check reports | |
| # neutral — the pull request would look unblocked precisely when it is broken. | |
| # `cancelled` is named explicitly for the same reason; a cancelled job is not | |
| # a passing one, and a queue timeout produces exactly that. | |
| # | |
| # A skipped dependency is accepted on purpose: jobs here are conditional on the | |
| # event, and failing on skip would break every push that legitimately runs a | |
| # subset. | |
| ci-pass: | |
| name: CI passed | |
| if: always() | |
| needs: [ci, e2e, playwright, e2e-cluster] | |
| runs-on: ubuntu-latest | |
| # Once this is the required check, a hung runner holds every merge behind it | |
| # for GitHub's default of six hours. The job is one comparison, so a short | |
| # ceiling costs nothing and bounds that. | |
| timeout-minutes: 5 | |
| steps: | |
| # Printed on success as well as failure. Without it the check reports a bare | |
| # red X and the reader has to open each dependency to find which one broke. | |
| - name: Report the result of every dependency | |
| env: | |
| NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| { | |
| echo "| Job | Result |" | |
| echo "| --- | --- |" | |
| echo "$NEEDS" | jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"' | sort | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| # Named for what the condition does: `skipped` is deliberately not a failure | |
| # here, so "did not succeed" would describe a stricter gate than this is. | |
| - name: Fail when any dependency failed or was cancelled | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| run: exit 1 |