Four correctness fixes to the GitLab CI expression parser in
internal/cicontext/eval.go:
- Multi-line: \n and \r are now treated as whitespace in skipWS so
block-scalar or folded-scalar if: values with || / && on continuation
lines evaluate correctly instead of falling back to permissive true.
- ${VAR} curly-brace variable syntax now supported in parseValue.
- Regex flags (/pattern/i, /pattern/m, /pattern/s) are now consumed and
translated to Go (?i)/(?m)/(?s) prefixes via applyRegexFlags.
- Variable on RHS of =~ / !~: when the right operand is $VAR, the
variable's value is interpreted as a /regex/[flags] string via
extractRegexFromString; non-regex values fall back to permissive true.
Adds 16 new unit tests covering all four cases and a testdata fixture
(rules_if_expr.yml) exercising multi-line, ${VAR}, and /pattern/i in a
real pipeline with context flags.
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
