Your pipeline is a program.
Calder is CI/CD for repositories with a Nix flake, and the pipeline is .calder/ci.ts, TypeScript with Effect. In the drawing, every step is a part cut to the shape of its type, and every rule is a rail drilled for the services it supplies.
Drag deploy onto the pull-request rail, or press Space on it and use the arrow keys.Tap deploy, then tap the pull-request rail.
deploy stands on its grant bit. Drag it back, or press Space on it and use the arrow keys.Tap it, then tap the push rail.
Point at a part, a socket or a line of code to read its type.A part you tap shows its type here.
.calder/ci.tstsc -p .calder --noEmit no errorsPress Space to pick the part up. Left and right arrow keys choose a rail, Space puts it down, Escape cancels.
Steps are typed values
The compiler infers a type for every step, Step<A, E, R, O, G>: A is the value it returns, E its typed failure, R the services it needs, O its named outputs and G the grants it declares. Three of them decide where a step fits, so the drawing cuts them into the part. E and O go on its data plate.
site is a Nix build, and its value is the store path. deploy names it in needs, so its body gets that StorePath and hands it to publish in deploy.ts. It ships the exact build that the checks in after: [typecheck, test] gated.
deploy at 2:1, taken apart from site. Its features are numbered as in the parts list.| No. | Feature | In ci.ts | Type |
|---|
Every fit is a compiler check
Each place where two parts meet in the drawing is a check tsc makes on ci.ts. Below are the five, each with an edit to the example you can make, the section where the fit is decided, and what the compiler says.
A plan is a function
Calder.plan(project, event) returns the steps an event runs, without running any of them. The rule that matches the event is the live rail; the parts its steps need come along, pulled in.
So a pipeline can have tests, like any other code. This one plans pull request #7 and checks that deploy isn't in it.
Calder keeps every result by what it read
Calder reads the program, so it knows what each step reads. A task's key is the git tree ids of its inputs, the key of the setup it starts from, its dev shell and its command, plus its env, outputs and the values it interpolates. Git has hashed every file already, so a key needs no pass over the files.
When a key already has a passed result, from main, a pull request or a check before pushing, the step settles at once and says where the result came from. Vitest.files gives each test file a key of its own, from what it imports plus the root manifests and the lockfile. Calder reuses a Nix build when its derivation hasn't changed.
| Edited file | install | typecheck | test of 2 files | site |
|---|
When the same key gives a different outcome than before, Calder flags it, because either the key misses an input or the test is flaky. A test that passes only on a retry counts as flaky, not failed.
It runs before you push
calder check runs the working copy, in jj or git, like a pull request. calder watch <dirs> does the same in the background for every jj workspace below those directories, once a workspace's snapshot has stayed the same for a while, 120 seconds by default. That suits several agents working at once.
Calder keeps their results by key like any other, so the pull request that follows finds them, and so does main after the merge.
typecheck, kept by its key. The check runs it, then the pull request and main reuse it.Libraries are functions
Pnpm.install, Bun.install, Vitest.files and Project.standard, the whole standard pipeline in one call, come from @calder-ci/std. They are ordinary functions that return steps. Your own modules work the same way, like the example's deploy from .calder/deploy.ts.
Actions are Effect code with typed failures. Step.retry and Step.timeout are policies you pipe a step through, as in .pipe(Step.retry({ times: 2 })) on deploy.
Calder is an experiment. It runs my own projects; there is no release, the API changes most weeks, it needs a Nix flake, and for now it only works with Gitea.