Every finding now carries the source file and exact line number of the job key in its YAML file. Format: [ERROR] job "name" (file.yml:12): message. Pipeline-level findings (workflow rules, no stages) reference p.SourceFile. Cross-file include jobs (local, project, component) carry the include source as their File, set via Pipeline.SetJobOrigin after each ParseBytes call in the resolver. Line numbers come from the yaml.Node key node (exact job-name line) in a new document-level first pass in ParseBytes, replacing the previous map[string]yaml.Node approach which only gave value-node lines. Also: jobs that declare extends: but have no script after resolution now emit WARNING instead of ERROR. The script may come from a base in a remote include that was not fetched (no token, offline), making the error a false positive in common project setups. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
glint
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 - Local include resolution —
include: local:entries are read from disk and recursively merged before linting, so multi-file pipelines are fully validated - Extended variable declarations —
variables:entries may use the{value, description, options}map form (GitLab CI 13.7+);default.imageaccepts both string and map form;rules.changes/rules.existsaccept both list and{paths, compare_to}map form - Graph output —
glint graphprints a job tree (stages → jobs) to the terminal;glint graph includesemits a Mermaid include dependency diagram;glint graph pipelinerenders a GitLab CI-style PNG/SVG - Context simulation — pass
--branch,--tag, or--sourcetoglint checkorglint graphto see which jobs would be active, manual, or skipped for a specific pipeline event; evaluatesrules:if:expressions andonly/exceptfilters
See ROADMAP.md for planned improvements.
Requirements
- Go 1.21 or later
- Task (optional, for development tasks)
Installation
git clone https://git.k3nny.fr/glint
cd glint
go build -o glint ./cmd/glint/...
Or with Task:
task build
Usage
glint [OPTIONS] <COMMAND>
Commands:
check Lint a pipeline file — exits 0 (clean) or 1 (errors found)
graph Visualise the pipeline as a job tree or Mermaid graph
Run glint <command> --help for command-specific options and examples.
glint check
glint check .gitlab-ci.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 glint can fetch them:
# personal access token (read_api scope)
GITLAB_TOKEN=glpat-xxxx glint check .gitlab-ci.yml
# CI/CD job token (when running inside a pipeline)
CI_JOB_TOKEN=$CI_JOB_TOKEN glint check .gitlab-ci.yml
# self-hosted GitLab
GITLAB_TOKEN=glpat-xxxx GITLAB_URL=https://gitlab.example.com glint check .gitlab-ci.yml
# or via flags
glint check --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; glint skips those fields rather
than producing false positive errors.
glint graph
Visualise the pipeline. Without a mode word, prints a job tree and the include
dependency graph separated by ---.
# Default: job tree + include dependency graph
glint graph .gitlab-ci.yml
# Job tree only (stages → jobs, like the tree command)
glint graph tree .gitlab-ci.yml
# Include dependency graph → Mermaid flowchart to stdout
glint graph includes .gitlab-ci.yml > includes.mmd
# GitLab-like pipeline layout → PNG (or SVG fallback) written to --out dir
glint graph pipeline .gitlab-ci.yml
# prints the output file path, e.g.: glint-out/pipeline-20260607-143022.png
# Mermaid to stdout + pipeline file path to stderr
glint graph all .gitlab-ci.yml > includes.mmd
# Custom output directory (pipeline mode)
glint graph pipeline --out /tmp/graphs .gitlab-ci.yml
Job tree (graph tree) — stages as branches, jobs as leaves. Jobs with
when: manual, when: delayed, or trigger: are annotated in brackets.
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 --out directory (default: glint-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.
Context simulation
Pass --branch, --tag, or --source to glint check to see which jobs
would run for a given pipeline event. The pipeline is still fully linted;
context output is printed first.
# What runs on a push to develop?
glint check --branch develop .gitlab-ci.yml
# What runs when a v1.2.0 tag is pushed?
glint check --tag v1.2.0 .gitlab-ci.yml
# Merge request pipeline
glint check --source merge_request_event .gitlab-ci.yml
# Arbitrary variable overrides (repeatable)
glint check --branch main --var DEPLOY_ENV=production .gitlab-ci.yml
Evaluated:
rules:if:— full expression language:==,!=,=~,!~,&&,||,!,(),$VAR, string literals,nullonly:/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 <name> |
CI_COMMIT_BRANCH, CI_COMMIT_REF_NAME, CI_COMMIT_REF_SLUG, CI_PIPELINE_SOURCE=push |
--tag <name> |
CI_COMMIT_TAG, CI_COMMIT_REF_NAME, CI_COMMIT_REF_SLUG, CI_PIPELINE_SOURCE=push (clears CI_COMMIT_BRANCH) |
--source <event> |
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 |
| WARNING | extends: references an unknown base job (resolver warning; extends chain skipped for that 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 build-windows # cross-compile for Windows x64 (requires a tagged commit → glint-<tag>.exe)
task build-linux # cross-compile for Linux x64 (requires a tagged commit → glint-<tag>-linux-amd64)
task clean # remove build artifacts
Project structure
.
├── cmd/glint/ # 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
