GitLab
What GitLab itself reads out of the repository: the .gitlab-ci.yml, read as GitLab writes it, and the CODEOWNERS file, read as sections of rules. For the pipeline, the stages it declares, what it includes, and for every job the stage it sits in, the image and runner tags it asks for, how long it may run, the rules that decide whether it runs at all, and each script section with the variables it interpolates. For CODEOWNERS, every section with whether it is optional and how many approvals it needs, the rules under each, and the owners that apply to the paths a guardrail asks about, resolved per section the way GitLab resolves them. Names and shapes only, never a secret, a variable value or an environment value.
| Facts key | gitlab |
| Version | v1 |
| Script | gitlab.py |
| Timeout | 60 seconds |
Inputs
| Input | Description | Default | Environment |
|---|---|---|---|
pipelines | Comma separated globs matched against every file in the working directory and against bare file names, naming the GitLab CI files to read. Narrows what discovery finds; a file it matches that is not named .gitlab-ci.yml or .gitlab-ci.yaml is still left out. | .gitlab-ci.yml,.gitlab-ci.yaml | GUARDRAIL_INPUT_PIPELINES |
paths | Comma separated paths to resolve owners for, relative to the repository root. A guardrail that cares about the files a merge request changed names them here. | README.md,LICENSE | GUARDRAIL_INPUT_PATHS |
A guardrail that declares an input of the same name passes it through, so the guardrail's configuration in buildnote.json is what decides these values.
A guardrail asks for these facts by name, and reads them back the same way:
{
"collect": ["gitlab"]
}gitlab = guardrail.facts("gitlab")Facts
The document gitlab collects. A path carrying [] is an entry of the list before it, and one carrying [path] is a key of the object before it.
| Fact | Meaning |
|---|---|
source | report when a GitLab CI file or a CODEOWNERS file was read, none when neither was. |
reason | Why nothing was read, present only when source is none. |
reports | Every file that was read, the GitLab CI files in path order and the CODEOWNERS file after them. |
reports[].path | Path of the file, relative to the directory the CLI runs in. |
reports[].modified | When it was last written, ISO 8601. |
reports[].format | gitlab-ci for a pipeline definition, codeowners for the CODEOWNERS file. |
pipelines | The pipelines themselves, one entry per file that was read. Absent when the repository carries none, so a check reads its absence rather than an empty list. |
pipelines[].path | Path of the file, relative to the directory the CLI runs in. |
pipelines[].stages | The stages the file declares, in the order it declares them. Empty when it declares none and takes GitLab's default stages. |
pipelines[].image | The top level image, exactly as written, or null. Every job that declares none runs in this one. |
pipelines[].default | The default block, exactly as written, or null. It carries what every job inherits when it declares none of its own. |
pipelines[].variables | Names of the top level variables. Names only, never values. |
pipelines[].includes | What include names, one entry per include: the local path, the project, the template, the remote URL or the component, whichever the entry declares. An included file is not itself read. |
pipelines[].workflowRules | How many rules the top level workflow block declares, which is what decides whether a pipeline is created at all. 0 when it declares none. |
pipelines[].jobs | The jobs the file declares. A key GitLab reserves, and a hidden .template key, is not a job and is left out. |
pipelines[].jobs[].id | Key the job is declared under, which is what needs names. |
pipelines[].jobs[].stage | The stage the job runs in, exactly as written, or null when it declares none and takes test. |
pipelines[].jobs[].image | The image the job runs in, exactly as written, or null when it declares none and takes the pipeline's. |
pipelines[].jobs[].tags | The runner tags the job asks for, which is what picks the runner it lands on. |
pipelines[].jobs[].timeout | The declared timeout, exactly as GitLab writes it, or null when the job declares none and takes the project's own limit. |
pipelines[].jobs[].timeoutMinutes | The same timeout in whole minutes, or null when the job declares none or writes a duration that could not be read. |
pipelines[].jobs[].environment | The environment the job deploys to, exactly as written, or null. |
pipelines[].jobs[].needs | Jobs this one waits for, by name. |
pipelines[].jobs[].rules | How many rules entries decide whether the job runs. 0 when it declares none. |
pipelines[].jobs[].when | The when the job declares, exactly as written, or null. manual is a job somebody has to start. |
pipelines[].jobs[].allowFailure | Whether the job is allowed to fail without failing the pipeline. |
pipelines[].jobs[].interruptible | Whether a newer pipeline may cancel this job. |
pipelines[].jobs[].extends | The hidden keys this job extends, by name. |
pipelines[].jobs[].trigger | The downstream pipeline this job triggers instead of running a script, exactly as written, or null. |
pipelines[].jobs[].variables | Names of the variables the job declares. Names only, never values. |
pipelines[].jobs[].secretsUsed | Names of the secrets the job's secrets block declares. Names only, never values: the facts are attached to the run event. |
pipelines[].jobs[].scripts | The script sections the job declares, in the order GitLab runs them. |
pipelines[].jobs[].scripts[].section | Which section it is: before_script, script or after_script. |
pipelines[].jobs[].scripts[].run | The shell body of the section, its lines joined, truncated to 2000 characters. |
pipelines[].jobs[].scripts[].variables | Every $VAR and ${VAR} the section interpolates, read from the whole body before it was truncated. Names only, never values. |
stages | Every stage declared across the files that were read. |
counts.pipelines | Files that were read. |
counts.jobs | Jobs across every file. |
counts.scripts | Script sections across every job. |
unparsed | Files that matched but were not read, because the YAML they are written in is outside the subset the collector can represent. A file here is not a file with nothing in it: nothing was read from it and no verdict should be drawn about it. |
unparsed[].path | Path of the file that was not read. |
unparsed[].reason | The construct that could not be represented, and the line it is on. |
codeowners | The CODEOWNERS file the repository carries. Absent when it carries none at any of the three locations GitLab reads one from, so a check reads its absence rather than an empty section list. |
codeowners.path | Path of the CODEOWNERS file that was read: the first of CODEOWNERS, .gitlab/CODEOWNERS and docs/CODEOWNERS that exists, the three locations GitLab reads a CODEOWNERS file from. |
codeowners.sections | Every section header the file declares, in file order, each carrying the rules written under it. A rule written above the first header belongs to the implicit default section that leads this list. |
codeowners.sections[].name | Name inside the brackets, exactly as written. The implicit default section is named codeowners, which is what GitLab calls it; read implicit rather than the name to tell it apart. |
codeowners.sections[].optional | Whether the header carries the ^ prefix, which makes the section's approval optional: its owners are still shown on the merge request, and their approval is not required to merge. |
codeowners.sections[].approvalsRequired | Approvals the section asks for, from the [2] a header may carry after its name, or null when it carries none and the project's own approval rule decides. |
codeowners.sections[].defaultOwners | Owners written on the header itself, which own every rule beneath it that names none of its own. |
codeowners.sections[].line | Line the header is declared on, or null for the implicit default section, which has no header. |
codeowners.sections[].implicit | Whether this is the default section holding the rules written above the first header, rather than a section the file declares. |
codeowners.sections[].rules | The rules written under this header, in file order, which is the order they resolve in within the section. |
codeowners.sections[].rules[].pattern | Pattern exactly as it is written, so an anchored /docs/ reads differently from a docs/ matched at any depth. |
codeowners.sections[].rules[].owners | Owners the rule ends up with: the ones written on the line, or the section's default owners when the line names none. Empty when neither names one, which leaves everything the rule matches unowned within this section. |
codeowners.sections[].rules[].inherited | Whether the owners came from the section header rather than from the rule's own line. |
codeowners.sections[].rules[].line | Line the rule is declared on, so a verdict points at the file. |
codeowners.owners | Every owner that ends up owning a rule, by owner, with how many rules name them. A section's default owners are counted once per rule they are inherited by, because that is what they own. |
codeowners.matched | Owners resolved for each path the paths input named, by the path exactly as it was given. A path nobody asked about is absent, so read the ones the guardrail configured rather than iterating. |
codeowners.matched[path].owners | Every owner of the path across all the sections that match it, in section order. GitLab resolves ownership once per section, so a path is owned by as many sets of owners as there are sections matching it. |
codeowners.matched[path].sections | One entry per section that matches the path, in section order. A section whose rules never match the path is absent rather than empty. |
codeowners.matched[path].sections[].section | Name of the section that matched. |
codeowners.matched[path].sections[].optional | Whether that section's approval is optional, so its owners are informed rather than required. |
codeowners.matched[path].sections[].approvalsRequired | Approvals that section asks for, or null when it asks for no particular number. |
codeowners.matched[path].sections[].owners | Owners the last rule of that section to match the path ends up with. Empty when that rule names none and the section hands it none. |
codeowners.matched[path].sections[].rule | Pattern of the last rule in that section to match the path, which is the one that decides its owners there. |
codeowners.matched[path].sections[].line | Line that rule is declared on. |
codeowners.unparsed | Every line that is neither a rule nor a section header, in file order. Comments and blank lines are not among them, because they are ignored rather than misread. |
codeowners.unparsed[].line | Line that could not be read. |
codeowners.unparsed[].reason | Why it could not be read. |
A collector that cannot finish prints what it had along with an incomplete key saying why, so the facts below the point it stopped are absent. A check that cares reads incomplete before the rest.
Example facts
The example facts collected by gitlab collector:
{
"source": "report",
"reports": [
{
"path": ".gitlab-ci.yml",
"modified": "2026-03-04T10:15:00Z",
"format": "gitlab-ci"
},
{
"path": "CODEOWNERS",
"modified": "2026-03-04T10:15:00Z",
"format": "codeowners"
}
],
"pipelines": [
{
"path": ".gitlab-ci.yml",
"stages": [
"build",
"deploy"
],
"image": null,
"default": {
"image": "gradle:8.10-jdk21"
},
"variables": [
"GRADLE_OPTS"
],
"includes": [],
"workflowRules": 0,
"jobs": [
{
"id": "build",
"stage": "build",
"image": null,
"tags": [
"docker"
],
"timeout": "15 minutes",
"timeoutMinutes": 15,
"environment": null,
"needs": [],
"rules": 0,
"when": null,
"allowFailure": false,
"interruptible": true,
"extends": [],
"trigger": null,
"variables": [],
"secretsUsed": [],
"scripts": [
{
"section": "script",
"run": "./gradlew check",
"variables": []
}
]
},
{
"id": "deploy",
"stage": "deploy",
"image": null,
"tags": [],
"timeout": "30 minutes",
"timeoutMinutes": 30,
"environment": {
"name": "production"
},
"needs": [
"build"
],
"rules": 1,
"when": null,
"allowFailure": false,
"interruptible": false,
"extends": [],
"trigger": null,
"variables": [],
"secretsUsed": [],
"scripts": [
{
"section": "script",
"run": "./deploy.sh",
"variables": []
}
]
}
]
}
],
"stages": [
"build",
"deploy"
],
"counts": {
"pipelines": 1,
"jobs": 2,
"scripts": 2
},
"codeowners": {
"path": "CODEOWNERS",
"sections": [
{
"name": "codeowners",
"optional": false,
"approvalsRequired": null,
"defaultOwners": [],
"line": null,
"implicit": true,
"rules": [
{
"pattern": "*",
"owners": [
"@acme/platform"
],
"inherited": false,
"line": 2
}
]
},
{
"name": "Backend",
"optional": false,
"approvalsRequired": 2,
"defaultOwners": [
"@acme/backend"
],
"line": 4,
"implicit": false,
"rules": [
{
"pattern": "/api/",
"owners": [
"@acme/backend"
],
"inherited": true,
"line": 5
},
{
"pattern": "/service/",
"owners": [
"@acme/backend"
],
"inherited": true,
"line": 6
}
]
},
{
"name": "Documentation",
"optional": false,
"approvalsRequired": null,
"defaultOwners": [
"@acme/docs",
"docs@acme.example"
],
"line": 8,
"implicit": false,
"rules": [
{
"pattern": "*.md",
"owners": [
"@acme/docs",
"docs@acme.example"
],
"inherited": true,
"line": 9
},
{
"pattern": "/docs/",
"owners": [
"@acme/docs",
"docs@acme.example"
],
"inherited": true,
"line": 10
}
]
},
{
"name": "Security review",
"optional": true,
"approvalsRequired": null,
"defaultOwners": [
"@acme/security/appsec"
],
"line": 12,
"implicit": false,
"rules": [
{
"pattern": "/terraform/",
"owners": [
"@acme/security/appsec"
],
"inherited": true,
"line": 13
},
{
"pattern": "/service/deploy/",
"owners": [
"@acme/security/appsec"
],
"inherited": true,
"line": 14
}
]
}
],
"owners": {
"@acme/platform": 1,
"@acme/backend": 2,
"@acme/docs": 2,
"docs@acme.example": 2,
"@acme/security/appsec": 2
},
"matched": {
"README.md": {
"owners": [
"@acme/platform",
"@acme/docs",
"docs@acme.example"
],
"sections": [
{
"section": "codeowners",
"optional": false,
"approvalsRequired": null,
"owners": [
"@acme/platform"
],
"rule": "*",
"line": 2
},
{
"section": "Documentation",
"optional": false,
"approvalsRequired": null,
"owners": [
"@acme/docs",
"docs@acme.example"
],
"rule": "*.md",
"line": 9
}
]
},
"LICENSE": {
"owners": [
"@acme/platform"
],
"sections": [
{
"section": "codeowners",
"optional": false,
"approvalsRequired": null,
"owners": [
"@acme/platform"
],
"rule": "*",
"line": 2
}
]
}
},
"unparsed": []
},
"unparsed": []
}Collected for
| Guardrail | Category | Inputs |
|---|---|---|
gitlab/codeowners-catch-all | gitlab | pipelines |
gitlab/codeowners-no-unowned-rules | gitlab | pipelines |
gitlab/codeowners-owners-per-rule | gitlab | pipelines |
gitlab/codeowners-parses | gitlab | pipelines |
gitlab/codeowners-present | gitlab | pipelines |
gitlab/codeowners-team-owned | gitlab | pipelines |
gitlab/job-timeout-set | gitlab | pipelines |