kotao theme push is the round-trip from your laptop to a live site. Here’s what happens.
The wire
When you run kotao theme push:
- Read project —
theme.json+kotao.theme.tomlare validated. Slugs must match;theme.json.versionmust be valid semver. - Resolve workspace — from
--workspace,KOTAO_WORKSPACE, the toml, an interactive picker, or your only workspace. - Ensure theme exists —
GET /sites/themeschecks whether your slug already has a registry entry. If not,POST /sites/themescreates one (slug + display name fromtheme.json). - Create version row —
POST /sites/themes/{tid}/versionswith the semver. Returns a presigned R2 PUT URL valid for 10 minutes and the newversion_id. - Pack the source — a deterministic gzipped tarball of your project (50 MB max). Default-ignored:
node_modules/,dist/,.git/,.env*,*.log. Add a.kotaoignorefor anything else. - PUT to R2 — directly from your laptop. The CLI doesn’t proxy the upload through api-service.
- Commit —
POST /sites/themes/versions/{vid}/commit. Verifies the R2 object exists and advances the row fromawaiting_upload→queued. - Poll — the CLI polls
GET /sites/themes/versions/{vid}every 2 seconds. Status transitions appear inline:queued→building→publishing→ready(orfailed). - Render result — on
ready, prints the install hint. Onfailed, fetches the build log and prints the last 50 lines.
The whole flow typically takes 30-90 seconds for a small theme. Most of that is the build itself.
Semver
theme.json.version is the canonical version. Bump it before every push. The api-service enforces per-theme uniqueness — re-pushing the same version is rejected with a 409.
Conventions:
- Patch (0.1.0 → 0.1.1) — bug fix, no visible change to authors using the theme.
- Minor (0.1.0 → 0.2.0) — new sections, new fields, additive change.
- Major (0.1.0 → 1.0.0) — breaking change. Authors using earlier versions still get the old build; switching them is opt-in via the editor.
There’s no enforcement on what counts as breaking; this is convention only.
The build
The sites-theme-builder service drains your queued row and runs your source through a sandboxed Kubernetes Job in the sites-builds namespace:
- No service-account token — the build pod can’t talk to the cluster API
- NetworkPolicy egress-allowlisted — only DNS + npm + R2 are reachable; everything else is blocked
- 5-minute deadline —
activeDeadlineSeconds; the pod is killed if the build takes longer - Resource caps — CPU + memory limits so a runaway build can’t take down the node
Inside the pod: bun install then astro build. Output: a Worker bundle (your theme baked into the runtime), a manifest (theme.json plus build metadata), and the build log. All three uploaded back to R2 via short-lived presigned PUT URLs.
If any of install, build, scan, or WfP upload fails, the row goes failed with an error_summary and your log is preserved for 24 hours.
After ready
The build’s output is a Cloudflare Worker script, named theme-{slug}-{semver-with-dashes}, uploaded to the Kotao WfP dispatch namespace. The storefront dispatcher resolves your theme’s slug to that script name on every render.
Apply your theme to a site (today this is a curl until the editor UI in 4D lands):
curl -X PATCH https://api.kotao.com/v1/workspaces/<workspace>/sites/<site_id> \
-H 'Authorization: Bearer <session>' \
-H 'Content-Type: application/json' \
-d '{"theme_id":"my-theme"}'
The dispatcher picks up the new theme on the site’s next render. No deploy step on your end; no cache to flush.
When the build fails
Common failures and their fixes:
install_failed
bun install returned non-zero. Usually a missing dep, a typo in package.json, or a network blip on the npm registry. The log shows the exact error — fix in your project, push a bumped version.
astro_build_failed
Your theme has a TypeScript error or a runtime error in a section that fires during SSR. The log tail tells you which file. Reproduce locally with astro build from inside the runtime.
scan_rejected
The output bundle failed the publish-time scan. Causes:
bundle_too_large— bundle exceeds the size cap. Trim deps or move large assets to R2.invalid_worker_module— Astro didn’t emit a valid Worker module. Re-checkastro.config.tsuses the@astrojs/cloudflareadapter.manifest_invalid—theme.jsondoesn’t validate against the SDK contract. Compare against the scaffolded version.section_coverage_incomplete— your theme declares it implements section typeXbut no renderer is wired insrc/theme.ts. Add it.
wfp_upload_failed
The build succeeded but pushing to WfP failed. Almost always a transient Cloudflare API blip — re-run kotao theme push (bump semver patch; same code, fresh version).