Development

Prerequisites#

Build#

git clone https://gitlab.com/gitlab-org/ci-cd/runner-tools/glci
cd glci
go build -o glci ./cmd/glci
make docker   # required: builds glci:local image

The glci:local Docker image contains the mock server binary. It must match the CLI binary’s build commit — the daemon refuses to start if there is a version mismatch. Always re-run make docker after pulling new changes or switching branches.

The build embeds the git commit hash via linker flags so the daemon can detect version mismatches:

make build    # builds ./glci with embedded commit hash
make install  # installs to $GOPATH/bin with embedded commit hash

Project structure#

glci/
├── cmd/glci/           # CLI entry point and subcommands
├── pkg/
│   ├── config/         # YAML parsing (offline + API-assisted), inputs, matrix expansion
│   ├── daemon/         # Background daemon: server, client, executor, eventbus,
│   │                   #   transport, crash recovery, execution state, registry
│   ├── gitutil/        # Git helper utilities (diff, clean env)
│   ├── glciconfig/     # Config file parsing: ~/.glci/config.toml + .glciconfig.toml
│   ├── history/        # Pipeline persistence (~/.glci/projects/{key}/pipelines/)
│   ├── logparse/       # Trace output parsing: strip timestamps, colorize sections
│   ├── mockserver/     # Mock GitLab API, git HTTP, artifacts, cache, auth
│   ├── muxproto/       # Binary protocol for docker exec transport
│   ├── pipeline/       # Pipeline graph rendering (ANSI, JSON, watch mode)
│   ├── planner/        # Execution plan: stages, DAG, filtering, auto-deps
│   ├── rules/          # rules: if: expression evaluation (boolean, regex, variables)
│   ├── runner/         # gitlab-runner integration: job spec JSON, config.toml generation
│   ├── scheduler/      # Concurrent job dispatch, DAG-aware, resource groups
│   ├── tui/            # Interactive TUI (bubbletea): app, graph, log viewer
│   ├── variables/      # CI variable generation, API fetch, slugify, context simulation
│   └── version/        # Build-time version metadata
├── e2e/                # End-to-end and compatibility tests
│   └── testdata/       # Test fixtures (comprehensive CI config, golden fixtures)
├── docs/plan/          # Planning and sprint documents
└── Makefile

Test#

Unit tests require no Docker and no network access:

make test             # run all unit tests
make test-v           # verbose output
go test ./pkg/...     # run unit tests for a specific package subtree
go test ./pkg/config/ # run tests for a single package

Lint:

go vet ./...

Test tiers#

Tests are organized into tiers of increasing scope. Each tier is a separate Make target with no overlap between targets.

Tier 0: Unit tests (always run)#

make test       # no Docker, no network

Tier 1: Parse and golden tests#

make test-e2e-parse    # parse comprehensive pipeline fixture (~1m18s)
make test-e2e-golden   # compare pipeline JSON against golden fixtures (~1m20s)

These validate CI config parsing and pipeline structure without running any containers.

Tier 2: TUI tests#

make test-tui          # TUI and CLI rendering tests (~2m14s)

Tier 3: Integration tests#

These run real pipelines with Docker containers:

make test-integration              # stateless pipeline execution (~7m, Docker required)
make test-integration-light        # artifact CLI, CLI commands, daemon logs (~1m27s, no Docker)
make test-integration-cli          # CLI functional tests: lint, jobs, doctor, history (~1m)
make test-integration-cli-daemon   # CLI daemon-dependent tests: run, ps, log, registry (~3m, Docker)
make test-integration-docker       # Docker infra, DinD, containerized pipelines (~7m, Docker)
make test-integration-registry     # Registry push/pull/mirror tests (~7m, Docker)
make test-integration-lifecycle    # Crash recovery, force stop, daemon lifecycle (~7m, Docker)
make test-e2e-simulate             # Simulate comprehensive pipeline (~5m, Docker)

Tier 4: Comprehensive compatibility#

make test-e2e-comprehensive          # full compat run (~4m, Docker, requires glci:local)
make test-e2e-comprehensive-release  # release pipeline compat (~3m, Docker)

These targets require the glci:local Docker image. Build with make docker first.

Tier 5: External (network required)#

make test-e2e-external   # real external repos (~50m, Docker + network)

Only run when explicitly needed. These clone external repositories and run full pipelines.

What to run for your changes#

