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

Guardrails / Collectors

CI pipeline definitions

The pipeline definitions in the checkout, read as they are written: which triggers start them, what permissions they hand a job, which runner each job asks for, and every action a step reaches for and how tightly it is pinned. Names and shapes only, never a secret, an input value or an environment value.

Facts keyworkflow
Versionv1
Scriptworkflow.py
Timeout60 seconds

Inputs

InputDescriptionDefaultEnvironment
pipelinesComma separated globs matched against every file in the working directory and against bare file names, naming the pipeline definitions to read. Narrows what discovery finds; a file it matches that is none of the known layouts is still left out..github/workflows/*.yml,.github/workflows/*.yaml,.gitlab-ci.yml,.gitlab-ci.yaml,azure-pipelines*.yml,azure-pipelines*.yaml,JenkinsfileGUARDRAIL_INPUT_PIPELINES

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": ["workflow"]
}
python
workflow = guardrail.facts("workflow")

Facts

The document workflow 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 pipeline definition was read, none when there was none.
reasonWhy nothing was read, present only when source is none.
reportsEvery pipeline definition that was read, in path order.
reports[].pathPath of the definition, relative to the directory the CLI runs in.
reports[].modifiedWhen it was last written, ISO 8601.
reports[].formatPlatform it was read as: github, gitlab, azure or jenkins.
workflowsThe definitions themselves, one entry per file that was read.
workflows[].pathPath of the file, relative to the directory the CLI runs in.
workflows[].platformgithub for a GitHub Actions workflow, gitlab for a GitLab CI file, azure for an Azure Pipelines file, jenkins for a Jenkinsfile.
workflows[].parsedWhether the file was read as a definition. false for a Jenkinsfile, which is Groovy and is recorded as present rather than parsed, so its jobs and steps are empty and count for nothing.
workflows[].nameName the file gives itself, or null when it names none.
workflows[].triggersWhat starts it: the on keys for GitHub Actions, the trigger, pr and schedules sections for Azure Pipelines, and empty for GitLab CI, which decides per job.
workflows[].permissionsThe workflow level permissions value, exactly as written. null when the file declares none, which is the token being whatever the repository defaults to rather than an empty set.
workflows[].defaultsThe workflow level defaults value for GitHub Actions, or default for GitLab CI, exactly as written. null when there is none.
workflows[].concurrencyWhether the file declares a workflow level concurrency group, so an in flight run is cancelled or queued rather than racing.
workflows[].jobsThe jobs the file declares.
workflows[].jobs[].idKey the job is declared under, which is what needs names.
workflows[].jobs[].nameDisplay name the job gives itself, or null.
workflows[].jobs[].runsOnThe runner the job asks for, exactly as written: a runs-on label, list or group for GitHub Actions, tags for GitLab CI, a pool for Azure Pipelines.
workflows[].jobs[].selfHostedfalse when the runner is one of the provider's hosted images, true when it is explicitly self-hosted or names no hosted image, and null when the label is an expression or the provider does not say, as GitLab tags and a pool without a vmImage do not.
workflows[].jobs[].permissionsThe job level permissions value, exactly as written. null when the job declares none and inherits whatever the workflow or the repository gives it.
workflows[].jobs[].timeoutThe declared timeout in minutes, exactly as written, or null when the job declares none and takes the platform's default.
workflows[].jobs[].environmentThe deployment environment the job runs in, exactly as written, or null. An environment is what carries a protection rule and its own secrets.
workflows[].jobs[].needsJobs this one waits for.
workflows[].jobs[].ifThe raw condition guarding the job, or null. condition for Azure Pipelines.
workflows[].jobs[].usesThe workflow or template this job calls instead of declaring steps, exactly as written, or null.
workflows[].jobs[].secretsUsedNames of the secrets referenced anywhere in the job. Names only, never values: the facts are attached to the run event.
workflows[].jobs[].stepsThe steps the job declares, in order.
workflows[].jobs[].steps[].nameDisplay name of the step, or null.
workflows[].jobs[].steps[].usesThe action the step runs, exactly as written, or null for a step that only runs a script. The task or template for Azure Pipelines.
workflows[].jobs[].steps[].actionThe part of uses before the @: owner/repo with any subdirectory for a published action, the path for a ./ local action, the image for a docker:// action.
workflows[].jobs[].steps[].refWhat follows the @ in uses, whatever it names, or null when it names nothing. A tag, a branch and a sha are indistinguishable here beyond pinned.
workflows[].jobs[].steps[].pinnedWhether ref is a 40 character hex commit sha, which is the only ref a tag cannot be moved out from under.
workflows[].jobs[].steps[].localWhether the action is a ./ path in this repository rather than a published one.
workflows[].jobs[].steps[].dockerWhether the action is a docker:// image rather than a published one.
workflows[].jobs[].steps[].runThe shell body the step runs, truncated to 2000 characters, or null.
workflows[].jobs[].steps[].shellThe shell the step declares for itself, or null when it takes the default the workflow or the platform sets.
workflows[].jobs[].steps[].withKeysNames of the inputs the step passes to the action. Names only, never values.
workflows[].jobs[].steps[].interpolationsEvery ${{ }} expression appearing in the run body, read from the whole body before it was truncated. An expression interpolated into a shell body is the script injection surface.
triggersEvery trigger declared across the files that were read.
untrustedTriggersOne entry per file and trigger that runs with the repository's own token on a change somebody outside it wrote: pull_request_target, issue_comment and workflow_run.
untrustedTriggers[].pathPath of the file declaring it.
untrustedTriggers[].triggerThe trigger itself.
counts.workflowsFiles that were read, a Jenkinsfile recorded as present included.
counts.jobsJobs across every file that was parsed.
counts.stepsSteps 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.

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 workflow collector:

json
{
  "source": "report",
  "reports": [
    {
      "path": ".github/workflows/build.yml",
      "modified": "2026-03-04T10:15:00Z",
      "format": "github"
    }
  ],
  "workflows": [
    {
      "path": ".github/workflows/build.yml",
      "platform": "github",
      "parsed": true,
      "name": "Build",
      "triggers": [
        "push",
        "pull_request"
      ],
      "permissions": {
        "contents": "read"
      },
      "defaults": null,
      "concurrency": false,
      "jobs": [
        {
          "id": "build",
          "name": "Build and test",
          "runsOn": "ubuntu-latest",
          "selfHosted": false,
          "permissions": {
            "contents": "read"
          },
          "timeout": 15,
          "environment": null,
          "needs": [],
          "if": null,
          "uses": null,
          "secretsUsed": [],
          "steps": [
            {
              "name": "Check out the repository",
              "uses": "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8",
              "action": "actions/checkout",
              "ref": "08c6903cd8c0fde910a37f88322edcfb5dd907a8",
              "pinned": true,
              "local": false,
              "docker": false,
              "run": null,
              "shell": null,
              "withKeys": [],
              "interpolations": []
            },
            {
              "name": "Set up the JDK",
              "uses": "actions/setup-java@v4",
              "action": "actions/setup-java",
              "ref": "v4",
              "pinned": false,
              "local": false,
              "docker": false,
              "run": null,
              "shell": null,
              "withKeys": [
                "distribution",
                "java-version"
              ],
              "interpolations": []
            },
            {
              "name": "Build",
              "uses": null,
              "action": null,
              "ref": null,
              "pinned": false,
              "local": false,
              "docker": false,
              "run": "./gradlew check",
              "shell": null,
              "withKeys": [],
              "interpolations": []
            }
          ]
        }
      ]
    }
  ],
  "triggers": [
    "push",
    "pull_request"
  ],
  "untrustedTriggers": [],
  "counts": {
    "workflows": 1,
    "jobs": 1,
    "steps": 3
  },
  "unparsed": []
}

Collected for

GuardrailCategoryInputs
pipeline/actions-pinned-by-digestpipelinepipelines
pipeline/job-timeout-setpipelinepipelines
pipeline/least-privilege-tokenpipelinepipelines
pipeline/no-script-injectionpipelinepipelines
pipeline/no-untrusted-checkoutpipelinepipelines

All collectors

Buildnote Limited
Registered in England and Wales, Reg: 16140412