https://github.com/ndisidore/cicada
CI that ticks the boxes
https://github.com/ndisidore/cicada
continuous-delivery continuous-integration
Last synced: 12 days ago
JSON representation
CI that ticks the boxes
- Host: GitHub
- URL: https://github.com/ndisidore/cicada
- Owner: ndisidore
- License: apache-2.0
- Created: 2026-02-04T18:39:01.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-06-10T14:57:36.000Z (13 days ago)
- Last Synced: 2026-06-10T16:25:42.318Z (13 days ago)
- Topics: continuous-delivery, continuous-integration
- Language: Go
- Homepage:
- Size: 691 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
- Agents: AGENTS.md
Awesome Lists containing this project
README
# Cicada. Hear your CI before it ships.
A container-native CI pipeline runner powered by [BuildKit](https://github.com/moby/buildkit).
Define pipelines in [KDL](https://kdl.dev), 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](https://dagger.io/) 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](https://mise.jdx.dev/) -- task runner and tool manager (handles Go + linter versions for you)
- A running [BuildKit](https://github.com/moby/buildkit) daemon (`buildkitd`)
## Quick Start
```bash
# 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](https://kdl.dev), a document language that's cleaner than YAML and more readable than JSON.
```kdl
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](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
```bash
# 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](docs/cli.md) for the complete flag reference.
## Development
[mise](https://mise.jdx.dev/) is the preferred way to interact with the project. All common tasks are a `mise` invocation away:
```bash
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
```text
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](ROADMAP.md) for what's coming next; matrix builds, modular configs, advanced caching, and more.