- Finding format now follows file:line: RULEID [severity] message, matching ruff and other modern linters (GL003 [error] job "x": ...) - glint check and glint graph default to --branch main --source push when no context flag is given; rules:if: is always evaluated - --list-vars flag on both commands prints sorted KEY=VALUE of all collected variables (YAML, workflow-rule union, effective context) - CHANGELOG [Unreleased] promoted to [0.2.11]; README badge updated; ROADMAP marks newly shipped items Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog. This project uses Semantic Versioning.
[0.2.11] - 2026-06-12
Added
-
Ruff-style finding output — findings now follow the
file:line: RULEID [severity] messageformat (e.g..gitlab-ci.yml:14: GL004 [error] job "deploy": stage "production" is not defined in 'stages'), matching the output convention used by ruff and other modern linters. Job-scoped findings prefix the message withjob "name":; pipeline-level findings omit the job prefix. Severity is lowercase inside brackets ([error],[warning]). -
Implicit default context (
--branch main --source push) — whenglint checkorglint graphis invoked without any of--branch,--tag,--source, or--var, the context now defaults to--branch main --source pushso thatrules:if:expressions are always evaluated. Previously the context was empty and no rule evaluation occurred. Any explicit context flag bypasses the defaults entirely. -
--list-varsdebug flag — available on bothglint checkandglint graph; prints all pipeline-level variables collected from the root file and every included file (sortedKEY=VALUE) to stderr, then continues normally. When a context is active, also prints the effective merged variable set (pipeline defaults + workflow-rule variables + CLI flags). Useful for diagnosing GL032 false positives. -
Included-file variables now visible to all lint rules —
variables:blocks declared in included files (local, remote, project, and component includes) are now merged into the pipeline's variable namespace before linting. This eliminates false-positive GL032 warnings for variables declared in shared CI templates. Root-pipeline variables take precedence over included-file variables when the same key appears in both (matching GitLab's own override behaviour). -
Variable reference validation (GL032) — glint now warns when a
rules:if:expression references a variable ($VARor${VAR}) that is not declared anywhere in the pipeline YAML: pipeline-levelvariables:, the job's ownvariables:, or anyworkflow:rules:variables:block. Predefined GitLab CI variable namespaces (CI_*,GITLAB_*,FF_*,RUNNER_*,TRIGGER_*,CHAT_*) are exempt. Because variables can also be set in GitLab CI/CD project settings (invisible to glint), the finding is a[WARNING]rather than an error. Each undeclared variable is reported at most once per job to keep the output concise. -
Structured rule IDs — every finding now carries a stable
GL###identifier (e.g.GL003) that appears in the output alongside the location and message:.gitlab-ci.yml:14: GL003 [error] job "deploy": missing required field 'script'. IDs are assigned per check function across 31 rules (GL001–GL031) and are stable across versions. Thelinter.Findingstruct exposes the ID as aRule stringfield for programmatic consumers. The README lint rules table is updated with ID columns. -
workflow:rules:variables:now propagate to job rule evaluation — when aworkflow:rules:entry matches, anyvariables:it defines are injected into the evaluation context so jobrules:if:expressions can reference them. Pipeline-levelvariables:are also available as defaults (lower priority). Variable priority order, highest first:--varCLI overrides →--branch/--tag/--sourceshortcuts → workflow-rule variables → pipeline-level variable defaults. This means$DEPLOY_TARGET == "production"in a job rule correctly evaluates when a workflow rule setsDEPLOY_TARGET: productionfor the matching branch. Theglint graph treecommand benefits from the same enrichment. -
rules:if:expression evaluator improvements — six correctness fixes to the GitLab CI expression parser:- Multi-line expressions — newlines (
\n,\r) are now treated as whitespace between tokens, so block-scalarif:values (e.g.if: | ...) and folded YAML scalars with||/&&on a continuation line are parsed correctly instead of falling back to permissivetrue. ${VAR}curly-brace variable syntax —${CI_COMMIT_BRANCH}is now equivalent to$CI_COMMIT_BRANCHeverywhere a value is expected.- Regex flags —
/pattern/i,/pattern/m,/pattern/sare now honoured; theiflag (case-insensitive) is translated to Go's(?i)prefix before compiling. Unknown flags are silently ignored. - Variable as regex RHS —
$BRANCH =~ $PATTERNwhere$PATTERNholds a/regex/[flags]string is now evaluated by extracting and compiling the pattern from the variable's value; if the value is empty or does not look like a regex literal the expression falls back to permissivetrue. true/falsekeywords — baretrueandfalse(without quotes) are now recognised as the string values"true"and"false", matching GitLab CI's own behaviour.$GATEWAY_ENABLED == trueand$FEATURE_FLAG == falsenow evaluate correctly.- Integer literals — bare integers (e.g.
$PARALLEL == 4,$ENABLED == 1,$DISABLED == 0) are now parsed as their decimal string representations and compared accordingly.
- Multi-line expressions — newlines (
-
File and line numbers on findings — every finding now includes the source file and line where the job is defined, e.g.
[ERROR] job "deploy" (src/deploy.yml:14): …. For jobs that come from local or fetched includes the file reflects the include source. Pipeline-level findings (workflow rules, missing stages) reference the root pipeline file. -
glint graph includesshows jobs per file — each node in the Mermaid include dependency graph now shows the jobs defined directly in that file. Jobs are rendered as rounded nodes ((name)) in a distinct light-purple style, connected with dashed arrows (-.->) to distinguish ownership from the include hierarchy (solid-->arrows). The root pipeline file always shows its direct jobs; local and fetched project/component nodes show theirs when the file can be read.
Fixed
-
include: remote:URL includes are now fetched and merged — glint fetches plain HTTPS URLs ininclude: remote:entries (no authentication), parses the resulting YAML, merges its jobs into the pipeline, and recursively resolves any sub-includes the remote file itself declares. Unreachable or unparseable URLs emit a[WARNING]and lint continues on the rest of the pipeline. Theglint graph includescommand now expands remote nodes with their jobs and sub-include tree, matching the behaviour of local and project includes. -
needs: optional: truedowngraded to warning — aneeds:entry that carriesoptional: trueand references a job not present in the pipeline now emits[WARNING]instead of[ERROR]. GitLab CI silently skips such dependencies at runtime (the job is absent when its include was not triggered), so the finding was a false positive. Non-optional missing needs remain errors. Optional missing deps are also excluded from the cycle-detection graph. -
extends:jobs with missing script downgraded to warning — a job that declaresextends:but has noscriptafter resolution now emits[WARNING]instead of[ERROR]. The script may legitimately come from a base job in a remote include that could not be fetched at lint time (e.g. no token configured). -
Variable map form now parses correctly —
variables:entries that use the extended{value, description, options}form (GitLab CI 13.7+) no longer causeyaml: cannot unmarshal !!map into string. BothPipeline.Variablesand per-jobVariablesnow accept either plain strings or map-form declarations. -
default.imagemap form now parses correctly —default: image: {name: ..., pull_policy: ...}used to causeyaml: cannot unmarshal !!map into string;DefaultConfig.Imageis now typed asanyto matchJob.Image. -
default.before_script/default.after_scriptnow accept both list and scalar forms — previouslyDefaultConfig.BeforeScriptandDefaultConfig.AfterScriptwere[]string, causing a parse error when the field was written as a block scalar string. They are now typed asanyto match the correspondingJobfields. -
rules.changes/rules.existsmap form now parses correctly — extendedchanges: {paths: [...], compare_to: "..."}syntax (GitLab CI 15.3+) used to causeyaml: cannot unmarshal !!map into []string.
[0.2.0] - 2026-06-11
Added
-
Subcommand CLI — reworked interface inspired by ruff:
glint check <file>— lint a pipeline (replaces bareglint <file>)glint graph [mode] <file>— visualise the pipeline (replaces--graphflag)- Graph modes: no-arg (tree + includes),
tree,includes,pipeline,all - Per-command
--helpwith ruff-style layout:Arguments:,Options:(flag declaration on its own line, description below),[env: ...]/[default: ...]/[possible values: ...]metadata,Examples:section
-
glint graph tree— jobs displayed as a terminal directory tree grouped by stage (like thetreecommand); job-type annotations ([manual],[delayed],[trigger]) when no context is set; evaluated-state annotations ([skipped],[manual]) when a context is provided via--branch/--tag/--source -
Context flags on
glint graph—--branch,--tag,--source,--varare now available onglint graphas well asglint check -
Local include resolution —
include: local:entries are now read from disk, recursively resolved, and merged into the pipeline before linting; enables cross-fileextends:andneeds:validation for multi-file pipelines -
Cross-platform release builds — two Taskfile tasks for tagged release binaries:
task build-windows— cross-compiles for Windows x64; output:glint-<tag>.exetask build-linux— cross-compiles for Linux x64; output:glint-<tag>-linux-amd64- Both tasks require an exact git tag on the current commit
Fixed
extends:unknown base no longer fatal — when a base job referenced byextends:does not exist, glint now emits a resolver warning and skips extends resolution for that job rather than aborting with exit code 2; linting continues on the job's own fieldsscript: |(block scalar) support — jobs using a multiline block scalar forscript:,before_script:, orafter_script:are now parsed correctly; previously caused false-positive "missing script" errors
Changed
glint <file>removed — useglint check <file>--graph <mode>removed — replaced byglint graph [mode]--graph-outrenamed to--out— now a flag onglint graph(glint graph pipeline --out <dir>)
[0.1.0] - 2026-06-07
Added
-
Context simulation (
--branch/--tag/--source/--var) — show which jobs would be active, manual, or skipped for a specific pipeline event without leaving the terminal:--branch <name>— simulates a branch push; populatesCI_COMMIT_BRANCH,CI_COMMIT_REF_NAME,CI_COMMIT_REF_SLUG, and defaultsCI_PIPELINE_SOURCEtopush--tag <name>— simulates a tag push; populatesCI_COMMIT_TAG, clearsCI_COMMIT_BRANCH--source <event>— setsCI_PIPELINE_SOURCEexplicitly (merge_request_event,schedule,web,api,pipeline, …)--var KEY=VALUE— sets any CI variable; repeatable; overrides shortcut values- Evaluates
rules:if:expressions (==,!=,=~,!~,&&,||,!, parentheses,$VAR, string literals,null) - Evaluates
only:/except:ref keywords (branches,tags,merge_requests,schedules,pushes,web,api,pipelines), branch name globs (feat/*), and/regex/patterns - Evaluates
workflow:rules:— warns when the pipeline itself would not start for the given context rules:changes:andrules:exists:are not evaluated (no git tree at lint time); rules withoutif:always match- Linting runs in full regardless of context; context output is printed before findings
- New
internal/cicontextpackage (context.go,eval.go,reachability.go); no new external dependencies - 33 unit tests covering the expression evaluator; 5 fixture runs in
task validate
-
Graph output (
--graph) — visualises the pipeline instead of running lint rules:--graph includes— Mermaid flowchart of include dependencies written to stdout; one node perinclude:entry (project, component, local, remote, template), colour-coded by type; pipe to a.mmdfile or paste into mermaid.live--graph pipeline— GitLab CI-style SVG/PNG pipeline graph written to a timestamped file in--graph-out(default:glint-out/); jobs rendered as white chip cards with a coloured status indicator (blue: regular, orange: manual, purple: trigger, amber: delayed); DAG mode draws job-to-job Bézier arrows when any job hasneeds:, classic mode draws L-shaped connectors between stage columns; converted to PNG automatically whenrsvg-convert,inkscape, ormagickis available--graph all— include Mermaid to stdout, pipeline file path to stderr- New
internal/graphpackage (includes.go,pipeline.go,render.go); no new external dependencies
-
CI/CD catalog component resolution — resolves
include: component:references from the GitLab CI/CD Catalog:- Reference format:
<host>/<project-path>/<component-name>@<version>(host determines which GitLab instance is queried) - Tries single-file layout (
templates/<name>.yml) then directory layout (templates/<name>/template.yml) automatically - Public catalog components are fetched without authentication (no token required)
- References containing CI variables (e.g.
$CI_SERVER_FQDN) are skipped with a warning — they cannot be resolved at lint time - Jobs imported from a component may use
$[[ inputs.xxx ]]input placeholders in stage names; the stage validation check is skipped for those values rather than producing false positives
- Reference format:
-
Remote project include resolution — fetches
include: project:templates from the GitLab REST API before linting; jobs from remote templates are merged into the pipeline soextends:,needs:, anddependencies:references can be validated across file boundaries- Token auto-discovery:
GITLAB_TOKEN(→PRIVATE-TOKENheader) →CI_JOB_TOKEN(→JOB-TOKENheader) →GITLAB_PRIVATE_TOKEN - Instance URL auto-discovery:
--gitlab-urlflag →CI_SERVER_URL→GITLAB_URL→https://gitlab.com --tokenand--gitlab-urlCLI flags for explicit overridesfile:accepts both string and list-of-strings forms- Project includes require a token; they are skipped with a
WARNINGwhen none is configured - Component includes attempt the fetch unauthenticated first; a
WARNINGis emitted only on failure
- Token auto-discovery:
-
Comprehensive keyword validation — checks for all major GitLab CI YAML keywords based on the official docs:
whenvalid values:on_success,on_failure,always,manual,delayed,neverstart_inonly allowed whenwhen: delayed; error if set without itparallelmust be integer 2–200 or map withmatrixkeyretrymax value 0–2;retry.whenfailure types validated against the full enumallow_failuremust be boolean or{exit_codes: ...}interruptiblemust be booleantriggerjobs cannot havescript; map form requiresprojectorincludecoveragemust be a regex pattern wrapped in/…/releaserequirestag_nameenvironment.urlrequiresenvironment.name;environment.actionvalidatedartifacts.whenvalid values;expose_asrequirespathscache.whenandcache.policyvalid valuesrules[*].whenvalidated per-ruleimagemap form requiresnameinherit.default/inherit.variablesmust be boolean or listworkflow.rules[*].whenrestricted toalways/never- Warning when
pagesjobartifacts.pathsdoes not includepublic
-
dependencies:validation — referenced jobs must exist and must be in an earlier stage -
run:keyword support — recognised as alternative toscript:(CI steps); no longer triggers "missing script" error -
spec:reserved key — top-levelspec:is now recognised as a CI component header, not a job -
New job model fields —
interruptible,resource_group,start_in,run -
Testdata fixtures —
keywords_valid.yml(clean pipeline exercising every new check),keywords_invalid.yml(18 deliberate violations) -
extends:resolution — resolves single and chained template inheritance before linting; deep-merges base job fields into derived jobs (child scalars/lists win, maps are merged recursively); cycle detection via topological sort -
needs:DAG validation — checks referenced jobs exist, respect stage ordering, and contain no circular dependencies; handles both the- job-nameshorthand and the- job: namemap form; cross-pipeline needs (pipeline:key) are skipped -
Hidden job support — jobs named with a leading
.are treated as reusable templates and exempted from thescriptrequirement and other per-job checks -
Core linter — initial set of lint rules:
- Missing
scripton non-trigger, non-template jobs (error) - Job
stagenot declared instages(error) only/rulesorexcept/rulesused together (error)- No
stagesblock defined (warning) - Deprecated
only/exceptusage (warning)
- Missing
-
CLI —
glint <file>exits 0 on clean pipelines, 1 on errors; prints findings with severity, job name, and message -
YAML parser — two-pass parse: reserved top-level keys (
stages,variables,default,include,workflow) are decoded into typed structs; remaining keys are treated as job definitions -
Taskfile —
build,test,lint-go,validate,ci,cleantasks via Task -
Testdata fixtures —
valid.yml,invalid.yml,extends.yml,needs.yml,needs_cycle.yml