2026-06-05 01:29:07 +02:00
# gitlab-sim
2026-06-07 20:13:03 +02:00
[](LICENSE)
> **Disclaimer:** This tool was built through iterative AI-assisted development with [Claude](https://claude.ai). 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.
2026-06-05 01:29:07 +02:00
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 `stage` must be declared in `stages`
- **`extends:` resolution** — resolves single and multi-level template inheritance before linting, so derived jobs are evaluated against their fully merged definition
- **`needs:` DAG validation** — checks that `needs:` references exist, respect stage ordering, and contain no circular dependencies
2026-06-07 20:13:03 +02:00
- **`dependencies:` 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
2026-06-05 01:29:07 +02:00
- **Deprecation warnings** — flags `only` /`except` usage in favour of `rules`
2026-06-07 20:13:03 +02:00
- **Graph output** — emits Mermaid diagrams for the include dependency tree and the pipeline jobs layout (DAG or classic stage ordering)
2026-06-05 01:29:07 +02:00
## Requirements
- Go 1.21 or later
- [Task ](https://taskfile.dev ) (optional, for development tasks)
## Installation
```bash
git clone https://git.k3nny.fr/gitlab-sim
cd gitlab-sim
go build -o gitlab-sim ./cmd/gitlab-sim/...
```
Or with Task:
```bash
task build
```
## Usage
```bash
2026-06-07 20:13:03 +02:00
gitlab-sim [ options] <pipeline.yml>
2026-06-05 01:29:07 +02:00
```
Exits `0` when no errors are found, `1` when at least one error is reported.
2026-06-07 20:13:03 +02:00
### Remote project includes
Pipelines that include templates from other GitLab projects are supported.
Provide a token so `gitlab-sim` can fetch them:
```bash
# 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 ](https://gitlab.com/explore/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:
1. `templates/<component-name>.yml` (single-file layout)
2. `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.
```bash
# 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 ](https://mermaid.js.org ) flowchart written to stdout.
Pipe to a `.mmd` file or paste into [mermaid.live ](https://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: manual` jobs
- Purple (`#6b4fbb` ): `trigger:` jobs
- Amber (`#fca326` ): `when: delayed` jobs
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.
2026-06-05 01:29:07 +02:00
### 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 ](https://taskfile.dev ) as a task runner.
```bash
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/
2026-06-07 20:13:03 +02:00
│ ├── fetcher/ # GitLab API client (project include fetching)
│ ├── graph/ # Mermaid and SVG/PNG graph generators
2026-06-05 01:29:07 +02:00
│ ├── linter/ # lint rules and findings
│ ├── model/ # pipeline data structures and YAML parser
2026-06-07 20:13:03 +02:00
│ └── resolver/ # extends: resolution and project include merging
2026-06-05 01:29:07 +02:00
├── testdata/ # sample pipelines used for manual validation
├── Taskfile.yml
└── go.mod
```