Skip to content

feat: static-site deploys through the deployments API (s3 arm, env-gated) - #585

Merged
netanelgilad merged 12 commits into
mainfrom
fullstack/static-workers-lane
Aug 4, 2026
Merged

feat: static-site deploys through the deployments API (s3 arm, env-gated)#585
netanelgilad merged 12 commits into
mainfrom
fullstack/static-workers-lane

Conversation

@netanelgilad

@netanelgilad netanelgilad commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Note

Description

Adds an env-gated lane for deploying a site's built output through the new deployments API, alongside the existing tar.gz upload. The output directory is hashed into a content-addressed asset manifest, the deployment is created at the commit's address (git_hash), only the assets the server still owes are PUT directly to presigned S3 URLs, and index.html is sent as the finalize sentinel. The lane is reachable only from base44 site deploy --git-hash <commit> and only when BASE44_STATIC_DEPLOYMENTS=1; with the gate off, behavior is unchanged from the legacy path.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

Core (src/core/site/)

  • manifest.ts — walks the output directory with globby (dotfiles included, symlinks not followed), honors .assetsignore via globby's ignoreFiles for real gitignore semantics (anchoring, directory patterns, negation), and hashes each file as the first 32 hex chars of sha256(utf8(app_id) || bytes). The app-id salt means a tenant can only collide with its own files. Caps: 25 MiB per file (stat before read), 100,000 files.
  • static-site.ts — the flow: build manifest → fail early if there is no /index.htmlcreateDeployment() → upload owed assets → finalize with the index.html bytes.
  • upload.ts — presigned PUTs via plain ky (no app client, no auth headers — the URL is the credential), echoing the server-signed Content-Type verbatim, 3 attempts with exponential backoff, concurrency via p-map (default 3, max 50).
  • api.ts / schema.tsPOST deployments and POST deployments/{id}/finalize with Zod snake_case→camelCase transforms; asset_uploads is modeled as a discriminated ADT ({type: "s3", ...} or null) so a worker arm can slot in later without protocol changes.
  • utils/git.tsisGitCommitHash(), matching the server's ^[a-fA-F0-9]{7,64}$.

CLI (site deploy)

  • --git-hash <hash> and --concurrency <n> are registered only when BASE44_STATIC_DEPLOYMENTS is 1/true, so with the gate off the flags are absent from --help and rejected as unknown options. Both validate in Commander argParsers, before the action runs.
  • The action forks on options.gitHash: present → deployStaticSite(), absent → the legacy deploySite() tar.gz upload. Each branch owns its spinner labels and result shape; --json emits {deploymentId, gitHash}.
  • base44 deploy (unified) is untouched — it still ships the site through the legacy tar.gz step, gate on or off.

Deps / repo

  • Adds p-map, bumps globby to ^16.2.2 (for ignoreFiles), both in devDependencies per the bundled-distribution rule.
  • .gitattributes pins test fixtures to LF so content-addressed hashes don't shift on Windows checkouts.

Docs

  • New docs/deployments.md topic guide (contract, hashing, gate, rules), linked from docs/AGENTS.md; updates to docs/resources.md (two site transports) and docs/testing.md (deployment mocks).

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

New coverage:

  • tests/cli/static_site_deployments.spec.ts (11 cases) — gate off hides and rejects --git-hash/--concurrency; gate off and gate-on-without-commit both keep the legacy tar.gz upload; full happy path asserting the create body (git_hash, manifest keys, no config), presigned bodies byte-for-byte with the signed Content-Type and no Authorization header, and a single-field index.html multipart finalize; asset_uploads: null sends no PUTs but still finalizes; --json shape; invalid --git-hash and out-of-range --concurrency.
  • tests/core/site-manifest.spec.ts (12 cases) — hash formula and app-id salting, manifest key/shape, .assetsignore globs/negation/anchoring/literal braces, dotfiles, always-ignored names, the 25 MiB limit, and hash dedupe.
  • TestAPIServer gains mockDeploymentCreate, mockPresignedUpload, mockDeploymentFinalize with request capture.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The lane is experimental and off by default: nothing about it surfaces until BASE44_STATIC_DEPLOYMENTS is set. A deployment addresses a build, not production — there is no --prod, promote, rollback, or list surface; publishing stays with the platform. Deployment ids are derived from git_hash, so re-deploying a commit is idempotent. Adopting the lane in base44 deploy is a deliberate follow-up.


🤖 Generated by Claude | 2026-08-04 05:36 UTC | ca3aebe

@netanelgilad netanelgilad self-assigned this Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.7-pr.585.ca3aebe

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.1.7-pr.585.ca3aebe"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.7-pr.585.ca3aebe"
  }
}

Preview published to npm registry — try new features instantly!

@netanelgilad
netanelgilad force-pushed the fullstack/static-workers-lane branch from 299efba to d4f34d4 Compare August 3, 2026 06:23
@netanelgilad netanelgilad changed the title feat: env-gated static-site deploys through the deployments API (SPA on Workers) feat: env-gated static-site deploys through the deployments API (discriminated create: S3 today, Workers when fullstack) Aug 3, 2026
@netanelgilad
netanelgilad force-pushed the fullstack/static-workers-lane branch 2 times, most recently from 0f8f74c to 4480729 Compare August 3, 2026 13:55
@netanelgilad netanelgilad changed the title feat: env-gated static-site deploys through the deployments API (discriminated create: S3 today, Workers when fullstack) feat: static-site deploys through the deployments API (direct uploads, discriminated create) Aug 3, 2026
…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
@netanelgilad
netanelgilad changed the base branch from fullstack/deploy-git-hash to main August 3, 2026 17:17
@netanelgilad
netanelgilad force-pushed the fullstack/static-workers-lane branch from 9eab252 to 3461f00 Compare August 3, 2026 17:17
@netanelgilad netanelgilad changed the title feat: static-site deploys through the deployments API (direct uploads, discriminated create) feat: static-site deploys through the deployments API (s3 arm, env-gated) Aug 3, 2026
@netanelgilad
netanelgilad enabled auto-merge (squash) August 4, 2026 05:35
@netanelgilad
netanelgilad merged commit 19d7989 into main Aug 4, 2026
12 checks passed
@netanelgilad
netanelgilad deleted the fullstack/static-workers-lane branch August 4, 2026 07:31
netanelgilad added a commit that referenced this pull request Aug 4, 2026
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>
netanelgilad added a commit that referenced this pull request Aug 4, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants