Define pipelines as code

Write workflows in TypeScript, compile them to a committed artifact.

Written By Dustin

Last updated About 3 hours ago

TOVIO has two co-equal pipeline front-ends: declarative .github/workflows/*.yml, and a config-as-code program using the @tovio/pipeline builder. Both lower to the same job DAG, the same runtime, and the same UI. YAML is permanent and is never degraded; config-as-code is an addition, for when you want types, reuse, loops, and shared constants.

A pipeline program

import { pipeline, run, uses, emit } from "tovio-node-sdk/pipeline";
const checkout = uses("actions/checkout@v4");
const ci = pipeline("CI")
  .on("pull_request", "push")
  .job("lint", {
    steps: [checkout, run("cargo clippy --all-targets -- -D warnings")],
  })
  .job("test", {
    needs: ["lint"],
    matrix: { node: ["20", "22"] },
    steps: [checkout, run("cargo test --workspace")],
  });
emit(ci);

Trigger names are GitHub Actions event names — pull_request, push, schedule, workflow_dispatch — which TOVIO maps onto its own change events. Matrix axis values must be strings: a bare JavaScript number like 3.10 silently becomes "3.1", so quote your versions.

Compile it

tovio ci compile pipelines/ci.pipeline.mjs

That runs your program under Node, parses the emitted workflow JSON into the same structural model YAML parses to, validates the DAG, content-addresses it with BLAKE3, and writes a *.pipeline.lock.json lockfile. Your program runs only on your machine — the Forge never executes pipeline code, which is why config-as-code needs no server-side sandbox at all.

Two artifacts, one of which actually runs

--emitWritesRole
lockfile (default)the source path with its final extension replaced — pipelines/ci.pipeline.mjs becomes pipelines/ci.pipeline.lock.jsonThe deterministic staleness-and-provenance record. The Forge does not schedule from it.
yaml.github/workflows/<stem>.ymlThis is what the Forge schedules. The scheduler reads only committed YAML.
bothbothWrite and gate both in one step.

.github/ is dot-ignored by default, so --fix-ignore adds !.github/ to .tovioignore and the generated workflow becomes tracked. A hand-authored workflow at the target path is never clobbered without --force.

The staleness gate

tovio ci compile pipelines/ci.pipeline.mjs --emit both --check

Recompiles and byte-compares, writing nothing. A stale or missing artifact is a non-zero exit with TVO-CI-001. Because the content address is a deterministic function of the compiled model, this is a real guard, not a heuristic. Run it as a required check from a hand-authored workflow — hand-authored so it can gate the generated workflows without gating itself.

Stricter than YAML on purpose

An empty or duplicate job id, a needs pointing at a job that does not exist, a cycle, a matrix with no axes, an axis with no values — all fail loudly at compile time. YAML would strand or silently drop the job at run time. Every one of these surfaces as TVO-CI-001 with a specific remediation.

What is deferred

Config-as-code produces a static DAG: the full node set is known at compile time. Tenant-authored code that runs on the platform and grows the DAG dynamically is not available. If you need a runtime-dynamic DAG today, this is not it.