ci: add a windows-latest leg and document Windows support

None of this was caught because nothing ever ran on Windows: every job in every
workflow was ubuntu-latest, and checks.yml did not even run on the PR that
reported it — only Vercel, which failed on fork authorization.

The check job now runs on both, fail-fast off so a Windows-only break still
reports the Linux result. `shell: bash` on the multi-line steps, since the
default shell there is pwsh, which shares none of that syntax; Git Bash ships
on the runner, so nothing needs rewriting. The two drift checks stay Linux-only
— they verify that a committed generated file matches its generator, which is a
property of the repo, not of the platform.

The new build step is unconditional and unscoped on purpose. `--affected` is
exactly what let these through: they lived in build scripts, so a PR touching
no affected package never ran them. A `pnpm clean` step guards the lane that
was broken in all eleven packages.

pnpm-workspace.yaml's release-age exclusions had drifted almost across the
board — turbo pinned at 2.10.1 against 2.10.9 in the lockfile, the Claude SDK
at 0.3.196 against 0.3.226, both codex packages a minor behind. A stale pin
does not fail loudly; it simply stops excluding anything, and the package falls
back under the gate. That is the silent optional-dep drop the file's own
esbuild comment documents, and every one of these ships per-platform packages
including win32.
This commit is contained in:
Nayan
2026-08-11 23:12:06 +05:30
parent 36facc6ba4
commit 5349607a38
5 changed files with 111 additions and 24 deletions
+40 -3
View File
@@ -21,7 +21,16 @@ concurrency:
jobs:
check:
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
# Windows is a first-class target — the CLI installs from npm onto it — but
# it went untested until a contributor reported that a fresh clone could not
# build there at all. Three separate path/line-ending bugs had shipped
# invisibly because every lane ran on Linux only. fail-fast is off so a
# Windows-only break still reports the Linux result, and vice versa.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
@@ -33,7 +42,12 @@ jobs:
- name: Lint (Biome via Ultracite)
run: pnpm lint
# `shell: bash` on every multi-line block below: the default shell on
# windows-latest is pwsh, which shares none of this syntax — no `[ -n … ]`,
# no `if ! cmd`, and `${VAR}` is not env expansion. Git Bash ships on the
# Windows runner, so pinning the shell is enough; nothing needs rewriting.
- name: Typecheck + test (affected)
shell: bash
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
run: |
@@ -43,10 +57,25 @@ jobs:
pnpm turbo run typecheck test
fi
# Unconditional and unscoped, unlike the step above. `--affected` is what
# let the Windows build bugs through: they lived in build scripts
# (check-css.mjs, vendor-assets.mjs, the gen.mjs front-matter readers), and
# a PR that touched none of the affected packages never ran them. This is
# the step that actually proves a clean checkout builds on both platforms.
- name: Build (every package)
run: pnpm build
# apps/cli/README.md is generated from the root README.md, and it is what
# npmjs.com renders for @airshiplabs/cli. Committed rather than built on
# demand so a clean checkout can publish without running the generator.
#
# Linux only, here and below: both of these check that a COMMITTED
# generated file matches what the generator emits. That is a property of
# the repo, not of the platform, so running it twice doubles the runtime
# and the flake surface for no extra signal.
- name: README is not stale
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
if ! node scripts/sync-readme.mjs --check; then
echo "::error file=apps/cli/README.md::The CLI README is stale. Run 'make readme' and commit the result."
@@ -57,16 +86,24 @@ jobs:
# Regenerated by BUILDING, not by `tsr generate`: two things write this
# file and they disagree — the router CLI emits the tree alone, while the
# Start Vite plugin appends the `declare module` block that registers the
# router type for SSR. The build's output is the committed one.
# router type for SSR. The build's output is the committed one. The build
# step above already wrote it; this only reads the result.
- name: Route tree is not stale
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
pnpm turbo run build --filter=@airship/web
if ! git diff --quiet -- apps/web/src/routeTree.gen.ts; then
git diff --stat -- apps/web/src/routeTree.gen.ts
echo "::error file=apps/web/src/routeTree.gen.ts::The route tree is stale. Run 'make web:build' and commit the result."
exit 1
fi
# Last, because it deletes what everything above produced. Every workspace
# `clean` was `rm -rf` until this lane existed to catch it — a command that
# does not exist on Windows, so `pnpm clean` failed in all eleven packages.
- name: Clean (removes what the build produced)
run: pnpm clean
commitlint:
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
+37
View File
@@ -30,6 +30,43 @@ Open <http://localhost:5174> and you are looking at Airship, with Airship's own
inside it. Pick the hero's button, ask for a change, and the diff lands in `apps/web/src/`.
`make run:solo` does both in one terminal via `--exec`.
### On Windows
Everything builds, tests and runs on Windows — `checks.yml` gates every PR on a
`windows-latest` leg alongside Linux, so a break there fails the PR.
`make` is the one thing that does not carry over: the Makefile declares
`SHELL := /bin/bash` and a handful of targets genuinely need it (`help` is an `awk`
program, `preflight` a shell conditional, `release` a bash script). It is a thin
wrapper either way — every recipe is one `pnpm` or `node` call — so use those directly:
| Instead of | Run |
| --------------- | ---------------------------------------------------------- |
| `make demo` | `pnpm install && pnpm build` |
| `make web:dev` | `pnpm dev:web` |
| `make run` | `node apps/cli/dist/index.js --target 5173 --cwd apps/web` |
| `make run:solo` | `node apps/cli/dist/index.js --cwd apps/web --exec "pnpm dev:web"` |
| `make doctor` | `node apps/cli/dist/index.js doctor --cwd apps/web` |
| `make check` | `pnpm lint && pnpm typecheck && pnpm test` |
| `make readme` | `node scripts/sync-readme.mjs` |
| `make storybook`| `pnpm turbo run storybook --filter=@airship/overlay` |
`pnpm dev:web` rather than a bare `vite dev`: the site cannot start until
`@airship/site-tokens` has emitted `dist/tokens.css`, and only turbo knows that.
Two things worth setting up once:
- **Git Bash**, which ships with Git for Windows, runs the Husky hooks and the release
scripts. Without a POSIX `sh` on PATH the pre-commit formatter silently does not run.
- **Developer Mode** (Settings → System → For developers), so pnpm can create the
symlinks its `node_modules` layout depends on without elevation.
Line endings are pinned to LF by [`.gitattributes`](.gitattributes) — do not override it
with `core.autocrlf`. Several generators parse their input with anchored regexes, and a
CRLF checkout makes them report a missing front-matter block rather than a wrong one.
If you cloned before that file existed, renormalize once with
`git rm --cached -r . && git reset --hard`.
## Repo layout
| Package | Role |
+2
View File
@@ -428,6 +428,8 @@ and provider it would use from your terminal.
Node 22.13 or later, and one of Claude Code, OpenAI Codex or OpenCode.
macOS, Linux and Windows. Every PR is built and tested on Linux and Windows.
## Links
- [airship.design](https://airship.design)
+2
View File
@@ -430,6 +430,8 @@ and provider it would use from your terminal.
Node 22.13 or later, and one of Claude Code, OpenAI Codex or OpenCode.
macOS, Linux and Windows. Every PR is built and tested on Linux and Windows.
## Links
- [airship.design](https://airship.design)
+30 -21
View File
@@ -24,28 +24,37 @@ minimumReleaseAgeExclude:
# a name pattern carrying a version union and enumerating all 25 for every
# bump is not maintainable. They only ever publish in lockstep with the
# version above, so the unpinned glob follows whatever it is pinned to.
#
# EVERY VERSION BELOW MUST MATCH THE LOCKFILE. A pin left behind at an older
# version does not fail loudly — it simply stops excluding anything, and the
# package silently falls back under the release-age gate, which is the
# esbuild failure above all over again. All of these but esbuild had drifted
# at once (turbo 2.10.1 vs 2.10.9, the Claude SDK 0.3.196 vs 0.3.226, both
# codex packages a minor behind), and every one of them ships per-platform
# optional deps including win32 — so the package most likely to go missing is
# the one for whichever platform is least represented in the team's machines.
# `pnpm why <pkg>` after a bump, and update the pin in the same commit.
- 'esbuild@0.28.2'
- '@esbuild/*'
- '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.196'
- '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.196'
- '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.196'
- '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.196'
- '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.196'
- '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.196'
- '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.196'
- '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.196'
- '@anthropic-ai/claude-agent-sdk@0.3.196'
- '@openai/codex-sdk@0.146.0'
- '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.226'
- '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.226'
- '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.226'
- '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.226'
- '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.226'
- '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.226'
- '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.226'
- '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.226'
- '@anthropic-ai/claude-agent-sdk@0.3.226'
- '@openai/codex-sdk@0.147.0'
# Unlike the other two SDKs this bundles no binary — the `opencode` CLI is a
# separate install, detected on PATH at runtime. See providers/opencode.ts.
- '@opencode-ai/sdk@1.18.13'
- '@openai/codex@0.146.0-darwin-arm64 || 0.146.0-darwin-x64 || 0.146.0-linux-arm64 || 0.146.0-linux-x64 || 0.146.0-win32-arm64 || 0.146.0-win32-x64 || 0.146.0'
- '@turbo/darwin-64@2.10.1'
- '@turbo/darwin-arm64@2.10.1'
- '@turbo/linux-64@2.10.1'
- '@turbo/linux-arm64@2.10.1'
- '@turbo/windows-64@2.10.1'
- '@turbo/windows-arm64@2.10.1'
- turbo@2.10.1
- ultracite@7.10.0
- '@opencode-ai/sdk@1.18.13'
- '@opencode-ai/sdk@1.18.15'
- '@openai/codex@0.147.0-darwin-arm64 || 0.147.0-darwin-x64 || 0.147.0-linux-arm64 || 0.147.0-linux-x64 || 0.147.0-win32-arm64 || 0.147.0-win32-x64 || 0.147.0'
- '@turbo/darwin-64@2.10.9'
- '@turbo/darwin-arm64@2.10.9'
- '@turbo/linux-64@2.10.9'
- '@turbo/linux-arm64@2.10.9'
- '@turbo/windows-64@2.10.9'
- '@turbo/windows-arm64@2.10.9'
- turbo@2.10.9
- ultracite@7.10.2