Cloudflare-Native CI Pipeline (design preview)
Goal: run a CI/CD pipeline entirely on Cloudflare — managed by Tamer — with no GitHub Actions and no Artifacts beta dependency. Containers + R2 + Workflows + Durable Objects only.
This is the "in tandem with Tamer" path: the CI pipeline is itself a Tamer stack, and its final step runs tamer deploy against the consumer stacks it builds.
Architecture
mermaid
flowchart TD
%% ── Boundaries ──────────────────────────────────────────
subgraph REPO["📦 Consumer Project (GitHub)"]
SRC["app source code"]
APPCFG["tamer/project.config.ts<br/>(app stack definition)"]
end
subgraph CF["☁️ Cloudflare Platform"]
subgraph CI["CI Stack — 'the pipeline'"]
direction TB
WH["Webhook Worker<br/><i>validates HMAC, calls create()</i>"]
WF["CI Workflow<br/><i>step.do per stage</i>"]
SB["Sandbox Containers<br/><i>git clone · install · lint ·<br/>test · typecheck · build</i>"]
DOH["Sandbox-Host DO<br/><i>required by containers[ ]</i>"]
R2[("R2 · ci-cache<br/><i>dep snapshots +<br/>build artifacts</i>")]
HEAL["Healer DO<br/><i>optional self-heal</i>"]
end
subgraph APP["App Stack — 'what gets shipped'"]
APPR["consumer app resources<br/>Workers · D1 · R2 · Queues · DO · Routes"]
end
end
subgraph TAMER["🛠️ Tamer (IaC tool + CLI)"]
CICFG["CI stack<br/>project.config.ts"]
end
%% ── Runtime flow (a push) ───────────────────────────────
SRC -->|"git push"| WH
WH -->|"env.CI.create( repo, ref, commit )"| WF
WF -->|"run command"| SB
SB <-->|"snapshot ↔ restore"| R2
SB -.->|"hosted by"| DOH
WF -.->|"on failure"| HEAL
WF ==>|"final step:<br/><b>bunx tamer deploy</b>"| APPR
%% ── Management / declaration ────────────────────────────
CICFG -.->|"tamer apply / deploy<br/>declares + reconciles"| CI
APPCFG -.->|"tamer deploy<br/>(invoked from sandbox)"| APP
%% ── Styling ─────────────────────────────────────────────
classDef managed fill:#1e6f3c,color:#fff,stroke:#0f4a26,stroke-width:2px
classDef passthrough fill:#6b7280,color:#fff,stroke:#374151,stroke-width:2px,stroke-dasharray:4 3
classDef app fill:#1d4ed8,color:#fff,stroke:#1e3a8a,stroke-width:2px
classDef ext fill:#9333ea,color:#fff,stroke:#6b21a8,stroke-width:2px
class WH,WF,DOH,R2,HEAL managed
class SB passthrough
class APPR app
class SRC,APPCFG ext
linkStyle 6 stroke:#1e6f3c,stroke-width:4px,color:#1e6f3cLegend
| Color | Meaning | Resources |
|---|---|---|
| 🟢 Green | First-class Tamer resource — drift / state / cf.* refs | Webhook Worker, CI Workflow, Sandbox-Host DO, R2 cache, Healer DO |
| ⚪ Grey dashed | Passthrough — correct by design (no independent lifecycle) | containers[] (couples to the DO class_name) |
| 🔵 Blue | Consumer app stack — also Tamer-managed, authored by the consumer | the shipped app's Workers/D1/R2/… |
| 🟣 Purple | Lives in the consumer's repo | app source, app project.config.ts |
Runtime flow — what happens on a push
- Push — developer pushes to the consumer repo; GitHub fires the webhook.
- Webhook Worker — validates the HMAC signature (secret via Tamer), then calls
env.CI.create({ repo, ref, commit }). This is the trigger, replacing the Artifactscf.artifacts.repo.pushedevent. - CI Workflow spins up — a Cloudflare Workflow where each
step.do()runs a command inside a sandbox container:checkout—git clone+git checkout <commit>install—bun install --frozen-lockfile, then snapshot to R2 keyed by lockfile hash (the cache; restored on cache-hit)- parallel
lint/test/typecheck/build— each restoring the install snapshot
- Deploy step — runs
bunx tamer deploy --env prodinside the sandbox against the consumer repo it just built. The App Stack updates. ✅ No GitHub Actions in the loop. - Healer DO (optional) — on a failed step, an agent can push a fix branch.
The two things the diagram makes visible
- The green loop at the bottom — Tamer declares the CI stack; the CI stack's deploy step runs Tamer against the App stack. Cloudflare ships Cloudflare.
- Only one grey box —
containers[]is the sole passthrough piece. Everything else (Webhook Worker, CI Workflow, R2 cache, both DOs) is already a first-classcf.*resource. That's "super close" made literal.
What Tamer covers today vs. what we build
| Piece | Status | Tamer surface |
|---|---|---|
| Webhook Worker | ✅ today | plain Worker + workflows binding |
| CI Workflow | ✅ today | cf.workflow("ci") |
| R2 cache | ✅ today | cf.r2("ci-cache") |
| Sandbox-Host DO | ✅ today | cf.durableObject("sandbox-host") + doMigrations |
| Healer DO | ✅ today | cf.durableObject("healer") + doMigrations |
containers[] | ⚠️ passthrough | correct — couples to DO class_name, no lifecycle of its own |
The only code we write is the CI Workflow's step.do() glue (checkout → install → checks → deploy) and a small R2 snapshot/cache helper. No @cloudflare/ci package, no Artifacts binding.
Open decisions (before implementation)
- Base container image — a
Dockerfilewith Bun + git + wrangler + thetamerCLI baked in; referenced viacontainers[].image. - Concurrency cap — Workflows fan out freely; a DO-backed semaphore may be wanted so a burst of pushes doesn't exceed sandbox limits.
- Private repo auth — deploy key / token as a Tamer secret, mounted into the sandbox for
git clone. - Per-stack step definition — hardcode
lint/test/buildin the Workflow, or read a.tamer/ci.tomlfrom each consumer repo so stacks define their own checks.