local-ci

module
v0.2.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MIT

README

Local CI

Local CI is a tool that allows you to run CI/CD pipelines locally using Docker containers. It helps developers test and debug their CI pipelines without pushing to remote repositories.

Features

  • Run CI pipeline jobs locally using Docker
  • Stages-based execution similar to GitLab CI
  • Configuration using YAML format
  • Reuse local images by default, with force_pull for refreshing from the registry
  • Private registry credentials at pipeline or job level
  • Global and job-specific environment variables
  • Working directory customization
  • Cache support for dependencies and build artifacts
  • Automatic file copying with .gitignore support
  • Real-time log streaming from containers
  • Automatic container cleanup
  • Stage-based pipeline execution
  • Job-based pipeline execution
  • Parallel job execution (all jobs at once, or per-stage) with a live status board
  • Per-job parallel: true keyword for detaching individual jobs from the sequential chain
  • Matrix builds: parametrize a job with matrix: to fan it out across variable combinations
  • Services: sidecar containers (databases, caches) on a private per-job network with readiness gates — services: [postgres:16]
  • Artifacts: pass build outputs from one job to the next with artifacts: paths: [...]
  • DAG scheduling: needs: [job] starts a job the moment its dependencies pass instead of waiting for the stage barrier
  • Per-job timeout: and retry: for slow or flaky jobs
  • Templates and includes: factor common config into .dot-prefixed templates and pull shared files with include:
  • Stage placeholders: splice an included file's stages into the main pipeline at a chosen position
  • GitLab utils
  • Bootstrap scripts
  • Cleanup scripts (companion to bootstrap)
  • Per-job bootstrap and cleanup scripts
  • Web UI: live pipeline graph, ANSI-colored searchable logs, run control (env vars, per-stage/per-job runs, failed-job re-run), desktop notifications, and a built-in YAML editor with live lint — served from a single binary with local-ci ui
  • Run history with git context (branch + commit per run), plus per-job duration trends and flaky-job detection; inspect from the UI or local-ci runs / local-ci log
  • Debug shell: local-ci shell <job> drops you into the job's exact container environment — image, variables, workspace, cache mounts, and running services
  • Watch mode: local-ci run --watch re-runs the pipeline on every file change
  • Config linting (local-ci validate) and a .gitlab-ci.yml importer (local-ci import gitlab)
  • Claude Code agent plugin

Installation

Prerequisites
  • Docker must be installed and running
  • Your user must have permissions to access the Docker daemon
  • Verify installation with docker --version
Homebrew (macOS / Linux)
brew install --cask MrPuls/local-ci/local-ci

Shell completions for bash, zsh, and fish are installed automatically.

Go Install

Requires Go 1.26 or later (download). The web UI build is committed, so the installed binary includes local-ci ui with no extra tooling.

go install github.com/MrPuls/local-ci/cmd/local-ci@latest
Manual

Download the latest binary for your platform (Linux, macOS, or Windows) from the releases page. The archives also carry completion scripts (completions/) for bash, zsh, fish, and PowerShell; or generate them on the fly with local-ci completion <shell>.

Shell completions

Completions are context-aware: local-ci shell <TAB> lists the jobs defined in your config, -j/-s complete job and stage names, local-ci log <TAB> completes recent run ids (with their status), and -c offers the discovered config files. Homebrew installs them for you; otherwise see local-ci completion --help.

Build from source

A Makefile wraps the common tasks (run make help for the full list). Building the UI needs Bun; the Go binary needs Go 1.26+.

make build      # build the web UI + the local-ci binary into ./bin
make install    # build the UI and `go install` the binary

After changing anything under web/, run make web and commit the regenerated internal/web/dist so the embedded UI stays in sync.

Usage

Check Installation
local-ci --version
Run a Pipeline
# Run using default .local-ci.yaml in current directory
local-ci run

# Run with a specific config file using --config/-c flag
local-ci run --config my-pipeline.yaml

# Run a specific job using --job/-j
local-ci run --job JobName

# Or to run multiple jobs
local-ci run --job jobName1,JobName2

# Run jobs from a specific stage using --stage/-s
local-ci run --stage stageName

# Or use multiple stages
local-ci run --stage stageName1,stageName2

# Run all jobs in parallel with --parallel/-p
local-ci run --parallel

# Or run stages in order, with jobs inside each stage in parallel
local-ci run --parallel-stages

# Clone/update the repository and run it's local-ci.yaml with --remote/-r
local-ci run --remote <repository_url>

