Skip to content

ci: split the gate into a lint job and an OS matrix - #163

Merged
awdr74100 merged 1 commit into
mainfrom
ci/lint-job-and-os-matrix
Aug 21, 2026
Merged

ci: split the gate into a lint job and an OS matrix#163
awdr74100 merged 1 commit into
mainfrom
ci/lint-job-and-os-matrix

Conversation

@awdr74100

Copy link
Copy Markdown
Owner

Description

The Windows job added in #159 was a hand-copied duplicate of check, named after its runner rather than its purpose, and the two jobs disagreed about what they were for: one ran the whole gate plus build and test, the other a silent subset guarded by a ten-line comment arguing why.

This splits CI by purpose instead of by platform:

job runs-on runs
lint ubuntu-latest typecheck, lint, format:check, knip
test ${{ matrix.os }}ubuntu-latest, windows-latest build, test

Once the platform-independent gates live in their own job, "Windows deliberately runs less" stops being a special case that needs explaining — both matrix legs run exactly the same steps.

Why this shape

I sampled the workflow files of 40 well-known Node/TS repositories (vite, vitest, rollup, rolldown, oxc, biome, prettier, eslint, pnpm, npm/cli, yarn, vue, svelte, kit, astro, nuxt, next, react-router, storybook, playwright, TypeScript, undici, nitro, changesets, turborepo, jest, webpack, zod, fastify, axios, execa, mcp typescript-sdk, gh cli, deno, nx, tailwindcss, ni, typescript-eslint, express, lodash):

  • 31 test on Windows. 25 express it as a matrix over os with Linux as the base; 8 keep a dedicated Windows job (vuejs/core, oxc, rolldown, deno, jest); 5 pass the OS into a reusable workflow.
  • Job ids: test appears in 20 repos, lint in 17 — the two most common by a wide margin. lint is the conventional umbrella name for the static gates even when the job also typechecks and formats (vite and vitest both do exactly that).
  • Repeating the checkout/pnpm/setup-node preamble per job is the majority idiom — only 5 of 17 comparable repos factor it into a composite action — so it is left duplicated here rather than abstracted.

Also fixed

  • Removed the claim that a Windows runner bills at 2x. This repository is public, and standard GitHub-hosted runners are free for public repositories; the 2x/10x multipliers only draw down private-repo minutes. The real cost of the Windows leg is wall clock: measured over the last 15 runs it takes ~102s (85-111s) against ~70s (56-89s) for Linux, so it sets the critical path.
  • Added timeout-minutes (10 / 15). Neither job had one. This suite spawns processes and races for a port, so a hung run would have burned the 360-minute default.
  • fail-fast: false, so one platform failing does not cancel the other — which of the two broke is the finding.

Considered and rejected

  • ubuntu-24.04-arm — technically viable (all 18 native binding packages in the lockfile ship linux-arm64-gnu) and free on public repos, but it saves nothing: total CI wall clock is bounded by the Windows leg, not the Linux one. It would also mean pinning a version manually, since there is no ubuntu-latest-arm alias.
  • ubuntu-slim — 1 vCPU / 5 GB, container rather than VM, unprivileged, 15-minute job cap, and documented as "not suitable for typical heavyweight CI/CD builds". The repos that use it (vite, vitest, typescript-eslint, TypeScript) reserve it for changed-file gates, spell checks and status aggregation. The three small workflows here already finish in 4-15s, and actionlint could not move there anyway — it runs a docker:// container action, which unprivileged containers do not support.
  • macos-latest — the maintainer develops on macOS, so it is continuously exercised by hand; Windows is the platform no contributor here can run. Worth revisiting if a compiled binary release ever needs to be produced on macOS.

Checklist

  • The canonical checks pass locally: pnpm typecheck && pnpm lint && pnpm format:check && pnpm knip && pnpm build && pnpm test
  • Tests are added or updated for any behavior change — n/a, this changes only workflow configuration; actionlint passes on all five workflows
  • Read-path / serializer changes are verified with a live round-trip — n/a, no source changes
  • Documentation is updated if needed — n/a, no documented behavior changes

The Windows job added in #159 was a hand-copied duplicate of `check` named
after its runner, and the two jobs disagreed about what they were for: one
ran the whole gate plus build and test, the other a silent subset with a
ten-line comment arguing why.

Split by purpose instead of by platform, which is what the ecosystem
converges on (of 40 sampled Node/TS repos, 25 of the 31 that test on Windows
express it as a matrix over `os`; `test` and `lint` are the two most common
job ids by a wide margin). `lint` owns the platform-independent gates —
typecheck, lint, format, knip — none of which need a build, which is why
they already ran before `pnpm build`. `test` owns build and test across
ubuntu-latest and windows-latest, so the subset stops being a special case
that needs explaining.

Drop the claim that a Windows runner bills at 2x: this repository is public,
where standard GitHub-hosted runners are free. The real cost is wall clock —
the Windows job runs ~102s against ~70s for Linux and sets the critical path.

Add timeout-minutes to both jobs. Neither had one, so a hung run — this
suite spawns processes and races for a port — would have burned the 360
minute default before anyone noticed.

Set fail-fast: false so one platform failing does not cancel the other;
which of the two broke is the finding.
@awdr74100
awdr74100 merged commit 263c768 into main Aug 21, 2026
6 checks passed
@awdr74100
awdr74100 deleted the ci/lint-job-and-os-matrix branch August 21, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant