Skip to content

Repository files navigation

Cicada. Hear your CI before it ships.

CI Go BuildKit KDL

A container-native CI pipeline runner powered by BuildKit. Define pipelines in KDL, run them anywhere BuildKit runs -- your laptop included.

No more "push and pray."

Motivation

Most CI systems share the same dirty secret: the only way to find out if your pipeline works is to push it and wait. You tweak some YAML, open a PR, stare at a spinner for 8 minutes, watch it fail on line 3, and do it all over again. It's the worst feedback loop in software engineering, and we've somehow normalized it.

Good CI should be better than that. Specifically, it should be:

  • Locally repeatable: The exact same pipeline that runs in CI should run on your machine. Not a "close enough" approximation. The same thing. If it passes on your laptop, it passes in CI. Full stop.

  • Debuggable: When something breaks, you should be able to drop into a shell, poke around, and iterate -- not squint at truncated logs from a VM you'll never touch.

  • Tool-centric: CI should be a thin wrapper around your existing scripts and task runners, not a vendor-specific reimagination of how builds work. Your mise tasks, your Makefile, your shell scripts -- those are the source of truth.

  • Portable: Switching CI providers shouldn't require rewriting your entire pipeline from scratch. Your build logic lives in your repo, not in some provider's proprietary DSL.

  • Fast via caching: Content-hash-based caching should prevent redundant work. You shouldn't reinstall your dependencies every single run because the CI system forgot what happened 5 minutes ago.

