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 key | workflow |
| Version | v1 |
| Script | workflow.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 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,Jenkinsfile | GUARDRAIL_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:
{
"collect": ["workflow"]
}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.
| Fact | Meaning |
|---|---|
source | report when a pipeline definition was read, none when there was none. |
reason | Why nothing was read, present only when source is none. |
reports | Every pipeline definition that was read, in path order. |
reports[].path | Path of the definition, relative to the directory the CLI runs in. |
reports[].modified | When it was last written, ISO 8601. |
reports[].format | Platform it was read as: github, gitlab, azure or jenkins. |
workflows | The definitions themselves, one entry per file that was read. |
workflows[].path | Path of the file, relative to the directory the CLI runs in. |
workflows[].platform | github for a GitHub Actions workflow, gitlab for a GitLab CI file, azure for an Azure Pipelines file, jenkins for a Jenkinsfile. |
workflows[].parsed | Whether 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[].name | Name the file gives itself, or null when it names none. |
workflows[].triggers | What 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[].permissions | The 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[].defaults | The workflow level defaults value for GitHub Actions, or default for GitLab CI, exactly as written. null when there is none. |
workflows[].concurrency | Whether the file declares a workflow level concurrency group, so an in flight run is cancelled or queued rather than racing. |
workflows[].jobs | The jobs the file declares. |
workflows[].jobs[].id | Key the job is declared under, which is what needs names. |
workflows[].jobs[].name | Display name the job gives itself, or null. |
workflows[].jobs[].runsOn | The 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[].selfHosted | false 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[].permissions | The 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[].timeout | The declared timeout in minutes, exactly as written, or null when the job declares none and takes the platform's default. |
workflows[].jobs[].environment | The 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[].needs | Jobs this one waits for. |
workflows[].jobs[].if | The raw condition guarding the job, or null. condition for Azure Pipelines. |
workflows[].jobs[].uses | The workflow or template this job calls instead of declaring steps, exactly as written, or null. |
workflows[].jobs[].secretsUsed | Names of the secrets referenced anywhere in the job. Names only, never values: the facts are attached to the run event. |
workflows[].jobs[].steps | The steps the job declares, in order. |
workflows[].jobs[].steps[].name | Display name of the step, or null. |
workflows[].jobs[].steps[].uses | The 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[].action | The 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[].ref | What 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[].pinned | Whether ref is a 40 character hex commit sha, which is the only ref a tag cannot be moved out from under. |
workflows[].jobs[].steps[].local | Whether the action is a ./ path in this repository rather than a published one. |
workflows[].jobs[].steps[].docker | Whether the action is a docker:// image rather than a published one. |
workflows[].jobs[].steps[].run | The shell body the step runs, truncated to 2000 characters, or null. |
workflows[].jobs[].steps[].shell | The shell the step declares for itself, or null when it takes the default the workflow or the platform sets. |
workflows[].jobs[].steps[].withKeys | Names of the inputs the step passes to the action. Names only, never values. |
workflows[].jobs[].steps[].interpolations | Every ${{ }} 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. |
triggers | Every trigger declared across the files that were read. |
untrustedTriggers | One 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[].path | Path of the file declaring it. |
untrustedTriggers[].trigger | The trigger itself. |
counts.workflows | Files that were read, a Jenkinsfile recorded as present included. |
counts.jobs | Jobs across every file that was parsed. |
counts.steps | Steps 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. |
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:
{
"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
| Guardrail | Category | Inputs |
|---|---|---|
pipeline/actions-pinned-by-digest | pipeline | pipelines |
pipeline/job-timeout-set | pipeline | pipelines |
pipeline/least-privilege-token | pipeline | pipelines |
pipeline/no-script-injection | pipeline | pipelines |
pipeline/no-untrusted-checkout | pipeline | pipelines |