Skip to content

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Anvil

CI CodeQL Go report card Go 1.25 MIT

Anvil is a continuous integration runner for a single machine with a container runtime. It reads a pipeline written in YAML, resolves the dependency graph between steps, and runs each step in its own container. Steps that do not depend on each other run in parallel; a step that fails takes its dependents down with it while the rest of the graph keeps moving. Logs stream live to a panel and to the command line as the run happens.

Two binaries: anvil runs the daemon, anvilctl drives it. It speaks the Docker Engine API over the runtime socket, so the only dependency outside the standard library is a YAML parser.

A run, start to finish

$ anvilctl run
run d1aabb227177 started
checkout     │ step passed
build        │ compiling ledger-service
lint         │ no findings
build        │ step passed
integration  │ step failed with exit 1, tolerated
unit         │ ok  142 passed
package      │ packaged ledger-service:demo
publish      │ published
anvil        │ pipeline passed in 19.503s

run d1aabb227177 passed

The panel draws the same run as an engineering schematic. Steps are laid out by stage, dependencies are drawn as dimension-line connectors, and each node lights up as its container starts, passes, or fails.

Anvil panel, a run in progress

What a run does

  1. A trigger arrives from the panel or anvilctl. Anvil loads the pipeline and builds the dependency graph.
  2. The graph is validated. Self-edges, references to unknown steps, and cycles are rejected before anything runs — the offending cycle path is named in the error.
  3. A per-run workspace volume is created and seeded once from the source tree.
  4. The scheduler runs each step as soon as its dependencies pass, bounded by a worker pool. Independent steps run at the same time.
  5. Each step runs in its own container mounting the shared workspace and a cache volume per cache entry. The exit code comes from waiting on the container; output streams to the run log.
  6. A failed step marks its dependents skipped and the skip cascades. A step may be allowed to fail without failing the run.

Full detail and a component diagram are in docs/architecture.md.

Anvil panel, a completed run

The pipeline

A pipeline is a set of named steps. Each step names an image and a script; needs wires the graph. Top-level image and env are defaults so common values are not repeated.

name: ledger-service
image: golang:1.25-alpine
env:
  CI: "true"

steps:
  build:
    cache:
      - key: modules
        path: /go/pkg/mod
        hash: [go.sum]
    run: go build ./...

  lint:
    run: go vet ./...

  test:
    needs: [build]
    memory: 512m
    cpus: 1.5
    run: go test ./...

  integration:
    needs: [build]
    allow_failure: true
    run: ./scripts/integration.sh

  package:
    needs: [test, lint]
    run: ./scripts/package.sh
Field Meaning
image container image the step runs in; falls back to the top-level image
run shell script executed with /bin/sh -c inside the workspace
needs step ids this step depends on
env environment for the step, merged over the top-level env
cache key, path, and hash — the cache key is derived from the contents of the hashed files
memory / cpus resource limits applied to the step container
allow_failure a non-zero exit is tolerated and does not fail the run
shell / workdir override the shell (/bin/sh) or working directory (/workspace)

The cache key is content-addressed. It is derived from the contents of the files named in hash, never from their path, so an unchanged dependency set reuses the cache and a changed one rotates it. The key names a runtime volume, so persistence across runs is owned by the container runtime.

Running it

Build the binaries:

$ make build

Run the daemon against a source directory that holds an .anvil.yml:

$ ./bin/anvil -config configs/anvil.yaml

The panel is served on the configured address; anvilctl talks to the same endpoint.

$ anvilctl status
$ anvilctl pipeline
$ anvilctl run

The demo

The demo stack runs Anvil in a container against the host runtime socket and forges a sample eight-step pipeline end to end, including a tolerated failure.

$ make demo          # panel on http://127.0.0.1:18110
$ make demo-down

The screenshots above are captured from this demo. The command-line session:

anvilctl forging the demo pipeline

Commands

Command Purpose
anvilctl status daemon version, runtime reachability, and run tally
anvilctl pipeline the current pipeline grouped by stage
anvilctl runs run history
anvilctl run trigger a run and follow its log to completion, exiting non-zero on failure
anvilctl logs <id> follow the log of a specific run

anvilctl reads ANVIL_ADDR and ANVIL_TOKEN, or takes -addr and -token.

Configuration

server:
  listen: "0.0.0.0:8080"
  state_file: /var/lib/anvil/state.json
  admin_token: "${ANVIL_TOKEN}"
  ui: true

run:
  source_dir: /var/lib/anvil/source
  pipeline_file: .anvil.yml
  concurrency: 2
  network: ""
  keep_runs: 20
  label: anvil

runtime:
  socket: /var/run/docker.sock

logging:
  level: info
  format: json

${VAR} references are expanded from the environment. anvil -config <path> -check validates a configuration and exits. When admin_token is set, the API and panel require it as a bearer token.

Building and testing

$ make test      # unit tests
$ make race      # tests under the race detector
$ make lint      # golangci-lint: errcheck, govet, staticcheck, gosec, and more
$ make cover     # coverage summary

The graph, scheduler, and cache carry the load-bearing tests: cycle detection and deterministic ordering, skip propagation and the concurrency limit, and content-addressed keys that ignore paths and refuse to reach outside the source tree.

License

MIT

About

Continuous integration runner that resolves a YAML pipeline into a container-per-step DAG

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages