02d8e63a98
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>
502 lines
23 KiB
Markdown
502 lines
23 KiB
Markdown
<p align="center">
|
||
<img src="assets/glint-logo.png" alt="glint logo" width="220" />
|
||
</p>
|
||
|
||
<h1 align="center">glint</h1>
|
||
|
||
<p align="center">
|
||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
|
||
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/release-v0.2.19-blue.svg" alt="Release"></a>
|
||
</p>
|
||
|
||
> **Disclaimer:** This tool was built through iterative AI-assisted development with [Claude](https://claude.ai). 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 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.image` accepts both string and map form; `rules.changes`/`rules.exists` accept both list and `{paths, compare_to}` map form
|
||
- **Graph output** — `glint 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 scalars** — `BUILD: 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](ROADMAP.md) for planned improvements.
|
||
|
||
## Requirements
|
||
|
||
- Go 1.21 or later
|
||
- [Task](https://taskfile.dev) (optional, for development tasks)
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
git clone https://git.k3nny.fr/glint
|
||
cd glint
|
||
go build -o glint ./cmd/glint/...
|
||
```
|
||
|
||
Or with Task:
|
||
|
||
```bash
|
||
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`
|
||
|
||
```bash
|
||
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.
|
||
|
||
```bash
|
||
# 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`):**
|
||
```json
|
||
{
|
||
"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.
|
||
|
||
```yaml
|
||
# .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:
|
||
|
||
```yaml
|
||
# 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:
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```bash
|
||
# 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](https://gitlab.com/explore/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:
|
||
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 `---`.
|
||
|
||
```bash
|
||
# 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](https://mermaid.js.org) flowchart written to stdout.
|
||
Pipe to a `.mmd` file or paste into [mermaid.live](https://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.
|
||
|
||
```bash
|
||
# 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 2–200, or map form missing `matrix` key |
|
||
| GL012 | ERROR | `retry` integer not in range 0–2, 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](https://taskfile.dev) as a task runner.
|
||
|
||
```bash
|
||
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
|
||
```
|