Files
glint/FEATURES.md
T
k3nny b21ef5c0bb
release / Build and publish release (push) Successful in 1m12s
ci / vet, staticcheck, test, build (push) Failing after 1m54s
feat(cicontext): rules:changes: path-glob evaluation; 100% test coverage
- Add --changes PATH and --changes-from REF flags to glint check and glint graph
  for rules:changes: evaluation. --changes marks files explicitly; --changes-from
  runs git diff --name-only <REF> automatically. Both flags can be combined.
- Implement doublestar glob matching (*, ** across path segments) in EvalJob and
  EvalWorkflow; extended {paths, compare_to} map form supported.
- Without --changes/--changes-from the condition stays permissive (existing behaviour).
- Context summary line now shows changed-file count when file data is provided.
- Achieve 100% statement coverage: comprehensive tests added across all packages;
  removed provably dead code; added testability seams (exit, userHomeDirFn,
  execCommandOutput variables) to cover previously unreachable paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:47:32 +02:00

268 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# glint — feature reference
This document describes every capability in the current release.
For planned work see [ROADMAP.md](ROADMAP.md).
---
## Lint rules
Every finding carries a stable rule ID (`GL001` `GL043`) that can be used to
suppress, filter, or look up the check. Run `glint explain <ID>` for a
description, bad-YAML example, and fix.
### Pipeline-level
| ID | Sev | Rule |
|----|-----|------|
| GL001 | WARN | No `stages:` block — GitLab falls back to its built-in default stages |
| GL002 | ERR | `workflow.rules[*].when` must be `always` or `never` |
| GL036 | ERR | `default.timeout` is not a valid GitLab CI duration string |
| GL040 | WARN | A stage name appears more than once in `stages:` |
### Job structure
| ID | Sev | Rule |
|----|-----|------|
| GL003 | ERR | Job missing required `script:` (or `run:`) |
| GL004 | ERR | Job `stage:` references a stage not declared in `stages:` |
| GL005 | ERR | `only:` and `rules:` used together |
| GL006 | ERR | `except:` and `rules:` used together |
| GL007 | WARN | `only:` / `except:` used (deprecated; prefer `rules:`) |
### Keyword constraints
| ID | Sev | Rule |
|----|-----|------|
| GL008 | ERR | `when:` has an invalid value |
| GL009 | ERR | `when: delayed` without `start_in:` |
| GL010 | ERR | `start_in:` set but `when:` is not `delayed` |
| GL011 | ERR | `parallel:` integer not in range 2200, or map form missing `matrix:` |
| GL012 | ERR | `retry:` integer not in range 02, or `retry.max` out of range |
| GL013 | ERR | `retry.when:` contains an unrecognised failure type |
| GL014 | ERR | `allow_failure:` is not a boolean or a map with `exit_codes:` |
| GL015 | ERR | `interruptible:` is not a boolean |
| GL016 | ERR | Trigger job also defines `script:` |
| GL017 | ERR | `trigger:` map missing `project:` or `include:` |
| GL018 | ERR | `coverage:` is not a regex wrapped in `/…/` |
| GL019 | ERR | `release:` missing required `tag_name:`, or is not a map |
| GL020 | ERR | `environment.url` set without `environment.name`, or invalid `action:` |
| GL021 | ERR | `artifacts.when` invalid, or `expose_as` set without `paths` |
| GL022 | WARN | `pages` job `artifacts.paths` does not include `public/` |
| GL023 | ERR | `cache.when` or `cache.policy` has an invalid value |
| GL024 | ERR | `rules[*].when` has an invalid value |
| GL025 | ERR | `image:` map form missing `name:` |
| GL026 | ERR | `inherit.default` / `inherit.variables` is not a boolean or list |
| GL034 | ERR | `services:` map form missing `name:`, or `alias:` is not a valid DNS label |
| GL036 | ERR | `timeout:` is not a valid GitLab CI duration string |
| GL037 | ERR | `id_tokens:` entry missing required `aud:` |
| GL038 | ERR | `secrets:` entry missing a provider key (`vault`, `gcp_secret_manager`, `azure_key_vault`) |
| GL039 | WARN | Job uses `pages:` keyword but `artifacts.paths` doesn't include the publish directory |
| GL041 | WARN | `cache.key.files` entry looks like a glob; must be an exact file path |
### Cross-job graph
| ID | Sev | Rule |
|----|-----|------|
| GL027 | ERR/WARN | `needs:` references a job that doesn't exist (WARN when `optional: true`) |
| GL028 | ERR | `needs:` references a job in a later stage |
| GL029 | ERR | Circular dependency in `needs:` graph |
| GL030 | ERR | `dependencies:` references a job that doesn't exist |
| GL031 | ERR | `dependencies:` references a job in the same or a later stage |
### Expression & reachability
| ID | Sev | Rule |
|----|-----|------|
| GL032 | WARN | `rules:if:` references `$VAR` not declared in any `variables:` block (may be false-positive for project-setting variables) |
| GL033 | WARN | Every rule in `rules:` has `when: never` — job permanently excluded (provable without evaluating `if:` expressions) |
| GL035 | WARN | `rules:changes` / `rules:exists` path is absolute — absolute paths never match in GitLab CI |
| GL042 | WARN | Every `rules:if:` condition evaluates to false given the declared variable values — job statically unreachable (only fires when all referenced variables are declared in YAML) |
### Inheritance
| ID | Sev | Rule |
|----|-----|------|
| GL043 | WARN | `inherit: default:` declared but the pipeline has no `default:` block (dead declaration), or the list form names fields not set in `default:` |
### Hidden jobs
Jobs whose name starts with `.` are reusable templates; most rules are skipped for them. This matches GitLab CI's own behaviour.
---
## Include resolution
| Capability | Notes |
|------------|-------|
| `include: local:` | Read from disk, recursively merged before linting |
| `include: remote:` | HTTPS URL fetched unauthenticated; unreachable URLs produce a warning, linting continues |
| `include: project:` | Fetched from the GitLab REST API using the configured token; skipped with a warning when no token is available |
| `include: component:` | Fetched from the GitLab CI/CD Catalog; public components work without a token |
| `include: inputs:` | `$[[ inputs.KEY ]]` and `$[[ inputs.KEY \| default(…) ]]` placeholders substituted from the `with:` block before parsing |
| Depth limit | Include chains capped at 100 levels (matches GitLab); circular cross-file references detected via visited-set tracking |
| Offline mode | `--offline` serves all remote includes from the cache; missing entries produce a warning |
| Include cache | `--cache-dir DIR` (or `~/.cache/glint` by default with `--offline`) persists fetched templates; keyed by SHA-256 of the request coordinates |
**Token resolution order** (first non-empty wins):
| Source | Header |
|--------|--------|
| `--token` flag / `GITLAB_TOKEN` env | `PRIVATE-TOKEN` |
| `CI_JOB_TOKEN` env | `JOB-TOKEN` |
| `GITLAB_PRIVATE_TOKEN` env | `PRIVATE-TOKEN` |
**URL resolution order:** `--gitlab-url` flag → `CI_SERVER_URL` env → `GITLAB_URL` env → `https://gitlab.com`
---
## Context simulation
Pass `--branch`, `--tag`, `--source`, or `--var` to evaluate `rules:if:` and
`only`/`except` filters against a specific pipeline event. When no context flag
is given glint defaults to `--branch main --source push` so that `rules:if:`
expressions are always evaluated.
**Evaluated at lint time:**
- `rules:if:` — full expression language: `==`, `!=`, `=~`, `!~`, `&&`, `||`, `!`, `(…)`, `$VAR`/`${VAR}`, string literals, `null`, regex flags (`/pat/i`)
- `only:` / `except:` — ref keywords, branch-name globs, and `/regex/` patterns
- `workflow:rules:` — evaluated to determine whether the pipeline would run; matching rule's `variables:` are injected before job evaluation
- `rules:changes:` — path-glob patterns evaluated against the supplied changed-file list (see `--changes` / `--changes-from` flags); `*` matches within a segment, `**` crosses `/` boundaries; the extended `{paths: [...], compare_to: ...}` form is supported; without changed-file data the condition is always treated as matching (permissive)
- Variable expansion — `$VAR` / `${VAR}` references in variable values expanded after all sources merge; transitive chains resolved (up to 10 passes)
**Not evaluated** (no git tree at lint time): `rules:exists:`.
**Predefined variables** set by 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; repeatable |
Use `--list-vars` to print the resolved variable table to stderr.
**Changed-file flags** (for `rules:changes:` evaluation):
| Flag | Effect |
|------|--------|
| `--changes PATH` | Mark PATH as changed; repeatable |
| `--changes-from REF` | Run `git diff --name-only REF` to auto-detect changed files |
---
## Output formats
Pass `--format` to `glint check`. In structured formats the summary line is
written to stderr so stdout contains only the machine-readable payload.
| Format | Flag | Description |
|--------|------|-------------|
| Text (default) | `--format text` | Ruff-style `file:line: RULE [sev] message` |
| JSON | `--format json` | Stable schema (version 1); `findings` array + `summary` block |
| SARIF 2.1.0 | `--format sarif` | Consumed by GitHub Code Scanning and GitLab SAST |
| JUnit XML | `--format junit` | CI test-report artifact (`artifacts:reports:junit`) |
| GitHub annotations | `--format github` | `::error file=…,line=…,title=RULE::message` inline PR comments |
**JSON schema (`schema_version: 1`):**
```json
{
"schema_version": 1,
"glint_version": "v0.2.20",
"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}
}
```
---
## Configuration
### `.glint.yml` project config file
Searched from the pipeline file's directory upward to the first `.git` boundary.
```yaml
# Suppress rules globally for this project.
ignore:
- GL007 # migrating from only:/except:
- GL032 # dynamic variables injected by CI
# Override rule severity (error | warning | ignore).
# 'ignore' is equivalent to listing the rule in ignore:.
severity:
GL004: warning # demote during a stage migration
GL035: error # promote to hard error for this project
# Extra stage names valid beyond those in the pipeline's own stages: block.
stages:
- quality
- security
# Default GitLab token (lower priority than --token and GITLAB_TOKEN).
token: glpat-xxxx
# Default GitLab instance URL.
url: https://gitlab.example.com
# Default cache directory.
cache_dir: ~/.cache/glint
```
**Priority chain:** `--token`/`--gitlab-url` flags > `.glint.yml` > environment variables.
### Inline suppression (`# glint: ignore`)
Suppress a finding for a specific job by placing a comment immediately before it:
```yaml
# glint: ignore GL007
legacy-job:
only: [main]
script: echo ok
# Multiple rules (comma- or space-separated):
# glint: ignore GL007, GL032
other-job:
script: echo ok
# Suppress every rule for this job:
# glint: ignore all
noisy-job:
script: echo ok
```
Suppressions are scoped to the single job they precede and do not affect other
jobs or pipeline-level findings.
---
## Graph visualization (`glint graph`)
| Mode | Output |
|------|--------|
| `tree` (default) | Terminal job tree: stages as branches, jobs as leaves; annotated with `[manual]`, `[delayed]`, `[trigger]` where applicable |
| `includes` | Mermaid flowchart to stdout; colour-coded nodes by include type (local, remote, project, component, template) |
| `pipeline` | GitLab CI-style SVG/PNG written to `--out` directory (default: `glint-out/`); converted to PNG when `rsvg-convert`, `inkscape`, or `magick` is available |
| `all` | `includes` to stdout + `pipeline` file path to stderr |
In DAG pipelines (any job has `needs:`) the pipeline graph uses job-to-job
Bézier connectors instead of stage-to-stage lines.
---
## Developer tools
| Tool | Description |
|------|-------------|
| `glint explain <RULE>` | Print description, rationale, bad-YAML example, and fix for a rule. Case-insensitive (`gl007` = `GL007`). |
| `glint explain` | List all rules with ID, severity, and title. |
| `--list-vars` | Print all resolved pipeline variables (pipeline + workflow rules + context) to stderr before linting. |
| `--version` / `-v` | Print the compiled version string. |