# gitlab-sim [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Release](https://img.shields.io/badge/release-v0.1.0-blue.svg)](CHANGELOG.md) > **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. 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 - **`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 - **Deprecation warnings** — flags `only`/`except` usage in favour of `rules` - **Graph output** — emits Mermaid diagrams for the include dependency tree and the pipeline jobs layout (DAG or classic stage ordering) - **Context simulation** — pass `--branch`, `--tag`, or `--source` to see which jobs would be active, manual, or skipped for a specific branch push, tag, or pipeline event; evaluates `rules:if:` expressions and `only`/`except` filters ## 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 gitlab-sim [options] ``` 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: ```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 ``` //@ 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/.yml` (single-file layout) 2. `templates//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. ### Context simulation Pass `--branch`, `--tag`, or `--source` to see which jobs would run for a given pipeline event. The pipeline is still fully linted; context output is printed first. ```bash # What runs on a push to develop? gitlab-sim --branch develop .gitlab-ci.yml # What runs when a v1.2.0 tag is pushed? gitlab-sim --tag v1.2.0 .gitlab-ci.yml # Merge request pipeline gitlab-sim --source merge_request_event .gitlab-ci.yml # Arbitrary variable overrides (repeatable) gitlab-sim --branch main --var DEPLOY_ENV=production .gitlab-ci.yml ``` **Evaluated:** - `rules:if:` — full expression language: `==`, `!=`, `=~`, `!~`, `&&`, `||`, `!`, `()`, `$VAR`, string literals, `null` - `only:` / `except:` — ref keywords (`branches`, `tags`, `merge_requests`, `schedules`, …), branch name globs (`feat/*`), and `/regex/` patterns **Not evaluated** (no git tree at lint time): `rules:changes:`, `rules:exists:`. Rules without an `if:` clause always match. **Predefined variables** set automatically by the shortcut flags: | Flag | Variables populated | |------|---------------------| | `--branch ` | `CI_COMMIT_BRANCH`, `CI_COMMIT_REF_NAME`, `CI_COMMIT_REF_SLUG`, `CI_PIPELINE_SOURCE=push` | | `--tag ` | `CI_COMMIT_TAG`, `CI_COMMIT_REF_NAME`, `CI_COMMIT_REF_SLUG`, `CI_PIPELINE_SOURCE=push` (clears `CI_COMMIT_BRANCH`) | | `--source ` | `CI_PIPELINE_SOURCE` | | `--var KEY=VALUE` | any variable; overrides shortcuts | ### Example output ``` # Clean pipeline, no context OK: .gitlab-ci.yml — no issues found (5 jobs, 3 stages) # With --branch develop context Context: branch=develop, source=push Active (3): build, deploy-staging, test Skipped (2): deploy-prod, release-notes OK: .gitlab-ci.yml — no issues found (5 jobs, 3 stages) # With --tag v1.0.0 context Context: tag=v1.0.0, source=push Active (4): build, deploy-prod, release-notes, test Skipped (1): deploy-staging 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/ │ ├── cicontext/ # CI variable context, rules:if: evaluator, job reachability │ ├── 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 ```