# Pass additional environment variables with --env/-e
local-ci run --env NEW_VAR=var_value,SECOND_VAR=new_value

When you don't pass -c/--config, run scans the working directory for config files — the canonical .local-ci.yaml/.local-ci.yml plus any <name>.local-ci.yaml / <name>-local-ci.yaml / <name>_local-ci.yaml variant — and asks which one to load (Enter picks the first). Non-interactive sessions never block: a single discovered file is used as-is, anything else falls back to .local-ci.yaml.

Developer loop
# Re-run the pipeline on every file change (Ctrl-C to stop)
local-ci run --watch

# Drop into a job's exact container environment to debug it from the inside:
# same image, variables, workdir, cache mounts, copied workspace — and its
# services running on the job network
local-ci shell Test

# Lint the config without running anything (exit 1 when invalid — git-hook friendly)
local-ci validate

# Convert an existing GitLab CI config (prints notes for anything that can't carry over)
local-ci import gitlab
Web UI

local-ci ui serves the whole web app — its UI and API — from this single binary, then opens it in your browser. No separate dev server or token to manage.

# Serve the UI for the project in the current directory and open a browser
local-ci ui

# Bind a specific port and don't auto-open a browser
local-ci ui --port 8080 --no-open

# Use a non-default config file
local-ci ui --config my-pipeline.yaml

It binds loopback only (127.0.0.1) and shows the configured pipeline as a live graph: trigger and cancel runs, watch job status update in real time, stream logs, and browse run history — all backed by the same engine as local-ci run.

On load the UI scans the project directory for config files (same patterns as run) and asks which one to drive the session; switch any time via the FILE: chip in the top bar. The EDITOR tab is a built-in YAML editor with syntax highlighting, live lint (undefined stages, unknown needs targets, missing fields — flagged before you save), validation feedback, a diff preview before discarding edits, and a live pipeline graph — Ctrl+S writes to disk.

The pipeline view exposes the full trigger surface: pick a run mode, set env vars, run a single stage from its column header, or a single job from the inspector (failed jobs get a one-click re-run). Logs render ANSI colors and are searchable. History shows each run's git context (branch@sha) and a JOB_TRENDS panel: per-job duration sparklines, pass rates, and an INTERMITTENT flag for jobs that flip between pass and fail. The bell in the top bar enables desktop notifications when a run finishes in a hidden tab.

For frontend development against a hot-reloading dev server, use local-ci serve (the API-only backend) together with the Vite dev server — see web/README.md.

Inspecting past runs

Every run (CLI or UI) is recorded to a local history store.

# List recent runs for the current project (newest first)
local-ci runs

# List runs across all projects, capping the count
local-ci runs --all --limit 50

# Show one run's per-job breakdown
local-ci runs <run-id>

# Print a recorded run's logs — all jobs, or one job
local-ci log <run-id>
local-ci log <run-id> --job Build
local-ci log <run-id> --job pipeline   # run-level diagnostics
Command reference
Command Description Flags
run Run the pipeline -c/--config, -j/--job, -s/--stage, -r/--remote, -e/--env, -p/--parallel, --parallel-stages, -w/--watch, --no-record
shell <job> Open an interactive shell in the job's container environment (services included) -c/--config, -v/--verbose
validate [file] Lint a config without running it (exit 1 when invalid) -c/--config
import gitlab [file] Convert a .gitlab-ci.yml into a local-ci config -o/--output (default .local-ci.yaml, - for stdout), --force
runs [run-id] List recorded runs, or show one run's details -a/--all, -n/--limit (default 20)
log <run-id> Print a recorded run's logs -j/--job (use pipeline for diagnostics)
ui Serve the embedded web UI and API from one binary, then open a browser --host (default 127.0.0.1), --port (default 4123), -c/--config, --no-open
serve Run the API-only backend (for the web dev server or a future desktop shell) --host (default 127.0.0.1), --port (default ephemeral), --token (default random), -c/--config

Global: local-ci --version, local-ci --help, local-ci <command> --help.

Quick Start

  1. Start Docker

  2. Create a .local-ci.yaml file in your project root:

stages:
  - build
  - test

variables:
  GLOBAL_VAR: global_value

Build:
  stage: build
  image: golang:1.21
  variables:
    GO_FLAGS: "-v"
  script:
    - echo "Building application..."
    - go build $GO_FLAGS
  cache:
    key: go-build-cache
    paths:
      - .go/
      - build/

