Skip to content

Lint Rules

The GitLab lexicon ships lint rules that run during chant lint and post-synth checks that validate the serialized YAML after chant build.

Lint rules analyze your TypeScript source code before build.

Severity: warning | Category: style

Flags usage of only: and except: keywords, which are deprecated in favor of rules:. The rules: syntax is more flexible and is the recommended approach.

lint-wgl001.ts
import { Job, Rule, CI } from "@intentius/chant-lexicon-gitlab";
// chant-disable-next-line WGL001
export const deployBad = new Job({
stage: "deploy",
script: ["npm run deploy"],
only: ["main"],
});
export const deployGood = new Job({
stage: "deploy",
script: ["npm run deploy"],
rules: [new Rule({
if: `${CI.CommitBranch} == ${CI.DefaultBranch}`,
})],
});

Severity: error | Category: correctness

A GitLab CI job must have script, trigger, or run defined. Jobs without any of these will fail pipeline validation.

lint-wgl002.ts
import { Job, Image, Trigger } from "@intentius/chant-lexicon-gitlab";
// chant-disable-next-line WGL002
export const wgl002Bad = new Job({
stage: "build",
image: new Image({ name: "node:20" }),
});
export const wgl002Good = new Job({
stage: "build",
image: new Image({ name: "node:20" }),
script: ["npm run build"],
});
export const downstream = new Job({
trigger: new Trigger({ project: "my-group/other-repo" }),
});

Severity: info | Category: style

Jobs should declare a stage property. Without it, the job defaults to the test stage, which may not be the intended behavior.

lint-wgl003.ts
import { Job } from "@intentius/chant-lexicon-gitlab";
// chant-disable-next-line WGL003
export const wgl003Bad = new Job({
script: ["npm run build"],
});
export const wgl003Good = new Job({
stage: "build",
script: ["npm run build"],
});

Severity: warning | Category: performance

Flags Artifacts without expireIn. Artifacts without expiry are kept indefinitely, consuming storage. Always set an expiration.

lint-wgl004.ts
import { Job, Artifacts } from "@intentius/chant-lexicon-gitlab";
// chant-disable-next-line WGL004
export const wgl004Bad = new Job({
script: ["npm run build"],
artifacts: new Artifacts({
paths: ["dist/"],
}),
});
export const wgl004Good = new Job({
script: ["npm run build"],
artifacts: new Artifacts({
paths: ["dist/"],
expire_in: "1 hour",
}),
});

Post-synth checks run against the serialized YAML after build. They catch issues only visible in the final output.

Severity: error

Flags jobs that reference a stage not present in the collected stages list. This causes a pipeline validation error in GitLab.

Severity: warning

Flags jobs where all rules: entries have when: "never", making the job unreachable. This usually indicates a configuration error.

lint-wgl011.ts
import { Job, Rule, CI } from "@intentius/chant-lexicon-gitlab";
// chant-disable-next-line WGL011
export const noop = new Job({
script: ["echo unreachable"],
rules: [
new Rule({ if: CI.CommitBranch, when: "never" }),
new Rule({ if: CI.CommitTag, when: "never" }),
],
});

Severity: warning

Flags properties marked as deprecated in the GitLab CI schema. Deprecation signals are mined from property descriptions (keywords like “deprecated”, “legacy”, “no longer available”). Using deprecated properties may cause unexpected behavior in future GitLab versions.

Severity: error

Flags jobs whose needs: entries reference a job not defined in the pipeline, or reference themselves. Both cause GitLab pipeline validation failures. When include: is present, the check is skipped since needed jobs may come from included files.

Severity: error

Flags jobs whose extends: references a template or hidden job not defined in the pipeline. GitLab rejects pipelines with unresolved extends references. When include: is present, the check is skipped since templates may come from included files.

Severity: error

Detects cycles in the needs: dependency graph. If job A needs B and B needs A (directly or transitively), GitLab rejects the pipeline. Reports the full cycle chain in the diagnostic message.

Severity: error

Detects hardcoded passwords, tokens, or keys in a variables: block. Move them to CI/CD masked variables instead of committing them to the pipeline. The precursor to the WGL038–040 secret-scoping checks.

Severity: warning

Flags Docker push/pull to a non-HTTPS registry in a job script. HTTP gives the registry traffic no transport integrity.

Severity: warning

