Skip to content

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:#1e6f3c

Legend

ColorMeaningResources
🟢 GreenFirst-class Tamer resource — drift / state / cf.* refsWebhook Worker, CI Workflow, Sandbox-Host DO, R2 cache, Healer DO
⚪ Grey dashedPassthrough — correct by design (no independent lifecycle)containers[] (couples to the DO class_name)
🔵 BlueConsumer app stack — also Tamer-managed, authored by the consumerthe shipped app's Workers/D1/R2/…
🟣 PurpleLives in the consumer's repoapp source, app project.config.ts

Runtime flow — what happens on a push

  1. Push — developer pushes to the consumer repo; GitHub fires the webhook.
  2. Webhook Worker — validates the HMAC signature (secret via Tamer), then calls env.CI.create({ repo, ref, commit }). This is the trigger, replacing the Artifacts cf.artifacts.repo.pushed event.
  3. CI Workflow spins up — a Cloudflare Workflow where each step.do() runs a command inside a sandbox container:
    • checkoutgit clone + git checkout <commit>
    • installbun 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
  4. Deploy step — runs bunx tamer deploy --env prod inside the sandbox against the consumer repo it just built. The App Stack updates. ✅ No GitHub Actions in the loop.
  5. 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 boxcontainers[] is the sole passthrough piece. Everything else (Webhook Worker, CI Workflow, R2 cache, both DOs) is already a first-class cf.* resource. That's "super close" made literal.

What Tamer covers today vs. what we build

PieceStatusTamer surface
Webhook Worker✅ todayplain Worker + workflows binding
CI Workflow✅ todaycf.workflow("ci")
R2 cache✅ todaycf.r2("ci-cache")
Sandbox-Host DO✅ todaycf.durableObject("sandbox-host") + doMigrations
Healer DO✅ todaycf.durableObject("healer") + doMigrations
containers[]⚠️ passthroughcorrect — 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)

  1. Base container image — a Dockerfile with Bun + git + wrangler + the tamer CLI baked in; referenced via containers[].image.
  2. Concurrency cap — Workflows fan out freely; a DO-backed semaphore may be wanted so a burst of pushes doesn't exceed sandbox limits.
  3. Private repo auth — deploy key / token as a Tamer secret, mounted into the sandbox for git clone.
  4. Per-stack step definition — hardcode lint/test/build in the Workflow, or read a .tamer/ci.toml from each consumer repo so stacks define their own checks.

Released under the Tamer Evaluation License.