feat(create-objectstack): ship a CI workflow in the blank template so a scaffolded app has gates from its first push - #16364
Merged
Conversation
The scaffolder already created `.github/` at runtime for one file (copilot-instructions.md) while the template's own gates — `validate` and `typecheck` — shipped as npm scripts nothing ever ran, so every scaffolded project started with zero CI and the "metadata mistakes fail silently at runtime, the gates are where they surface" claim rested on a human remembering to type the command. Adds `.github/workflows/ci.yml` to the blank template: one job, one file, checkout -> pnpm/action-setup -> setup-node (Node 22, pnpm cache) -> `pnpm install --frozen-lockfile` -> `pnpm validate` -> `pnpm typecheck`. No `pnpm lint` step: the blank template declares no `lint` script and neither scaffolder writes one, so that step would fail on the first push of every scaffolded project. The new test derives the allowed step set from the template's package.json so the two cannot drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
…he walk collects `walkSourceFiles` prunes every dot-directory under `packages/**` as build residue, and `--self-test` re-measures the premise that makes that safe rather than quoting it. The premise was stated over ALL tracked files, but the walk collects `.ts` and nothing else, so it was strictly stronger than the property the prune rests on. The blank project template now commits `.github/workflows/ci.yml` — a template asset this audit has never had a reason to read — which reds the pin while the prune's actual safety is untouched. A false positive about a real premise is the shape most likely to get a good pin deleted, so the measurement is scoped to the extension the walk collects and stays armed for the first tracked `.ts` under a dot-directory, which is the case the comment describes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
…runs it The workflow shipped `validate` and `typecheck` only, because the template declared no `lint` script and a step naming one would have failed on the first push of every scaffolded project. Adding the script is inside this package, so the omission rested on an untested option rather than on a measurement. Measured instead: scaffolded a project for real from the repo-built scaffolder, installed its dependencies from the registry (@objectstack/cli 17.3.0), wired `"lint": "objectstack lint"` and ran it — exit 0, "All checks passed". So the script and the step both land. `lint` is not a second spelling of `validate`. `validate.ts` and `lint.ts` share the authoring-rule engine, but `checkHookBodyLowering` is imported by `lint.ts` alone — so without this step a scaffolded project has no way to reach the hook-body lowering rule, one of the two defects this card was filed about. The derived-step test now requires `lint` alongside `validate` and `typecheck`, and still derives the ALLOWED set from the template's own package.json, so the step list and the script list cannot drift apart in either direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16330
The blank template now carries
.github/workflows/ci.yml, so a freshlyscaffolded project has CI from its first push instead of a set of npm scripts
nobody runs.
What landed
packages/create-objectstack/src/templates/blank/.github/workflows/ci.yml—one job, one file,
on: [push, pull_request]:Plus the one script that step needed: the blank template's
package.jsongains"lint": "objectstack lint", and the README's "Verify your changes" block namesit beside the other two.
Action majors are pinned the way this repo pins its own; the pnpm-before-node
ordering is the one
showcase-smoke.ymldocuments (setup-node'scache: pnpmshells out to pnpm, so pnpm has to be on PATH first).
pnpm lint: inferred out, then measured back inThe first revision of this PR dropped the card's
pnpm lintstep, reasoningthat the template declared no
lintscript so the step would fail on the firstpush. That reasoning was incomplete: adding the script is a one-line change to
packages/create-objectstack/src/templates/blank/package.json, which is insidethis card's file surface. The omission rested on an untested option, so it was
tested.
The measurement. A project scaffolded for real from the repo-built
scaffolder (
node packages/create-objectstack/bin/create-objectstack.js accept-app --skip-install --skip-skills), dependencies installed from theregistry (
npm installresolved@objectstack/cliand@objectstack/specat17.3.0— the normal path, nolatestfallback), then the workflow's threesteps run in order against the project as generated:
So the step goes in. That also settles the card's acceptance criterion — "that
workflow's steps pass against the scaffolded project as generated" — by
measurement rather than by inference.
Why it is not a second spelling of
validate.validate.tscallsrunAuthoringRules('validate', …), sosecurity-owd-unset— the first defectthis card cites — is already reachable from
pnpm validate. ButcheckHookBodyLowering, which produces the second (hook-body/not-lowerable),is imported by
packages/cli/src/commands/lint.tsand by nothing else:git grep hook-body-lowering -- packagesreturns that import and the rule's owntest file. Without this step a scaffolded project has no route to that rule at
all, and the card would have delivered one of the two defects it was filed
about.
What is still out of surface.
packages/cli/src/commands/init.ts— thesecond scaffolder — writes the same five scripts and still has no
lint, so thetwo now disagree. That is a
packages/cli/change and stays out of this PR;#16350 was narrowed to exactly that parity gap, with the measurement above
recorded on it so it is mechanical rather than open.
The three premises the card asked to verify
1. The template carried no
.github. Confirmed againstorigin/main3e270d4e:src/templates/blank/held one dotfile,.dockerignore, and no dotdirectory.
2. Does a nested dot-directory survive packaging and the copy? Yes, both
halves, measured rather than assumed — no packaging change is needed.
template-consistency.test.tsruns a realnpm packwith the realfilesallowlist and compares the extracted tree tosrc/templates/. It is green with.github/workflows/ci.ymlin the set, andthis PR adds an assertion naming that path literally, next to the existing
.dockerignoreone, so a strip reads as itself rather than as "some file wentmissing".
copyDirwalks withreaddirSync(…, { withFileTypes: true })andrecurses into directories, so dot entries were never at risk there. A test
runs the real
copyDirover the template into a tempdir and asserts the filelands at
.github/workflows/ci.ymlbyte-identical — and the end-to-endscaffold above produced a project carrying both
.github/copilot-instructions.mdand
.github/workflows/ci.yml._gitignoreis the exception and this is not:npm packstrips.gitignoreand.npmrcspecifically, at every depth. That list is not "everydotfile" —
TEMPLATE_FILE_ALIASES' own header records.dockerignorepackingfine and staying literal, and
.github/now joins it.3. Pin Node to the template's
engines. Falsified as stated: the template'spackage.jsondeclaresengines.pnpm(>=10.15) and noengines.node. Thetemplate states its Node floor in its
Dockerfile(FROM node:22-slim), so theworkflow is pinned from there, and a test holds the two equal rather than
restating
22a third time. (The repo's own.nvmrcandcreate-objectstack'sengines.node >=22.0.0agree.)Outside the declared file surface — called out rather than buried
The card scoped this to
packages/create-objectstack/plus a changeset. Twofiles fall outside it, and neither is optional:
pnpm-lock.yaml— three lines, the mechanical consequence of declaring theyamldevDependency. CI installs--frozen-lockfile, so omitting it is a redbuild, not a smaller diff.
scripts/docs-audit/affected-docs.mjs— a judgement call, and the mostinteresting finding of the card.
walkSourceFilesprunes every dot-directory underpackages/**as buildresidue, and its
--self-testre-measures the premise that makes that safeinstead of quoting it:
Adding the template's
.github/makes that zero a one, andnode scripts/docs-audit/check-affected-docs.mjsgoes red. That gate is notnoise — it is a deliberate trip-wire whose comment says the remedy is "a choice
made deliberately: move the file, or go back to pruning the residue trees by
name." Both of its two named remedies are wrong here: the path IS the
deliverable, and its own header argues at length against re-listing residue
trees by name.
So the premise is narrowed to the files the walk actually collects —
.ts, theonly extension it reads (
if (!e.isFile() || !e.name.endsWith('.ts')) continue;)— which is the property the prune's safety genuinely rests on. A non-
.tsfilewas never collectable, so nothing the prune could hide has changed. One-line
filter change plus the comments that record why.
Proven not to be a disarming, from the committed state: staging a
.tsunder adot-directory in
packages/**still reds the pin by name (expected walkSourceFiles=0, got 1), and unstaging it returns the gate to exit 0 with aclean tree.
The underscore-rename escape (
_github/mapped back at copy time, the way_gitignoreworks) was considered and rejected:TEMPLATE_FILE_ALIASES' ownheader states the map covers only what
npm packgenuinely strips, a pinnedtest asserts
.dockerignoreis NOT in it, and this PR measured that.github/packs fine. Aliasing it to dodge an unrelated gate would be a workaround that
also puts a false statement into the alias map.
Tests
New:
packages/create-objectstack/src/template-ci-workflow.test.ts— the fileexists, parses as YAML, triggers on push and pull_request, runs only scripts the
template declares (and runs all three of them), pins the Dockerfile's Node
major, acquires pnpm before setup-node, pins every action, and survives the real
scaffold copy into its dot directory.
pnpm --filter create-objectstack test— 15 files, 188 tests, all green.pnpm --filter create-objectstack typecheck— clean (andtsc --listFilesconfirms the new test is in the program, so that green is about this file).
Every new assertion class was ablated from the committed implementation and
restored byte-identically (
git hash-objectback to theHEADblob, plus anempty
git diff HEAD), each with an on-disk proof that the mutation landed:.github/workflows/ci.yml"lint"from the template, keep the stepdeclares no such scriptpnpm lintstep, keep the scriptexpected [ 'validate', 'typecheck' ] to include 'lint'The last two are the same pin measured in both directions: it catches a step
without a script and a script without a step.
Packaging note
yaml@^2.9.0is added todevDependencies(the range already used by@objectstack/specand@objectstack/cli) for the YAML parse the acceptancecriterion asks for; devDependencies never reach the published tarball, whose
filesshipsdist,README.mdandCHANGELOG.md. The siblingscaffold-e2e-boot-probe.test.tsdeliberately hand-parses a workflow instead —it needs a
run:block's bytes verbatim and a parser would normalise amalformed file away. Here the parse is the assertion, so that reasoning
inverts; the new test says so in its header.
Changeset:
create-objectstackminor.Filed separately, not addressed in this PR
os createwrites nolintscript, so the two scaffolders now disagree on the generated script set #16350 — narrowed to thepackages/cli/src/commands/init.tsparity gap.@better-auth/core/dbdoes not exportcreateLocalAccountIssuer#16373 — every CLI command in a scaffolded project prints a NodeSyntaxErrorwarning (@better-auth/core/dbdoes not exportcreateLocalAccountIssuer), seen on bothvalidateandlintduring themeasurement above. Exit codes are unaffected;
os verifydoes not load.skills add --allwrites the bundle three times (~1.6 MB, byte-identical) and the template_gitignoreexcludes none of it, so every new project's FIRST commit vendors it in triplicate #16331, which touches_gitignoreandsrc/index.tsin this package and is deliberately left untouched here.No change to this repository's own
.github/workflows/.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackre-derived for the final path set: 84 commands, the same 84 as before the round-2
edits (the added paths land in families already covered). Re-run in full at
4f566942: 80 green, 0 red, 4 not measured —pnpm check:dual-build-cjs-loads,pnpm check:type-check-debt— exit 3,PREREQUISITE NOT MET: they read a fully built closure this tree does nothave. Not a pass and not a finding. (
check:dts-closureDID run this roundand is green, the two package builds having given it something to sweep.)
node scripts/check-plugin-teardown-shape.mjs --self-test— cannot reach itspinned positive-control commit in a shallow clone (
git fetch --deepen=500did not bring it into reach). Checker-health only; the gate proper passes.
pnpm check:pm-dispatch-gates— its self-test battery exceeds thiscontainer's ~10 minute foreground cap (killed at 560s, every case green up to
the cut). CI runs it.
node scripts/docs-audit/check-affected-docs.mjs, the one gate this difftripped, is green in this sweep. So are the two template-specific gates the
dispatch called out:
pnpm --filter @objectstack/spec run check:template-manifestsand
pnpm check:template-version-sync, pluspnpm check:published-files.Because the diff edits a gate script, its own suites were run on top of the
derived family:
pnpm check:docs-audit-scope,pnpm check:docs-transcript-drift(after
pnpm --filter '@objectstack/lint...' buildsatisfied its prerequisite)and
node scripts/check-self-test-wired.mjs— all green.pnpm lint(eslint . --no-inline-config) — the full repo-wide run, not anarrowed one — exits 0 at
4f566942, the final commit.pnpm check:nul-bytesgreen, plus a manual control-character grep over every changed path: clean.
turbo ls --affectedagainst the base names 8 packages. Seven of them(
@objectstack/cli,qa/dogfood,qa/downstream-contract, four example apps)are reached by turbo's input hashing on
scripts/**, not by any surface thisdiff moves — the edited file is a CI gate's own self-test filter. Their suites
are declared to CI.
Generated by Claude Code