Flags jobs without an explicit timeout:. The instance default (often 1 hour) is too long for most jobs and lets a hung job hold a runner.

Severity: info

Deploy-stage jobs benefit from a retry: strategy to ride out transient infrastructure failures. Informational, not required.

Severity: error

Detects multiple jobs that resolve to the same kebab-case key in the serialized YAML. GitLab silently merges duplicate keys, so one job’s config quietly overwrites the other.

Severity: warning

Flags global variables: not referenced by any job script — usually stale configuration adding noise.

Severity: warning

Flags artifacts: without expire_in:. Depending on instance config the default is “never expire,” which bloats storage.

Severity: info

Flags a job whose only rule is when: always with no conditions (if:, changes:, …). That disables all pipeline filtering for the job, which is usually unintended.

Severity: warning

Flags when: manual jobs that don’t set allow_failure: true. Without it the manual job blocks the pipeline from progressing past its stage until someone triggers it.

Severity: warning

Flags cache: without a key:. GitLab falls back to the default key, causing cache collisions between unrelated jobs on the same runner.

WGL026 — Privileged services without TLS

Section titled “WGL026 — Privileged services without TLS”

Severity: warning

Flags Docker-in-Docker (DinD) services that don’t set DOCKER_TLS_CERTDIR, leaving the Docker daemon on an unencrypted socket. Extended by WGL036 for the merge-request-reachable case.

Severity: error

Detects jobs with script: [] or only empty strings. GitLab rejects empty scripts at pipeline validation time.

Severity: info

Detects needs: entries already implied by stage ordering. Not incorrect, but redundant needs add noise and make the pipeline harder to maintain.

WGL029 onward are a CI/CD supply-chain security pass, the GitLab counterpart to the github lexicon’s GHA029–058: pin & vet includes/components/images, scope CI_JOB_TOKEN and OIDC, guard trust boundaries against untrusted CI input, mask/protect/scope secrets, reject unsound rules: expressions, and keep artifacts/caches honest. They run statically on the emitted .gitlab-ci.yml.

The checks that need a moving external truth — whether a pinned component/include ref still resolves, whether an upstream was archived or moved, whether a new advisory covers a component in use — live in the operational layer instead. Schedule the PipelineAuditOp (temporal lexicon) for that live half; it reads the emitted include: / component: / image: references and reports drift via report | issue | merge-request.

WGL029 — Unpinned include:project / component

Section titled “WGL029 — Unpinned include:project / component”

Severity: warning

Flags an include:project or CI/CD component: resolved by a moving ref — a branch, a missing ref: (defaults to the default branch), or a floating component version — instead of a pinned tag or commit SHA.

WGL030 — Mutable or insecure include:remote

Section titled “WGL030 — Mutable or insecure include:remote”

Severity: error (HTTP) / warning (HTTPS)

Flags include:remote URLs fetched over HTTP (no transport integrity) or over HTTPS but inherently mutable. Prefer a pinned include:project or component. Generalizes WGL017 to includes.

WGL031 — Container image without a digest

Section titled “WGL031 — Container image without a digest”

Severity: warning

Flags image: and services: references not pinned to an immutable @sha256: digest. Variable-based references (e.g. $CI_REGISTRY_IMAGE:tag) are skipped.

WGL032 — Look-alike include/component source

Section titled “WGL032 — Look-alike include/component source”

Severity: warning

Flags an include:project / component: source that is a near-miss (edit distance 1–2) of a well-known GitLab CI source but not an exact match — a likely typo or impersonation. Backed by a vendored reference list.

WGL033 — OIDC id_token without a scoped audience

Section titled “WGL033 — OIDC id_token without a scoped audience”

Severity: warning

Flags an id_tokens: declaration with no aud: or a wildcard audience. The audience binds the minted OIDC token to a relying party; without it a leaked token is accepted anywhere.

WGL034 — OIDC id_token mintable from a merge-request pipeline

Section titled “WGL034 — OIDC id_token mintable from a merge-request pipeline”

Severity: warning

Flags a job that declares id_tokens: and is reachable from merge-request pipelines, which outside contributors can trigger. Restrict OIDC jobs to protected refs or require approval.

