VCS Guardrails
Enforce version control security standards including branch protection rules, required approvals, status checks, and merge strategies. Prevent unauthorized changes to critical branches.
vcs to your lunar-config.yml:uses: github://earthly/lunar-lib/policies/vcs@v1.0.5
Included Guardrails
This policy includes 17 guardrails that enforce standards for your repository and ownership.
branch-protection-enabled
Requires branch protection rules to be enabled on the default branch. Branch protection is the foundation for all other VCS security controls.
require-pull-request
Requires all changes to go through pull requests before merging. Prevents direct pushes to protected branches without review.
minimum-approvals
Enforces a minimum number of approving reviews before merge (default 1). Configurable via the min_approvals input for stricter requirements.
require-codeowner-review
Requires approval from designated code owners defined in CODEOWNERS file. Ensures domain experts review changes to critical paths.
dismiss-stale-reviews
Automatically dismisses approvals when new commits are pushed to a PR. Prevents merging outdated approvals after significant changes.
require-status-checks
Requires CI status checks to pass before merging pull requests. Prevents merging code that fails tests or linting.
require-branches-up-to-date
Requires PR branches to be up-to-date with the base branch before merging. Prevents merging stale branches that may have integration issues.
disallow-force-push
Prohibits force pushes to protected branches to preserve commit history. Force pushes can destroy audit trails and cause data loss.
disallow-branch-deletion
Prevents deletion of protected branches (typically main/master). Protects against accidental or malicious branch removal.
require-linear-history
Enforces linear commit history by requiring squash or rebase merges. Makes git history cleaner and easier to bisect for debugging.
require-signed-commits
Requires all commits to be GPG or SSH signed for authenticity verification. Proves commits were made by the claimed author.
disallow-bypass-actors
Fails when anyone can bypass protection on the default branch: a ruleset bypass actor, or (classic protection) admins not enforced or a bypass allowance. Accepted actors go in allowed_bypass_actors.
pr-commits-signed
Requires every commit in a pull request to carry a signature GitHub verified. Skips when commit data isn't collected.
release-commits-merged-via-pr
Every commit since the previous release tag must have reached the default
branch through merged pull requests. Gate releases with
enforcement: block-release.
require-private
Ensures repository visibility is set to private, not public. Prevents accidental exposure of proprietary code.
require-default-branch
Validates the default branch name matches the required name (default "main"). Helps standardize branch naming across repositories.
allowed-merge-strategies
Restricts merge strategies to an allowed list (merge, squash, rebase). Enforces consistent merge practices across the organization.
How Guardrails Fit into Lunar
Lunar guardrails define your engineering standards as code. They evaluate data collected by integrations and produce pass/fail checks with actionable feedback.
Policies support gradual enforcement—from silent scoring to blocking PRs or deployments—letting you roll out standards at your own pace without disrupting existing workflows.
Learn How Lunar Works →Required Integrations
This policy evaluates data gathered by one or more of the following integration(s).
Make sure to enable them in your lunar-config.yml.
Configuration
Configure this policy in your lunar-config.yml.
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
min_approvals
|
Optional |
1
|
Minimum number of required approvals (integer, or omit to skip check) |
required_default_branch
|
Optional |
main
|
Required default branch name for the require-default-branch policy. Defaults to 'main' |
allowed_merge_strategies
|
Required | — | Comma-separated list of allowed merge strategies for the allowed-merge-strategies policy (merge, squash, rebase). Only listed strategies will be allowed |
allowed_bypass_actors
|
Required | — | Bypass actors the disallow-bypass-actors check accepts, one per line or comma-separated. An entry is an actor type (`OrganizationAdmin`, `DeployKey`) or `<type>:<id>`: a ruleset bypass actor's `actor_id` (`Integration:98765`, `RepositoryRole:5`), or a classic bypass allowance's login or slug (`User:jdoe`, `Team:release-managers`, `Integration:release-bot`). `#` comments out the rest of a line: ```yaml with: allowed_bypass_actors: | Integration:98765 # release app, tags after CI passes ``` |
Documentation
View on GitHubVCS Guardrails
Version control system (VCS) best practices and security policies for repositories
Overview
This policy plugin enforces version control system security standards, focusing on branch protection rules that prevent unauthorized or risky changes to critical branches. Branch protection is a fundamental security control that ensures code review, testing, and approval processes are followed before changes reach production. Development teams using any supported version control system (such as GitHub or GitLab) should use this policy to enforce consistent protection rules across repositories.
Policies
This plugin provides the following policies (use include to select a subset):
Branch Protection Policies
These policies enforce specific branch protection requirements. Use include to select which policies to enforce.
| Policy | Description |
|---|---|
branch-protection-enabled |
Branch protection must be enabled |
require-pull-request |
Pull requests must be required before merging |
minimum-approvals |
Pull requests must have minimum number of approvals (configurable via min_approvals input) |
require-codeowner-review |
Code owner review must be required |
dismiss-stale-reviews |
Stale reviews must be dismissed on new commits |
require-status-checks |
Status checks must be required to pass |
require-branches-up-to-date |
Branches must be up to date before merging |
disallow-force-push |
Force pushes must be disallowed |
disallow-branch-deletion |
Branch deletions must be disallowed |
require-linear-history |
Linear history must be required |
require-signed-commits |
Signed commits must be required |
disallow-bypass-actors |
Nobody can bypass protection: no ruleset bypass actors, admins enforced and no bypass allowances under classic protection (configurable via allowed_bypass_actors) |
Change Evidence Policies
These read what happened on each change rather than the settings: the commits of a pull request, and the commits a release ships.
| Policy | Description |
|---|---|
pr-commits-signed |
Every commit in a pull request must carry a signature GitHub verified (PRs only) |
release-commits-merged-via-pr |
Every commit since the previous release tag must have reached the default branch through merged pull requests (default branch only; needs the github collector's opt-in release-range) |
A commit reached the default branch through merged pull requests when its own pull request merged into it, or merged into a branch that a later pull request carried on into it. Gitflow needs the second route, because GitHub links a feature commit to its pull request into develop and not to the release pull request from develop into the default branch. A branch that was merged by pull request and then pushed straight to the default branch fails. In a range GitHub couldn't list in full, a commit that can't be traced is unknown, so the check skips rather than fails.
Repository Settings Policies
| Policy | Description |
|---|---|
require-private |
Repository visibility must be private |
require-default-branch |
Default branch must match required name (configurable via required_default_branch input, defaults to "main") |
allowed-merge-strategies |
Merge strategies must match allowed list (configurable via allowed_merge_strategies input) |
Required Data
This policy reads from the following Component JSON paths:
| Path | Type | Description |
|---|---|---|
.vcs.branch_protection.enabled |
boolean | Whether branch protection is enabled |
.vcs.branch_protection.require_pr |
boolean | Whether pull requests are required |
.vcs.branch_protection.required_approvals |
integer | Number of required approvals |
.vcs.branch_protection.require_codeowner_review |
boolean | Whether code owner review is required |
.vcs.branch_protection.dismiss_stale_reviews |
boolean | Whether stale reviews are dismissed |
.vcs.branch_protection.require_status_checks |
boolean | Whether status checks are required |
.vcs.branch_protection.require_branches_up_to_date |
boolean | Whether branches must be up to date |
.vcs.branch_protection.allow_force_push |
boolean | Whether force pushes are allowed |
.vcs.branch_protection.allow_deletions |
boolean | Whether branch deletions are allowed |
.vcs.branch_protection.require_linear_history |
boolean | Whether linear history is required |
.vcs.branch_protection.require_signed_commits |
boolean | Whether signed commits are required |
.vcs.visibility |
string | Repository visibility |
.vcs.default_branch |
string | Default branch name |
.vcs.merge_strategies.allow_merge_commit |
boolean | Whether merge commits are allowed |
.vcs.merge_strategies.allow_squash_merge |
boolean | Whether squash merges are allowed |
.vcs.merge_strategies.allow_rebase_merge |
boolean | Whether rebase merges are allowed |
.vcs.branch_protection.rulesets[] |
array | Rulesets on the default branch; bypass_actors[] when the token can see them |
.vcs.branch_protection.enforce_admins |
boolean | Classic protection applies to administrators |
.vcs.branch_protection.bypass_pull_request_allowances |
object | Classic users, teams, apps allowed to skip the pull-request requirement |
.vcs.pr.commits[] |
array | The PR's commits with signature.verified and signature.reason |
.vcs.release_range |
object | Commits since the previous release tag, each with its merged pull_request when there is one |
Note: This policy requires a VCS collector (such as github or gitlab) that populates the .vcs data.
Installation
Add to your lunar-config.yml:
policies:
# Every check: branch protection and repository settings
- uses: github://earthly/lunar-lib/policies/vcs@v1.0.0
on: "domain:your-domain" # Or use tags like production, critical
enforcement: report-pr # Options: draft, score, report-pr, block-pr, block-release, block-pr-and-release
include:
- branch-protection-enabled
- require-pull-request
- minimum-approvals
- require-codeowner-review
- dismiss-stale-reviews
- require-status-checks
- require-branches-up-to-date
- disallow-force-push
- disallow-branch-deletion
- require-linear-history
- require-signed-commits
- disallow-bypass-actors
- pr-commits-signed
- release-commits-merged-via-pr
- require-private
- require-default-branch
- allowed-merge-strategies
with:
min_approvals: 2
allowed_merge_strategies: "squash"
# required_default_branch: "main"
# Use include to run only specific policies
- uses: github://earthly/lunar-lib/policies/vcs@v1.0.0
include:
- branch-protection-enabled
- require-pull-request
- minimum-approvals
- disallow-force-push
- require-signed-commits
- require-private
on: "domain:your-domain"
enforcement: block-pr
with:
min_approvals: 2
# Gate releases (`lunar policy ok-release`) on every commit arriving by merged PR.
# Needs release_tag_pattern set on the github collector.
- uses: github://earthly/lunar-lib/policies/vcs@v1.0.0
include:
- release-commits-merged-via-pr
on: "domain:your-domain"
enforcement: block-release
# Run only repository settings policies
- uses: github://earthly/lunar-lib/policies/vcs@v1.0.0
include:
- require-private
- require-default-branch
- allowed-merge-strategies
on: "domain:your-domain"
enforcement: report-pr
with:
allowed_merge_strategies: "squash,rebase"
Examples
Passing Example - Production Repository
A production repository with strict security controls:
{
"vcs": {
"provider": "github",
"visibility": "private",
"default_branch": "main",
"branch_protection": {
"enabled": true,
"branch": "main",
"require_pr": true,
"required_approvals": 2,
"require_codeowner_review": true,
"dismiss_stale_reviews": true,
"require_status_checks": true,
"require_branches_up_to_date": true,
"allow_force_push": false,
"allow_deletions": false,
"require_linear_history": false,
"require_signed_commits": true
},
"merge_strategies": {
"allow_merge_commit": false,
"allow_squash_merge": true,
"allow_rebase_merge": false
}
}
}
With policy configuration using include to select desired checks:
include:
- branch-protection-enabled
- require-pull-request
- minimum-approvals
- require-codeowner-review
- dismiss-stale-reviews
- require-status-checks
- require-branches-up-to-date
- disallow-force-push
- disallow-branch-deletion
- require-signed-commits
- require-private
- require-default-branch
- allowed-merge-strategies
with:
min_approvals: 2
allowed_merge_strategies: "squash"
This passes because all security controls match policy requirements.
Failing Example - Branch Protection Not Enabled
{
"vcs": {
"branch_protection": {
"enabled": false,
"branch": "main"
}
}
}
With policy configuration:
include:
- branch-protection-enabled
- minimum-approvals
with:
min_approvals: 1
Failure messages:
branch-protection-enabled:"Branch protection is not enabled on main"minimum-approvals:"Branch protection is not enabled"
Failing Example - Wrong Settings
{
"vcs": {
"visibility": "public",
"default_branch": "master",
"merge_strategies": {
"allow_merge_commit": true,
"allow_squash_merge": true,
"allow_rebase_merge": false
}
}
}
With policy configuration:
include:
- require-private
- require-default-branch
- allowed-merge-strategies
with:
allowed_merge_strategies: "squash"
# required_default_branch defaults to "main"
Failure messages:
"Repository visibility is 'public', but policy requires 'private'""Default branch is 'master', but policy requires 'main'""Merge commits are enabled, but policy does not allow them (should be disabled)"
Remediation
Branch Protection Policies
When branch protection policies fail, configure branch protection rules in your repository settings:
- In your VCS provider, open the repository's branch protection settings (e.g. GitHub: Settings → Branches → Branch protection rules; GitLab: Settings → Repository → Protected branches and Merge request approvals)
- Select your default branch (typically
mainormaster) or create a new rule - Enable the required protection settings based on the policy failures (setting names vary by provider):
- Require pull request reviews before merging - Set the number of required approvals
- Require review from Code Owners - Enable if codeowner review is required
- Dismiss stale pull request approvals when new commits are pushed - Enable if required
- Require status checks to pass before merging - Enable and select required checks
- Require branches to be up to date before merging - Enable if required
- Do not allow bypassing the above settings - Disable "Allow force pushes" and "Allow deletions"
- Require linear history - Enable if required by policy
- Require signed commits - Enable if required by policy
- Save the branch protection rule
- Re-run the Lunar collector and policy to verify compliance
Bypass and Change Evidence Policies
disallow-bypass-actors: remove the listed bypass actors from each ruleset (Settings → Rules → Rulesets), or under classic protection turn on "Do not allow bypassing the above settings" and clear "Allow specified actors to bypass required pull requests". An actor you accept goes inallowed_bypass_actors. A skip means the token can't see a ruleset's bypass list; GitHub shows it only to callers with write access to the ruleset.pr-commits-signed: sign commits with a GPG, SSH or S/MIME key registered to your GitHub account, then re-sign the listed commits (e.g.git rebase --exec 'git commit --amend --no-edit -S' <base>) and force-push the branch.release-commits-merged-via-pr: the listed commits reached the default branch without a pull request into it. Either they were pushed there directly, or they were merged into another branch that was then pushed straight to the default branch. Land them through a pull request (revert and re-land), or record an exception with a Lunar release bypass. Require pull requests on the default branch so it can't recur.
Repository Settings Policies
When repository settings policies fail, update repository settings:
- In your VCS provider, open the repository's settings (e.g. GitHub: Settings → General; GitLab: Settings → General and Repository)
- Update the relevant settings based on the policy failures:
- Repository visibility (General section):
- Change visibility to match policy requirements (Private or Public)
- Note: Changing visibility may have security implications - consult your security team
- Default branch (General section):
- Rename your default branch if required (e.g., from
mastertomain) - Most providers offer a default-branch rename control (e.g. GitHub's "Rename branch" button)
- Update local clones and CI/CD configurations after renaming
- Rename your default branch if required (e.g., from
- Merge button (Pull Requests section):
- Enable only the merge strategies listed in
allowed_merge_strategies - Disable any strategies not in the allowed list
- Example: If policy specifies
"squash,rebase", enable those two and disable merge commits
- Enable only the merge strategies listed in
- Repository visibility (General section):
- Save the changes
- Re-run the Lunar collector and policy to verify compliance
Open Source
This policy is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.
Common Use Cases
Explore how individual guardrails work with specific integrations.
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 300+ built-in guardrails.