.calder/ci.ts37 lines

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.

Point at a part, a socket or a line of code to read its type.A part you tap shows its type here.

Key
    Drawn from.calder/ci.ts
    Checked bytsc -p .calder --noEmit no errors
    Plan for

    Press 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.

    ci.d.tswhat tsc infers
    deploy at 2:1, taken apart from site. Its features are numbered as in the parts list.
    No.FeatureIn ci.tsType

    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.

    A test with @effect/vitest

    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.

    What a pull request runs after main ran everything, for an edit to one file. Pick the file.
    Edited fileinstalltypechecktest of 2 filessite

      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.

      
          
      One result of 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.