Conversation
ff1d672 to
9b9e94e
Compare
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.1.17-pr.626.a210a49Prefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.1.17-pr.626.a210a49"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.1.17-pr.626.a210a49"
}
}
Preview published to npm registry — try new features instantly! |
Adds `base44 publish` — build, record the result as a version, serve that
version — over the platform's new version endpoints, plus the two halves as
commands of their own:
base44 build run the build and print its artifact set
base44 version create record built output, without deploying
base44 version deploy <id> serve a recorded version (also a rollback)
base44 publish all three, in order
Not `site deploy`. That command drives the legacy full-stack hosting lane,
whose own source says nothing there publishes, and whose `deploymentId` means a
Cloudflare script. A caller that could not tell them apart would publish by
accident, so this is a separate command with separate envelope field names —
`versionId`, `manifestHash`, `deploymentId`, `revision`.
A file is named by a FULL sha256 over its bytes. That is artifact identity, and
the platform signs it into the upload URL so S3 refuses any other body. It is
not `hashAsset`, a 32-hex truncation of sha256(app id ‖ bytes) that keys a
provider's asset cache — conflating them gives a file that uploads fine and
never dedupes, or a digest check that fails on a correct file.
Entity and agent payloads go up raw. The platform's extractor and validation are
authoritative, and the CLI's own entity schema refuses real Builder apps — which
is exactly why `site deploy` reads no resources at all.
`uploadPresignedAssets` is shared with the static lane; it now sends the
server's `x-amz-checksum-sha256` when the URL carries one. Uploads default to 8
in parallel: measured on the sandbox's pipe, 3 needed ~930s of the ~450s a build
leaves, and 16 failed a degraded pipe.
A Builder repo carries no CLI config, so one is defaulted — without writing
anything. Only a WHOLLY absent config is defaulted; a config that omits a field
said so deliberately and still gets today's error.
The failing step travels out through the `--json` envelope as `step`. A user's
build failing, a rejected artifact set and a lost publication race are three
incidents with three responses, and one exit code for all of them is how a
sandbox log stops being diagnostic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It runs a local command and reads local files — it never calls the API, so prompting for a login was always pointless. It matters now because it is the step a publish sandbox runs BEFORE minting its publish key: requiring auth there either fails that exec or puts the key beside the repo-controlled code the build runs. The app id is still required; it is injected into the build as VITE_BASE44_APP_ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`base44 version` printed a command group's help while `base44 --version` printed the version number, two lines apart in the same `--help` output. Anyone reaching for the CLI's version got the wrong thing. Plural is also this CLI's own convention for a group you have many of — `agents`, `entities`, `functions`, `secrets`, `workflows` — against the singular groups you have exactly one of: `auth`, `site`, `sandbox`, `types`. `create` and `deploy` keep their names. `create` is the word this CLI already uses for bringing a new thing into existence (`base44 create`), and `deploy` is already its verb for making something live (`functions deploy`, `site deploy`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9b9e94e to
c473ba9
Compare
`base44 publish` and the `versions` group are not registered without the env var, so with it off they are absent from `--help` and typing one is an unknown command. Not `.hidden()`: a command that runs but is unlisted is discoverable by anyone who reads the source, and cannot be un-shipped once someone scripts against it. This is the shape `site deploy` already uses for the flags that only mean something on the deployments lane. Deliberately a second var rather than `BASE44_DEPLOYMENTS_API`. That one selects the legacy deployments transport, and the build sandbox already sets it for that arm; one var switching both lanes would make them impossible to roll out apart. `base44 build` stays ungated — it is a pre-existing public command, and what changed there is what lets the sandbox build before it holds a publish key. Adds `docs/versions.md` alongside `docs/deployments.md`: the house rule is to gate the lane out of the CLI's surface and document it in `docs/`, not to hide it from the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--output-dir`, `--target`, `--git-hash` and `--concurrency` were defined once per command, with the flag names, descriptions, defaults and validators copied between `publish`, `versions create` and `versions deploy`. Nothing stopped the copies drifting, and a user comparing two `--help` screens would have been the one to find out. Four plain functions returning an `Option`, composed with `addOption`. 82 lines out, 20 in. Kept deliberately small — the CLI has no other shared option builders, so this is not a pattern to generalize until something else needs it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PublishTarget`, `PublishStep` and `PublishResult` are each used only inside their own module, and `DeclareVersionResponse` was used nowhere at all — the response is consumed through the schema's inferred type at the call site. Exporting them put four names on the package's surface that no consumer names, which is what `knip` flags. The alias is deleted; the other three stay where they are, unexported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The house pattern from api-patterns.md — every response parsed through its Zod schema, a mismatch raised as SchemaValidationError — is the whole mechanism, and the doc now says so rather than leaving the next reader to ask. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every docstring and comment added by this branch, trimmed to one or two lines: no narration of what the code already says, and no paragraph where a clause works. 58 lines out, no behaviour change. What stays is the non-obvious — why the digest is not `hashAsset`, why resources go up unvalidated, why the gate is a second env var rather than the deployments one, why `build` needs no credential, and the measurements behind the upload concurrency. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`deduplicated` was derived state: the response already carries `manifest_hash`, which IS the version's identity, so a caller asking whether a rebuild changed anything compares that. The flag also answered two different questions — line 245 hardcoded it for "you already made this call", the other branch computed "this content already existed" — and an existing version is not necessarily the one being served, so it was misleading either way. `revision` was read back from the environment AFTER publication, so a concurrent deploy could bump it in between and the caller would be told someone else's number. `store.publish` returns the right one; `deploy()` logs it and drops it, because `Deployment` has no such field. Reporting it honestly would mean widening the primitive, and nothing consumes it. Both existed to print five words. The conditional-write fence that makes publication safe is untouched — that is `Environment.revision` in the store, which nothing on the wire ever needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**EMFILE at ~1.2k files.** `collectBuildOutput` opened one descriptor per file through a bare `Promise.all`, so it died far below the 100k it advertises. Now bounded with `p-map` at 32. The test writes its fixture sequentially and was verified to fail with EMFILE against the unbounded version under `ulimit -n 256` and pass against the bounded one. **A missing output directory emitted no `step`.** Resolving the directory and reading it ran outside `tagStep`, so the sandbox could not tell a rejected build output from a transport failure. Producing the artifact set is part of create_version, so it is tagged as one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Declare signed one presigned URL per declared file at a ceiling of 100 000, and held the whole set in Redis for an hour. Measured: ~108 µs of blocking crypto per URL and ~138 bytes per file, so the ceiling was ~10.8 s of pinned event loop and ~13.8 MB of Redis in a single request — at 600 requests a minute, because that limit was copied from `fullstack_deploy` when the comparable endpoint, `dist_deploy`, allows 5. Three changes. The ceiling is 50 000, roughly 2x the largest frontend ever measured through the build sandbox (25 500 assets) and ~5.4 s at the very top. Declare gets its own rate limit of 10/min, since its cost scales with the app while finalize and deploy are flat; those drop to 60/min. A test pins the CPU bound, so raising the ceiling is a decision someone makes rather than a number that drifts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AssetFile.contentType` exists for the Cloudflare arm, which puts it on each multipart part. The s3 arm echoes the type the server signed into the presigned URL — deriving a second opinion here is how the two diverge — so the version lane was filling the field with `application/octet-stream` purely to satisfy the shape. It read as though it decided what the file is served as, and it did not. The field is optional now, and the version lane omits it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| entities: artifacts.entities, | ||
| agents: artifacts.agents, | ||
| source_commit: options.sourceCommit, | ||
| frontend_commit: options.sourceCommit, |
There was a problem hiding this comment.
why both?
and source_commit should be optional (for BaaS)
There was a problem hiding this comment.
Done in fe5c313. frontend_commit is gone from the wire, the session, the contract and the log — a version describes an app at a source, and its frontend and backend are that same app. build still refuses two inputs naming different commits, which is what earns the single field. And source_commit was already optional server-side (str | None); it stays that way, so a build outside a checkout is still a complete version.
| DeployVersionResponseSchema, | ||
| await ( | ||
| await post( | ||
| `versions/${encodeURIComponent(versionId)}/deployments`, |
There was a problem hiding this comment.
let's make it an API on an Environment object
PATCH /environments/
{ version: ... }
There was a problem hiding this comment.
Done in fe5c313 — agreed, this is the truer model. PATCH /environments/{name} { version_id } replaces POST /versions/{id}/deployments, on its own router rather than under versions, since an environment outlives every version it has pointed at. The Deployment record still exists internally (it is what the conditional pointer write points at) and comes back in the response so a log line can be correlated — but nobody asks for one.
| @@ -0,0 +1,146 @@ | |||
| import { createHash } from "node:crypto"; | |||
There was a problem hiding this comment.
check how to reduce duplication here with the deployments api code
There was a problem hiding this comment.
Done in 3fddea1. It was worse than similar: my collector had its own copy of the globby call, the .assetsignore handling, the ALWAYS_IGNORED set and even the comment about globby's ignore/ignoreFiles footgun. Two collectors disagreeing would mean the two lanes publish different sets from the same directory. walkBuildOutput in core/site/manifest.ts is now the one rule; both callers use it and do their own per-file work. The hashes stay separate deliberately — hashAsset is a salted, truncated provider cache key, digestFile is full sha256 artifact identity.
There was a problem hiding this comment.
Done in 3fddea1 — my mistake, those were local review artifacts. Untracked and added to .gitignore.
**Duplication (review comment).** `collectBuildOutput` had its own copy of the
deployments lane's globby call, its `.assetsignore` handling, the
`ALWAYS_IGNORED` set and even the comment about globby's `ignore`/`ignoreFiles`
footgun. What counts as "a file this build produced" is one rule, and two
collectors disagreeing about it would mean the two lanes publish different sets
from the same directory.
`walkBuildOutput` in `core/site/manifest.ts` is now that rule; both callers use
it and then do their own per-file work. The hashes stay separate on purpose —
`hashAsset` is a salted, truncated provider cache key, `digestFile` is full
sha256 artifact identity.
**Stray files.** `pr-review-626-24525.{md,html}` were committed by accident;
untracked and gitignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two review comments from @netanelgilad. **`PATCH /environments/{name} { version_id }`** replaces `POST /versions/{id}/deployments`. An environment is the thing that serves, and it serves one version — so making a version live is editing that pointer, not creating a resource. The `Deployment` record the switch leaves behind is how the plane remembers what it prepared; it is returned so a caller can correlate a log line, not asked for. `versions deploy <id>` keeps its name and now points an environment at that version. **One commit, not two.** `source_commit` and `frontend_commit` were both sent and both stored, and the CLI put the same value in each. A version describes an app at a source, and its frontend and backend are that same app. `build` still refuses two inputs that name different commits — that check is what earns the single field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Netanel's point on this PR: the full-stack deploy lane already knows how to read what a framework built, and this lane should not learn it a second time. It doesn't — `collectSiteWorker` reuses `detectFullStackArtifact`, `resolveWranglerConfig` and `collectModules` whole. A second reader would be a second opinion about what the build produced. Two consequences of a Worker being present: - The frontend comes from the Worker's own `assets.directory`, not from the project's `site.outputDirectory`. That is where a full-stack build puts the files the Worker serves, and publishing the output directory instead would declare the wrong frontend or none at all. - `main` is sent as the module set names it. The platform matches the entry against the names it was sent, so a surviving "./" would name a module nothing in the set provides — resolved by identity here rather than by position. Uploads now pair with declared files by POSITION rather than by path. The frontend and the Worker's modules are separate namespaces, so the same name can appear in both and mean two different files; resolving by path would upload one of them under the other's checksum. `uploadPresignedAssets` keeps resolving by path for the site lane, and the PUT they share is `putPresigned`. Nothing deploys this yet: the platform records the Worker on the version and refuses a full-stack app at admission. This is the transport, sent and stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… output dir Two bugs from the same wrong assumption — that every build has a frontend the platform serves from S3. `?? requireOutputDir(target)` fired whenever a full-stack build had no assets directory, sending the publish to the project's own `site.outputDirectory`. That is not where a full-stack app's frontend lives, so it would have declared the wrong files or thrown a config error on a perfectly valid app. A Worker that answers every path itself has no assets at all, and that is now what gets declared: nothing. `collectBuildOutput` also required index.html unconditionally. That is the S3 serving contract — the platform answers any unmatched path with that one file — and it does not apply when a Worker serves: the legacy deploy lane reads an index only on its non-worker branch, because a server-rendered app renders its own HTML. The requirement is now the caller's to state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`build` and `publish` both read `app?.id ?? ""`. Neither can reach the fallback today — both declare `requireAppContext`, so the lifecycle has already run `initAppContext`, which returns an app or throws on every path. The `??` was appeasing a type, not handling a state. It is worth removing anyway, because of what it would do if it ever fired. `execa` extends `process.env` rather than replacing it, so `VITE_BASE44_APP_ID: ""` overrides whatever the sandbox supplied; Vite inlines that at build time (`app-params.js` reads it); and the build, the upload and the publish all succeed. The failure surfaces in the visitor's browser, against an app id that addresses nothing. One `requireAppContext: false` — a plausible edit on a command that calls no API and already sets `requireAuth: false` — is all it takes, and `build.ts` even carried a comment saying the app id is required, two lines above the line defaulting it. `requireApp` narrows instead. It lives beside `ensureAppContext`, which is what put the value there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both lanes ask the same two questions of a build directory and were answering them separately. `resolveFullStackBuild` is now the single answer to "what did the framework build": the redirect file, the wrangler config, the modules, and an assets directory confirmed to exist. The deploy lane shapes that into a Cloudflare worker config; the versions lane hashes the modules into artifacts. Two readers of one directory eventually give two answers, and these two lanes are meant to agree — the full-stack path is the next one to move onto the versions API. `describeBuildOutput` is the same for a build's files: walk under `.assetsignore` rules, locate, size. The hash is deliberately NOT in it. A deployment keys assets by sha256(app id ‖ bytes) truncated to 32 — a per-tenant cache key — and a version addresses artifacts by a full sha256 signed into its upload URL. One value for both would either never dedupe or 403 a correct file. What they do share is the mechanism, `hashFileInto`, so neither writes the streaming loop again. Behaviour is unchanged; the only difference is that the asset walk now stats with bounded concurrency rather than one file at a time, which at the lane's own 100k ceiling it wanted anyway. `collectSiteWorker` claimed in its docstring to reuse the full-stack lane whole. It reused three collectors and re-did the orchestration around them. Now it does what it said. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`static_bundle` is not a description of an app's files — the platform reads it as "serve this from S3" and hands the prefix straight to the dist service. So a full-stack build declaring its assets there was asking for exactly the wrong thing: those files would be served raw, past `run_worker_first`, `not_found_handling` and every server-rendered route the Worker owns. They now ride as `site_worker.assets`, and the two are mutually exclusive on the wire — a version is a static app or a full-stack one, and the server refuses a declaration naming both. Backend functions are untouched by that rule; either kind may have them. `collectSiteWorker` gathers them itself, because what a Worker serves is part of what the Worker is, and the entry-file rule does not apply to it. `publish` no longer picks a directory: with a Worker there is simply no static bundle to declare. Uploads still pair by position; the order is now static frontend, then modules, then assets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`// One commit: an app's frontend and backend are the same app at the same source.` sat above `source_commit: options.sourceCommit`. It said nothing the field name did not, and it was not alone. Deleted the narration and tightened the rest to the part a reader cannot derive: a measured number, an ordering requirement, a rejected alternative. 113 lines out, 53 in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three findings from review, two of them real defects. `versions create` never called `collectSiteWorker`. A full-stack app recorded through it would declare the Worker's own files as `static_bundle` — the one thing the platform reads as "serve from S3", past every route the Worker owns. The either/or rule this lane just gained was enforceable on the wire and side-stepped by a second collector. There is now one, `collectArtifacts`, and both commands go through it. `tagStep` called `Object.defineProperty` on whatever it caught. On a frozen or non-extensible error that throws a TypeError from inside the catch, so the original — message, status, request id — is lost entirely. Worse than untagged. Guarded with `Object.isExtensible`. `resolvePublishTarget` ran outside every tag, so an invalid `config.jsonc` reached the envelope with no step at all. It is `create_version`, for the reason the neighbouring test already gives: local validation is part of producing the version. Plus what the review found stale or dead: the upload progress claimed "N of N" from a number a guard two lines down forces to be equal; `versions deploy` declared `--target` inline while `publish` used the shared option the PR says is shared; the `hashAsset` docstring had been orphaned above a different function; two comments blamed file descriptors for a bound that is the libuv thread pool; `git-hash` promised `null` and returns `undefined`; and three doc claims had drifted from the code. Also added the path assertion the review asked for: an order drift between declared files and signed URLs now fails before the first PUT rather than as an S3 checksum rejection part way through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`collectSiteWorker` read both of these and then dropped them on the way into the
artifact set: each module's `type`, and the `assets` block the Worker is served
under (`run_worker_first`, `html_handling`, `not_found_handling`, `headers`,
`redirects`). `resolveFullStackBuild` already supplied them — the versions
adapter just did not carry them.
Both are behaviour. The same bytes are a string under `text` and an ArrayBuffer
under `data`; `run_worker_first` decides whether the Worker runs for a request at
all. Two builds differing only there produced identical declarations, and since
the manifest digest IS the version's identity, the platform deduped them onto one
row — after which nothing could tell them apart or roll back between them.
A config stating no setting collapses to `null` on the wire, the same as no
config at all: a bare `assets: { directory }` and an absent block describe the
identical Worker, and recording them apart would split one version in two for no
reason — the same bug in the other direction.
Needs the matching apper change, which adds `assets_config` and a per-module
`type` to the declare contract; the request models forbid unknown fields, so the
two land together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…retried Two findings from the review, both in the versions adapter. **An existing but empty assets directory was refused.** `collectBuildOutput` rejected a zero-length collection before the entry rule was even consulted, so `requireEntry: false` relaxed only half of what it looked like it relaxed. A Worker that answers every path itself SUCCEEDED with no assets directory and FAILED with an empty one. The emptiness and entry rules are one rule — a set nothing can enter is as broken empty as it is without its entry — so they now travel together under `entryFile`, which only the set the platform serves passes. **A dropped deploy response was not replayed.** Ky retries neither PATCH nor POST by default, and rightly: a repeat is a second request unless something makes it the same one. An idempotency key is exactly that, and the command already sends one — so a lost response failed the command over a publication the server had already committed, and running it again minted a NEW key and published a second time. Retries are now bounded and attached ONLY when a key is sent; without one a repeat is a deliberate redeploy and must stay one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`retryOnTimeout` defaults to false in ky, and the previous commit did not set it — so the retry it added covered network errors and 5xx, and skipped the case the idempotency key most exists for. The 180s deadline expiring says nothing about whether the server committed; HTTP 408 in the status list is the SERVER reporting a timeout, which is a different event. The tests it shipped with were the other half of the miss: they mocked `patch` itself, so they asserted the options we passed and exercised no retry at all. Configuration that reads correctly and retries nothing is precisely the bug. They now run a real ky client over a fake network, so ky's own retry loop decides. Three of the five fail with `retryOnTimeout` back at false, and they pin the whole contract: a timed-out keyed deploy is replayed and answered from the publication the server already made, the replay carries the SAME key, a keyless call is never replayed because a repeat there is a second publish, and the replays are bounded at four attempts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…import Two of the three cleanups the review left optional. `Base44Command` is what every command runs through, and it imported `core/version/publish.ts` for one function — `stepOf`, to put the failing step in the `--json` envelope. A framework reaching into a feature module to read its own output is the dependency backwards, and the next command that wants a step tag would either import publish too or invent a second symbol. The mechanism moves to `core/errors.ts` where the rest of the envelope lives; the three step names stay in `publish.ts` behind a narrowed `tagStep`, so its call sites keep their spelling check and nothing else gains a publish vocabulary. Behaviour is unchanged, including the two details worth naming: the innermost tag wins, and the property is defined non-enumerable. `AssetFile.contentType` goes back to required. This branch made it optional, and the reason given — that `putPresigned` no longer needs it — was true but beside the point: `putPresigned` never took an `AssetFile` at all. The one producer always sets it and the cf arm always reads it, so optional described nothing and only moved a missing type from a compile error to an upload with no Content-Type. Not taken: moving `resolvePublishTarget` out from under `core/version`. It is the PUBLISH target by name and by use — three of its four callers are publish or versions, and `build` uses it because a build is the step before a publish, as that command's own comment says. Relocating it would need a rename away from "publish" to stay honest, which buys less clarity than it spends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The third cleanup from the review, which I argued against and was wrong about. I defended it by its NAME — `resolvePublishTarget` — and by counting callers, three of four being publish or versions. Both are the wrong test. Read the fields it resolves: root, config dir, build command, output directory, entities dir, agents dir. Not one of them is about a version, a manifest, an artifact or an environment. It is a thin defaults layer over `core/project`, which it already imports, filing the gaps a Builder repo leaves. The caller count says who happens to use it today, not what it is — and the one caller that broke the pattern is the one that mattered: `base44 build` is not a publish command and needed the identical resolution, so it imported the versions lane to ask a question versions does not own. So: `core/version/project.ts` becomes `core/project/target.ts`, and `resolvePublishTarget`/`PublishTarget` become `resolveBuildTarget`/`BuildTarget` — the rename is the point, not cosmetic. A name that says "publish" is what made the wrong home look right. `build.ts` now imports no versions module at all. Pure move plus rename: no behaviour changes, and the same tests cover it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rename left `resolvePublishTarget` and `project.ts` behind in three places, and the module overview still listed a file that is no longer part of this lane. Both now name `resolveBuildTarget` in `core/project/target.ts`, and the overview says why build resolution is not here: nothing it resolves is about a version, and `base44 build` needs the same answer without importing this lane. Its file list now matches `ls core/version/` exactly. Two more passages were stale for the same reason — this branch changed the thing they describe: - The Worker's identity is no longer just its compatibility settings. Each module's `type` and the whole `assets_config` are part of it too, for the reason the paragraph already gave about compatibility dates: the same bytes read as `text` instead of `data` are a different Worker. The wire shape of `assets_config` is spelled out, including why a block stating nothing is sent as `null`. - The deploy call is now retried, and the condition is the interesting half: the idempotency key is what makes a repeat the same call, so the retry exists only when a key is sent — and it covers our own timeout, which says nothing about whether the server committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| site_worker: { | ||
| main: artifacts.siteWorker.main, | ||
| modules: artifacts.siteWorker.modules.map(declaredModule), | ||
| assets: artifacts.siteWorker.assets.map(declaredFile), |
There was a problem hiding this comment.
assets: artifacts.siteWorker.assets.map(declaredFile),
Can it actually replace static_bundle: artifacts.files.map(declaredFile),
and let the server handle that differently if it needs to, but we should have "assets" and optionally, may have a "site_worker" to come along with it.
Backend functions, although they are also workers, are different and are not related to that.
The declaration carried the app's files in two fields — `static_bundle`, or `site_worker.assets` — and the platform kept a validator to refuse both. That distinction exists nowhere else: the server already stores a Worker's assets under the frontend's own artifact kind, so a static app and a full-stack app shipping identical files share one content-addressed copy. So the set names `assets` once, and `siteWorker` beside it says who serves them. `collectArtifacts` no longer produces an empty array to mean "not this mode", and the entry-file rule applies only when the platform is what serves — which is where it belonged, since a Worker answers an unmatched path itself. Resolving the build moved up into `collectArtifacts`, which needs the assets directory as well as the worker; `collectSiteWorker` stays as the thin public wrapper over a new `describeSiteWorker`. The wrangler config is still read exactly once — a second read is a second opinion about what the framework built. Needs the matching apper change, which is on base44-dev/apper#24525. **A note on the test run.** `tests/cli/` spawns `bin/run.js`, so it exercises `dist/`, not `src/` — the suite went green against a stale build while the wire had already changed underneath it. These numbers are from a rebuilt `dist`, and `TestAPIServer` now mirrors the new declare shape rather than the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`assetsConfig` was the wrong noun twice over. It is not the assets' config — `assets` is now the set of FILES on the artifact set, so the name was the same word for two different things, one nested inside the other. And it is not the Worker's config either: `runWorkerFirst` decides whether the Worker is invoked at all, which is a fact about routing rather than about either side of it. Every field here decides what happens to a request BEFORE the Worker runs. That is serving, so `servingConfig`, and `serving_config` on the wire. The FIELDS stay wrangler's, name for name, because that is what the producer read. Only the container is renamed, and the legacy deploy lane keeps `ResolvedWranglerConfig.assetsConfig` untouched — that one is Cloudflare's block on its way to Cloudflare. Matching apper change on base44-dev/apper#24525; the request models forbid unknown fields, so the two land together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # packages/cli/src/core/site/deployment.ts
Note
Description
Adds the versions lane: a build can be recorded as an immutable version (every frontend file plus the app's raw entity and agent payloads) and then served by pointing an environment at it. Recording and serving are separate acts, so a rollback is just a deploy of an older version rather than a second code path. The whole lane —
base44 publish,base44 versions create,base44 versions deploy— is registered only whenBASE44_VERSIONS_API=1, so with the gate off the commands are absent from--helpand rejected as unknown. This replaces a python driver in the platform's build sandbox that used to overwritebase44/config.jsoncbefore building, destroying checked-in configuration.Related Issue
None
Type of Change
Changes Made
New
core/version/moduleapi.ts— the three-call flow: declare (POST versions) → upload presigned PUTs → finalize (POST versions/{session}/finalize), in that order so nothing is recorded until the bytes are in place. Uploads are paired to declared files by position (frontend files, Worker modules and Worker assets are separate namespaces, so a path can repeat), with count and order-drift guards before the first PUT.setEnvironmentVersionis onePATCH /environments/{name}carrying an idempotency key, with a keyed retry policy that also retries on client-side timeout — our own deadline expiring says nothing about whether the server committed.artifacts.ts— onecollectArtifactsreader for bothpublishandversions create; streams a fullsha256digest per file (bounded fan-out,HASH_CONCURRENCY = 32), enforcesMAX_FILE_COUNT = 50_000to match the server, and readsentities//agents/raw (the CLI's stricter entity schema refuses real Builder apps, so the platform's validation stays authoritative).publish.ts— orchestration plus thebuild/create_version/deploystep vocabulary;schema.ts— Zod-validated wire types with snake_case→camelCase transforms;gate.ts—BASE44_VERSIONS_API, read in exactly one place (program.ts).Full-stack (Worker) apps
static_bundleorsite_worker— never both.site_workercarriesmain(resolved to the module-set name, not the config's./-prefixed path), the typed module set, its ownassets,compatibility_date/compatibility_flagsand a collapsedassets_config. No entry-file rule for a Worker: itsnot_found_handlinganswers unmatched paths, and a Worker with zero assets is a complete app.resolveFullStackBuildintocore/site/full-stack.tsso the deploy lane and the version lane share one reader of.wrangler/deploy/config.json.New commands (gated)
base44 publish [--no-build] [--output-dir] [--target] [--git-hash] [--concurrency]— build, record, serve;--jsonemits{environment, versionId, manifestHash, deploymentId}.base44 versions create— record built output without serving it.base44 versions deploy <version-id>— point an environment at a recorded version (also the rollback). Shared flags are built once inversions/options.tswith validating arg parsers.Supporting changes
core/project/target.ts—resolveBuildTarget/requireOutputDir: defaults (npm run build,dist) apply only when a repo has no config at all, and nothing is written to disk. A present config wins field by field.base44 buildnow resolves throughresolveBuildTargetand requires no credential (the sandbox builds before minting a publish-capable key); a missing app id is no longer silently defaulted to"".core/errors.ts—tagStep/stepOf: tags an error with its step via a non-enumerable symbol without wrapping it (innermost tag wins, respects non-extensible errors), andBase44Commandwrites it into the--jsonerror envelope asstep.requireApp(ctx)middleware helper narrows the optionalCLIContext.appinstead of defaulting at call sites.core/site/manifest.ts— extracteddescribeBuildOutput(one walk,.assetsignoresemantics, boundedstatfan-out) andhashFileInto; the two lanes' hashes stay deliberately distinct (truncated salted upload key vs. full content digest).core/site/upload.ts— extractedputPresigned, which now forwards the server'sx-amz-checksum-sha256verbatim when present (optional, since the legacy static lane's URLs pin only type and length).resolveProvenanceCommitincore/site/git-hash.tsreturnsundefinedoutside a checkout — for a version the commit is provenance, not identity.docs/versions.md(linked fromdocs/AGENTS.md) covering the two hashes, the flow, the static/full-stack split, the Worker's identity fields, concurrency measurements, the file-count ceiling, the env gate and the sandbox's two-exec usage..gitignorepicks up localpr-review-*artifacts.Testing
npm test)66 new specs across ~1,300 lines:
tests/cli/publish.spec.ts(digests, raw resources, checksummed uploads, request-body shape,--jsonenvelope, step tagging, gate on/off),tests/core/version-artifacts.spec.ts,version-api.spec.ts,version-publish.spec.ts,version-gate.spec.ts,project-target.spec.ts,site-full-stack.spec.ts,require-app.spec.ts(all undertests/core/), plus abase44 buildcase covering the no-credential path,TestAPIServermocks for declare/finalize/environment (andPATCHsupport), and a newfixtures/publishableproject.Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
BASE44_VERSIONS_APIunset the new commands are not registered at all. The one behaviour change outside the gate isbase44 build: it no longer requires auth, and it now resolves its config throughresolveBuildTarget, so a repo with nobase44/config builds withnpm run buildinstead of erroring. What it builds and prints for a configured project is unchanged.BASE44_DEPLOYMENTS_API, which selects the legacy transport forsite deployand is already set in the sandbox — one var switching both lanes could not be rolled out apart.assets_configis apper#24525, and the request models forbid unknown fields — so the two land together or the declare 400s.lint,typecheck,build,knip,publish-preview,functions-compiler,claude-reviewand all fourtestmatrix jobs (ubuntu/windows × npm/binary) pass. Lint keeps one pre-existing informational diagnostic inminiflare-function-manager.ts, untouched by this branch.🤖 Generated by Claude | 2026-09-22 11:07 UTC | e28b7e8