Guardrails
A guardrail is a reusable policy that runs in your pipeline and produces a verdict. You enable one by listing it in buildnote.json and running buildnote guardrails. The CLI fetches the guardrail, runs its check on the runner, records the verdict in Buildnote as a guardrail event, comments the result on the pull request, and optionally fails the build.
Guardrails cover the rules a team agrees on but has no good place to enforce: commit message format, no merge commits, a dependency policy, a coverage floor. You configure them rather than write them, and every run is recorded, so you can see how a rule trends across builds instead of only whether today's build passed.
Browse the library
Buildnote ships 110 guardrails. Filter them by name, by what they check, by category or by severity, and open one for its inputs, its configuration example and how to fix what it finds.
110 guardrails. Showing the first 40.
build4
- Dependency updates are automatedbuild/dependency-updates-configuredThe repository configures a tool that opens the dependency update, so upgrades arrive as reviewable pull requests rather than as a task nobody has time for.warning
- The Gradle wrapper is no older than the floorbuild/gradle-version-floorThe Gradle version the wrapper pins is at or above the floor the team sets, so the build runs on a toolchain that is still supported.warning
- The build runs on a managed CI runnerbuild/runs-on-hosted-ciThe artifact was built by a CI platform rather than on somebody's workstation, so the build is attributable and repeatable.error
- The build tool distribution is verifiedbuild/wrapper-distribution-verifiedThe Gradle wrapper pins the checksum of the distribution it downloads, so the toolchain cannot be swapped underneath the build.error
clojure4
- Clojure dependencies are pinnedclojure/dependencies-pinnedEvery dependency the manifests declare names one released version, or the exact commit a git dependency builds from.warning
- No dependencies on a local rootclojure/no-local-dependenciesNo dependency is taken from a :local/root, so the build resolves everything from this commit rather than from the machine it runs on.warning
- Repositories are ones the team allowsclojure/repositories-allowedEvery Maven repository the manifest adds beyond the defaults resolves from a host on the allowed list.error
- Clojure version is declaredclojure/version-declaredThe project depends on the Clojure release it is built against, and that release is no older than the configured floor.warning
cpp4
- CMake floor is recent enoughcpp/cmake-minimum-floorA CMake build names the CMake version whose policy defaults it configures under, and that version is no older than the configured floor.warning
- Dependencies come from a package managercpp/package-manager-declaredThe checkout declares its dependencies through vcpkg or Conan, so it names the versions it builds against rather than taking whatever the machine carries.warning
- C++ standard is declaredcpp/standard-declaredA CMake build that enables C++ declares the standard it compiles against, and that standard is no older than the configured floor.warning
- C++ standard is required, not preferredcpp/standard-requiredA CMake build that enables C++ requires the standard it declares, so a compiler that does not offer it fails rather than falling back to an older one.warning
docs7
- Repository instructs the coding agents working in itdocs/agent-instructionsThe repository carries an agent instruction file, and it is long enough to say something and short enough to be read.info
- Repository keeps a changelogdocs/changelogThe repository carries a changelog, so what changed between two releases is written down rather than reconstructed from commits.info
- Repository publishes a code of conductdocs/code-of-conductThe repository carries a CODE_OF_CONDUCT.md, so the standard contributors are held to is written down and so is who enforces it.info
- Repository says how to contribute to itdocs/contributingThe repository carries a CONTRIBUTING.md saying how a change is proposed, built and reviewed.info
- Repository states its licencedocs/license-presentThe repository carries a licence file, so what may be done with the code is written down rather than assumed.warning
- Repository has a READMEdocs/readmeThe repository has a README, and it says more than its title.warning
- Repository documents how to report a vulnerabilitydocs/security-policyThe repository carries a SECURITY.md naming where a security problem should be reported.warning
dotnet4
- Package versions are managed centrallydotnet/central-package-managementOne Directory.Packages.props decides the version of every package, rather than each project file deciding its own.warning
- Package references carry a versiondotnet/dependencies-versionedEvery PackageReference names a version itself or takes one from a central PackageVersion.error
- .NET SDK is pinneddotnet/sdk-pinnedglobal.json names the SDK the build uses and holds it there, rather than letting a newer one on the runner take over.warning
- Target framework is no older than the floordotnet/target-framework-floorEvery net<major>.<minor> framework a project targets is no older than the configured floor.warning
elixir3
- Elixir dependencies are lockedelixir/lockfile-committedThe project commits mix.lock, so a fetch of the same commit resolves the same packages and verifies them.warning
- No dependency is fetched from gitelixir/no-git-dependenciesNo dependency the project declares is fetched from a git repository.warning
- Elixir version is declaredelixir/version-declaredThe project declares the Elixir version it is compiled against, and that version is no older than the configured floor.warning
git6
- Commits come from a known identitygit/author-identity-domainEvery commit in the build range was authored and committed by an address on a domain the organisation controls.warning
- The change is small enough to reviewgit/changed-files-budgetThe build range changes few enough files that a reviewer can hold the whole change in their head.info
- Commits follow Conventional Commitsgit/conventional-commitsEvery commit message in the build range follows the Conventional Commits specification: the subject line, the blank line before the body, and the BREAKING CHANGE footer.error
- No merge commitsgit/no-merge-commitsThe build range contains no merge commits.warning
- No work in progress commitsgit/no-wip-commitsThe build range carries no commit that says it was never meant to be merged.warning
- Commits reference a work itemgit/work-item-referenceEvery commit in the build range, or the branch it is on, names the change record it belongs to.warning
golang4
- Go module checksums are committedgolang/checksums-committedThe module commits go.sum, so every module a build downloads is checked against the checksum the commit recorded.warning
- No module is replaced by a directorygolang/no-local-replacementsNo replace directive redirects a module to a filesystem path, so the build reads only source the checkout carries.error
- Go toolchain is pinnedgolang/toolchain-pinnedThe module names the toolchain that compiles it, rather than leaving the go directive as a floor.warning
- Go version is declaredgolang/version-declaredThe module declares the Go version it is built against, and that version is no older than the configured floor.warning
iac4
- Dockerfile base images are pinnediac/dockerfile-base-pinnedEvery image a Dockerfile builds from is pinned to a digest, so a rebuild of the same commit starts from the same bytes.error
- Dockerfiles declare a health checkiac/dockerfile-healthcheckEvery Dockerfile declares a HEALTHCHECK, so the orchestrator can tell a container that is running from one that is working.warning
- Dockerfile lint findings are cleaniac/dockerfile-lint-cleanEvery finding the Dockerfile linter reported sits below the severity the team gates on.warning
- Dockerfiles declare no credential argumentsiac/dockerfile-no-build-secretsNo ARG or ENV a Dockerfile declares is named like a credential.error
Categories
Each category is one directory of the library, sharing a version and the inputs its guardrails have in common. Its page lists every guardrail in it and the inputs they share.
| Category | Guardrails |
|---|---|
build | 4 |
clojure | 4 |
cpp | 4 |
docs | 7 |
dotnet | 4 |
elixir | 3 |
git | 6 |
golang | 4 |
iac | 11 |
java | 3 |
kotlin | 4 |
maven | 4 |
nodejs | 4 |
ownership | 6 |
php | 3 |
pipeline | 5 |
python | 4 |
ruby | 4 |
rust | 4 |
scala | 4 |
secrets | 4 |
security | 4 |
supply-chain | 6 |
tests | 4 |
To write your own, read Writing a guardrail. A guardrail rarely gathers its own evidence: it declares a collector, which gathers the repository, the build and the files once per run and hands the same document to every guardrail that asked.
Configuration
Every option is described in Configuring guardrails.
Running
buildnote guardrails --org acme --project web --module ci --build 1234In a CI environment that Buildnote recognises, the organisation, project, module, build, commit and ref are all detected for you, so the command is usually just:
buildnote guardrailsUse --only <id> to run a single guardrail, --dry-run to print the verdicts without recording or reporting anything, and --fail-on never to report without gating. See the command reference for every option.
Verdicts
A guardrail run ends in one of three verdicts.
| Verdict | Meaning | Event status |
|---|---|---|
passed | The check ran and found nothing over the guardrail's threshold. | successful |
failed | The check ran and found more violations than the guardrail tolerates. | failed |
skipped | The check could not gather its evidence, so it has no opinion. | skipped |
Skipping is a first class outcome. A guardrail that cannot see what it needs never fails your build. The CLI decides skipped when:
- the check reports it cannot run, such as a base ref that does not resolve on a shallow clone
- the check produces no readable result
- the interpreter is missing on the runner, or the check runs past its timeout
- the guardrail's source cannot be reached, so its definition or script could not be read
- the pinned
sha256does not match the script that was fetched, which prints a warning rather than passing quietly
After a check runs, the exemptions of your check entry are applied to the violations it reported, and what remains is compared against the guardrail's maxViolations. That comparison decides failed or passed.
Failing the build
failOn decides which verdicts gate the build.
| Value | The build fails when |
|---|---|
error (default) | a guardrail with severity error fails |
warning | a guardrail with severity error or warning fails |
never | never |
The command always records the verdicts and posts the pull request comment before it exits, so a gated build still reports everything it found. A guardrail whose events cannot be submitted still gates.
A guardrail that cannot be loaded at all is a different thing from a verdict, and failOn does not cover it. A definition that is malformed, declares a version your check entry does not expect, or names a script whose extension selects no interpreter is a configuration error: the command reports it, runs every other guardrail, and then exits non zero whatever failOn says. A source that simply cannot be reached is treated as missing evidence instead, so it is recorded as skipped.
Reporting
Guardrails report through the same reporters as buildnote report, configured in the same report section of buildnote.json. Every one of them receives the same verdict table, most severe first, with the evidence and remediation for each failed guardrail.
| Reporter | What it does |
|---|---|
github | Writes the markdown to the job summary, and posts it on the pull request as a comment titled Guardrails. A re-run updates that comment rather than adding another, and it never collides with the test summary comment. |
html | Writes a standalone Guardrails document. With no outputFile it is buildnote-guardrails.html; with one it is written beside it as a -guardrails sibling, so build/report.html gives build/report-guardrails.html and your test report is not overwritten. |
slack | Posts a notification with a line and a status icon per guardrail. |
discord | The same notification, as a Discord message. |
A reporter runs when it is enabled and its condition passes, exactly as for buildnote report. A condition is evaluated against the guardrail run rather than the test run, so the same expressions mean the guardrail equivalent:
| Variable | Meaning |
|---|---|
successful failed skipped | Booleans for the run's overall verdict. |
successful.count failed.count skipped.count | How many guardrails reached each verdict. |
total | How many guardrails ran. |
violations | Violations across the whole run. |
status | successful, failed, skipped or unknown. |
always never | true and false. |
org project module build sha ref | The build coordinates. |
An expression that cannot be resolved degrades to empty rather than aborting the run, and a reporter that cannot be resolved at all is reported and skipped. Neither changes the exit code.
The pull request comment needs a GitHub token, which comes from report.github.token or the --github-token option. Without a token, without a pull request, or with "comment": false, the command skips the comment and carries on. No reporter failure changes the exit code: the verdict decides that, not whether somebody could be told about it.
Querying verdicts
Every run is recorded as two kinds of guardrail event, queryable through the guardrails table. One run event summarises the whole invocation, carrying the total violation count and, as attachments, the facts every collector gathered. One check event per guardrail carries its own verdict and relates back to the run:
SELECT guardrail, status, violations FROM guardrails WHERE build = '1234' AND category = 'check'SELECT guardrail, COUNT(*) AS failures FROM guardrails WHERE status = 'failed' AND category = 'check' GROUP BY guardrail ORDER BY failures DESCRe-running a guardrail in the same build replaces its earlier verdict, so a build holds one row per guardrail. The entity_id is stable across builds, so you can trend one guardrail over time.