GitLab keyword compatibility
This page is generated from the GitLab frontend's keyword tables in libs/loom-check (run pnpm run gitlab:compat:write). Do not edit it by hand.
Statuses: executable compiles and runs locally; resolved is consumed while composing the document; diagnosed is accepted but reported through a capability or limit diagnostic; preserved stays in the document and is reported as GL_SOURCE_UNSUPPORTED when a selected job or the root uses it.
Root keywords
| Keyword | Status | Notes |
|---|---|---|
after_script | resolved | folded into default: during resolution |
before_script | resolved | folded into default: during resolution |
cache | resolved | folded into default: during resolution |
default | resolved | twelve released default keywords are filtered by inherit and copied whole when unspecified; downstream support varies |
hooks | preserved | pre_get_sources_script has no local equivalent |
image | resolved | folded into default: during resolution |
include | resolved | local: literal/star includes compose ordered selected entry bytes (symlinks contribute target text); public HTTP(S) remote sources support integrity, TTL cache and file-scoped inputs; project, template and component includes are diagnosed |
services | resolved | folded into default: during resolution |
spec | resolved | file-scoped typed inputs bind before composition; broader component/module inputs remain unsupported |
stages | executable | orders job phases; pipelines whose included jobs are only .pre/.post do not run |
variables | executable | scalar/rich values use released conversion and raw controls; root metadata validates authored defaults; one Runner environment pass and separate rule/cache phases |
workflow | executable | workflow:rules decide whether the pipeline runs |
Job keywords
| Keyword | Status | Notes |
|---|---|---|
after_script | executable | runs in a fresh shell after script; ordinary finalizer failures do not replace the job outcome |
allow_failure | executable | job failure does not fail the pipeline |
artifacts | executable | paths are exported from the workspace; retention is not enforced |
before_script | executable | runs before script in the same shell |
cache | executable | keys and paths are restored and saved locally |
coverage | executable | the regex is accepted; report processing is unavailable |
environment | diagnosed | scripts run; deployment tracking, protection, scoped secrets, lifecycle actions and predefined variables are unavailable |
id_tokens | diagnosed | no OIDC issuer exists locally |
image | executable | selects the Docker image |
inherit | resolved | filters default keywords and YAML root variables independently; explicit parent/job and caller values survive |
interruptible | executable | cancellation is ignored locally |
needs | executable | orders execution; same-pipeline transfers restore available archives |
pages | diagnosed | default public output is collected as local artifacts; hosting, URL variables, path prefixes and expiry are unavailable; custom publish is not executable |
parallel | executable | Integer 1..200 expands ordinary native instances with source/exact selection and runtime node facts. Matrix expansion remains required and unsupported. |
resource_group | diagnosed | no cross-run locking exists locally |
rules | executable | if, changes, when, allow_failure, needs and job variable actions are evaluated |
script | executable | runs in the job container or on the host |
services | executable | service containers are started; privileged services need --privileged-service |
stage | executable | assigns the job to a stage |
tags | diagnosed | runner tags must be listed in the context runner_tags |
variables | executable | caller strings replace whole root/job definitions, including empty values and raw mode |
default: keywords
| Keyword | Status | Notes |
|---|---|---|
after_script | executable | copied whole when selected and unspecified, including null; runs as finalization |
artifacts | executable | copied whole; report processing and retention remain unavailable |
before_script | executable | copied whole when selected and unspecified, including null; runs as setup |
cache | executable | copied whole; job cache mappings replace this default |
hooks | preserved | copied whole; selected hooks remain unsupported |
id_tokens | diagnosed | copied whole; local issuance remains unavailable |
image | executable | copied whole; existing image validation applies |
interruptible | executable | copied whole; cancellation is ignored locally |
retry | preserved | copied whole; selected retry remains unsupported |
services | executable | copied whole; existing service validation and capability policy apply |
tags | diagnosed | copied whole; runner tag capability policy applies |
timeout | preserved | copied whole; selected timeout remains unsupported |
rules: keys
| Keyword | Status | Notes |
|---|---|---|
allow_failure | executable | overrides the job's allow_failure when the rule matches |
changes | executable | list shorthand or exactly one of paths/regexp, optionally with compare_to; globs support brace alternation, character classes and escaping; regexp subset below |
exists | preserved | not evaluated locally |
if | executable | GitLab rule expressions over pipeline variables |
needs | executable | replaces the job's needs when the rule matches |
variables | executable | job-only scalar assignments replace whole definitions after first match; caller values win, including empty; workflow actions remain unsupported |
when | executable | per-scope allowlists below; on_failure and delayed are preserved but not executable |
Composition and inheritance
Local star patterns expand with released ordered RE2 quoting/replacements: **/, then **, then *. Other glob punctuation is literal. Local paths remove only leading slashes and preserve authored spaces for literals and patterns; matching uses canonical source names independently of authored directory spelling. Wildcard reads cannot use the explicit external literal/source exception. Matches retain Git tree order and repeated occurrences; no match adds no include. The generated filter has a 1000-byte bound, not a 1000-match cap. The current included same-project local parent is skipped; a matching top-level root can be included once before that rule applies. Literal self-includes retain deterministic cycle rejection. Git-backed compilation reads selected snapshot origins, including dirty/eligible untracked sources and retained HEAD bytes while excluding ignored dependency/runtime YAML. GitLab local includes read exact selected entry bytes, including symlink target text as YAML without following the target; missing logical children beneath links remain missing. Root-file authoring admission retains its separate contained-link policy. Valid unborn repositories support check/compile; local execution still requires HEAD. Non-Git roots use bounded rooted discovery. Added discovery has a local 64 Mi-unit guard; existing Git scans and incomplete upstream include-limit parity remain separate.
Public HTTP(S) remote includes and URL shorthand share native source retrieval, optional strict-base64 SHA256 integrity, opt-in raw-content TTL reuse and file-scoped inputs. Nested remote includes are anonymous and cannot read caller-local files. Every occurrence verifies integrity and binds independently; the hash-only source closure pins requested/final address and content identities across preflight and isolated compilation. The source cache is separate from job caches. See public remote sources for cache clearing forms, containment and cancellation. Project/template/component sources and variable URLs remain unsupported.
Include rules support ordered ordinary if predicates and when: always/never before selected-source validation, wildcard expansion, reads and target input binding. The first matching rule wins; omitted actions include and no match excludes. Caller lookup preserves raw explicit context strings, undefined versus empty, and excludes YAML root/job variables; nested remote lookup is anonymous. Absent/null/false rule lists bypass selection; empty lists/maps and empty individual rules fail. Null if/when fields are compacted away. Every outer rule validates before selection, including later rules behind an unconditional match. Include exists/changes, variable addresses, full phase/provenance and released include-limit parity remain required future work.
Local includes merge before references, then ordered hidden or executable parents. Later parents win, then the child; mappings merge recursively and arrays replace. Executable parents retain their own job identity; reserved root mappings can also be parents without becoming jobs. Omitted, null or empty-list extends selects no parents. Missing/nonmapping parents, cycles and more than eleven inheritance edges fail.
inherit.default and inherit.variables independently accept booleans or name lists. Omitted/null controls select all; empty lists select none. Only YAML root defaults/variables are filtered: explicit parent/job values and supplied context survive. Job rules, cache expansion and runtime variables share the filter. Workflow predicates still see their unfiltered root/context scope. Workflow-rule variable assignments remain unsupported.
All twelve configured defaults are source-validated before inheritance filtering, even when every job filters or overrides them. Validation follows the released default profile: four raw top-level cache entries and two cache-key files with the increased-file-limit flag disabled; hosted settings/flags can differ. The twelve default keywords above copy as whole keywords when selected and the composed job value is omitted or null. Explicit empty lists/maps or false remain specified and use the keyword's own validator. A child null can therefore discard a parent value and receive a default again. Legacy root image, before_script, after_script, cache and services fold into defaults; two specified values conflict, while null is unspecified. Recognizing and copying a default does not implement its downstream behavior: hooks, retry and timeout remain unsupported; other capability limitations remain visible.
Rule expressions
- Operators:
!,==,!=,=~,!~,&&,||. Expressions retain scalar/null/boolean types and support reversed/grouped operands and unary!. Regex operands accept literal, quoted and variable-held/pattern/flagsforms. workflow:rulesacceptwhen:always,never.- Job
rulesacceptwhen:always,manual,never,on_success;on_failureanddelayedare preserved but not executable. - Integer collision winners are fixed before job rules. Job contexts receive structural
CI_NODE_INDEX/CI_NODE_TOTALdefaults; workflow contexts do not. Use direct predicates for these facts. - Integer base needs expand source aliases before job rules. Every normalized base list and literal
rules:needslist validates before job/request pruning; duplicate final names fail regardless of optional/artifact flags. Rule overrides name literal final IDs and do not expand aliases. - If-variable lookup uses raw winning root/job/caller strings; it does not recursively expand aliases or pattern contents. Undefined variables evaluate as
null;changeswithoutpathsin the context are resolved withgit diff --merge-base.
Changed-path regular expressions
rules:changes:regexp supports ASCII literals, ordinary character classes, alternation, capturing/noncapturing groups, quantifiers, nested negative lookahead, and absolute \A/\z anchors. It matches any changed path; an empty changed-path list does not match. Use exactly one of paths and regexp; either can use compare_to.
Existing variables expand in the rule phase with global, job, then context precedence; undefined references stay literal. Raw and expanded patterns are limited to 255 characters. A finite per-path match timeout produces a source-invalid, rule_invalid result rather than guessing a selection. The timeout is approximate and does not claim GitLab's exact timing behavior.
This is a limited Ruby-compatible syntax surface, not complete Ruby regex support. Line anchors ^/$, inline flags, lookbehind, positive lookahead, backreferences, omitted-minimum quantifiers, class extensions, Unicode pattern syntax and unsupported escapes are rejected. Use absolute anchors for filenames containing newlines. Existing rules:if expressions and glob matching keep their RE2 behavior.
YAML tags
-
!reference [job, key, ...]resolves against the merged catalog afterinclude:and beforeextends:. It preserves scalar, mapping and sequence targets. Cycles and missing/null/false targets are rejected; empty string/map/list targets remain subject to the consuming keyword's validation. String path segments may be empty. No other explicit YAML tag is accepted. -
References have no independent ten-hop limit; concrete copies consume the shared YAML materialization budget.
before_script,scriptandafter_scriptflatten nested command arrays up to ten array levels, preserving order and string bytes. Services, cache and needs normalize their own composable arrays; job rules and stages enforce their own ten-array-level limit. Tags, paths and service entrypoints remain flat string arrays. -
An authored
scriptmust be present and non-blank: a blank scalar or top-level[]is invalid. An array containing an empty string is valid;[[]]is a valid zero-command main phase. Optional before/after commands may be null or empty. Native required scripts retain their nonempty-command requirement.
Diagnostic codes
| Code | Meaning | Fix hint |
|---|---|---|
GL_SOURCE_SYNTAX | the YAML could not be parsed | — |
GL_SOURCE_COMPOSITION | extends, include or anchors produced an invalid document | the extended template must be a mapping; hidden keys that nothing extends are ignored, as in GitLab |
GL_SOURCE_LIMIT | a source or rule matching limit was exceeded | — |
GL_SOURCE_DOCUMENT | the document is not a GitLab CI mapping | — |
GL_SOURCE_TYPE | a keyword has the wrong type or an unsupported value | define the job as a mapping of GitLab keywords such as script and stage; a list or scalar is not a job |
GL_SOURCE_UNSUPPORTED | a keyword or include source is preserved but not executable locally | use include: local: <path> or a public HTTP(S) remote; project, template and component includes are not resolved locally; use when: on_success, never, manual or always, or omit when to keep the GitLab default |
GL_CONTEXT_PHASE_UNAVAILABLE | a runner-phase variable was supplied or referenced before it exists | — |
GL_VARIABLE_CYCLE | variable expansion loops | — |
GL_VARIABLE_EXPANSION_UNSUPPORTED | an expansion form the local runtime cannot evaluate | remove the $ from the cache key (GitLab writes $$ for a literal $) or move the value into a pipeline variable |
GL_VARIABLE_EXPANSION_LIMIT | expansion exceeded the size or substitution budget | — |
GL_RULE_REGEX_UNSUPPORTED | a literal rule regex has invalid or unsupported syntax | — |
GL_RULE_CHANGES_UNAVAILABLE | rules:changes could not be resolved from git | — |
GL_PIPELINE_SPECIAL_STAGES_ONLY | rule-included jobs are only .pre/.post, so the pipeline cannot run | — |
GL_RULE_MANUAL_BLOCKING | a blocking manual job stops the local pipeline | — |
GL_CAPABILITY_RUNNER_TAGS_UNSATISFIED | job tags are missing from the context runner_tags | add the job's tags to runner_tags in the --context file |
GL_CAPABILITY_PRIVILEGED_SERVICE_NOT_ALLOWED | a privileged service alias needs --privileged-service | pass --privileged-service <alias> to loom run |
GL_CAPABILITY_HOSTED_MACOS_UNAVAILABLE | SaaS macOS runners have no local equivalent | — |
GL_CAPABILITY_ID_TOKENS_UNAVAILABLE | id_tokens cannot be issued locally | — |
GL_CAPABILITY_PAGES_UNAVAILABLE | custom Pages publish directories are not executable locally | — |
GL_CAPABILITY_RESOURCE_GROUP_UNAVAILABLE | resource_group locking is not enforced locally | — |
GL_LIMIT_OPTIONAL_MANUAL_NOT_EXECUTED | optional manual jobs are skipped | — |
GL_LIMIT_WORKFLOW_AUTO_CANCEL_IGNORED | workflow:auto_cancel is ignored | — |
GL_LIMIT_INTERRUPTIBLE_IGNORED | interruptible cancellation is ignored | — |
GL_LIMIT_PAGES_DEPLOYMENT_UNAVAILABLE | local public artifacts do not create a Pages deployment or hosted variables | — |
GL_LIMIT_ENVIRONMENT_TRACKING_UNAVAILABLE | environment tracking, protection, scoped secrets, lifecycle actions and predefined variables are unavailable; caller variables are unchanged | — |
GL_LIMIT_REPORT_PROCESSING_UNAVAILABLE | artifact reports are exported but not processed | — |
GL_LIMIT_ARTIFACT_RETENTION_NOT_ENFORCED | expire_in is not enforced | — |
GL_LIMIT_WHEN_ALWAYS_NOT_ENFORCED | when: always runs as on_success locally | — |