Lessons
What setting up the repositories' builds and deploys taught, for whoever changes them next. Each lesson says what happened and what to do about it. Lessons that belong to one repository stay there, like SvelteKit's in base's src/content/lessons.md.
pnpm
- An exact pin with
onFail: errorbreaks on every pnpm release. A newer global pnpm refused to run withERR_PNPM_BAD_PM_VERSION.onFail: downloadmakes pnpm fetch and run the pinned version instead, which was tested with a global 12.10.1 and a pinned 12.9.1. A range indevEnginesalso works, but lets machines run different versions. - Some script names never run.
pnpm deploy,pnpm audit,pnpm publishandpnpm ciare pnpm's own commands, so a script by one of those names is skipped. That's why the deploy script isshipand the drift check isdrift. npxrefuses to run in a repository whosedevEnginesnames pnpm. Usepnpm execfor an installed tool, andpnpm dlxfor one that isn't.- An exception for one version needed an edit on every release. ship and base turned pnpm's one-day wait off, and the others named each of our new versions in
minimumReleaseAgeExclude. pnpm writes that list itself on install, in a formvp checkrejects, which failed a pull request in pagez. The wait is pnpm's default everywhere now, with@amitkaps/*excluded, a pattern pnpm accepts, so a release changes nothing here. - Turning the wait back on failed a lockfile that had skipped it. base's lockfile held a
@cloudflare/workers-typespublished that morning, resolved while its wait was 0.pnpm installpassed locally, butpnpm install --frozen-lockfile, which CI runs, checks each locked version against the wait and failed.pnpm updateon the package resolved it to the newest one past the wait. Run the frozen install locally before pushing a change to these settings.
Vite+
- Type-aware lint finds what the default lint misses. Turning it on here flagged an
unknownvalue in a template string indrift.ts. It needs notypescriptdependency, since Vite+ installs tsgolint. WithtypeCheck: trueit also reports compiler errors, likeTS2322, socheckreplacestsc. - There's no
vp fix.vp check --fixwrites the format and lint fixes, socheckandfixare one command with and without a flag.vp checkalso runs the type check whentypeCheckis on, sovp lint && vp fmt --checkwas doing the same job in two steps. - With no
fmtblock,vp fmtprints "No config found, using defaults." It still passes. An emptyfmt: {}keeps the defaults and drops the notice.
The survey
The Cloudflare build can't see the other repositories. Workers Builds clones only this one, so a survey that read sibling folders found nothing there. It reads them from GitHub's
maininstead, which is public and needs no token. A GitHub outage shows in the table rather than failing the build, so it never blocks a deploy.Checking versions found drift the setup checks missed. prose asked for Node 24 in
devEngines, had no vite override at all, and two repositories were a minor release behind on markz. Each was a version nobody had looked at since it was set. A tool this repository doesn't use is compared with npm's latest, read at build time.One script name ran two commands. ship and base ran
prose ., and markz and sitez ranprose. prose readsbuildonly as its first argument, sopnpm prose buildbecameprose . buildin two of them. The script isprosealone, since it reads the current folder anyway, and the survey checks it.A sixth repository made the first table scroll. Each word in a command was kept from breaking, so a flag's hyphen never split, and
cloudflare.config.tsno longer fit its narrower column. Only hyphenated words are kept whole now. A new column is worth a look at the page at a laptop's width.Reading the domains found one the config didn't name. prose's domain was set only in the dashboard, so its heading on the page had no address to link. A domain read from the config is one a deploy can't drop, so prose's
wrangler.tomlnames it now, as markz's does.
Moving base back
- A framework needs steps around
vp check. SvelteKit generates$app/tsconfigand the Worker's types, sosvelte-kit syncandwrangler typesrun first, and oxlint doesn't type-check.sveltefiles, sosvelte-checkruns after. The standard holdscheckto runningvp check, not to being only that.fixstays exact. - Moving between toolchains needs a fresh lockfile. A lockfile keeps the optional peers it already resolved, so base needed
node_modulesandpnpm-lock.yamlremoved before installing. - Dependabot can't bump Vite+ alone. The override in
pnpm-workspace.yamlhas to move with it, and Dependabot doesn't edit that file, so it ignoresvite-plus. Vite+ moves from here, in every repository at once. - Dependabot's pull requests duplicated the survey. Only base and markz had it, and every npm pull request it opened was closed, since versions move from here and the survey compares them with npm's latest. Its merged ones were three action bumps in base. The survey checks action versions now, and Dependabot is gone.
- wrangler.jsonc hides its comments from prose. prose reads comments in
.tomlbut not.jsonc, so a repository still on wrangler useswrangler.toml.
GitHub and agents
- Five repositories protected
mainfour ways. Classic protection in two, with different settings, a ruleset in one, and nothing in two, including this one. A ruleset is one JSON document, so it can live here and be applied everywhere withgh api, which classic protection can't do as cleanly. - GitHub shows a public repository's branch rules to anyone, but not its merge settings.
GET /repos/{owner}/{repo}/rules/branches/mainworks without a login, so the home page checks the rules. Settings like squash-only need the owner's token, sopnpm drift --githubchecks those. - GitHub's rate limit looked like drift. Logged out, GitHub answers 60 requests an hour, and each
pnpm driftmade 8, so a busy afternoon of runs turned every branch rule and label red with HTTP 403. The rules live on GitHub, not in each repository's.github/, so there's no file to read instead.pnpm driftnow logs in withghwhen it can, and a check the limit stopped says it wasn't made. - Copied rules drift. The
@proserules were copied into fourAGENTS.mdfiles and had already started to differ. EachAGENTS.mdnow links to the one copy in prose's docs, and the survey checks the link section is the same everywhere.
Releases
- Three copies of one release workflow had drifted. markz staged with pnpm and sent pre-releases to
next. prose and sitez staged with npm from a temporary folder, and skipped a version already on npm. Only markz ran its size budget, and only markz had labels, so prose's notes listed every pull request, plans included. The workflow now names no package, so one file is copied everywhere and checked word for word. - publint needn't be a separate step.
vp packruns it withpublint: { strict: true }in thepackblock, and fails the build on a problem, which was tested by pointingexportsat a missing file. It needspublintinstalled, or the pack fails with "Failed to import module". - A summary in an annotated tag can't be reviewed or fixed. markz's breaking release put what to change in the tag's message. It's now the description of the pull request that bumps the version, which is reviewed like any change, and the workflow reads it from the tagged commit.
- Supporting an older Node than we build on took a workaround. The packages allowed Node 24 in
engineswhile building on 26. pnpm won't run on a Node older thandevEnginesasks for (ERR_PNPM_BAD_RUNTIME_VERSION), so a CI job for 24 had to install on 26, switch Node and runnode_modules/.bin/vp test --run. Their only user was on 26, soenginesbecame>=26and the job went. If a package gains users on an older Node, that job is how to test it. - A branch named like its tag blocks the push. sitez's bump branch was
v0.4.0, sogit push origin v0.4.0couldn't tell the branch from the tag, andgit taghad already made the tag. The branch isrelease-X.Y.Znow. To push a tag whose name is ambiguous, name it in full:git push origin refs/tags/v0.4.0. - Pushing a tag by hand was the step that went wrong. A release was a merge, then a tag pushed from a fresh
main, then npm's approval. The tag held nothing the merge didn't, and pushing it is where the branch namedv0.4.0got in the way. The workflow now runs on the merge and tags the commit itself, andpnpm releaseopens the pull request. Only npm's 2FA approval is left by hand, since it's the one check a merge shouldn't get past. The pull request is opened with the owner's login, not by a workflow, because GitHub runs no CI on a pull request a workflow's token opens. - Bundling our own packages removes the release order. prose already had markz as a dev dependency, inside its
dist/, so it depends on nothing at install time. sitez's published types never mention markz, so it can do the same, and then any package can release at any time.
New repositories
- A generator copies the standard, so it's tested against it.
pnpm newbuilds each file from ship's own, andpnpm testruns the survey andverifyon what it writes. The first run found two things a template would have hidden. oxfmt keeps short arrays on one line whereJSON.stringifydoesn't, so the generator runsfixrather than copying oxfmt's rules.vp packcan't write a package's types withouttypescriptinstalled, unlessisolatedDeclarationsis on, which also needsdeclaration. That's now how every package writes its types. vp checkdoesn't report whatisolatedDeclarationsneeds. Turned on in markz and sitez, the check passed, andvp packfailed on an export with no written type (TS9010).verifyruns both, so it's caught either way.isolatedDeclarationsintsconfig.jsonreached files nobody publishes. VS Code applied it to every file the tsconfig includes, so markz'svite.config.tsand tests showed 14 errors thatvp checknever reported.dts: { generator: "oxc" }in the pack block makes the same choice for only what's packed. markz'sdist/came out byte for byte the same, and a missing type on an export still fails the pack.- pagez was the first repository
pnpm newstarted. CI passed on the first push, and the release workflow stopped at "isn't on npm yet", as it should. The one fix was cosmetic: a description in the packages' style, "pagez — one Markdown page, one HTML file.", came out as "pagez: pagez — …" inAGENTS.md. Running it without--pushfirst left room to read the files before anything reached GitHub. - A package's first push would have run the release. The push changes
package.json, and the package isn't on npm yet, so staging would fail. The workflow now stops for a package npm doesn't have, since its first version is published by hand.
Cloudflare
- The Git integration deploys whether or not GitHub CI passed. The fix is a build command that runs the checks.
pnpm run verifyfails first, and nothing deploys. - The build and deploy commands can't live in the repository.
cloudflare.config.tshas no field for them, as of cf 1.0.0-beta.14 and@cloudflare/config0.24.1. So the dashboard holds two commands that never change, andpackage.jsondecides what they run. - The Git integration installs dependencies itself. A build command that starts with
pnpm install &&installs twice. - The integration reads pnpm's version from the repository, but not Node's. Set
NODE_VERSIONon every Worker, ordevEnginesstops the build on the default Node. - cf can't deploy SvelteKit yet. The adapter doesn't write cf's Build Output, so base stays on wrangler. Its lessons have the detail.
- cf deploys an assets-only Vite site.
@cloudflare/vite-pluginwithassetsOnly: truewrites the Build Output, andcf deploy --prebuiltuploads it. - cf can't deploy a folder that Vite didn't build. Tried on markz, whose site
prose buildwrites into.prose. cf's config has noassets.directory, as of cf 1.0.0-beta.14 and@cloudflare/config0.24.1. Without--prebuilt,cf deploydelegates to wrangler.cf pages deploytakes a folder, but that's Pages, not a Worker. So markz and prose stay on wrangler, held, until cf's config takes a folder. - cf has no command to rename a Worker, but the dashboard does. Settings → General → Name renames it in place, keeping its ID, Git connection and domains. Rename it there first, then change the name in the config, since a different name in the config deploys to a different Worker.
- Reconnecting a repository resets the Worker's build settings. After the GitHub repository was renamed and reconnected, the commands went back to
pnpm run buildandnpx wrangler deploy,NODE_VERSIONwas gone, the cache was off and previews were on. Check them after any reconnect, withpnpm drift --cloudflare. - The config lists the Worker's domains too. Change a domain in the dashboard and in
cloudflare.config.tstogether, so a deploy can't put the old one back. - cf needs
CLOUDFLARE_ACCOUNT_IDwhen the login has several accounts. It won't pick one in a script.
Docs
- prose builds the last commit, not the working tree. ship's new docs were missing from its site until they were committed, since
prose buildreads what's committed. Commit before building to see a new file, and the Cloudflare build always does. - prose's pages beside a dashboard left the dashboard out. ship's build added prose's pages to its Vite output, but its home page had no way into them, and prose's frame can't hold a page from outside the commit without becoming a site generator. A package's site is
prose buildalone. A site renders its docs its own way, and ship uses markz, as base does. - A hand-written
.htmlpage ships its comments. Vite doesn't strip them, so a<!-- @prose -->would show in the page source. Pages here carry no prose, and the README says what they are.