From 5349607a380347fc0020216a66900e186b0d6d45 Mon Sep 17 00:00:00 2001 From: Nayan Date: Tue, 11 Aug 2026 23:12:06 +0530 Subject: [PATCH] ci: add a windows-latest leg and document Windows support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/checks.yml | 43 +++++++++++++++++++++++++++--- CONTRIBUTING.md | 37 ++++++++++++++++++++++++++ README.md | 2 ++ apps/cli/README.md | 2 ++ pnpm-workspace.yaml | 51 +++++++++++++++++++++--------------- 5 files changed, 111 insertions(+), 24 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 1bcd5f6..f75fe9f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49a7e16..2fe5cc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,43 @@ Open 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 | diff --git a/README.md b/README.md index 5bb3ea1..1e1f50c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/apps/cli/README.md b/apps/cli/README.md index 1b6de36..8aaf37e 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -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) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 14f46b3..6fffefd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 ` 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