Test:
  stage: test
  image: golang:1.21
  script:
    - echo "Testing with global var: $GLOBAL_VAR"
    - go test ./...
  cache:
    key: go-build-cache
    paths:
      - .go/
  1. Run the pipeline:
local-ci run

YAML Configuration

Basic Structure
# Define stages and their order
stages:
  - build
  - test
  - deploy

# Global variables (available to all jobs)
variables:
  GLOBAL_KEY: global_value

# Job definitions
job_name:
  stage: build    # Must match one of the defined stages
  image: image_name
  workdir: /path   # Optional
  variables:       # Job-specific variables (override globals)
    KEY: value
  script:
    - command1
    - command2
  cache:           # Optional cache configuration
    key: cache-key
    paths:
      - path/to/cache
      - another/path
Image Pull Policy

Images are reused from the local Docker daemon when available and pulled only when missing:

image: ubuntu

To always pull from the registry before running the job:

image:
  name: ubuntu
  force_pull: true

The same image options work for services and interactive shells. force_pull defaults to false.

Private Registries

Private registry credentials can be configured once for all jobs and services:

registries:
  registry.example.com:
    username: ci-user
    password_env: REGISTRY_TOKEN

Set REGISTRY_TOKEN in the environment running local-ci, then use an image such as registry.example.com/team/build:latest. username_env is also supported. For registries without an entry, local-ci uses the existing Docker login for the image's registry. See registry configuration for all options.

You can also declare the registry login and image in the same job:

stages: [build]

Build:
  stage: build
  registries:
    registry.example.com:
      username: ci-user
      password_env: REGISTRY_TOKEN
  image: registry.example.com/team/build:latest
  script:
    - make build

Job credentials are available before pulling that job's image and services, overriding pipeline credentials for matching hosts. Other jobs keep their own settings. The same configuration works with local-ci shell Build. See job-level registries for precedence and template inheritance.

Claude Code Agent

Local CI ships as a Claude Code plugin. Once installed, Claude Code can autonomously write pipeline configs, run them, and validate your code in real Docker environments.

Install the plugin

/plugin marketplace add MrPuls/local-ci

/plugin install local-ci@MrPuls-local-ci

See the Technical Reference for details.

Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License

Directories

Path Synopsis
cmd
local-ci command
internal
gitlabimport
Package gitlabimport converts a .gitlab-ci.yml into a local-ci config.
Package gitlabimport converts a .gitlab-ci.yml into a local-ci config.
runmanager
Package runmanager owns in-process pipeline execution for the server: it triggers runs (each engine.Run on its own goroutine + bus), tracks active runs so they can be cancelled, and fans their live events out to SSE subscribers.
Package runmanager owns in-process pipeline execution for the server: it triggers runs (each engine.Run on its own goroutine + bus), tracks active runs so they can be cancelled, and fans their live events out to SSE subscribers.
server
Package server exposes the engine, run manager and store over a loopback JSON API plus an SSE live-event stream.
Package server exposes the engine, run manager and store over a loopback JSON API plus an SSE live-event stream.
sink/eventlog
Package eventlog is a sink that appends the engine's event stream to a per-run events.ndjson file (one JSON-encoded WireEvent per line).
Package eventlog is a sink that appends the engine's event stream to a per-run events.ndjson file (one JSON-encoded WireEvent per line).
sink/recorder
Package recorder is the persistence sink: it records the engine's event stream into the durable run store (SQLite metadata + per-run log files under <xdg>/local-ci/runs/<id>/).
Package recorder is the persistence sink: it records the engine's event stream into the durable run store (SQLite metadata + per-run log files under <xdg>/local-ci/runs/<id>/).
sink/terminal
Package terminal renders the engine's event stream to a terminal, exactly reproducing the output the CLI produced before the engine/sink split: streamed logs for sequential jobs, a repainting status board for concurrent groups, "[detached]" status lines, and per-job log files under .local-ci/logs/<timestamp>/.
Package terminal renders the engine's event stream to a terminal, exactly reproducing the output the CLI produced before the engine/sink split: streamed logs for sequential jobs, a repainting status board for concurrent groups, "[detached]" status lines, and per-job log files under .local-ci/logs/<timestamp>/.
store
Package store is the durable run-history layer: an embedded SQLite database (pure-Go modernc driver) plus a central directory of per-run log files.
Package store is the durable run-history layer: an embedded SQLite database (pure-Go modernc driver) plus a central directory of per-run log files.
web
Package web embeds the built single-page app so `local-ci ui` can serve the whole UI from one binary.
Package web embeds the built single-page app so `local-ci ui` can serve the whole UI from one binary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL