Rust · deterministic DAG scheduler
Relay is a from-scratch CI/CD engine whose scheduler is a pure, deterministic DAG executor you can unit-test without running anything. Give it a pipeline and it decides what runs, in what order, what runs in parallel, what gets skipped when a job fails, and what is served straight from a content-hash cache. Pick a preset below and press Run to watch the same rules resolve a job graph, live.
Pick a preset to load a job graph: a diamond DAG, a failure that skips a subtree, a wide fan-out, or a cache hit. The jobs slider sets the concurrency limit (how many jobs may run at once), and fail-fast stops scheduling new work after the first failure.
Press Run and watch each node move through pending, running, and then success, failed, skipped, or cached, exactly as the scheduler resolves the graph. The event log and summary show the order and the peak concurrency actually observed against the limit. Reset clears the run.
This is the Rust scheduler ported to JavaScript with a simulated executor. Pick a preset, set the concurrency limit, and press Run. Watch jobs go pending, running, then success, failed, skipped, or cached, exactly as the engine resolves them.
The core is one function, schedule::run(&pipeline, &mut executor, limits) -> Report. The scheduler never spawns a process or a thread. It drives an Executor trait in virtual time, so a MockExecutor that records what ran concurrently lets every rule be asserted deterministically. Swap in the ShellExecutor and the same scheduler runs real commands.
$ relay run examples/failure.relay [ 0] setup ok success [ 1] unit xx failed [ 2] docs ok success package -- skipped deploy -- skipped summary: 2 success, 0 cached, 1 failed, 2 skipped
A failed job marks its transitive dependents skipped and they never run, while independent branches keep going. Parallelism is real and bounded: the mock proves the peak concurrency is exactly the limit and never more. Cache keys are a hand-rolled FNV-1a digest of a job's config plus its declared inputs, folded together with every upstream key, so a change in a non-cached upstream busts the downstream key too. Same pipeline, same result and same order, every run, with ties broken by config order.
Real CI platforms run your pipeline; Relay is built to let you study and test the one part they hide, the scheduler, without running anything.
Hosted pipelines wired to runners, containers, and the network. The ordering, parallelism, skip, and cache logic is entangled with execution, so you cannot exercise a scheduling rule without actually running jobs.
A mature, plugin-driven automation server. Powerful and heavy, and the DAG behavior lives inside a running controller rather than in a small function you can read end to end.
Pulls the scheduler out as one pure function over an Executor trait. Every rule, dependency order, bounded parallelism, skip propagation, and upstream-aware caching, is asserted against a mock in virtual time, with no process or thread spawned. Swap in the ShellExecutor and the same scheduler runs real commands. Zero external dependencies, pure Rust std.
The parts that matter
Each is a real rule in src/schedule.rs, and each has a test that pins it against the mock executor.
schedule::run drives an Executor trait in virtual time and never spawns a process or a thread. That single seam is what makes every rule testable.
Up to N jobs run at once. The mock records what ran concurrently and proves the peak is exactly the limit and never more.
A failed job marks its transitive dependents skipped and they never run. Independent branches keep going. A continue-on-error failure does not skip its dependents.
A job's cache key is a hand-rolled FNV-1a digest of its config plus the content of its declared input paths. A stored match resolves the job cached and skips execution.
The key folds in every dependency's resolved key and handed-down artifacts, so a change in a non-cached upstream job busts the downstream key too.
Same pipeline, same result and same order, every run. Ties break by config order, and fail-fast stops scheduling new work after the first failure.
relay run, relay plan, and relay graph over a readable .relay pipeline file, with --jobs N and --fail-fast.
The whole engine is schedule::run(&pipeline, &mut executor, limits) -> Report, one function over the Executor trait.
A MockExecutor that returns scripted outcomes for tests, and a ShellExecutor that runs real commands with per-job working dirs, artifact hand-off, and process-group timeouts.
A text format with a parser and a matching printer that round-trip. Declared paths are confined to the workspace, so a pipeline cannot read or clobber files outside its base dir.
# the whole test suite runs with no real processes, over the mock executor cargo test cargo clippy --all-targets -- -D warnings # plan the DAG and wave order, run nothing relay plan examples/diamond.relay --jobs 2 # run a pipeline: print each status and a summary relay run examples/failure.relay # print just the dependency graph relay graph examples/diamond.relay