Skip to content

ci(examples): build every starter with its own build script (DEV-2737) - #302

Merged
demtario merged 1 commit into
masterfrom
fix/DEV-2737-examples-build-ci
Sep 2, 2026
Merged

ci(examples): build every starter with its own build script (DEV-2737)#302
demtario merged 1 commit into
masterfrom
fix/DEV-2737-examples-build-ci

Conversation

@demtario

@demtario demtario commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes DEV-2737.

The gap

No workflow ran a starter's own build, so the type-checking half of examples/ was uncovered. ci.yml's jobs are all working-directory: runner and never enter examples/*; e2e-starter-matrix.yml boots FRAMEWORK_DEV.cmd, a dev server, and explicitly skips even a workspace build; import-starters.yml only ever runs pnpm install --lockfile-only, which resolves without building. That is how DEV-2727 (18.1.0 tightened a public type, examples/angular stopped compiling, found by a person opening the demo after release) and DEV-2730 (TS2688 in examples/base-web outside the repo tree) both shipped.

server-examples-build.yml already closed the same gap for server-examples/ when DEV-2731 landed. This is the examples/ half, and that workflow is the template: a discover job emits the 16 starters from the checkout, a frozen-lockfile lane guards starter edits on paths: examples/**, and a weekly lane moves Handsontable to the current release.

Three design points, all measured rather than assumed

The oracle is the starter's own package.json build, not the generated bucket artifact. The bucket looks like the more faithful target — it is what ships — but the only product path that runs a build (share.ts:266) passes the command through snapshotBuildCommand, which strips a leading tsc / tsc -b / vue-tsc. Building what ships would reproduce the stripping and stay green on the entire class of defect this workflow exists to catch. Worth flagging because the ticket's own text proposed the bucket.

The build runs outside the repo tree. With the pre-DEV-2730 tsconfig.node.json restored, examples/base-web emits dist in-tree and fails TS2688 from a copy under a clean parent — same starter, same config, because a types entry resolves through this repo's untracked root node_modules/@types/node. An in-tree job would have missed DEV-2730 entirely.

The canary lane uses pnpm update --latest, not pnpm add <pkg>@latest. Every starter's committed spec is already "latest", against which add is a no-op: on a copy whose lock pinned 18.0.0 with 18.1.0 on the registry, it reported "Already up to date" and left 18.0.0 installed. A canary that rebuilds the pinned version forever is worse than no canary, so the lane also asserts the resolved version equals npm's dist-tag — printing the version proves nothing, since a silent no-op prints a plausible number.

Also in here: a real drift in BUILD_CONFIG

BUILD_CONFIG.vue.buildCommand was vite build, while examples/vue builds with run-p type-check "build-only {@}" --. The vue-tsc pass existed in the starter and nowhere in our config — dropped in config/frameworks.json, upstream of any stripping.

Declared now as vue-tsc --build --force && vite build, which snapshotBuildCommand reduces to exactly vite build, so container behaviour is byte-identical to today. Deliberately not the starter's literal run-p form: that string does not match the strip regex, so the share path would start running a type-check in ephemeral containers — the thing the strip exists to prevent — and would need npm-run-all2 present there.

build-command-drift.test.mjs pins it as a property (BUILD_CONFIG never drops a type-check the starter's own build performs) rather than string equality, because the one framework an equality allowlist would have to exempt is the defect itself. The property also lets nuxt's intentional nuxt generate divergence through on its own merit, with no allowlist at all.

Coverage is partial, and the header says so

10 of 16 starters type-check on build; remix is covered through its own typecheck script, which the workflow runs when a starter ships one. Genuinely uncovered: ant-design, javascript, react-js (no TS surface at all — zero tsconfigs), astro (ships no astro check), and nuxt, which cannot type-check without adding vue-tsc to it. Worth doing, but not in this workflow's scope.

All 16 are built regardless, not just the type-checking ones: a curated list would drift, and the other six still get their bundler and config exercised for the price of one short job.

This does not duplicate pipeline/starter-type-roots.test.mjs — it complements it. That test is the fast static half and declares two limits for itself (unfollowed extends chains, top-level tsconfigs only, so examples/nuxt/server/tsconfig.json is never inspected). A real build covers both empirically for all 16 starters, while the static test stays green-on-PR in seconds and rides the cherry-pick onto prod-examples/*.

Verification

Every lane has a must-fail proof, because a job that cannot go red is decorative.

check result
All 16 starters, frozen install + build, outside the tree green; remix typecheck exits 0
DEV-2730 reintroduced (pre-fix tsconfig.node.json) TS2688: Cannot find type definition file for 'node', no dist
…the same defect built in-tree passes, emits dist — the out-of-tree requirement, measured
DEV-2727 reintroduced (GridSettings annotation dropped) TS2322: … not assignable to type 'GridSettings' in the component template, no dist
Output-presence guard on an empty build exits 1
Dist-tag assertion after the no-op pnpm add fails (18.0.0 vs 18.1.0); passes after update --latest
pnpm test 969 tests, 0 fail; drift test red before the vue fix, green after
pnpm typecheck clean

The one thing not verified locally is the workflow itself on a real runner — this PR touches a path in its own trigger filter, so the 16 build jobs run here. build-latest is non-PR only and needs a workflow_dispatch or the Monday 05:00 UTC cron.

Two notes on runner specifics, both deliberate: no cache: pnpm (the restore runs before the extraction and resolves the store relative to the checkout, so an install under RUNNER_TEMP can miss it with no signal — 16 cold installs is the cheaper trade), and tar rather than rsync, which is not guaranteed on the ubuntu-latest image.

Ticket corrections

Three premises in DEV-2737 did not survive contact, and the description should be updated:

  • DEV-2731 was the DEV-2727 class, not the DEV-2730 class. 3aa26d22 changed zero tsconfigs; all seven server-examples Angular frontends already carry "types": []. It was 18.1 type tightening.
  • 10 of 16 starters type-check on build, not 11. remix vite:build does not, which is why it ships a separate typecheck script.
  • Design question 4 is already answeredserver-examples/ has had its own build workflow since DEV-2731.

🤖 Generated with Claude Code


Note

Low Risk
Primarily new CI gates and metadata alignment; share-time container builds stay unchanged because vue-tsc is still stripped before execution.

Overview
Adds .github/workflows/examples-build.yml so every examples/* starter with a build script is installed and built in CI—something existing workflows never did. A discover job auto-matrices starters; build runs on PRs (examples/**) with frozen lockfile, copies each starter outside the repo tree (so ancestor node_modules cannot mask missing @types), runs pnpm build, optional typecheck/type-check scripts, and asserts Handsontable resolves and output dirs are non-empty. build-latest (schedule/dispatch only) bumps Handsontable packages via pnpm update --latest and asserts the resolved version matches npm’s dist-tag.

Aligns Vue buildCommand in frameworks.json / generated catalog and BUILD_CONFIG with the starter’s type-check (vue-tsc --build --force && vite build); share snapshots still run only vite build after snapshotBuildCommand strips the prefix. Adds build-command-drift.test.mjs so BUILD_CONFIG cannot drop type-check steps that starters perform, and documents the workflow in TESTING.md.

Reviewed by Cursor Bugbot for commit b86c0b4. Bugbot is set up for automated code reviews on this repo. Configure here.

Nothing in CI ran a starter's own `build`, so the type-checking half of
`examples/` was uncovered. ci.yml's jobs are all `working-directory:
runner`; e2e-starter-matrix.yml boots a dev server and skips even a
workspace build; import-starters.yml only ever runs `pnpm install
--lockfile-only`. That gap is how DEV-2727 and DEV-2730 shipped.

examples-build.yml mirrors server-examples-build.yml: a discover job
emits the 16 starters from the checkout, a frozen-lockfile lane guards
starter edits on `paths: examples/**`, and a weekly lane moves
Handsontable to the current release to detect upstream type tightening
the day it lands.

Three design points that are easy to get wrong, all measured rather than
assumed:

The oracle is the starter's own `package.json` build, not the generated
bucket artifact. The bucket looks more faithful — it is what ships — but
the only product path that builds (share.ts) passes the command through
`snapshotBuildCommand`, which strips a leading tsc / tsc -b / vue-tsc.
Building what ships would reproduce the stripping and stay green on the
whole class of defect.

The build runs outside the repo tree. With the pre-DEV-2730
tsconfig.node.json restored, `examples/base-web` emits dist in-tree and
fails `TS2688` from a copy under a clean parent — same starter, same
config. An in-tree job would have missed it.

The canary lane uses `pnpm update --latest`, not `pnpm add <pkg>@latest`.
Every starter's committed spec is already "latest", against which `add`
is a no-op: on a copy whose lock pinned 18.0.0 with 18.1.0 on the
registry it reported "Already up to date" and left 18.0.0 installed. A
canary that rebuilds the pinned version is worse than none, so the lane
also asserts the resolved version equals npm's dist-tag — printing it
proves nothing, since a silent no-op prints a plausible number.

Also fixes a drift the mapping turned up: BUILD_CONFIG's vue entry was
`vite build` while examples/vue builds with `run-p type-check
"build-only {@}" --`, so the vue-tsc pass existed in the starter and
nowhere in our config. Declared as `vue-tsc --build --force && vite
build`, which `snapshotBuildCommand` reduces to exactly `vite build` —
container behaviour is unchanged — and pinned by
build-command-drift.test.mjs as a property rather than string equality,
since the one framework an equality allowlist would have to exempt is
the defect itself.

Coverage is partial and the workflow header says so: ant-design,
javascript and react-js have no TS surface, astro ships no `astro
check`, and nuxt cannot type-check without adding vue-tsc. remix is
covered through its own `typecheck` script.

Verified: all 16 starters build green out of tree at the frozen
resolution; the two reintroduced defects (TS2688 for DEV-2730, TS2322 on
the GridSettings holder for DEV-2727) each fail with no output emitted;
the output-presence guard exits 1 on an empty build; the dist-tag
assertion fails on the no-op install and passes after `update --latest`;
969 unit tests pass, and the drift test is red before this fix and green
after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@demtario
demtario marked this pull request as ready for review September 2, 2026 12:50
@demtario
demtario merged commit f75d970 into master Sep 2, 2026
24 checks passed
@demtario
demtario deleted the fix/DEV-2737-examples-build-ci branch September 2, 2026 12:57
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.

2 participants