feat: full-stack deploy — build a commit's worker from the CLI (cf arm, stacked on #585) - #584
Open
netanelgilad wants to merge 8 commits into
Open
feat: full-stack deploy — build a commit's worker from the CLI (cf arm, stacked on #585)#584netanelgilad wants to merge 8 commits into
netanelgilad wants to merge 8 commits into
Conversation
Contributor
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.1.8-pr.584.c8b5c9fPrefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.1.8-pr.584.c8b5c9f"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.1.8-pr.584.c8b5c9f"
}
}
Preview published to npm registry — try new features instantly! |
…ted)
Adds the deployments core (commit-addressed create/finalize, asset
manifest hashing, presigned uploads) and routes site.outputDirectory
through it when BASE44_STATIC_DEPLOYMENTS is set: POST deployments with
{git_hash, asset_manifest} and no worker config, PUT each requested
file directly to its presigned URL echoing the signed content_type
(the URL also signs content_length), finalize with the index.html
bytes as the completion sentinel. asset_uploads: null means nothing
is owed — re-deploying a commit is idempotent.
The create response is a type-discriminated ADT so the worker (cf)
arm can slot in next to s3 without protocol changes. Gate off keeps
the legacy tar.gz upload byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvhQfqxACcq25XAQRpoSh9
Stacks the worker lane on the deployments API: detect the
@cloudflare/vite-plugin redirect artifact, resolve the generated
wrangler config (no_bundle only), collect modules, and create the
deployment with the worker config — which the server answers with the
cf arm: asset buckets POSTed directly to Cloudflare with the
upload-session jwt (never through the app client), finalize with
payload{completion_jwt} plus the module parts. A full-stack artifact
wins over the static transports; nothing here publishes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvhQfqxACcq25XAQRpoSh9
netanelgilad
force-pushed
the
fullstack/deploy-git-hash
branch
from
August 3, 2026 17:17
5c32b34 to
a588d65
Compare
netanelgilad
changed the base branch from
main
to
fullstack/static-workers-lane
August 3, 2026 17:17
Main landed PR #585 (static-site deploys) in a reworked form after review: the code moved to `core/site/`, the `.assetsignore` walk switched to globby, uploads switched to p-map + ky retry, and `--concurrency` was added. This branch had built the full-stack (cf) arm on top of #585's original layout in `core/deployments/`. Resolution: keep this branch's structure and CLI contract, adopt main's review improvements into it. Structure — kept `core/deployments/`; the cf arm deploys Workers, which is not site-specific. Reverted main's duplicate `core/site/{manifest,static-site, upload}.ts` and the deployment additions to `core/site/{api,schema}.ts`; `core/site/deploy-app.ts` remains the bridge from the site module. Adopted from main: - globby-based `.assetsignore` via `ignoreFiles`, replacing the hand-rolled matcher — gains real gitignore semantics including negation. Kept the MIME table and `AssetFile.contentType`, which the cf arm's multipart parts need. - p-map for upload concurrency (both arms) and ky's built-in retry for presigned PUTs, so a 403 from an expired URL fails fast. - `--concurrency <n>` (default 3, max 50) and `isGitCommitHash()` in `core/utils/git.ts`; `GIT_HASH_PATTERN` dropped from the deployments schema. - Main's manifest suite (negation, brace/extglob, anchoring, dotfiles). - v0.1.8, globby ^16.2.2, p-map ^7.0.6. Contract kept from this branch — main gated `--git-hash`/`--concurrency` registration on BASE44_STATIC_DEPLOYMENTS. That cannot hold here: full-stack deploys are ungated and need `--git-hash`, which now defaults to the checkout's HEAD. Both flags are therefore always registered, on `deploy` and `site deploy` alike, from a shared `addDeploymentOptions()`. The env gate now decides only whether a static output takes the deployments API or the legacy tar.gz path. Main's flag-hiding tests were dropped as no longer applicable; its `--git-hash`/`--concurrency` validation tests were ported. One behavior change: a malformed `--git-hash` is now rejected by the option's argParser ("Expected a git commit hash") before the action runs, rather than later by resolveGitHash. typecheck, lint, and knip clean; 713 tests pass. The suite has a pre-existing "Body is unusable" flake under parallel load that reproduces on pristine main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merge kept the full-stack deploy code in `core/deployments/`, but main had already settled this: deployments are a transport of the site module, not a module of their own, and they live in `core/site/`. Follow main. `core/deployments/` is gone. One flow per file — `full-stack.ts` (Workers, the cf arm), `static-site.ts` (deployments-API static, the s3 arm), `deploy.ts` (legacy tar.gz) — over shared `manifest.ts`, `upload.ts`, `modules.ts`, `wrangler-config.ts`, and `git-hash.ts`. The deployment requests and schemas merge into the module's existing `api.ts` / `schema.ts` next to the tar.gz upload, as main has them. `deploy-app.ts` stays the transport picker. The Workers flow is `full-stack.ts` rather than `deploy.ts` because that name is already the legacy tar.gz path; it reads as a pair with `static-site.ts`. Unit tests follow the same naming: `tests/core/site-*.spec.ts`. No behavior change — moves, import rewrites, and the barrel/doc updates that follow from them. While merging the two `api.ts` files, the worker-module read switched to the existing `@/core/utils/fs.js` helper instead of a second raw `readFile` binding, for typed FileNotFound/FileRead errors. typecheck, lint, and knip clean; the 8 deploy spec files pass (77 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The s3 arm already used pMap for concurrency and ky's own retry. The cf arm had a hand-rolled attempt loop, a `sleep()` setTimeout helper, and bespoke 429 bookkeeping alongside it. Two idioms in one file for the same job. Both arms now share `UPLOAD_RETRY` and let ky do the retrying. ky retries network errors and its default status codes only (408/413/429/500/502/503/504), which is what these uploads actually want: an expired credential (401/403) fails fast instead of burning every attempt, and a 429 waits out the server's `Retry-After` instead of the flat 15s the old loop invented. The 401/403 "upload session expired" mapping is unchanged. Two things worth knowing, both now covered: - POST is absent from ky's default retry `methods`, so the cf arm has to name it explicitly or bucket uploads would silently never retry. The new test fails if that option is dropped. - ky clones a pristine request before sending, so the FormData body survives being resent. The test asserts every attempt carried the full body, since a consumed body would fail as "Body is unusable" only under real retries. `uploadAssetBucket` moved from api.ts into upload.ts, where it can share the retry config without a cycle. That also sharpens the split: api.ts is the app-client (authenticated Base44 API) calls, upload.ts is the direct-to-storage uploads that deliberately bypass that client. Dropped: MAX_RATE_LIMIT_WAITS, RATE_LIMIT_DELAY_MS, uploadBucketWithRetry, sleep(). The 429 fixed-wait behavior they implemented had no test. typecheck, lint, and knip clean; the deploy specs pass (51 tests, 3 runs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…omment
Comment and docstring cleanup across the site deploy module — the prose was
carrying its own weight badly: restating what the next line of code already
said, re-explaining the same protocol fact at every call site, and narrating
obvious parameters. Net -108 lines with no behavior change.
Also corrects one comment that was actively wrong. CfAssetUploads said the
*last bucket's* reply carries the completion token. Verified against
Cloudflare's direct-upload docs and wrangler's syncAssets(): the server decides
completeness by manifest membership ("once every file in the manifest has been
uploaded"), so the token goes to whichever request completes the set. Buckets
upload concurrently, so that is usually not buckets[n-1] — indexing the final
bucket would read an empty result and discard a token already in hand. The
implementation was already right; only the comment lied.
typecheck, lint, and knip clean; full suite green (714 tests, 71 files).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`base44 deploy` had grown the whole deployments lane: --git-hash, --concurrency, full-stack artifact detection, and a deployment summary. It doesn't need any of it. The fullstack flow is an addition to the existing *site* deploy flow, so that is where it lives. `base44 deploy` reverts to what it was: resources plus the legacy tar.gz site step through `deployAll()`. Reverted to origin/main verbatim — cli/commands/project/deploy.ts (drops the flags, detectAppDeployKind, `site: false`, printDeploymentSummary and the deployment JSON output) and core/project/deploy.ts (drops the `site` option and the buildCommand clause in hasResourcesToDeploy, which only existed to serve the removed flow). `base44 site deploy` keeps the lane and is now its only entry point. With one caller left, the shared addDeploymentOptions() helper was over-abstraction: the two option definitions and their parsers are inlined and cli/commands/site/deploy-options.ts is deleted. Specs for both arms move from the unified deploy to `site deploy`, which also drops their resource-push mocks — nothing pushes resources there. Added a test pinning the decision: `base44 deploy` rejects --git-hash and --concurrency as unknown options and shows neither in --help. typecheck, lint, and knip clean; full suite green (715 tests, 71 files). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #585 pulled the spinner wiring and result shaping out of `deployAction` into `deployToDeploymentsApi` / `deployTarball`. This branch had undone that: the action ended in an if/else chain over a result union, and the progress wiring had moved off to its own cli/commands/site/run-app-deploy.ts. Back to main's shape, with the full-stack flow folded in as a third helper. `deployAction` now reads as plan → confirm → build → dispatch, and each transport owns its own labels, progress and result: - deployFullStackApp — Workers, the cf arm - deployToDeploymentsApi — deployments-API static, the s3 arm - deployTarball — legacy tar.gz (unchanged from main) `runDeployTask` holds the spinner/progress wiring the two deployments-API helpers share, and `deploymentResult` the outro + --json document. run-app-deploy.ts is deleted. That let core shed an orchestrator it no longer needs: `deployAppSite()` and the AppDeployResult union are gone, and core/site/deploy-app.ts is just the planner now — `planAppDeploy()` returns the plan (with outputDir where there is one) and the command calls deployFullStack / deployStaticSite / deploySite itself. Core still decides which transport applies; the CLI no longer round-trips through a second dispatch to find out what it already asked for. The command plans twice, deliberately: once before the build for the prompt and the no-config error, once after for the transport it acts on, since a full-stack artifact is itself a build output. typecheck, lint, and knip clean; full suite green (715 tests, 71 files). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Note
Description
Adds the worker (
cf) arm of the deployments API, so a full-stack framework build (React Router 7, TanStack Start, Astro 6, vinext — anything built through@cloudflare/vite-plugin) can be deployed from the CLI as a Workers deployment addressed by the commit that produced it. The CLI detects the.wrangler/deploy/config.jsonredirect artifact, resolves and validates the generated wrangler config, collects the unbundled worker modules and static assets, POSTs asset buckets directly to Cloudflare with the upload-session JWT, then finalizes with the worker modules. Deploy still only builds — nothing here publishes, and re-deploying the same commit stays idempotent.The static (
s3) arm has since landed onmainas #585, so this branch is merged with it: both arms now share one transport — one upload shape, one retry idiom — and the code lives incore/site/, since deployments are a transport of the site module rather than a module of their own. A final pass trims the module's comments (net −108 lines, no behavior change) and corrects one that was wrong about where the asset-upload completion token comes from.Related Issue
None (pairs with the
cfarm of apper#18596's discriminated create response; #585 has landed, so this is no longer stacked on it)Type of Change
Changes Made
core/site/wrangler-config.ts): the only trigger is the.wrangler/deploy/config.jsonredirect from@cloudflare/vite-pluginbuilds, withconfigPathresolved relative to the redirect file's own directory; hand-authored root configs are deliberately not artifacts. The resolved config must beno_bundle: true, and only fields the deploy acts on are declared, so bindings and the workernameride along ignored.core/site/modules.ts): entry module plus therulesglobs, relative paths as module names,wrangler.json/.dev.vars/the assets dir excluded, sourcemaps beside modules (all of them underupload_source_maps), 40 MB total cap under the server's 50 MB.cfcreate arm (core/site/schema.ts):asset_uploadsbecomes a discriminated union (cfalongsides3); the request carries an optional workerconfigwhose presence is what selects the storage target server-side. Each flow rejects the arm it did not ask for.core/site/upload.ts,api.ts): buckets POST straight to Cloudflare with?base64=trueand a bearer session JWT — never the app client, which would leak app auth. 401/403 maps to "upload session expired — rerun deploy". The completion token is read from whichever bucket reply carries one (the server decides completeness by manifest membership, and buckets upload concurrently, so it is usually not the last bucket), and finalize sends it alongside one multipart part per module.sleep()and bespoke 429 bookkeeping are replaced by a sharedUPLOAD_RETRYon ky. ky retries network errors and its default statuses only, so an expired credential fails fast and a 429 honorsRetry-After. The cf arm must namemethods: ["post"]or bucket uploads would silently never retry.core/site/full-stack.ts): resolve → collect → manifest → create → buckets → finalize, warning (rather than erroring or silently changing behavior) on missingnodejs_compat, wranglervars,_headers/_redirectsandrun_worker_firstroute arrays.core/site/deploy-app.ts):deployAppSite()picks the transport for both commands, and a full-stack artifact wins over both static ones — it carries the server, so shipping static output instead would silently drop the worker.base44 deploynow passessite: falsetodeployAll()and ships the site itself, after the optional build step, so a freshly generated artifact is seen;hasResourcesToDeploy()also counts abuildCommand, since a full-stack project may configure nothing else.--git-hash/--concurrencydefined once inaddDeploymentOptions()and ungated (BASE44_STATIC_DEPLOYMENTSnow only picks a transport, never a flag's existence); git hash from the flag elsegit rev-parse HEAD(core/site/git-hash.ts); one shared spinner runner (cli/commands/site/run-app-deploy.ts) with anonWorkerstep,log.warnwarnings, aDeployment: <id> (commit <hash>)row, and--jsonemitting{deploymentId, gitHash}from both commands.core/site/manifest.ts): per-file MIME lookup for the cf arm's multipart parts; the s3 arm still echoes the server-signedContent-Typeverbatim.core/deployments/folded intocore/site/—full-stack.ts,static-site.ts,deploy.ts(legacy tar.gz) over sharedmanifest.ts,upload.ts,modules.ts,wrangler-config.ts,git-hash.ts,api.ts,schema.ts.docs/deployments.mdrewritten for both arms plus a "never hand-roll upload retry or backoff" rule;docs/resources.md,docs/testing.mdanddocs/AGENTS.mdupdated.Testing
npm test)New:
tests/cli/fullstack_deploy.spec.ts(8 cases — manifest/bucket/finalize relay, null completion token when every asset is already stored, git-hash requirement andargParserrejection,--json,nodejs_compatwarning, bucket retry after a transient failure, session-expired mapping, create failure),tests/core/site-modules.spec.ts(8) andtests/core/site-wrangler-config.spec.ts(7), plus transport-precedence cases insite_deploy.spec.tsand the retargetedstatic_site_deployments.spec.ts. Testkit gainsmockAssetUpload/mockAssetUploadAfterFailures/mockAssetUploadErrorandassetUploadRequests, with a newtests/fixtures/fullstack-project/fixture (redirect file +build/serverworker +build/clientassets with.assetsignore).Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
main.retry.methods, and ky clones a pristine request so a FormData body survives being resent. Dropped with the old loop:MAX_RATE_LIMIT_WAITS,RATE_LIMIT_DELAY_MS,uploadBucketWithRetry,sleep()— the fixed 15s 429 wait had no test.--git-hashis now rejected by the option'sargParserbefore the action runs, not later byresolveGitHash.deployAll()into the deploy command — that's where to look if a site deploy regresses.🤖 Generated by Claude | 2026-08-04 09:46 UTC | 6f5ac21