sykli

Crates.iosykli
lib.rssykli
version0.3.0
created_at2025-12-23 01:38:32.606492+00
updated_at2026-01-09 23:55:24.241595+00
descriptionCI pipelines defined in Rust instead of YAML
homepagehttps://github.com/yairfalse/sykli
repositoryhttps://github.com/yairfalse/sykli
max_upload_size
id2000593
size176,383
Yair Etziony (yairfalse)

documentation

https://docs.rs/sykli

README

Sykli Rust SDK

CI pipelines defined in Rust instead of YAML.

use sykli::Pipeline;

fn main() {
    let mut p = Pipeline::new();
    p.task("test").run("cargo test");
    p.task("build").run("cargo build --release").after(&["test"]);
    p.emit();
}

Installation

Add to your Cargo.toml:

[dependencies]
sykli = "0.2"

[[bin]]
name = "sykli"
path = "src/bin/sykli.rs"

For optional dependency (recommended for libraries):

[features]
sykli = ["dep:sykli"]

[dependencies]
sykli = { version = "0.2", optional = true }

[[bin]]
name = "sykli"
path = "src/bin/sykli.rs"
required-features = ["sykli"]

Quick Start

Create src/bin/sykli.rs:

use sykli::Pipeline;

fn main() {
    let mut p = Pipeline::new();

    p.task("lint").run("cargo clippy -- -D warnings");
    p.task("test").run("cargo test");
    p.task("build")
        .run("cargo build --release")
        .after(&["lint", "test"]);

    p.emit();
}

Create an empty sykli.rs in your project root:

touch sykli.rs

Run:

sykli run

Core Concepts

Tasks

Tasks are the basic unit of work:

p.task("test").run("cargo test");

Dependencies

Define execution order with .after():

p.task("deploy")
    .run("./deploy.sh")
    .after(&["build", "test"]);  // Runs after both complete

Independent tasks run in parallel automatically.

Input-Based Caching

Skip unchanged tasks with .inputs():

p.task("test")
    .run("cargo test")
    .inputs(&["**/*.rs", "Cargo.toml", "Cargo.lock"]);

Outputs

Declare task outputs for artifact passing:

p.task("build")
    .run("cargo build --release -o ./app")
    .output("binary", "./app");

Conditional Execution

Run tasks based on branch, tag, or event:

// String-based conditions
p.task("deploy")
    .run("./deploy.sh")
    .when("branch == 'main'");

// Type-safe conditions (compile-time checked)
use sykli::{Branch, Tag};

p.task("release")
    .run("./release.sh")
    .when_cond(Branch::new("main").or(Tag::pattern("v*")));

Templates

Templates eliminate repetition:

let src = p.dir(".");
let cache = p.cache("cargo-registry");

// Define template
let rust = p.template("rust")
    .container("rust:1.75")
    .mount(&src, "/src")
    .mount_cache(&cache, "/usr/local/cargo/registry")
    .workdir("/src");

// Tasks inherit from template
p.task("lint").from(&rust).run("cargo clippy");
p.task("test").from(&rust).run("cargo test");
p.task("build").from(&rust).run("cargo build --release");

Containers

Run tasks in isolated containers:

let src = p.dir(".");
let registry_cache = p.cache("cargo-registry");
let target_cache = p.cache("cargo-target");

p.task("test")
    .container("rust:1.75")
    .mount(&src, "/src")
    .mount_cache(&registry_cache, "/usr/local/cargo/registry")
    .mount_cache(&target_cache, "/src/target")
    .workdir("/src")
    .run("cargo test");

Convenience Methods

// Mount current dir to /work
p.task("test")
    .container("rust:1.75")
    .mount_cwd()
    .run("cargo test");

// Mount to custom path
p.task("build")
    .container("rust:1.75")
    .mount_cwd_at("/app")
    .run("cargo build");

Composition

Parallel Groups

let checks = p.parallel("checks", vec![
    p.task("lint").run("cargo clippy"),
    p.task("fmt").run("cargo fmt --check"),
    p.task("test").run("cargo test"),
]);

