CI that knows what changed.
Most of it never runs.
Frazer CI plans every push to Fir as a graph of tasks, each keyed by the hash of its inputs. A task whose inputs did not change is a cache hit: no machine starts, nothing is checked out, nothing is billed. The rest run on Frazer Runners, a fresh microVM per task that reads only what the task declared, and report results the host signs.
- Definitions
- Fir Actions in typed Starlark, in .frazer/ci, or none at all for common layouts
- Unit of work
- A task with declared inputs, outputs and a key
- Results
- Written and signed by the host, never by the job
- Price
- Compute that ran, per second; a cache hit is free
# .frazer/ci/storefront.star load("@frazer//go:v1", "go_test", "go_binary") load("@frazer//oci:v1", "image") GO = toolchain("go@1.27.1") # resolved to a content ID go_test( name = "payments-test", packages = ["./services/payments/..."], inputs = ["services/payments/**", "go.mod", "go.sum"], toolchains = [GO], level = "sealed", shards = "auto", ) bin = go_binary(name = "payments", toolchains = [GO], main = "./services/payments/cmd/payments") image(name = "payments-image", files = {"/payments": bin}) check("presubmit", tasks = affected(tests()), required_on = ["main"]) check("verify", tasks = affected(all()), post_merge = True) deliver("payments", artifacts = ["payments-image"], environment = "acme-staging/platform", on = "verified")
$ frazer ci runs plan --workspace ws/s-81f2 --explain 148 tasks · 132 hits · 3 joined · 13 to run ran because test web ×3 upstream output: build checkout-api test payments ×4 services/payments/webhook/retry_test.go test checkout-api ×2 upstream output: build payments joined 3 keys still running for version 1 hits 132 keys with a result: no machine first build checkout-api → build web → test web
→ ci_status { "pull_request": 482 } ← presubmit passed at v2 · 13 ran · own server $0 → ci_explain { "task": "test web 2/3" } ← ran: upstream output build checkout-api changed → ci_failures { "pull_request": 482 } ← 0 failures · 1 flaky: test payments 3/4 same key failed, then passed · score updated
$ frazer ci runs list --pull-request 482 r-8c2f41 presubmit v2 passed 3 min 30 s $ frazer ci tests list --flaky --sort score go://services/search#TestIndexWarmup 0.93 $ frazer ci tasks repro search-test#2 sandbox with the exact projection, image and toolchain $ frazer ci workflow-imports create \ --repository acme/storefront --from github-actions draft Fir Actions in a pull request; unmatched steps listed
- A
- The whole graph, known up frontEvery task and its key are in the plan before anything runs, and the critical path starts first.
- B
- Hits start nothingA task whose key already has a result is done: no machine, no checkout, no charge. Running keys are joined.
- C
- Why a task ranEach one names the input that changed its key, so nothing runs on a hunch.
- D
- Who wrote each resultResults are signed by the host with its tier; verify re-runs them before anything deploys.
No YAML. No marketplace.
No checkout step.
Most CI today decides trust by an event’s name, runs other people’s code with your token, and lets any job write the shared cache. Each of those has been the way in for a supply-chain attack.
Frazer CI does not carry them. Nothing comes back through a compatibility layer; a converter reads your workflows once, at the boundary.
| Typical CI today | Frazer CI | |
|---|---|---|
| Trust | Decided by the event’s name and where the workflow file sits | Decided by who wrote the code, the paths’ merge policy and where the definition came from |
| Secrets | Readable by any workflow in the repository | Bound to one task, branch class and lane; nothing readable before merge |
| Shared steps | Third-party code by a mutable tag, running with the job’s token | Fir Actions loaded by content ID with a lock; images and toolchains by digest |
| Cache | Keys chosen by each workflow; any writer can push | Keys computed from inputs; only the host writes, and signs |
| Configuration | YAML with an expression language spliced into shells | Fir Actions in typed Starlark, evaluated before anything runs; no templating |
| First step | Check out the repository | Read only the declared inputs; a hit starts nothing |
| Graph size | One graph per file, capped by syntax | One plan per push, bounded by quota |
| Billing | Every job rounded up to a minute | Per second from command start, no minimum |
| History | Checks and runs expire with a retention setting | Verdicts and provenance last as long as the repository |
Moving from GitHub Actions
- InventoryA lint lists every git call and version stamp in your workflows and build scripts, with its replacement.
- DraftA converter reads your workflows once and an agent drafts Fir Actions as an ordinary pull request you review.
- ShadowFor 14 days and at least 500 commits, both systems run side by side while Fir follows your repository. Deploys run dry.
- SwitchWhen every difference is explained, the repository moves to Fir with native checks in its merge policy.
Keys, not triggers.
Decided before a machine starts.
A task is a command in an image with declared inputs and outputs. Its key hashes everything that can change its result, including the Fir tree IDs of its inputs, so whether a task must run is a lookup, not a checkout.
On a typical agent pull request in a monorepo, most keys already have a result. Those tasks cost nothing, and the rest run on the critical path first.
- 01
Affected tasks only
A task runs when a changed path is in its inputs or an upstream key changed. Everything else is a hit, recorded as one.
- 02
One execution per key
Two pull requests, two agents or two attempts that need the same key share one execution and its result.
- 03
Failures first
Tasks likely to fail and the critical path start first, and test results stream as they happen.
- 04
Zero-config
Go, Rust, JavaScript and Python workspaces, Bazel and Buck2, and Dockerfiles get tasks without a Fir Actions file.
- 05
Typed, deterministic Fir Actions
Starlark that reads no network, clock or environment, so the plan is data and errors arrive, with a file and line, before anything runs.
- 06
Graphs that grow
A task can emit more of the graph, still cached by key, with fan-out bounded by quota instead of syntax.
A cache only the host writes.
Shared by key, trusted by tier.
Job code never holds a cache credential. The host outside the microVM computes the key from what it mounted, hashes the outputs and signs the result, so a job can only ever answer for the inputs it was given.
Agents share every sealed result in the project. What deploys reads only results from hosts that have run nothing but protected work.
| Level | Network | Result reused by | Typical tasks |
|---|---|---|---|
| open | by pool policy | nobody: never cached | deploy hooks, tests against outside services |
| declared | dependency proxies only, each fetch logged | the same writer class, while fetches resolve the same | builds that resolve packages at run time |
| sealed | none | every requester in the project | compiles, unit tests, codegen, linters, image builds |
| reproducible | none | as sealed, after an independent rebuild matches | release artifacts |
main@verified moves on green.
Culprits found from keys.
After a pull request merges, Frazer CI verifies main on protected hosts and moves main@verified only to commits that passed. Deploys, seeds and new agent work read it, so a red merge never ships.
When a test fails, the candidates are the commits where that test’s key changed, which catches toolchains, images and lockfiles too. A confirmed culprit is reverted automatically, and its pull request reopens with the evidence.
Results an agent can read.
Not a log to scrape.
Every task result has the same typed shape in the API, the CLI’s JSON and the MCP tools, with no timestamps or random IDs in the parts an agent reads, so the bytes stay friendly to prompt caches.
Agents preflight a workspace before they open a pull request, and a superseded version’s tasks are canceled the moment a new version arrives.
{ "task": "search-test#2", "pull_request": 483, "version": 2, "status": "FAILED", "failure_kind": "TEST", "first_error": { "path": "services/search/index/segment_test.go", "line": 41, "message": "want 1 load span, got 0" }, "failing_tests": [{ "test_id": "go://services/search/index#TestSegmentLoad", "flaky_score": 0.01, "pre_existing": false }], "excerpt": { "token_count": 612, "log_range": "120:180" }, "repro": { "command": "frazer ci tasks repro search-test#2" }, "cost": { "vcpu_seconds": 212, "usd": 0.0027 } }
→ ci_failures { "pull_request": 483, "version": 2 } ← { "failures": [ { "task": "search-test#2", "failure_kind": "TEST", "test_id": "go://services/search/index#TestSegmentLoad", "first_error": "segment_test.go:41: want 1 load span, got 0", "pre_existing": false } ], "pre_existing": [ { "cluster": "TestReserveStock: timed out", "since": "main@48196", "sent_to": "the culprit’s author" } ] }
$ frazer ci tasks logs search-test#2 --failed --lines 120:180 --json { "task": "search-test#2", "log_range": "120:180", "token_count": 612, "lines": [ "--- FAIL: TestSegmentLoad (0.04s)", " segment_test.go:41: want 1 load span, got 0", "FAIL services/search/index" ] } $ frazer ci tasks repro search-test#2 sandbox with the exact projection, image and toolchain
Results for models
A typed result per task: the failure kind, the first error, a bounded excerpt with its token count, and the command that reproduces it.
One failure, one fix
Failures are clustered across pull requests, so 200 agents never all “fix” one broken test. Pre-existing failures go to the culprit’s author.
The loop closes itself
A failed task wakes the agent’s session with the result; the next version re-runs only what it changed. Three rounds, then a person.
Budgets that hold
Per session, agent, campaign and project. The estimate is debited before a task starts; at the cap, tasks wait.
From a verified build
to a verified deploy.
CI builds; Delivery deploys. Release tasks run from main@verified, push to Frazer Registry by digest with provenance, and hand off. Delivery writes the digest into your environment repository as a pull request, checks it like any other, and GitOps syncs exactly what was verified.
Production waits for its gates: provenance that verifies, no freeze window, the approvals the environment asks for, and a canary with analysis.
A microVM per task.
Never reused.
Every task that has to run gets its own microVM, started warm from a pool, on your servers or the Frazer fleet. A sealed task has no network device at all. Inputs arrive as a read-only projection from the node’s Fir store, so nothing is cloned.
Frazer Runners run only Frazer CI’s tasks. GitHub runners is the separate product for repositories that stay on GitHub: it runs their GitHub Actions jobs on hosts and pools of its own, at the same price. The two never share a host, a pool or a queue.
Linux · amd64
Every task class, sealed included.
Linux · arm64
Tasks on arm64 servers you bring.
GPU
GPU tasks on your GPU servers or the fleet.
Fast where CI loses time.
Every number is a target until measured.
These are the targets Frazer CI is built and tested against. Each run page already shows its own breakdown, from the push to the signed result.
A target becomes a claim only with the measurement, the method and the commit behind it, published next to the same jobs on GitHub runners.
| Metric | Target | How |
|---|---|---|
| Plan ready after a push | p50 ≤ 500 ms · p99 ≤ 2 starget | Tree IDs instead of file reads; plans cached as data |
| Task running, from a warm pool | p50 ≤ 1 s · p99 ≤ 3 starget | A booted microVM is claimed; no runner registration |
| Inputs readable inside the task | p50 ≤ 200 ms · p99 ≤ 1 starget | A projection is smaller than a checkout, prefetched when queued |
| First failing test reported | p50 ≤ 60 s · p90 ≤ 3 mintarget | Likely failures first; results stream |
| Verdict when every task is a hit | p50 ≤ 5 s · p99 ≤ 15 starget | No machine starts |
| Merge to main@verified | the affected critical path + 2 mintarget | Verify lanes with floors; one run per batch |
| CI runs per merged pull request, at 25× today’s load | ≤ 2.0target | Keys, and re-checks only where paths overlap |
| Wrong results in audit re-runs | 0 in 10,000 a monthtarget | One key implementation; results written by the host |
Pay for compute that ran.
A cache hit costs nothing.
One rate for Frazer Runners and GitHub runners, metered per second from the moment your command starts. Booting, restoring and waiting in the queue are ours to pay. There are no seats and no fee per pipeline or check. Frazer CI includes 200 minutes a month on a standard 4 vCPU runner, for every personal account and every organization.
Example arithmetic, not a measurementA pull request whose plan has 400 tasks, of which 30 run for 60 seconds on 4 vCPUs: 7,200 vCPU-seconds, or 2 vCPU-hours, is $0.09 on the fleet. The 370 hits cost nothing.
See every price- Frazer fleet
- $0.045 per vCPU-hour with 4 GiB per vCPU, per second from command start, no minimum
- Free minutes
- 200 a month on a standard 4 vCPU runner, for every personal account and every organization; public repositories draw from the same minutes
- Your servers
- $0 per minute, part of $1.50 per physical core per month
- A cache hit
- Free: no machine ran, no minute used
- Planning, results
- Test intelligence, insights, MCP and the console: included, within fair use on your servers
- Storage
- Cache, logs and artifacts: free on your servers; $0.012 per GiB-month above the allowance on the fleet
- GPU tasks
- The fleet GPU price plus a 4 vCPU runner
- Seats, pipelines
- None, and no fee per check
Do we have to write Fir Actions?
No. Zero-config reads the tree and proposes tasks for Go modules, Cargo, pnpm and npm workspaces, uv and Poetry projects, Bazel and Buck2 workspaces and Dockerfiles; you override only what you need. When you do write Fir Actions, in typed Starlark under .frazer/ci, frazer ci runs plan returns typed errors at the file and line, and the plan diff, before anything runs.
Can we keep GitHub Actions?
Yes, on GitHub runners: repositories that stay on GitHub run their Actions jobs there, at the same price, on hosts, pools and queues of their own. Frazer CI comes with Fir and runs Fir Actions, which are native, not a GitHub Actions dialect. When a repository moves, a converter drafts Fir Actions from its workflows once, both run side by side during Fir’s rehearsal, and you switch when every difference is explained.
Are there free minutes?
Yes. Every personal account and every organization gets 200 Frazer CI minutes a month on a standard 4 vCPU runner on the Frazer fleet, and public repositories draw from the same minutes. Cache hits never use them. On your own servers tasks have no per-minute fee at all. GitHub runners have no free minutes.
Can another CI gate merges on Fir?
Yes. Fir asks for checks through an event any CI can consume and accepts check reports from any system with the permission. What only Frazer CI gives you is cache hits without a checkout, one execution per key, and culprits found from keys.
Is a cached result safe for a release?
Results carry the tier of the host that produced them. Agents and people may reuse any sealed result; verification, release, deploy and prebuild accept only results from protected hosts. Release artifacts are also rebuilt without any cache on a second runner class, and the digests must match.
What happens when a budget runs out?
Tasks are held, never canceled, and the session sees the estimated remaining cost. Cache hits keep flowing, because they cost nothing.