gitlab-sim
Disclaimer: This tool was built through iterative AI-assisted development with Claude. It is experimental, incomplete, and not intended for production use. Coverage of GitLab CI keywords is best-effort and may lag behind GitLab's evolving spec. Use it at your own discretion — no correctness guarantees are made. Contributions and bug reports are welcome.
A local tool to validate and lint .gitlab-ci.yml pipelines without needing a GitLab server.
Features
- YAML validation — detects malformed pipeline files early
- Stage validation — every job's
stagemust be declared instages extends:resolution — resolves single and multi-level template inheritance before linting, so derived jobs are evaluated against their fully merged definitionneeds:DAG validation — checks thatneeds:references exist, respect stage ordering, and contain no circular dependenciesdependencies:validation — checks that artifact dependency references exist and are in earlier stages- Keyword validation — validates constraints on
when,parallel,retry,allow_failure,trigger,artifacts,cache,release,environment,coverage,rules, and more - Remote project includes — fetches
include: project:templates from the GitLab API so extends/needs can be validated against the full merged pipeline - CI/CD catalog components — resolves
include: component:references from the GitLab CI/CD Catalog; public components work without a token - Deprecation warnings — flags
only/exceptusage in favour ofrules - Graph output — emits Mermaid diagrams for the include dependency tree and the pipeline jobs layout (DAG or classic stage ordering)
Requirements
- Go 1.21 or later
- Task (optional, for development tasks)
Installation
git clone https://git.k3nny.fr/gitlab-sim
cd gitlab-sim
go build -o gitlab-sim ./cmd/gitlab-sim/...
Or with Task:
task build
Usage
gitlab-sim [options] <pipeline.yml>
Exits 0 when no errors are found, 1 when at least one error is reported.
Remote project includes
Pipelines that include templates from other GitLab projects are supported.
Provide a token so gitlab-sim can fetch them:
# personal access token (read_api scope)
GITLAB_TOKEN=glpat-xxxx gitlab-sim .gitlab-ci.yml
# CI/CD job token (when running inside a pipeline)
CI_JOB_TOKEN=$CI_JOB_TOKEN gitlab-sim .gitlab-ci.yml
# self-hosted GitLab
GITLAB_TOKEN=glpat-xxxx GITLAB_URL=https://gitlab.example.com gitlab-sim .gitlab-ci.yml
# or via flags
gitlab-sim --token glpat-xxxx --gitlab-url https://gitlab.example.com .gitlab-ci.yml
Project includes require a token; without one they are skipped with a warning and the rest of the pipeline is linted as-is.
Component includes (include: component: ...) attempt the fetch
unauthenticated, so public CI/CD Catalog
components work without a token. A warning is emitted if the fetch fails.
Token resolution order (first non-empty wins):
| Source | Header used |
|---|---|
--token flag / GITLAB_TOKEN |
PRIVATE-TOKEN |
CI_JOB_TOKEN |
JOB-TOKEN |
GITLAB_PRIVATE_TOKEN |
PRIVATE-TOKEN |
Instance URL resolution order: --gitlab-url flag → CI_SERVER_URL →
GITLAB_URL → https://gitlab.com
Component reference format
<host>/<project-path>/<component-name>@<version>
gitlab.com/components/secret-detection/secret-detection@v0.1.0
gitlab.com/my-org/ci-catalog/lint@main
gitlab.example.com/platform/components/build@~latest
The component file is looked up in order:
templates/<component-name>.yml(single-file layout)templates/<component-name>/template.yml(directory layout)
Component input parameters (with:) are not validated — they are resolved by
GitLab at runtime. Jobs in fetched components may use $[[ inputs.xxx ]]
placeholders in fields like stage; gitlab-sim skips those fields rather
than producing false positive errors.
Graph output
Pass --graph to visualise the pipeline instead of running lint rules.
# Include dependency graph (which files include which) → Mermaid to stdout
gitlab-sim --graph includes .gitlab-ci.yml > includes.mmd
# GitLab-like pipeline layout → PNG (or SVG fallback) written to --graph-out dir
gitlab-sim --graph pipeline .gitlab-ci.yml
# prints the output file path, e.g.: gitlab-sim-out/pipeline-20260607-143022.png
# Both at once: Mermaid to stdout + pipeline file path to stderr
gitlab-sim --graph all .gitlab-ci.yml > includes.mmd
# Custom output directory
gitlab-sim --graph pipeline --graph-out /tmp/graphs .gitlab-ci.yml
Include graph (--graph includes) — Mermaid flowchart written to stdout.
Pipe to a .mmd file or paste into mermaid.live.
One node per include entry, colour-coded by type:
- Orange (bold): the main pipeline file
- Purple:
project:includes - Green:
component:includes - Blue:
local:includes - Grey:
remote:URL includes - Light orange: GitLab-provided
template:includes
Pipeline graph (--graph pipeline) — GitLab CI-style SVG rendered to a timestamped file
in the --graph-out directory (default: gitlab-sim-out/). Converted to PNG automatically
when rsvg-convert, inkscape, or magick is available; falls back to SVG otherwise.
Jobs are colour-coded by type:
- Blue (
#1f75cb): regular jobs - Orange (
#fc6d26):when: manualjobs - Purple (
#6b4fbb):trigger:jobs - Amber (
#fca326):when: delayedjobs
DAG mode (job-to-job Bézier arrows) activates automatically when any job has a needs: list.
Classic mode draws L-shaped or straight connectors between stage columns otherwise.
Example output
# Clean pipeline
OK: .gitlab-ci.yml — no issues found (5 jobs, 3 stages)
# Pipeline with issues
[ERROR] job "deploy": stage "production" is not defined in 'stages'
[ERROR] job "test": needs unknown job "build-app"
[WARNING] job "old-job": 'only'/'except' are deprecated; prefer 'rules'
3 finding(s): 2 error(s)
Lint rules
Pipeline-level
| Severity | Rule |
|---|---|
| ERROR | workflow.rules[*].when is not always or never |
| WARNING | No stages defined (GitLab falls back to default stages) |
Job-level — structure
| Severity | Rule |
|---|---|
| ERROR | Job is missing required script (or run) — non-trigger, non-template jobs |
| ERROR | Job references a stage not declared in stages |
| ERROR | only and rules used together on the same job |
| ERROR | except and rules used together on the same job |
| WARNING | only/except used (deprecated, prefer rules) |
Job-level — keyword constraints
| Severity | Rule |
|---|---|
| ERROR | when is not one of on_success, on_failure, always, manual, delayed, never |
| ERROR | when: delayed without start_in |
| ERROR | start_in set but when is not delayed |
| ERROR | parallel integer not in range 2–200 |
| ERROR | parallel map form missing matrix key |
| ERROR | retry integer not in range 0–2 |
| ERROR | retry.max not in range 0–2 |
| ERROR | retry.when contains an invalid failure type |
| ERROR | allow_failure is not a boolean or a map with exit_codes |
| ERROR | interruptible is not a boolean |
| ERROR | trigger job also has script |
| ERROR | trigger map missing project or include |
| ERROR | coverage is not a regex pattern wrapped in / |
| ERROR | release missing required tag_name |
| ERROR | environment.url set without environment.name |
| ERROR | environment.action is not one of start, stop, prepare, verify, access |
| ERROR | artifacts.when is not on_success, on_failure, or always |
| ERROR | artifacts.expose_as set without artifacts.paths |
| ERROR | cache.when is not on_success, on_failure, or always |
| ERROR | cache.policy is not pull, push, or pull-push |
| ERROR | rules[*].when is not one of the valid when values |
| ERROR | image map form missing name key |
| ERROR | inherit.default / inherit.variables is not a boolean or list |
| WARNING | pages job artifacts.paths does not include public |
Cross-job graph
| Severity | Rule |
|---|---|
| ERROR | needs: references a job that does not exist |
| ERROR | needs: references a job in a later stage |
| ERROR | Circular dependency detected in needs: graph |
| ERROR | dependencies: references a job that does not exist |
| ERROR | dependencies: references a job in the same or a later stage |
| ERROR | extends: references an unknown job |
| ERROR | Cycle detected in extends: graph |
Hidden jobs (templates)
Jobs whose name starts with . are treated as reusable templates and skipped for most rules. This matches GitLab's own behaviour.
Development
This project uses Task as a task runner.
task # list available tasks
task build # compile the binary
task test # run Go unit tests
task lint-go # run go vet
task validate # run the binary against all testdata fixtures
task ci # full check: vet → test → build → validate
task clean # remove build artifacts
Project structure
.
├── cmd/gitlab-sim/ # CLI entrypoint
├── internal/
│ ├── fetcher/ # GitLab API client (project include fetching)
│ ├── graph/ # Mermaid and SVG/PNG graph generators
│ ├── linter/ # lint rules and findings
│ ├── model/ # pipeline data structures and YAML parser
│ └── resolver/ # extends: resolution and project include merging
├── testdata/ # sample pipelines used for manual validation
├── Taskfile.yml
└── go.mod