253 lines
9.9 KiB
Markdown
253 lines
9.9 KiB
Markdown
# gitlab-sim
|
||
|
||
[](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.
|
||
|
||
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)
|
||
|
||
## 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] <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:
|
||
|
||
```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.
|
||
|
||
### 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/
|
||
│ ├── 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
|
||
```
|