Skip to content

ci(scorecard): resolve visibility through the API and pin v2.4.4 (#45) #106

ci(scorecard): resolve visibility through the API and pin v2.4.4 (#45)

ci(scorecard): resolve visibility through the API and pin v2.4.4 (#45) #106

Workflow file for this run

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