Roadmap & V0 Plan
FlareDispatch — Roadmap & V0 Plan
Project-management reference for FlareDispatch: the phased delivery roadmap, then the detailed V0 walking-skeleton build plan. The run catalog is in 02-runs; the product framing is in PRD.
Implementation status (HEAD). V0 has shipped and implementation has continued out of strict V order — well past the original phasing. Live at HEAD: 11 registered runs (
/healthlists them sorted:cdp-acceptance,ci-triage-pr,deploy-smoke,matrix-fanout,offload-test,playwright-demo,playwright-e2e,pr-review,product-demo,refresh-fixtures,spec-drift-pr); all three trigger modes — Action mode, Schedule mode (live crons), and Webhook mode (POST /v1/webhooks/github, shipped but opt-in / off by default — a deploy withoutGITHUB_WEBHOOK_SECRET503s that route); the GitHub App manifest-creation install flow; the JS-bundled flare-dispatch-action; ~15 run-author capabilities + 10 primitives over Containers / Browser Rendering / R2 / D1 / KV — including themodelGateway,childRuns,oidccapabilities and theawsAssumeRole,fanOut, andwaitForChildrenprimitives. Also live: agentic PR review (pr-review, configurable single- or multi-agent), the global admission queue, per-container leases, dual sandbox images, browsable artifact bundles,/v1/admin/events/:wf_id+step.waitForEvent, the OIDC issuer routes, and writeback (refresh-fixtures). Still Planned:step.sleep/step.sleepUntil(acknowledged stubs), parts of thegithubread capability (github.repositories/github.openPullRequestsstill die — V3),security-scan+custom-sandbox(V3), OpenTelemetry / Logpush /initCLI (V4). Per-section Status callouts in 01-architecture, 02-runs, 03-dsl, 04-gha-integration, and 05-byoc carry the detailed pinning.
Roadmap
Delivery phasing — what ships in each version and the exit criterion that closes it. V0 is the slice that proves the model; V1–V4 are incremental and independently shippable.
timeline
title FlareDispatch delivery roadmap
V0 Walking skeleton : Dispatcher + one Workflow + one Sandbox + check-run callback : ships offload-test
V1 Fan-out, cache, artifacts : Queues for matrix, R2 cache helper, R2 artifact upload : ships matrix-fanout, cache-pnpm, r2-artifacts
V2 Browser e2e and acceptance : Browser Rendering integration, CDP observation helper : ships playwright-e2e, cdp-acceptance
V3 Long-running and security : step chaining past the Workflow step limit, security scans, Schedule mode (Cron Triggers + scheduling Workflow) : ships security-scan, custom-sandbox, pr-review-sweep recipe
V4 Polish : OpenTelemetry export, Logpush, retention policies, init CLIPhases
| Phase | Scope | Runs shipped | Exit criterion |
|---|---|---|---|
| V0 — Walking skeleton | Dispatcher Worker + one Workflow + one Sandbox + check-run callback | offload-test | A pnpm test executing in CF Sandbox reports green/red to a PR check |
| V1 — Fan-out + cache + artifacts | Queues for matrix; R2 cache helper; R2 artifact upload with signed URLs — shipped: matrix-fanout shards a command across N parallel containers within one Workflow via the sharded primitive; Webhook mode (POST /v1/webhooks/github, opt-in) + IDEMPOTENCY_KV receiver-dedup; browsable artifact bundles (GET /v1/artifacts/<exec>/<name>/) | + matrix-fanout, + cache-pnpm, + r2-artifacts (primitives) | 8-shard test matrix on CF beats GHA wall time on a real repo — met (matrix-fanout shards via the sharded primitive) |
| V2 — Browser e2e + acceptance | Browser Rendering integration; CDP observation helper; browser-pooled playwright-e2e — shipped | + playwright-e2e, + cdp-acceptance | Sharded Playwright suite reports per-shard status; gctrl-board acceptance suite executes — met |
| V3 — Long-running + security | Step chaining for suites past the Workflow step limit; security scan runs; Schedule mode — scheduled() handler, triggers.crons, scheduling Workflow (enumerate + fan-out), schedules on defineRun, step.sleepUntil. Shipped here: /v1/admin/events/:wf_id + step.waitForEvent (human-in-loop signalling, opt-in via ADMIN_TOKEN — 503 unset) | + security-scan, + custom-sandbox; pr-review-sweep recipe | 30-min suite completes; npm audit / cargo audit / trivy run in Sandbox; a cron tick fans out a pr-review per open PR. Still Planned: step.sleep / step.sleepUntil (stubs); security-scan + custom-sandbox; the github.openPullRequests read used by the PR-sweep cron (still dies) |
| V4 — Polish | OpenTelemetry export, Logpush integration, retention policies, flare-dispatch init CLI | — | Time-to-first-green-check < 30 min on a fresh CF account |
| V3.5 — OIDC federation (parallel to V3) | oidc capability + awsAssumeRole primitive; /.well-known/openid-configuration + /.well-known/jwks.json issuer routes; OIDC_SIGNING_JWK Worker secret + pnpm cli oidc keygen helper | + awsAssumeRole primitive | Met by pr-review’s bedrock backend: the run mints short-lived AWS creds via OIDC federation (issuer → JWKS → STS AssumeRoleWithWebIdentity) per execution, then calls Bedrock InvokeModel SigV4-signed through the AI Gateway — zero long-lived AWS keys in Worker Secrets. Spec contract: 03-dsl § oidc, 05-byoc § AWS federation trust policy. Lifted upstream from a downstream consumer so adopters wiring AWS don’t fork. |
| Beyond the roadmap — agentic review + in-Worker LLM + capacity control (shipped out of V order) | Agentic PR review — the pr-review run, configurable single- or multi-agent (pr-review.agents); diff capped to the chosen model’s context window, scannable summary-table + per-finding headings + GitHub blob links, re-reviews auto-resolve fixed findings. modelGateway capability — review runs call an @effect/ai model gateway in-Worker with backends selectable from CONFIG_KV without redeploy: Workers AI catalog incl. DeepSeek reasoners (binding-as-auth or BYOK, the workers-ai route), Anthropic-via-AI-Gateway (BYOK), Bedrock-via-AI-Gateway (OIDC → STS → SigV4). Global admission queue — a per-pool FIFO in D1 caps in-flight runs at ADMISSION_CAP (default 16); overflow queues + drains FIFO, crashed-holder slots reclaimed by heartbeat TTL, only a never-freed pool fails AdmissionTimedOut; zero new operator config (migration 0003). Writeback — a run regenerates files in a credential-free container and the Worker (sole creds holder) validates a changed-files manifest (path-traversal / allowlist / byte+count caps, .github/workflows opt-in gate) and commits a branch + PR via the Git Data API; best-effort — a failure annotates the green check, never flips the run red. | + pr-review, + refresh-fixtures (writeback worked example) | A dispatched PR posts an agentic review comment; review backend is swappable from CONFIG_KV without redeploy; in-flight runs never exceed the container pool; refresh-fixtures opens a PR from a credential-free container — all met at HEAD. |
V0 walking-skeleton plan
The smallest end-to-end slice that proves the model from the roadmap above:
V0 acceptance: a
pnpm testexecuting in CF Sandbox reports green/red to a PR check.
Everything else from V1–V4 is deferred. This plan covers what we build, in what order, and how we know it works.
1. Scope
- Dispatcher Worker — HMAC verify on
POST /v1/dispatch/offload-test, instantiate the Workflow, return202 {executionId}. PlusGET /healthand a single artifact endpointGET /v1/artifacts/:execution/:namethat 302-redirects to a short-lived R2 signed URL. - One Workflow class —
RunWorkflow extends WorkflowEntrypoint, dispatches tooffload-test.rununder an Effect runtime. - One run —
offload-test(clone → exec → upload log → finalize). Inputs/outputs per 02-runs § 1. - Sandbox / Container binding — single container per execution, default Node image.
- R2 bucket —
logs/<execution-id>/<step>.ndjsononly (nocache/, noartifacts/directory tar pipeline). - D1 —
executions+stepstables per 05-byoc § D1 schema. - GitHub App — JWT → installation token →
POST /repos/.../check-runs(in_progress) andPATCH .../check-runs/{id}(completed). - Effect-TS DSL surface —
defineRun,step,sandbox.git.clone,sandbox.exec,artifact.upload(logs only),io.now,io.uuid,io.log. Tagged errors from 03-dsl § Errors. All other DSL surface stubbed toEffect.die("not implemented in V0"). - GHA Action —
action.yml+ a bundled JS entry that HMAC-signs the body and POSTs. Fire-and-forget only. (Originally landed as a composite Action with a ~30 LOCdispatch.sh; replaced in PR #27 with a bundled JS Action built frompackages/cli/src/action-entry.tsfor typed errors +safeForCmdescaping.)
2. Out of scope (deferred to V1+)
| Deferred | Why |
|---|---|
| Matrix fan-out (Queues + Coordinator DO + child Workflows) | Adds three components (Queue, DO, spawner) and the per-shard check-run aggregation. None of it is needed to prove a single container reports green/red. → V1. Since shipped: matrix-fanout shards a command across N parallel containers (one container per shard) within a single Workflow via the sharded primitive. The childRuns capability + waitForChildren primitive (independent child-Workflow fan-out, PR #80) are live too, as a separate mechanism. |
Browser Rendering binding, playwright-e2e, cdp-acceptance | Requires browser pool + CDP plumbing + report merging. Orthogonal to “Sandbox → check-run.” → V2. Since shipped: both runs are registered; the browser-pooled playwright-e2e and the CDP bridge (GET /v1/browser/cdp) are live. |
Cache restore/save (cache.restoreOr, cache.save) | Optimization, not a correctness primitive. V0 re-executes pnpm install every execution; that’s fine for a smoke. → V1. |
Other runs (matrix-fanout, security-scan, custom-sandbox) | One run is enough to prove the contract; the others are variations on the same DSL. → V1/V3. |
CLI (flare-dispatch init, flare-dispatch dispatch, etc.) | A curl script and wrangler deploy cover V0 onboarding. → V4. |
| OpenTelemetry export | Workflows’ built-in metrics + R2 NDJSON logs are enough to debug V0. → V4. |
Multi-environment (env.staging / env.prod) | Single deploy on *.workers.dev. Splitting environments is mechanical once V0 works. → V4. |
| Retention crons (R2 lifecycle, D1 prune) | At V0 volumes, retention is “delete the bucket if you want a reset.” → V4. |
| Custom domain | https://flare-dispatch-v0.<account>.workers.dev is the public endpoint. Custom domain is DNS, not code. → V4. |
await mode in the GHA Action | Fire-and-forget covers the acceptance criterion. Await mode adds polling + GHA timeout logic. → V1. |
App-webhook trigger surface (POST /v1/webhooks/github) | V0 proves the HMAC-POST dispatch path end-to-end. The autonomous App-webhook trigger (04-gha-integration § Webhook mode) adds receiver-side gate logic, run-level trigger config, and check_run.rerequested re-run handling — orthogonal to “Sandbox → check-run.” → V1. Since shipped: the receiver is live but opt-in / off by default — a deploy without GITHUB_WEBHOOK_SECRET 503s the route. It verifies X-Hub-Signature-256, dedups on X-GitHub-Delivery, and fans out a Workflow per matching triggers[]. |
Schedule-mode trigger surface (scheduled() handler + triggers.crons) | V0 has no time-triggered runs. Schedule mode (04-gha-integration § Schedule mode) adds the scheduled() handler, the scheduling Workflow (enumerate targets → fan out), and schedules on defineRun. It builds on the V1 App-JWT enumeration + dedup KVs and the V1 createBatch fan-out — nothing to prove until those exist. → V3. |
step.waitForEvent + /v1/admin/events/:wf_id | Human-in-loop runs (release approval, manual gates) aren’t part of the V0 acceptance criterion. The DSL primitive is documented in 03-dsl § Human-in-the-loop but the Dispatcher route + CF Access wiring lands with the first run that needs it. → V2/V3. Since shipped: step.waitForEvent delegates to StepRunner.waitForEvent, and the POST /v1/admin/events/:wf_id route signals it (opt-in via ADMIN_TOKEN — 503 unset; bearer-over-Access). Receiver-level (wf_id, decider_email) dedup still lands with the first run that uses it. |
IDEMPOTENCY_KV / INSTALL_TOKEN_KV KV namespaces | V0 dispatches are caller-driven (one POST per CI run, no redelivery storm to absorb), and a single Worker process can keep the install token in memory. The dedicated KVs land with the App-webhook trigger in V1, where receiver-level dedup on X-GitHub-Delivery is load-bearing. → V1. Since shipped: the Webhook receiver dedups on X-GitHub-Delivery via IDEMPOTENCY_KV (24h TTL) when the binding is present. |
| Container image build & publish to GHCR | V0 references node:lts-slim or a hand-built local image; the OHC base images are a separate stream. → V1. |
3. Repository layout for V0
flare-dispatch/
├── wrangler.jsonc # bindings: Workflow, Container, R2, D1; no DO/Queue/Browser in V0
├── package.json # pnpm workspace root
├── pnpm-workspace.yaml # packages/* + apps/dispatcher
├── tsconfig.base.json # strict TS + Effect-friendly settings
├── .github/
│ └── workflows/
│ └── ci.yml # typecheck + vitest on every PR
├── apps/
│ └── dispatcher/
│ ├── package.json # depends on @flare-dispatch/core + @flare-dispatch/runtime-cf
│ ├── src/
│ │ ├── index.ts # Worker entry: fetch handler dispatching to routes
│ │ ├── routes/
│ │ │ ├── dispatch.ts # POST /v1/dispatch/:run — HMAC verify + instantiate Workflow
│ │ │ ├── artifacts.ts # GET /v1/artifacts/:execution/:name — sign + 302 redirect to R2
│ │ │ └── health.ts # GET /health — returns {status, runs}
│ │ ├── hmac.ts # constant-time HMAC-SHA256 verify
│ │ ├── workflow.ts # RunWorkflow class extending WorkflowEntrypoint
│ │ └── env.ts # typed Env interface for bindings
│ └── tsconfig.json
├── packages/
│ ├── core/ # @flare-dispatch/core — DSL primitives
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts # public exports
│ │ │ ├── define-run.ts # defineRun constructor + Run<I,O> type
│ │ │ ├── step.ts # step() — wraps an Effect in a Workflow checkpoint
│ │ │ ├── errors.ts # Schema.TaggedError classes from 03-dsl § Errors
│ │ │ ├── context.ts # RunContext = Context.Tag union of services
│ │ │ ├── services/ # capabilities — one Context.Tag per namespace
│ │ │ │ ├── sandbox.ts # Context.Tag for SandboxService + interface
│ │ │ │ ├── artifact.ts # Context.Tag for ArtifactService + interface
│ │ │ │ ├── io.ts # Context.Tag for IOService + interface
│ │ │ │ ├── checks.ts # Context.Tag for ChecksService (GitHub check-runs)
│ │ │ │ └── executions.ts # Context.Tag for ExecutionsService (D1 metadata writes)
│ │ │ ├── primitives/ # reusable compositions — 03-dsl § Primitives
│ │ │ │ ├── index.ts # @flare-dispatch/core/primitives public exports
│ │ │ │ ├── workspace.ts # acquire + clone (+ optional cached install)
│ │ │ │ ├── install-cached.ts # cache.restoreOr keyed on lockfile hash
│ │ │ │ ├── sharded.ts # count-and-index parallel fan-out
│ │ │ │ ├── boot-app.ts # runDetached + waitForPort
│ │ │ │ └── probe-http.ts # curl-and-classify endpoint probe
│ │ │ └── fakes/ # in-memory Layers for unit tests
│ │ │ ├── sandbox-fake.ts # records exec calls; returns canned ExecResult
│ │ │ ├── artifact-fake.ts # in-memory map of name → fake signed URL
│ │ │ ├── io-fake.ts # deterministic now/uuid for tests
│ │ │ ├── checks-fake.ts # records check-run create/update calls
│ │ │ └── executions-fake.ts # in-memory executions + steps tables
│ │ └── tsconfig.json
│ ├── runtime-cf/ # @flare-dispatch/runtime-cf — live CF bindings
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts # exports CFRuntimeLive Layer
│ │ │ ├── sandbox-cf.ts # SandboxService via Containers binding
│ │ │ ├── artifact-r2.ts # ArtifactService backed by R2 bucket
│ │ │ ├── io-live.ts # IOService using globalThis.crypto + Date
│ │ │ ├── executions-d1.ts # ExecutionsService via D1 binding (INSERT executions/steps)
│ │ │ └── checks-github.ts # ChecksService via GitHub App installation token
│ │ └── tsconfig.json
│ └── github-app/ # @flare-dispatch/github-app — App auth helpers
│ ├── package.json
│ ├── src/
│ │ ├── index.ts
│ │ ├── jwt.ts # sign App JWT with RS256 from PEM secret
│ │ ├── installation-token.ts # exchange JWT for installation token; cache in-memory
│ │ └── check-runs.ts # POST/PATCH /repos/{owner}/{repo}/check-runs
│ └── tsconfig.json
├── runs/
│ └── offload-test.ts # the V0 run (see 03-dsl § Top-level shape)
├── infra/
│ ├── migrations/ # wrangler-tracked D1 migrations (executions + steps in 0001)
│ └── github-app-manifest.json # GitHub App manifest (see 05-byoc § GitHub App setup)
├── actions/
│ └── flare-dispatch-action/
│ ├── action.yml # JS Action: 'using: node20', main dist/index.js (PR #27)
│ ├── dist/index.js # bundled from packages/cli/src/action-entry.ts
│ └── README.md # usage snippet
├── README.md # quickstart: wrangler deploy + Action snippet
└── specs/ # this directory (unchanged in V0)
4. PR sequence
Each PR targets main, is independently mergeable, and ships a single concern. The order makes downstream PRs reviewable in isolation (the prior PR’s surface is already merged).
PR 1 — Repo scaffold + wrangler config
- What: pnpm workspace,
tsconfig.base.json,wrangler.jsoncdeclaring V0 bindings only (Workflow, Container, R2, D1 — no Queue/DO/Browser),infra/d1-schema.sql, emptyapps/dispatcher/src/index.tsreturning{status: "ok"}on/health, CI workflow executingpnpm typecheck+pnpm test. - Verifiable acceptance:
pnpm install && pnpm typecheck && wrangler deploy --dry-runexits 0;wrangler d1 execute flare-dispatch-v0 --file infra/d1-schema.sql --localcreates both tables.
PR 2 — @flare-dispatch/core DSL + tagged errors + fakes
- What:
defineRun,step, allSchema.TaggedErrorclasses from 03-dsl § Errors,Context.Tags forSandboxService/ArtifactService/IOService/ChecksService/ExecutionsService, and the in-memory fake Layer for each. No live implementations. - Verifiable acceptance:
pnpm --filter @flare-dispatch/core testpasses. A unit test composesstep("a", () => Effect.succeed(1))and asserts the run runtime invokes the fakeExecutionsServiceonce per step.Match.exhaustiveon every tagged error compiles.
PR 3 — offload-test run + run-level unit tests
- What:
runs/offload-test.tsexactly as sketched in 03-dsl § Top-level shape. Uses onlysandbox.git.clone,sandbox.exec,artifact.upload,io.now. Unit tests underruns/offload-test.test.tsusingCFRuntimeTest+ asandboxFakeProgrammatching the 03-dsl § Unit-testing runs pattern. - Verifiable acceptance:
pnpm testpasses for: (a) green path — fakepnpm testexits 0, output.exitCode === 0; (b) red path — fake exits 1, output.exitCode === 1, no thrown error; (c) timeout — fake raisesExecTimeout, run re-fails with the same tag.
PR 4 — Live runtime Layers (@flare-dispatch/runtime-cf) + RunWorkflow class
- What:
SandboxCloudflareLivecalling the Containers binding,R2ArtifactLivewriting log NDJSON,D1ExecutionsLivewritingexecutions/stepsrows,IOLiveusing platformcrypto.randomUUID()/Date.now().apps/dispatcher/src/workflow.tsexportsRunWorkflow extends WorkflowEntrypoint, whoserun(event, step)maps eachstep.do(name, ...)call to a runstep(name, ...)boundary.wrangler.jsoncworkflows binding added. - Verifiable acceptance:
pnpm dev(wrangler dev) +curl -X POST http://localhost:8787/v1/dispatch/offload-test -H 'X-FlareDispatch-Signature: sha256=<hmac>' -d @fixtures/dispatch.jsonreturns202 {executionId}, andwrangler d1 execute --localshows one row inexecutionsand N rows insteps. R2logs/<executionId>/exec.ndjsonexists in local Miniflare R2.
PR 5 — Dispatcher routes + HMAC verify + artifact signed-URL endpoint
- What:
apps/dispatcher/src/routes/dispatch.tsdoes Schema-validate the body againstoffload-test.inputs, HMAC-verify with constant-time compare againstenv.HMAC_SECRET, then callenv.RUNS_WORKFLOW.create({...}). AddGET /v1/artifacts/:execution/:namethat signs an R2 URL and 302-redirects.GET /healthlists registered runs. - Verifiable acceptance:
vitest run apps/dispatchercovers: invalid HMAC → 401; valid HMAC + invalid body → 400 with Schema error inlined; valid HMAC + valid body → 202 +{executionId}. A separate test fetches/v1/artifacts/<executionId>/exec.ndjsonafter a fake execution and asserts 302 with a signed URL pointing at R2.
PR 6 — GitHub App auth (@flare-dispatch/github-app) + ChecksService live binding
- What: RS256 JWT signer using the PEM secret, installation-token exchange + cache (Worker memory only in V0 — the
INSTALL_TOKEN_KVfallback is deferred to V1 per § 2; see 04-gha-integration § Check-runs callback),POST/PATCHto/repos/{owner}/{repo}/check-runs. WireChecksGithubLiveinto the runtime Layer sooffload-testpostsin_progresson start andcompletedwith conclusion at end. - Verifiable acceptance: Integration test against an MSW-mocked
api.github.comasserts: (a) one POST to/repos/.../check-runswithstatus: in_progress; (b) one PATCH withstatus: completedandconclusion: successfor green,failurefor red. End-to-end manual: dispatch against a real test repo, observe check-run appears on the commit’s Checks tab.
PR 7 — GHA Action + acceptance smoke
- What:
actions/flare-dispatch-action/action.ymlcalling a HMAC-signing entry point that POSTs the dispatch body and exits 0 on 202. Originally landed as ausing: compositeAction withdispatch.sh(~30 LOC:openssl dgst -sha256 -hmac,curlPOST); refactored in PR #27 to ausing: node20JS Action bundled frompackages/cli/src/action-entry.ts(typed errors,safeForCmdworkflow-command escape,http(s)-only scheme allowlist). Plus a.github/workflows/acceptance.ymlin this repo that uses the local action against the live deploy. QuickstartREADME.mdwith copy-paste deploy steps. - Verifiable acceptance: A PR against this repo triggers
.github/workflows/acceptance.yml, which calls the Action, which dispatchesoffload-testwithcommand: "pnpm test"against this repo’s own SHA. The check-run posted by the Worker turns green and appears as a required-status candidate on the PR. End-to-end timing recorded in PR comment.
5. Acceptance test
The full V0 walking skeleton works iff this sequence executes green from a fresh clone.
# 0. Prereqs — Cloudflare Workers Paid, gh CLI authed, wrangler ≥ 4
git clone https://github.com/openhackersclub/flare-dispatch && cd flare-dispatch
pnpm install
pnpm typecheck # PR1 + PR2 + PR3 + PR4 invariants
pnpm test # all unit tests across packages
# 1. Provision CF resources
wrangler r2 bucket create flare-dispatch-v0
wrangler d1 create flare-dispatch-v0
wrangler d1 migrations apply RUNS_METADATA --remote
# wrangler writes IDs back into wrangler.jsonc
# 2. Set secrets
wrangler secret put HMAC_SECRET # 32-byte base64
wrangler secret put GITHUB_APP_ID # numeric
wrangler secret put GITHUB_APP_PRIVATE_KEY < ./app.pem # piped from PEM
wrangler secret put GITHUB_WEBHOOK_SECRET # not used in V0 but present
# 3. Deploy
wrangler deploy
# Note the deployed URL, e.g. https://flare-dispatch-v0.<account>.workers.dev
# 4. Health check
curl -fsS https://flare-dispatch-v0.<account>.workers.dev/health
# Expected: {"status":"ok","runs":["offload-test"]}
# 5. Install the GitHub App on a test repo
# (manual: visit the app install URL from infra/github-app-manifest.json setup)
# 6. Direct dispatch — simulates what the GHA Action does
BODY='{"run":"offload-test","github":{"repo":"owner/test-repo","ref":"refs/heads/main","sha":"<sha>","installation_id":<id>},"inputs":{"repo":"owner/test-repo","sha":"<sha>","command":"pnpm test"},"trigger":{}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -binary | xxd -p -c 256)
curl -fsS -X POST https://flare-dispatch-v0.<account>.workers.dev/v1/dispatch/offload-test \
-H "Content-Type: application/json" \
-H "X-FlareDispatch-Signature: sha256=$SIG" \
-d "$BODY"
# Expected: 202 {"executionId":"01J..."}
# 7. Observe — within the run's wall-time ceiling
gh pr checks <pr-number-of-test-commit>
# Expected: flare-dispatch/offload-test PASS (or FAIL with the test command's exit)
# 8. Inspect via D1
wrangler d1 execute flare-dispatch-v0 --remote --command "SELECT id, status, completed_at FROM executions WHERE id = '01J...'"
# Expected: status = success | failure, completed_at populated
# 9. Inspect log
wrangler d1 execute flare-dispatch-v0 --remote --command "SELECT log_uri FROM steps WHERE execution_id = '01J...' AND name = 'exec'"
# Open the returned log_uri (signed via /v1/artifacts/...); see NDJSON of exec output
# 10. PR-driven smoke (the real acceptance bar)
# In a downstream repo:
# .github/workflows/ci.yml:
# - uses: openhackersclub/flare-dispatch-action@v0
# with:
# run: offload-test
# endpoint: ${{ vars.FLAREDISPATCH_ENDPOINT }}
# hmac-secret: ${{ secrets.FLAREDISPATCH_HMAC }}
# inputs: '{"repo":"${{ github.repository }}","sha":"${{ github.sha }}","command":"pnpm test"}'
gh pr create --title "smoke: flare-dispatch v0" --body "Tests the V0 walking skeleton"
gh pr checks <pr>
# Expected: flare-dispatch/offload-test reports green when `pnpm test` passes, red when it fails.
If steps 4, 6, 7, and 10 all pass, V0 is complete and the roadmap V0 exit criterion is met.
6. Risks + open questions
- Container image is upstream of V0. The run assumes a working Node container image. The OHC base images (
flare-dispatch-node:latest) are a separate workstream; for V0 we either (a) hand-build a local image and reference it by digest, or (b) usenode:lts-slimdirectly. PR1 should resolve which. Risk: slow first deploy if image isn’t cached on CF’s edge. - Sandbox / Containers binding API. The spec assumes a
RUNS_SANDBOXbinding with afetch-like exec surface. The current Cloudflare Containers API has been evolving; PR4’sSandboxCloudflareLiveis the most likely spot to discover a mismatch between 01-architecture § Sandbox and reality. Mitigation: keep theSandboxServiceTag interface narrow (clone,exec) so the live binding is a small surface to revise. - D1 write rate under load. V0 writes one row per step transition (start + end). With only
offload-test(4 steps), this is well within budget — but 01-architecture § Platform limits flags D1 hot-path writes as a concern at V1+ matrix scale. Worth a row-count assertion in PR4’s test. - Coordinator DO + Queue declared but unused. 05-byoc § Wrangler config shows the Coordinator DO and
flare-dispatch-fanoutQueue inwrangler.jsonc. For V0 we omit both — they’re unused and a DO migration is irreversible. Open question: do we ship a stubCoordinatorclass in V0 to make the V1 migration a no-op, or land it cleanly in V1? Plan currently chooses the latter. - Artifact endpoint scope ambiguity. 03-dsl § artifact describes
artifact.uploadas a building block that returns a signed URL embedded in the check-run summary. For V0 we use it for logs only — but the dispatcher endpointGET /v1/artifacts/:execution/:namestill needs to exist so the check-run summary’s “view logs” link works. PR5 covers this; just noting the scope creep risk. - GitHub App per-installation token cache eviction. 04-gha-integration § Check-runs callback caches installation tokens in Worker memory with KV fallback. V0’s PR6 ships memory-only — if the Worker is recycled mid-execution, the next check-run write does a fresh JWT exchange. Acceptable for V0 throughput; flag for V1.
- Run replay determinism. 03-dsl § step Rules requires non-determinism to flow through
io.now/io.uuidso Workflow checkpoint replay is consistent. Theoffload-testrun is simple enough that this is easy to enforce; PR3’s unit test should explicitly assert no directDate.now()/crypto.randomUUID()calls in the run body (lint or grep guard). - HMAC verification surface. 04-gha-integration § Dispatch body and 05-byoc § Secrets both reference
HMAC_SECRET, but neither pins the canonicalization of the signed body (raw bytes vs. JSON-normalized). PR5 must lock this down — recommend signing raw request bytes as received, no normalization — and document inapps/dispatcher/src/hmac.ts. - Workflow step duration vs. run wall-time.
offload-testdeclaresmaxDurationSecat the run level (per 02-runs § 1), but a singlesandbox.execstep is also bounded by the Workflow step duration ceiling. For V0 the run wall-time is shorter than the step ceiling so this is moot, but 01-architecture § Long-running test handling introduces chunked/detached execution that V0 explicitly does not implement. Open question: should the run error tag for “command exceeded the Workflow step ceiling” be a distinctStepDurationExceededor fold intoExecTimeout? Plan currently folds — flag for V1 revisit.
V1 / V2 plan — toward cdp-acceptance
V0 ships offload-test. The next target is cdp-acceptance — the Playwright/CDP acceptance run recipes/cdp-acceptance sketches. It needs three capabilities V0 stubbed: a config store (for injected secrets), a cache (for workspace({ install: true })), detached container execution (bootApp), and Browser Rendering. That work is split across two PRs so each is independently reviewable; the run itself lands in PR9.
PR 8 — config + cache live Layers + loadSecrets primitive
- What:
makeConfigKvLive— the KV-backedConfigService(config.get/config.getJSON), wired intomakeCFRuntimeLivewhen theCONFIG_KVnamespace binding is present (absent → the dyingConfigDeferredstub, so a config-reading run fails loudly).makeCacheR2Live— the R2 + Containers-backedCacheService(restoreOr/save), always wired, replacing V0’s dyingCacheDeferred; a cache entry is a gzipped tar atcache/<key>.tar.gz.loadSecrets(@flare-dispatch/core/primitives) — resolves a named set of config keys into theRecord<string,string>env a run hands tosandbox.exec/bootApp, the path that moves credentials out of GHA repo secrets into the FlareDispatch config store.CacheService.restoreOr/savegain an optionaldir(the directorypathsare relative to).wrangler.jsoncadds thekv_namespacesCONFIG_KVbinding. - Verifiable acceptance:
pnpm typecheck+wrangler deploy --dry-runexit 0 (theCONFIG_KVbinding resolves).vitest runcoversmakeConfigKvLiveagainst a Miniflare KV (getreads stored values / degrades unset keys toundefined;getJSONdecodes against a Schema and collapses malformed-JSON / Schema-mismatch / unset toOption.none()), andloadSecretsagainst fakes (present keys land in the env record, unset keys are omitted + logged atwarn, the optional prefix namespaces the lookup).makeCacheR2Live’s container-sidetarI/O is typecheck-only — Miniflare has no container runtime, the same constraintsandbox-cf.tsdocuments.
PR 9 — Sandbox detached execution + Browser CDP + cdp-acceptance run
- What:
SandboxCloudflareLive.runDetached/waitForExit/waitForPort— the V0Effect.diestubs replaced with the@cloudflare/sandboxstartProcess/Process.waitForExit/Process.waitForPortsurface, so thebootAppprimitive works.makeBrowserRenderingLive— the liveBrowserService;newCDPSessioncomposes the Browser Rendering/connectwss://endpoint the container’s Playwright process dials directly, authenticated with a Cloudflare API token carried as a query parameter. The connect URL + token are operator-pinnedBROWSER_CDP_*Worker secrets (no Workersbrowserbinding — the container, not the Worker, opens the WS); absent, the runtime keeps the dyingBrowserstub.runs/cdp-acceptance.tsis the real run — modeled onrecipes/cdp-acceptanceand extended withloadSecrets-based credential injection into both the app boot and the test command — registered in the Dispatcher registry andRunWorkflow. - Verifiable acceptance:
cdp-acceptanceunit-tested againstCFRuntimeTest+ asandboxFakeProgram+ the Browser fake — green / red / test-command-timeout / secret-injection paths, the same pattern asruns/offload-test.test.ts. Live detached + Browser Rendering Layers are typecheck +wrangler deploy --dry-runverified; the end-to-end acceptance suite is awrangler devsmoke. This closes the roadmap V2 exit criterion.