// Build depends on all checks
p.task("build")
    .run("cargo build --release")
    .after_group(&checks);

Chains

let test = p.task("test").run("cargo test");
let build = p.task("build").run("cargo build --release");
let deploy = p.task("deploy").run("./deploy.sh");

// test -> build -> deploy
p.chain(&[&test, &build, &deploy]);

Artifact Passing

p.task("build")
    .run("cargo build --release -o /out/app")
    .output("binary", "/out/app");

// Automatically depends on "build"
p.task("package")
    .run("docker build -t myapp .")
    .input_from("build", "binary", "./app");

Matrix Builds

// Test across Rust versions
p.matrix("rust-test", &["1.70", "1.75", "1.80"], |version| {
    p.task(&format!("test-rust-{}", version))
        .container(&format!("rust:{}", version))
        .mount_cwd()
        .run("cargo test")
});

// Deploy to environments
p.matrix_map("deploy", &[
    ("staging", "staging.example.com"),
    ("prod", "prod.example.com"),
], |(env, host)| {
    p.task(&format!("deploy-{}", env))
        .run(&format!("deploy --host {}", host))
        .when("branch == 'main'")
});

Service Containers

p.task("integration")
    .container("rust:1.75")
    .mount_cwd()
    .service("postgres:15", "db")
    .service("redis:7", "cache")
    .env("DATABASE_URL", "postgres://postgres:postgres@db:5432/test")
    .env("REDIS_URL", "redis://cache:6379")
    .run("cargo test --features integration")
    .timeout(300);

Secrets

// Simple secret
p.task("deploy")
    .secret("DEPLOY_TOKEN")
    .run("./deploy.sh");

// Typed secrets with explicit source
use sykli::{FromEnv, FromVault, FromFile};

p.task("deploy")
    .secret_from("GITHUB_TOKEN", FromEnv::new("GH_TOKEN"))
    .secret_from("DB_PASSWORD", FromVault::new("secret/db#password"))
    .secret_from("API_KEY", FromFile::new("/run/secrets/api-key"))
    .run("./deploy.sh");

Retry & Timeout

p.task("flaky-test")
    .run("./integration-test.sh")
    .retry(3)       // Retry up to 3 times
    .timeout(300);  // 5 minute timeout

Kubernetes Execution

use sykli::{K8sOptions, K8sResources};

// Pipeline-level defaults
let mut p = Pipeline::new_with_k8s_defaults(K8sOptions {
    namespace: Some("ci-jobs".into()),
    resources: K8sResources {
        memory: Some("2Gi".into()),
        ..Default::default()
    },
    ..Default::default()
});

// Task-specific K8s settings
p.task("train-model")
    .container("pytorch/pytorch:2.0")
    .run("python train.py")
    .k8s(K8sOptions {
        gpu: Some(1),
        resources: K8sResources {
            memory: Some("32Gi".into()),
            ..Default::default()
        },
        node_selector: [("gpu".into(), "nvidia-a100".into())].into(),
        ..Default::default()
    });

// Hybrid: local + k8s
p.task("test").run("cargo test").target("local");
p.task("train").run("python train.py").target("k8s");

Language Presets

let mut p = Pipeline::new();

// Rust preset
p.rust().test();                                    // cargo test
p.rust().lint();                                    // cargo clippy
p.rust().build("target/release/app").after(&["test", "lint"]);

p.emit();

Dry Run / Explain

use sykli::ExplainContext;

p.explain(&ExplainContext {
    branch: "feature/foo".into(),
    tag: None,
    ci: true,
});

// Output shows execution order and skipped tasks

Examples

See the examples directory for complete working examples:

  • 01-basic/ - Tasks, dependencies, parallel execution
  • 02-caching/ - Input-based caching and conditions
  • 03-containers/ - Container execution with mounts
  • 04-templates/ - DRY configuration with templates
  • 05-composition/ - Parallel groups, chains, artifacts
  • 06-matrix/ - Matrix builds, services, secrets

API Reference

See REFERENCE.md for the complete API documentation.

License

MIT

Commit count: 289

cargo fmt