Cicada takes these ideas seriously. Pipelines are declared in KDL (not YAML -- you're welcome), executed inside containers via BuildKit, and run the same way everywhere. Your laptop is a first-class CI environment.

Comparison to Dagger

Dagger is a major inspiration for Cicada. It proved that BuildKit is a fantastic execution engine for CI and that local-first pipelines are not just possible but preferable. Cicada wouldn't exist without the trail Dagger blazed.

That said, Dagger's power comes with friction that Cicada tries to avoid:

Dagger Cicada
Pipeline definition Go / TypeScript / Python SDK KDL config file
Learning curve SDK APIs, generated clients, GraphQL engine internals One config format, handful of options
Upgrade path Regenerate ./internal/dagger, SDK version coupling, potential Go version mismatches go install / update a binary
API surface Large, partially chainable, partially not Deliberately small -- steps, mounts, caches, dependencies
Module ecosystem Daggerverse (powerful, but unclear trust/security model for secrets and env access) None yet -- your pipeline is self-contained
Runtime overhead GraphQL engine + SDK runtime + BuildKit BuildKit (that's it)

The short version: Dagger gives you a full programming language and an ecosystem to go with it. Cicada gives you a config file and gets out of the way. If your pipeline needs loops, conditionals, and dynamic graph construction, Dagger is the better tool. If your pipeline is "run these commands in these containers in this order," Cicada is the lighter path to get there.

Both agree on the thing that matters most: CI should run on your laptop.

Prerequisites

  • mise -- task runner and tool manager (handles Go + linter versions for you)
  • A running BuildKit daemon (buildkitd)

Quick Start

# Install tools (Go, golangci-lint) via mise
mise install

# Build cicada
mise build

# Validate a pipeline
./bin/cicada validate examples/hello.kdl

# Run a pipeline
./bin/cicada run examples/hello.kdl

Pipeline Syntax

Pipelines are written in KDL, a document language that's cleaner than YAML and more readable than JSON.

pipeline "hello" {
  step "greet" {
    image "alpine:latest"
    run "echo 'Hello from Cicada!'"
  }

  step "build" {
    image "rust:1.76"
    depends-on "greet"
    mount "." "/src"
    workdir "/src"
    run "cargo build"
  }
}

Step / Job Options

Option Scope Description Example
image job Container image to run in image "golang:1.25"
run step Shell command (multiple allowed) run "go test ./..."
depends-on job Job dependency (runs after) depends-on "build"
mount job/step Bind mount from host mount "." "/src"
mount (ro) job/step Read-only bind mount mount "." "/src" readonly=true
workdir job/step Working directory inside container workdir "/src"
cache job/step Persistent cache volume cache "gomod" "/go/pkg/mod"
env job/step Environment variable env "GO111MODULE" "on"
secret job/step Inject a declared secret secret "TOKEN" env="TOKEN"
shell job/step Override default shell shell "/bin/bash" "-e" "-c"
timeout job/step Execution timeout timeout "5m"
retry job/step Retry on failure retry { attempts 3; delay "5s" }
allow-failure step Step failure does not fail the job allow-failure
when job/step Conditional execution (CEL expression) when "branch == 'main'"
no-cache job/step Disable BuildKit cache no-cache
platform job OCI target platform platform "linux/arm64"
export job/step Export a path to the host export "/out/app" local="./bin/app"
publish job Publish filesystem as OCI image publish "ghcr.io/user/app:latest"

See docs/schema.md for the full reference and inheritance rules. Dependencies between jobs are resolved via topological sort; Cicada catches cycles and missing references before anything runs.

CLI Usage

# Validate without running
cicada validate pipeline.kdl

# Render a dependency graph
cicada visualize pipeline.kdl
cicada visualize pipeline.kdl --output pipeline.d2

# Run a pipeline
cicada run pipeline.kdl

# Run against a remote BuildKit daemon
cicada run pipeline.kdl --addr tcp://buildkit.example.com:1234

# Dry run (generate LLB without executing)
cicada run pipeline.kdl --dry-run

# Skip cache
cicada run pipeline.kdl --no-cache

# Pre-pull images, then run offline
cicada pull pipeline.kdl
cicada run pipeline.kdl --offline

# Manage the local BuildKit engine
cicada engine start
cicada engine status
cicada engine stop

See docs/cli.md for the complete flag reference.

Development

mise is the preferred way to interact with the project. All common tasks are a mise invocation away:

mise build        # Build the CLI binary to ./bin/cicada
mise test         # Run tests with -race
mise vet          # Run go vet
mise lint         # Run golangci-lint
mise fmt          # Format code with gofmt
mise fmt:check    # Check formatting (CI-friendly)
mise ci           # Run the full CI suite locally (build + fmt + vet + lint + test)

Yes, mise ci runs the actual CI checks on your machine. Locally repeatable CI -- we meant it.

Project Layout

cmd/cicada/            CLI entry point
pkg/pipeline/          Pipeline types and validation (importable)
pkg/parser/            KDL-to-Pipeline parser (importable)
pkg/conditional/       CEL-based condition evaluation (importable)
pkg/slogctx/           slog context utilities (importable)
pkg/gitinfo/           Git metadata helpers (importable)
internal/builder/      BuildKit LLB generation
internal/runner/       BuildKit execution engine
internal/cache/        Cache analytics and spec parsing
internal/secret/       Host-side secret resolution
internal/runtime/      Container runtime detection and abstraction
internal/tracing/      OpenTelemetry setup and vertex observer
examples/              Example KDL pipelines

Packages under pkg/ are stable and safe for external consumers to import. Packages under internal/ are implementation details.

Why Go?

Cicada was originally planned in Rust, and if you go back to the first few commits, you'll see that is how it started. But the container ecosystem speaks Go. BuildKit, containerd, the OCI spec libraries, Docker itself -- they're all Go projects with Go APIs. Writing Cicada in Rust would have meant maintaining FFI bindings or shelling out to CLI wrappers, or attempting to maintain complex gRPC-based session code for core functionality, trading real engineering time for a language preference.

Go gave us native BuildKit integration (LLB construction, solve API, session management) with zero glue code. The tradeoff was worth it.

Roadmap

See ROADMAP.md for what's coming next; matrix builds, modular configs, advanced caching, and more.

About

CI that ticks the boxes

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages