Status: operational notes from the first end-to-end sites.smol.ai fixture
trial. This is not a production-readiness claim. It records what happened
when Forge accepted real Git pushes, built and published the three fixtures,
and served them from the hosted edge.
The trial proved the intended end-to-end shape works:
Git push -> exact SHA -> build -> trusted ingest -> immutable deployment -> hosted URL.
It also exposed a reliability gap common to multi-service platforms: a Git push can be accepted while later deployment work is silently absent or blocked by a live-version/configuration mismatch. The fixes below are about making that situation observable, recoverable, and eventually impossible.
The important product rule is:
A successful Git ref update is not deployment success. Forge must show the handoff, build, ingest, provider publication, activation, and live-hostname evidence as separate states.
The first four contracts were rolled out and verified on their owning Cloudflare surfaces on 2026-07-24.
| Contract | Shipped implementation | Live proof |
|---|---|---|
| Durable Git handoff | Enabled-Site ref mutations create a D1 outbox event in the same SQLite statement. A leased dispatcher creates idempotent deployments; a scheduled reconciler repairs missing handoffs and expired leases. | Migrations are ledgered remotely; fixture pushes converged on one deployment. A rollback soak exposed and fixed a reconciler race that could otherwise rebuild the branch head while older code was intentionally active. |
| Versioned release readiness | Each service has a checked-in required-binding contract, version metadata, and a fail-closed health response. The API records release incidents instead of treating missing hosting configuration as a no-op. | API, runner, edge, and runtime binding digests matched their release contracts before activation. |
| Execution evidence | Deployments receive an append-only phase timeline and an execution tuple containing API, runner, image, toolchain, ingest, edge, runtime, provider, and namespace identities. Repository admins can expand “Execution evidence” in deployment history. | Worker versions, runner image/toolchain, D1 records, dynamic runtime behavior, wildcard hostname responses, and usage counters were checked independently. |
| Runner rollout safety | An operator-controlled draining -> verifying -> ready gate blocks new admissions, pins the intended Worker/image identity, and requires a deep toolchain probe. The image exposes a Forge-owned toolchain wrapper with checked esbuild, Wrangler, and SQLite versions plus one digest over the complete contract. Existing Workflow retry classification remains bounded and idempotent. |
A runner can become ready only after a deep service-binding probe confirms the pinned image, esbuild 0.25.8, Wrangler 4.113.0, SQLite 3.37.2, and the aggregate toolchain digest. |
Verification covers fresh and upgraded schema application, binding-manifest drift, TypeScript contracts, retry/backoff policy, diagnostic redaction, runner identity admission, and the existing API/runner/edge/runtime behavioral suites.
429 realtime_quota.noindex, automatic publication protection,
and rollback reconciliation soak passed on live hostnames.The pre-migration recovery set includes a D1 Time Travel bookmark, a full SQL
export, and a schema-only export. The schema-only export imported into scratch
SQLite with integrity_check=ok. The full export downloaded and was hashed but
is not represented as restore-tested because a transcript data statement did
not parse in scratch SQLite.
| Area | What happened | User-visible effect | Immediate recovery | Durable improvement |
|---|---|---|---|---|
| Git-to-deploy handoff | Git receive registered its background work from a pull-driven response stream. The worker could return before waitUntil() retained the work. |
Branch head moved; no deployment row appeared. | Register a deferred background handoff before returning the streaming response. | Transactional outbox plus a reconciler; alert when an eligible ref lacks a deployment. |
| API release configuration | A live API version omitted SITES_HOST_SUFFIX, DEPLOY_RUNTIME, and admin bindings. The Sites trigger treated a missing suffix as a no-op. |
Git push looked healthy but no Site deployment was scheduled. | Redeploy from the canonical Worker configuration. | Release-time binding contract check; fail closed and log an error for eligible Sites when hosting bindings are absent. |
| Site build configuration | The assets-only static fixture still had npm run build recorded in its Site configuration, although its repository intentionally contains prebuilt dist/ assets. |
The deployment failed before ingest because there was no package.json. |
Clear the stale build command. | Show the effective build plan in the UI before queueing; validate it against the exact SHA; provide an explicit “no build” option. |
| Trusted asset ingest | The runner code used a FixedLengthStream to upload R2 assets, but the deployed runner was older than that code. |
Ingest failed with a readable-stream-length error. | Deploy the tested runner version. | Pin runner code, container image, and ingest-policy version in every deployment record and verify them before queueing. |
| Runner upgrade lifecycle | The first jobs after a Durable Object/container runner rollout hit “Durable Object reset because its code was updated.” | A short burst of builds failed immediately after the rollout. | Retry after the runner stabilized. | Drain/health-gate upgrades; classify this error as retryable; do not expose a raw terminal failure until retry budget is exhausted. |
| Runner image availability | Dynamic fixture bundling initially reported /usr/local/bin/esbuild missing despite the desired image containing it. Existing runner/container instances were still converging. |
Dynamic reservations and realtime builds failed; static succeeded. | Retry after the new runner image became active. | Startup self-test of the exact runner image; versioned tool manifest; admission gate that blocks dynamic jobs until the healthy image revision is confirmed. |
| Evidence gathering | Deployment state, Worker version/bindings, runner logs, D1 records, and live hostname each answered different questions. | It was easy to mistake “push succeeded” for “deployment succeeded.” | Trace each surface separately. | One deployment timeline that links every controlling ID and surface, with a single support bundle. |
The immediate waitUntil() change prevents a streaming-response timing race,
but it is not the final reliability boundary. A correct control plane needs a
database-backed outbox.
repository_ref_changed outbox record in one
transaction.The idempotency key should include repository, ref, exact SHA, trigger type, and effective project configuration version. Replays must create at most one deployment for the same intended work.
An absent hosting binding is not “this repository has no Site” once Forge has already identified an enabled, eligible Site. The trigger should:
At deploy time, each Worker must validate its required bindings and variables against a checked-in release manifest. The deployment should fail before traffic is switched if the actual binding set differs from that manifest.
Every deployment should record the versions that actually executed it:
The UI should display these in an advanced “execution evidence” panel, while support tooling can search them directly. A deployment should only start when the runner has reported the requested image/toolchain as healthy.
Deploying a Worker that owns Durable Objects or container instances is a rolling operational event, not merely a code upload.
Provider traffic can point at the new Worker before every Sandbox reservation
observes the new container image. --containers-rollout=immediate does not make
those two completion proofs atomic. Release verification must keep the exact
Worker version pinned while it retries only explicit toolchain-convergence
codes. Forge bounds this to six probes with 110 seconds of total delay and
re-publishes verifying before each probe because a failed probe moves control
to failed. Any different Worker version, non-toolchain binding failure, or
exhausted budget fails the release and enters the normal rollback path.
The image cannot define which digest an arbitrary Worker version validates.
Current Workers publish readiness.observed_toolchain_digest; Deploy control
persists that exact value. Compatibility with an older Worker is a field-
absence fallback to its checked esbuild digest, including when that Worker sees
a v2 image. If a Worker publishes the field with an invalid value, verification
fails closed rather than silently selecting a digest from image metadata.
Required upgrade behavior:
draining before the rollout;ready only after a real synthetic static build and dynamic bundle
check pass;The hard-coded esbuild executable path should become a checked startup assertion. Better still, the trusted toolchain should expose one Forge-owned wrapper command whose version and digest are recorded; bundling code should not rely on an undocumented global path.
Deployment UI and API should show, for the exact commit:
| Field | Examples |
|---|---|
| Build command | none, npm run build, node scripts/build.mjs |
| Output root | dist, build, or manifest asset source |
| Manifest source | forgeBuild.ts path and normalized digest |
| Entrypoint | absent for static-only; source file for an application |
| Requested capabilities | assets, HTTP, durable state, realtime |
Before allocating a runner, Forge should reject obvious contradictions:
This turns a remote runner failure into a clear configuration diagnosis.
The fixed-length upload requirement is correct: the trusted ingest path should know the byte count before R2 accepts a stream. Keep these invariants:
Add a remote integration test that uploads a streamed artifact through the actual Worker binding. Local mocks alone cannot detect provider stream semantics.
Forge should explicitly present these independent facts:
stateDiagram-v2
[*] --> RefUpdated
RefUpdated --> HandoffQueued
HandoffQueued --> DeploymentCreated
DeploymentCreated --> Building
Building --> Ingesting
Ingesting --> ProviderPublished
ProviderPublished --> Verified
Verified --> PreviewReady
PreviewReady --> Active: auto publish or promote
Building --> Failed
Ingesting --> Failed
ProviderPublished --> Failed
Failed --> Retrying
Retrying --> BuildingFor each edge, retain a timestamp, attempt number, idempotency key, and a
sanitized diagnostic. The UI must never collapse RefUpdated into
PreviewReady or Active.
For every deployment, generate a compact support bundle containing:
Provider identifiers belong in this advanced evidence record; Forge IDs remain the primary product handles.
Track at least:
git_ref_to_deployment_created_seconds and the count of eligible refs with
no deployment after the service-level objective;Alerts should distinguish a single repository configuration error from a global API-binding, runner-image, R2, dispatch, or edge outage.
GET /api/admin/deploy-reliability/usage is restricted to platform Site
administrators. It reports the durable D1 control-plane totals for repository
event and deployment queues, successful deployment artifact files/bytes, and
deployment counts and reuse totals by execution_path.
Request and bandwidth usage has a different owning surface and is not copied
into D1. The Sites edge writes one point per response to the Cloudflare
Analytics Engine dataset forge_sites_usage_preview through the SITE_USAGE
binding:
| Slot | Meaning |
|---|---|
| index | Site ID, or platform before a Site is resolved |
| blob 1 | Deployment ID |
| blob 2 | production, preview, or platform |
| blob 3 | Request class: asset, application, realtime, client, health, or platform_error |
| blob 4 | Sites Worker version ID |
| double 1 | Request units, currently 1 |
| double 2 | Response bytes from trusted Content-Length, or 0 when unknown |
| double 3 | Edge handling duration in milliseconds |
| double 4 | HTTP response status |
| double 5 | Dynamic request units, 1 for application/realtime and otherwise 0 |
Cost and bandwidth reporting must query this dataset by Site and time window. The D1 endpoint intentionally returns the dataset contract rather than presenting incomplete request or byte totals as authoritative.
Reliability machinery can itself become an abuse multiplier, so the trial slice applies the following limits:
ready is forbidden; readiness requires the deep
runner probe.Before broader self-service, add retention for delivered/dead-letter outbox rows, per-repository handoff rate metrics, anomaly alerts for repeated ref churn, and a global emergency pause that halts dispatcher claims without deleting intent. These are capacity and abuse controls, not substitutes for existing repository authorization or Sites suspension.
Run these in order so a failure is attributable and rollback remains small:
/health, run one normal push, and verify the ref event, deployment,
workflow, and handoff timeline share the expected SHA.draining, wait for active work to settle, set verifying, and invoke
the deep probe. Only a matching Worker/image/toolchain tuple may become
ready.Forge has no production data contract yet. Schema changes replace the five size-bounded fragments of one fresh-install schema and require recreation of the pre-production D1 database. The verifier applies those fragments to an empty database and fails on integrity, foreign-key, required-object, seed-signature, or forbidden compatibility-object mismatches. No ledger-adoption or upgrade procedure exists.
Before opening the trial beyond the allowlist, automate these remote checks:
index.html, while an asset fetch still returns a real 404.