Skip to content
BETAGuardrails are in beta. The library, the configuration format and the CLI command can still change.

Guardrails / Collectors

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 keygitlab
Versionv1
Scriptgitlab.py
Timeout60 seconds

Inputs

InputDescriptionDefaultEnvironment
pipelinesComma 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.yamlGUARDRAIL_INPUT_PIPELINES
pathsComma 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,LICENSEGUARDRAIL_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:

json
{
  "collect": ["gitlab"]
}
python
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.

FactMeaning
sourcereport when a GitLab CI file or a CODEOWNERS file was read, none when neither was.
reasonWhy nothing was read, present only when source is none.
reportsEvery file that was read, the GitLab CI files in path order and the CODEOWNERS file after them.
reports[].pathPath of the file, relative to the directory the CLI runs in.
reports[].modifiedWhen it was last written, ISO 8601.
reports[].formatgitlab-ci for a pipeline definition, codeowners for the CODEOWNERS file.
pipelinesThe 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[].pathPath of the file, relative to the directory the CLI runs in.
pipelines[].stagesThe stages the file declares, in the order it declares them. Empty when it declares none and takes GitLab's default stages.
pipelines[].imageThe top level image, exactly as written, or null. Every job that declares none runs in this one.
pipelines[].defaultThe default block, exactly as written, or null. It carries what every job inherits when it declares none of its own.
pipelines[].variablesNames of the top level variables. Names only, never values.
pipelines[].includesWhat 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[].workflowRulesHow 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[].jobsThe jobs the file declares. A key GitLab reserves, and a hidden .template key, is not a job and is left out.
pipelines[].jobs[].idKey the job is declared under, which is what needs names.
pipelines[].jobs[].stageThe stage the job runs in, exactly as written, or null when it declares none and takes test.
pipelines[].jobs[].imageThe image the job runs in, exactly as written, or null when it declares none and takes the pipeline's.
pipelines[].jobs[].tagsThe runner tags the job asks for, which is what picks the runner it lands on.
pipelines[].jobs[].timeoutThe declared timeout, exactly as GitLab writes it, or null when the job declares none and takes the project's own limit.
pipelines[].jobs[].timeoutMinutesThe same timeout in whole minutes, or null when the job declares none or writes a duration that could not be read.
pipelines[].jobs[].environmentThe environment the job deploys to, exactly as written, or null.
pipelines[].jobs[].needsJobs this one waits for, by name.
pipelines[].jobs[].rulesHow many rules entries decide whether the job runs. 0 when it declares none.
pipelines[].jobs[].whenThe when the job declares, exactly as written, or null. manual is a job somebody has to start.
pipelines[].jobs[].allowFailureWhether the job is allowed to fail without failing the pipeline.
pipelines[].jobs[].interruptibleWhether a newer pipeline may cancel this job.
pipelines[].jobs[].extendsThe hidden keys this job extends, by name.
pipelines[].jobs[].triggerThe downstream pipeline this job triggers instead of running a script, exactly as written, or null.
pipelines[].jobs[].variablesNames of the variables the job declares. Names only, never values.
pipelines[].jobs[].secretsUsedNames of the secrets the job's secrets block declares. Names only, never values: the facts are attached to the run event.
pipelines[].jobs[].scriptsThe script sections the job declares, in the order GitLab runs them.
pipelines[].jobs[].scripts[].sectionWhich section it is: before_script, script or after_script.
pipelines[].jobs[].scripts[].runThe shell body of the section, its lines joined, truncated to 2000 characters.
pipelines[].jobs[].scripts[].variablesEvery $VAR and ${VAR} the section interpolates, read from the whole body before it was truncated. Names only, never values.
stagesEvery stage declared across the files that were read.
counts.pipelinesFiles that were read.
counts.jobsJobs across every file.
counts.scriptsScript sections across every job.
unparsedFiles 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[].pathPath of the file that was not read.
unparsed[].reasonThe construct that could not be represented, and the line it is on.
codeownersThe 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.pathPath 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.sectionsEvery 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[].nameName 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[].optionalWhether 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[].approvalsRequiredApprovals 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[].defaultOwnersOwners written on the header itself, which own every rule beneath it that names none of its own.
codeowners.sections[].lineLine the header is declared on, or null for the implicit default section, which has no header.
codeowners.sections[].implicitWhether this is the default section holding the rules written above the first header, rather than a section the file declares.
codeowners.sections[].rulesThe rules written under this header, in file order, which is the order they resolve in within the section.
codeowners.sections[].rules[].patternPattern exactly as it is written, so an anchored /docs/ reads differently from a docs/ matched at any depth.
codeowners.sections[].rules[].ownersOwners 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[].inheritedWhether the owners came from the section header rather than from the rule's own line.
codeowners.sections[].rules[].lineLine the rule is declared on, so a verdict points at the file.
codeowners.ownersEvery 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.matchedOwners 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].ownersEvery 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].sectionsOne 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[].sectionName of the section that matched.
codeowners.matched[path].sections[].optionalWhether that section's approval is optional, so its owners are informed rather than required.
codeowners.matched[path].sections[].approvalsRequiredApprovals that section asks for, or null when it asks for no particular number.
codeowners.matched[path].sections[].ownersOwners 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[].rulePattern of the last rule in that section to match the path, which is the one that decides its owners there.
codeowners.matched[path].sections[].lineLine that rule is declared on.
codeowners.unparsedEvery 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[].lineLine that could not be read.
codeowners.unparsed[].reasonWhy 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:

json
{
  "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

GuardrailCategoryInputs
gitlab/codeowners-catch-allgitlabpipelines
gitlab/codeowners-no-unowned-rulesgitlabpipelines
gitlab/codeowners-owners-per-rulegitlabpipelines
gitlab/codeowners-parsesgitlabpipelines
gitlab/codeowners-presentgitlabpipelines
gitlab/codeowners-team-ownedgitlabpipelines
gitlab/job-timeout-setgitlabpipelines

All collectors

Buildnote Limited
Registered in England and Wales, Reg: 16140412