Files
glint/README.md
T
k3nny 02d8e63a98
ci / vet, staticcheck, test, build (push) Successful in 2m25s
release / Build and publish release (push) Successful in 1m24s
feat(cli): .glint.yml config and inline suppression comments
Adds project-level configuration and per-job suppression directives:

.glint.yml (searched from pipeline dir up to the git root):
- ignore: [GL007, GL032] — suppress rules globally for the project
- severity: {GL004: warning} — override rule severity (error/warning/ignore)
- stages: [quality] — extra stages beyond the pipeline's stages: block
- token: / url: / cache_dir: — defaults for flags; lower priority than
  CLI flags and environment variables

Inline suppression (# glint: ignore):
- Place "# glint: ignore GL007" immediately before a job definition to
  suppress that rule for the specific job only
- Multiple rules: "# glint: ignore GL007, GL032" (comma or space separated)
- Wildcard: "# glint: ignore all" suppresses every finding for the job
- Suppressions are scoped to the annotated job; pipeline-level findings
  are unaffected
- Parsed from yaml.Node head/line comments in the first parse pass;
  stored in Pipeline.Suppressions (root file only, not includes)

New packages: internal/config (Load, walk-up search, .git boundary stop)
New files: cmd/glint/filter.go (applyConfig, isSuppressed helpers)
Tests: config_test.go, parser_suppress_test.go, filter_test.go
Validate fixtures: testdata/config_ignored/, config_severity/, config_suppress/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 10:23:33 +02:00

23 KiB
Raw Blame History

glint logo

glint

License Release

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 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
  • Local include resolutioninclude: local: entries are read from disk and recursively merged before linting, so multi-file pipelines are fully validated
  • Extended variable declarationsvariables: entries may use the {value, description, options} map form (GitLab CI 13.7+); default.image accepts both string and map form; rules.changes/rules.exists accept both list and {paths, compare_to} map form
  • Graph outputglint graph prints a job tree (stages → jobs) to the terminal; glint graph includes emits a Mermaid include dependency diagram; glint graph pipeline renders a GitLab CI-style PNG/SVG
  • Context simulation — pass --branch, --tag, or --source to glint check or glint graph to see which jobs would be active, manual, or skipped for a specific pipeline event; evaluates rules:if: expressions and only/except filters
  • Variable expansion$VAR and ${VAR} references inside variable values are expanded after all sources are merged (pipeline defaults → workflow-rule overrides → CLI flags); transitive chains resolve automatically; visible via --list-vars
  • Non-string variable scalarsBUILD: true, RETRIES: 3 and other bare boolean/integer variable values are handled correctly throughout: they render in --list-vars output and are injected into the evaluation context as their string equivalents, matching GitLab CI's behaviour
  • Static reachability (GL033) — warns when a job's rules: block can never activate: if every rule has when: never the job is permanently excluded from any pipeline run, provable without evaluating any if: expressions
  • services: validation (GL034) — map form requires a name key; alias must be a valid DNS label (letters, digits, hyphens, dots; no leading/trailing hyphens)
  • rules:changes / rules:exists glob safety (GL035) — warns when paths are absolute (start with /), which can never match since GitLab CI paths are always relative to the repository root
  • timeout: format (GL036) — validates that job and default: timeout values are valid GitLab CI duration strings (1h 30m, 90 minutes, 2 hours, etc.)
  • id_tokens: validation (GL037) — each OIDC token entry must have an aud key (missing aud is a GitLab API error at runtime)
  • secrets: validation (GL038) — each secret entry must declare exactly one provider (vault, gcp_secret_manager, or azure_key_vault)
  • pages: keyword + artifacts.paths (GL039) — warns when a job uses the pages: keyword but artifacts.paths does not include the publish directory (default: public)
  • Duplicate stage names (GL040) — warns when a stage name appears more than once in stages:; GitLab silently merges duplicates, which can cause confusing ordering
  • cache.key.files glob detection (GL041) — warns when cache.key.files entries contain glob metacharacters; this field requires exact file paths, not patterns
  • Recursive include depth limit — include chains are capped at 100 nesting levels (matching GitLab's own limit); project and component includes are now tracked in the visited-file set to prevent cross-include cycles
  • include: inputs: substitution — when a component: entry has a with: block, all $[[ inputs.KEY ]] and $[[ inputs.KEY | default(…) ]] placeholders in the fetched template are substituted before parsing, so component-scoped jobs get their correct stage: and keyword values instead of $[[…]] placeholders
  • Offline mode + include cache — pass --cache-dir DIR to cache fetched remote templates (project: and component: includes) to disk; --offline serves entirely from the cache without making network calls
  • Structured output formats--format json emits a stable JSON report; --format sarif emits SARIF 2.1.0 (consumed by GitHub Code Scanning and GitLab SAST); --format junit emits JUnit XML (consumable as a CI test-report artifact); --format github emits GitHub Actions annotation lines (::error file=…::) so findings appear as inline PR comments
  • .glint.yml project config — rule suppression (ignore: [GL007]), severity overrides (severity: {GL004: warning}), extra stages allowlist (stages: [quality]), and default token/URL/cache-dir so flags are not needed on every invocation
  • Inline suppression comments# glint: ignore GL007 (or # glint: ignore all) immediately before a job definition suppresses the specified rule(s) for that job without touching other jobs
  • Sorted findings output — findings are sorted by source file then line number, so all issues from the same file appear together in order; pipeline-level findings (no file) sort first
  • Consistent ruff-style warnings — all warnings (unresolvable includes, skipped extends chains, workflow non-start) use the same path: [warning] message format as lint findings
  • --version / -v flag — prints the compiled version string (e.g. glint v0.2.14); the version is also shown at the top of every --help output

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.

Output formats

Pass --format to control the output. Plain text is the default.

# Default: ruff-style text (human-readable)
glint check .gitlab-ci.yml

# JSON — stable schema, machine-readable
glint check --format json .gitlab-ci.yml

# SARIF 2.1.0 — GitHub Code Scanning / GitLab SAST
glint check --format sarif .gitlab-ci.yml > glint.sarif

# JUnit XML — CI test-report artifact (GitLab: artifacts:reports:junit)
glint check --format junit .gitlab-ci.yml > glint-junit.xml

# GitHub Actions annotations — inline PR diff comments
glint check --format github .gitlab-ci.yml

In structured formats (json, sarif, junit, github) the summary line (OK: … no issues found or N finding(s): M error(s)) is written to stderr so stdout contains only the machine-readable payload.

JSON schema (schema_version: 1):

{
  "schema_version": 1,
  "glint_version": "v0.2.18",
  "pipeline": ".gitlab-ci.yml",
  "findings": [
    {"rule":"GL004","severity":"error","file":".gitlab-ci.yml","line":14,
     "job":"deploy","message":"stage \"production\" is not defined in 'stages'"}
  ],
  "summary": {"total": 1, "errors": 1, "warnings": 0}
}

GitHub annotation lines:

::error file=.gitlab-ci.yml,line=14,title=GL004::job "deploy": stage "production" is not defined in 'stages'

Project configuration (.glint.yml)

Place a .glint.yml file next to your pipeline (or anywhere in the directory tree up to the repository root) to configure glint for that project. glint searches upward from the pipeline file's directory, stopping at the first .git boundary.

# .glint.yml

# Suppress specific rules entirely.
ignore:
  - GL007   # we still use only:/except:, migration in progress
  - GL032   # lots of dynamic variables injected by CI

# Override the severity of specific rules.
severity:
  GL004: warning  # demote stage errors to warnings during a migration
  GL035: error    # promote absolute-path warning to error for this project

# Extra stages that are valid but not declared in the pipeline YAML itself
# (e.g. injected by an include template we can't edit).
stages:
  - quality
  - security

# Default token — overridden by --token flag and GITLAB_TOKEN env.
token: glpat-xxxx

# Default GitLab instance URL.
url: https://gitlab.example.com

# Default cache directory for fetched remote includes.
cache_dir: ~/.cache/glint

Priority chain for token and URL: --token/--gitlab-url flags > .glint.yml values > GITLAB_TOKEN/CI_SERVER_URL environment variables.

Inline suppression (# glint: ignore)

Suppress a finding for a specific job by placing a # glint: ignore RULE comment immediately before the job definition:

# glint: ignore GL007
legacy-job:
  stage: build
  only:
    - main
  script: echo ok

# Multiple rules — comma- or space-separated:
# glint: ignore GL007, GL032
another-job:
  stage: build
  script: echo ok

# Suppress all rules for this job:
# glint: ignore all
noisy-job:
  stage: build
  script: echo ok

Inline suppressions are scoped to the single job they precede. They do not affect other jobs or pipeline-level findings. For project-wide suppression use .glint.yml ignore:.

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.

Include cache and offline mode

Pass --cache-dir to cache fetched remote templates so repeated runs skip the network:

# First run: fetches and caches
glint check --cache-dir ~/.cache/glint .gitlab-ci.yml

# Subsequent runs: served from cache
glint check --cache-dir ~/.cache/glint .gitlab-ci.yml

# Fully offline (uses ~/.cache/glint automatically when --cache-dir is absent)
glint check --offline .gitlab-ci.yml
glint check --offline --cache-dir ~/.cache/glint .gitlab-ci.yml

Cache entries are keyed by SHA-256 of the full request URL/coordinates and stored as plain YAML files in the cache directory. There is currently no automatic expiry — delete the directory or individual entries to force a fresh fetch.

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_URLGITLAB_URLhttps://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; 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: 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 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, null
  • only: / except: — ref keywords (branches, tags, merge_requests, schedules, …), branch name globs (feat/*), and /regex/ patterns
  • Variable expansion — $VAR / ${VAR} references within variable values are expanded after all sources are merged; use --list-vars to inspect the resolved values

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 (implicit default: --branch main --source push)
Context: branch=main, source=push

Active  (5): build, deploy-staging, test, ...

OK: .gitlab-ci.yml — no issues found (5 job(s), 3 stage(s))

# 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 job(s), 3 stage(s))

# 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 job(s), 3 stage(s))

# Pipeline with issues
.gitlab-ci.yml:14: GL004 [error] job "deploy": stage "production" is not defined in 'stages'
.gitlab-ci.yml:22: GL027 [error] job "test": needs unknown job "build-app"
.gitlab-ci.yml:31: GL007 [warning] job "old-job": 'only'/'except' are deprecated; prefer 'rules'

3 finding(s): 2 error(s)

Lint rules

Every finding includes a stable rule ID (e.g. GL003) that can be used to filter output or reference a specific check in documentation.

Pipeline-level

ID Severity Rule
GL002 ERROR workflow.rules[*].when is not always or never
GL001 WARNING No stages defined (GitLab falls back to default stages)
GL036 ERROR default.timeout is not a valid GitLab CI duration string
GL040 WARNING A stage name appears more than once in stages:

Job-level — structure

ID Severity Rule
GL003 ERROR Job is missing required script (or run) — non-trigger, non-template jobs
GL004 ERROR Job references a stage not declared in stages
GL005 ERROR only and rules used together on the same job
GL006 ERROR except and rules used together on the same job
GL007 WARNING only/except used (deprecated, prefer rules)

Job-level — keyword constraints

ID Severity Rule
GL008 ERROR when is not one of on_success, on_failure, always, manual, delayed, never
GL009 ERROR when: delayed without start_in
GL010 ERROR start_in set but when is not delayed
GL011 ERROR parallel integer not in range 2200, or map form missing matrix key
GL012 ERROR retry integer not in range 02, or retry.max out of range
GL013 ERROR retry.when contains an unrecognised failure type
GL014 ERROR allow_failure is not a boolean or a map with exit_codes
GL015 ERROR interruptible is not a boolean
GL016 ERROR trigger job also has script
GL017 ERROR trigger map missing project or include
GL018 ERROR coverage is not a regex pattern wrapped in /
GL019 ERROR release missing required tag_name, or is not a map
GL020 ERROR environment.url set without environment.name, or invalid environment.action
GL021 ERROR artifacts.when invalid, or artifacts.expose_as set without artifacts.paths
GL022 WARNING pages job artifacts.paths does not include public
GL023 ERROR cache.when or cache.policy has an invalid value
GL024 ERROR rules[*].when is not one of the valid when values
GL025 ERROR image map form missing name key
GL026 ERROR inherit.default / inherit.variables is not a boolean or list
GL034 ERROR services: map form missing name, or alias is not a valid DNS label
GL036 ERROR timeout: is not a valid GitLab CI duration string (e.g. 1h 30m, 90 minutes)
GL037 ERROR id_tokens: entry is missing the required aud key
GL038 ERROR secrets: entry is missing a provider key (vault, gcp_secret_manager, or azure_key_vault)
GL039 WARNING Job has pages: keyword but artifacts.paths does not include the publish directory
GL041 WARNING cache.key.files entry looks like a glob pattern; must be an exact file path

Cross-job graph

ID Severity Rule
GL027 ERROR/WARNING needs: references a job that does not exist (WARNING when optional: true)
GL028 ERROR needs: references a job in a later stage
GL029 ERROR Circular dependency detected in needs: graph
GL030 ERROR dependencies: references a job that does not exist
GL031 ERROR dependencies: references a job in the same or a later stage

Expression validation

ID Severity Rule
GL032 WARNING rules:if: references $VAR not declared in variables: (pipeline, job, or workflow:rules:variables:) — may be a false positive for variables set in GitLab CI/CD project settings
GL033 WARNING Every rule in rules: has when: never — job is permanently excluded from the pipeline (statically provable without context)
GL035 WARNING rules:changes / rules:exists path is absolute; GitLab CI paths are relative to the repo root — absolute paths will never match

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