The project-level CI_JOB_TOKEN allowlist and a variable’s protected status are project settings, not emitted pipeline YAML, so they are out of scope for these post-synth checks (see issue #298’s caveat).

WGL035 — Untrusted CI variable in a script

Section titled “WGL035 — Untrusted CI variable in a script”

Severity: warning

Flags an attacker-controllable predefined variable (branch/tag name, commit or MR title/description, author) referenced in a script: command, where a crafted value can inject shell commands. Quote it and avoid using it in sensitive commands.

WGL036 — Privileged DinD reachable from merge requests

Section titled “WGL036 — Privileged DinD reachable from merge requests”

Severity: warning

Flags a Docker-in-Docker (privileged) job reachable from merge-request pipelines, which outside contributors can trigger. Restrict privileged services to protected refs. Complements WGL026.

Severity: warning

Flags a rules:if that gates on a regex match (=~) over an attacker-controllable ref variable — a crafted branch/tag name can satisfy the pattern. Match the full ref with == or gate on a protected condition.

WGL038 — Secret reachable from a merge-request pipeline

Section titled “WGL038 — Secret reachable from a merge-request pipeline”

Severity: warning

Flags a user-defined secret-like variable read by a job reachable from merge-request pipelines, which can run untrusted code. Gate the job to protected refs or mark the variable protected. (Built-in CI_* variables are excluded.)

Severity: warning

Flags a script: command that prints a secret-like variable (echo/printf/cat). Logs are broadly readable and masking can be defeated by transforms.

Severity: error

Flags a docker login (or compatible) passing a literal password via -p/--password instead of a variable or --password-stdin. Extends WGL016 to scripts.

Variable masking and protected status are GitLab project/CI-settings, not emitted YAML — WGL016 already nudges toward masked variables; this group covers the exposure paths visible in the pipeline.

Severity: warning

Flags a tautological rules:if where both sides of ==/!= are identical, so the condition is always true or always false. Generalizes WGL011 to conditions presented as gates.

WGL042 — Unreachable rules after an unconditional match

Section titled “WGL042 — Unreachable rules after an unconditional match”

Severity: warning

Flags rules listed after an unconditional rule (no if:, not when: never). GitLab takes the first matching rule, so a catch-all makes everything after it dead. Put specific rules first.

Severity: warning

Flags a rules:if whose =~ regex matches every value (empty, dot-star, anchored dot-star) — a filter that admits everything. Tighten the pattern or remove the gate.

The auto-fix acceptance item is N/A at the post-synth layer (PostSynthDiagnostic has no fix channel); it belongs to the declarative source-lint rules.

Severity: warning

Flags artifacts:public: true, which makes build output downloadable by anyone. Keep artifacts private unless they are meant to be world-readable.

WGL045 — Credential-bearing artifact path

Section titled “WGL045 — Credential-bearing artifact path”

Severity: error

Flags an artifacts:paths: entry that looks like a credential or sensitive file (.env, *.pem, id_rsa, *.key, .npmrc, .netrc, credentials). Artifacts flow downstream and are downloadable — exclude the file.

WGL046 — Cache poisoning from a merge-request pipeline

Section titled “WGL046 — Cache poisoning from a merge-request pipeline”

Severity: warning

Flags a job that writes a cache (push policy) and is reachable from merge-request pipelines. An MR can poison the cache for a later protected run that restores the same key. Restrict cache writes to protected refs or scope the key.

Artifact / dependencies: flow across a protected boundary depends on a ref’s protected status, a project setting not present in emitted YAML — out of scope here.

WGL047 — Software fetched and piped to a shell

Section titled “WGL047 — Software fetched and piped to a shell”

Severity: warning

Flags a script: command that pipes a network download straight into a shell (curl ... | bash). The fetched code is unpinned and unverified — download to a file, verify a checksum/signature, then run it.

Severity: info

Flags a pipeline that defines a workflow: block but no workflow:name. A pipeline name aids identification in the GitLab UI and audit output.

Two #304 items stay out of scope: a broad privileged-service / DinD check (already covered by WGL026 for DinD-TLS and WGL036 for MR-reachable DinD; a runner’s privileged isolation is not in emitted YAML) and an optional include/component allowlist policy (configuration, overlapping WGL032’s vendored source list).

Terminal window
# Lint your chant project
chant lint
# Lint with auto-fix where supported
chant lint --fix

To suppress a rule on a specific line:

// chant-disable-next-line WGL001
export const deploy = new Job({ only: ["main"], script: ["deploy"] });

To suppress globally in chant.config.ts:

export default {
lint: {
rules: {
WGL003: "off",
},
},
};