Changed areaRun
Anything in pkg/make test
CLI commands (cobra)make test + make test-integration-cli + make test-integration-cli-daemon
TUImake test + make test-tui
Config parsing, rules, plannermake test + make test-e2e-parse
Pipeline JSON, when/allow_failuremake test + make test-e2e-golden
Mock server, scheduler, DAGmake test + make test-e2e-simulate
Pipeline execution, daemonmake test + make test-integration
Docker executor, DinDmake test + make test-integration-docker
Registry, push-throughmake test + make test-integration-registry
Daemon lifecycle, recoverymake test + make test-integration-lifecycle
Broad or uncertainmake test + make test-e2e-comprehensive

Override the timeout for long-running tests:

make test-e2e-comprehensive COMPAT_TIMEOUT=30m

Tail daemon logs during tests#

E2e tests run isolated daemon instances with their own GLCI_HOME. To tail the active test daemon’s log:

make test-logs

This finds the most recent daemon.log under .glci-compat-*/ or .glci-e2e-*/ and tails it.

Golden fixture updates#

If you change pipeline JSON output or parsing behavior, golden fixtures may need updating:

make golden-update    # regenerate golden fixtures from the GitLab API (network required)

Then verify the diff makes sense before committing.

Adding comprehensive test fixtures#

When adding a new CI keyword, runner config field, or mock server capability:

  1. Add a job exercising it to e2e/testdata/comprehensive/ci/jobs.yml
  2. Update the parse test (compat_pipeline_test.go)
  3. Update the full-run test (compat_test.go)
  4. Update all three golden fixtures in e2e/testdata/golden/comprehensive/*.json
  5. Rebuild glci:local with make docker
  6. Run make test-e2e-comprehensive

Key dependencies#

glci prefers external libraries over reimplementing. Key dependencies (see go.mod):

LibraryPurpose
cobraCLI framework
yaml.v3YAML parsing
BurntSushi/tomlTOML config files
bubbletea / bubbles / lipglossInteractive TUI
go-containerregistryOCI registry implementation
testifyTest assertions
fsnotifyFile watching
schollz/progressbarProgress bars
mattn/go-isattyTTY detection
stdlib net/httpHTTP routing (no framework)

Development plan#

Development is organized in incremental sprints documented in docs/plan/. Each sprint has a dedicated design document covering the problem, approach, and implementation details.

Architecture and design decisions are documented in ARCHITECTURE.md.

Git hooks#

Install a pre-push hook that runs a quick lint pipeline before each push:

make install-hooks

This installs a pre-push hook that runs glci run -p precheck.

Release process#

Releases are automated via Make:

make release              # bump minor version, generate changelog, tag, push
make release BUMP=patch   # bump patch version instead
make release BUMP=major   # bump major version

This generates a changelog from GitLab merge request labels, commits it, creates a git tag, and pushes.

Demo recordings#

The home page features terminal demo videos recorded with VHS. Tape files (recording scripts) live in demos/tapes/, numbered in display order:

demos/
├── tapes/
│   ├── 01-lint.tape          # CI Lint
│   ├── 02-run.tape           # Run Pipeline
│   ├── 03-context.tape       # Context Simulation
│   ├── 04-artifacts.tape     # Artifact Download
│   ├── 05-registry.tape      # Local Docker Registry
│   ├── 06-pages.tape         # Pages Preview
│   └── 07-tui.tape           # Interactive TUI
├── output/                   # Generated mp4 files
└── project/                  # Demo project with .gitlab-ci.yml

Prerequisites#

Make targets#

make demos           # build only changed demos + copy to site/static/demos/
make demos-rebuild   # clean all + rebuild everything
make demos-clean     # delete all generated mp4s

Adding a new demo#

  1. Create demos/tapes/NN-name.tape following the existing tape format (same Set Theme, Set FontSize 36, Set Width 2400, Set Height 1200)
  2. Set the output path: Output ../output/NN-name.mp4
  3. If the demo needs a custom CI config, add it to demos/project/ (e.g. .gitlab-ci.registry.yml)
  4. Add a tab button and video panel in site/layouts/home.html
  5. Run make demos to record and copy to site/static/demos/

Tape conventions#

All tapes share identical settings for visual consistency:

Playback speed varies per demo (Set PlaybackSpeed). The site JS also applies 1.5× playback on videos, so the effective speed is PlaybackSpeed × 1.5.

Rebuilding a single demo#

cd demos/project
vhs ../../demos/tapes/01-lint.tape
cp ../output/01-lint.mp4 ../../site/static/demos/

License#

MIT

Esc