Today Platform Web — Dev Docs
Workflow

CI affected scope

How `scripts/affected.ts` computes which packages a PR actually touches and how that scopes typecheck / test / lint / visual-regression on CI.

CI scales by running only what changed. The scoping happens in scripts/affected.ts, called from the Detect Changes job in ci.yml.

What affected.ts does

It reads:

  1. The list of changed file paths (from gh api .../pulls/<n>/files, or from a SHA comparison for push / merge_group events)
  2. pnpm-workspace.yaml to discover every workspace package dynamically
  3. Each package's package.json to map files → internal @todayai-labs/* deps

It outputs a JSON payload:

interface AffectedResult {
  fullRun: boolean
  changedPackages: string[]
  affectedPackages: string[]
  affectedApps: Record<string, boolean> // { web: true, admin: false, ... }
  pnpmFilter: string // "--filter @todayai-labs/web... --filter @todayai-labs/admin"
  lintPaths: string // "apps/web/ design-system/"
  runVisual: boolean
}

pnpmFilter becomes the CI input for pnpm <script> invocations. lintPaths constrains oxlint to changed directories. runVisual gates the Visual Regression job.

Dynamic discovery — no hardcoded lists

The script does not maintain a hand-written list of packages. It reads pnpm-workspace.yaml's packages: globs and expands them by stat-ing each candidate directory's package.json. A new package under apps/, packages/, or design-system/ is picked up automatically on the next CI run — no changes to affected.ts required.

This is the reason PR-CA1 didn't need to register apps/dev-docs anywhere in CI config (Codex's review caught a draft that proposed adding a hardcoded path map; the actual codebase doesn't need one).

What triggers a fullRun

fullRun: true means "treat every package as affected" — typecheck, test, lint, and visual regression all run across the whole workspace. Triggers:

TriggerWhy
A changed file doesn't belong to any packageRoot config (tsconfig.json, oxfmt.config.ts, .moon/, .github/) potentially affects all builds
Lockfile-only changes (pnpm-lock.yaml)Dependency updates can affect any consumer
Empty stdin (workflow_dispatch / fresh branch)No diff context; safest default is everything

For a typical PR that touches apps/web/src/... only, fullRun is false and CI shaves off all the design-system, admin, onboarding work.

Reading affectedPackages vs changedPackages

FieldMeans
changedPackagesPackages with at least one changed file
affectedPackagesChanged packages + every downstream package that depends on them

affectedPackages ⊇ changedPackages. A change in design-system/ui is in changedPackages: ['@todayai-labs/tdx-ui'], and affectedPackages adds @todayai-labs/web, @todayai-labs/admin, @todayai-labs/opal (everything that imports tdx-ui).

Visual regression gating

runVisual is true if apps/web is in affectedApps. Visual regression is heavy (Playwright + Storybook build + snapshot diff) and only apps/web has the visual specs; skipping it on design-system-only or admin-only changes is significant CI time saved.

Triggers and their event shapes

GitHub eventDiff source
pull_requestgh api repos/<repo>/pulls/<n>/files — listed files
merge_grouprepos/<repo>/compare/<base_sha>...<head_sha> from the speculative branch
push (non-first)compare/<event.before>...<sha>
push (first push)empty diff → fullRun
workflow_dispatchempty diff → fullRun

The merge-queue case uses the speculative branch's diff so affected scope only covers PRs in this batch, not the whole queue.

CI fan-out

Downstream of Detect Changes, the CI workflow uses the outputs as inputs:

# excerpt — ci.yml downstream jobs
jobs:
  typecheck:
    needs: changes
    if: ${{ needs.changes.outputs['pnpm-filter'] != '' }}
    runs-on: ubuntu-latest
    steps:
      - run: pnpm ${{ needs.changes.outputs['pnpm-filter'] }} typecheck

So pnpm --filter @todayai-labs/web... --filter @todayai-labs/admin typecheck runs on a PR that touches both, scoped to their dependency closures via the ... specifier.

Common confusions

  • "My change typechecks locally but the PR is BLOCKED on Typecheck." You probably ran pnpm --filter @todayai-labs/web typecheck (scoped) but the change also affects @todayai-labs/admin or a sibling. Run pnpm typecheck (unscoped) — that's what CI does.
  • "Why did CI re-run on the merge queue?" Speculative branch — see Merge queue for the double-run explanation.
  • "My docs-only change ran full visual regression." Did you touch a root config (.gitignore, package.json, etc.) by accident? Those trigger fullRun.

Local equivalent

To preview what CI will run before pushing:

echo "apps/web/src/lib/foo.ts" | node --no-warnings \
  --experimental-strip-types scripts/affected.ts

Outputs the same JSON CI consumes. Useful when investigating why a PR is running more (or less) than expected.

On this page