One enforced, deeply-typed
github_repository_ruleset— the modern, secure-by-default replacement for branch protection that governs a single repository's branches, tags, and pushes from one place, with every rule, condition, and bypass actor expressed as a validatedobjectschema. Built for integrations/github v6.x.
- 🛡️ Creates one
github_repository_rulesetpinned to a single repository — the enforced, API-v4 successor togithub_branch_protection. - 🎯 Targets branches, tags, or pushes (
target = "branch" | "tag" | "push"). - ⚙️ Projects a fully-typed
rulesobject onto the provider: scalar toggles (non_fast_forward,required_signatures, …) plus block rules (pull_request,required_status_checks,merge_queue,required_deployments, push file restrictions, and Enterprise-only pattern / code-scanning / Copilot rules). - 🔀 Targets refs precisely via
conditions.ref_name.include / exclude, including the~ALLand~DEFAULT_BRANCHtokens. - 🚦 Grants least-privilege bypass —
bypass_actorsdefaults to[](nobody bypasses); every entry is validated foractor_typeandbypass_mode. - 🔒 Secure by default:
enforcement = "active", PR review withdismiss_stale_reviews_on_pushandrequire_code_owner_reviewon,required_approving_review_count = 1, and strict status-check policy on.
💡 Why it matters: rulesets are GitHub's current, layerable enforcement model — an org ruleset and a repo ruleset can both match a ref and the most restrictive rule wins. This module gives a single, validated, reviewable boundary for "what may land on a protected branch" without hand-editing the GitHub UI.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart LR
repo["tf-mod-github-repository<br/>(keystone)"]
team["tf-mod-github-team"]
rs["tf-mod-github-repository-ruleset<br/>(THIS module)"]
orgrs["tf-mod-github-organization-ruleset"]
bp["tf-mod-github-branch-protection<br/>(legacy alternative)"]
collab["tf-mod-github-repository-collaborators"]
repo -- "id (repo name)" --> rs
team -- "id (Team bypass actor)" --> rs
repo -. "alternative".-> bp
orgrs -. "org-scope sibling".-> rs
repo --> collab
style rs fill:#8957E5,color:#fff
style repo fill:#24292F,color:#fff
This module consumes a repository name (from tf-mod-github-repository.id) and optional Team / App / Role ids for bypass actors, and emits the ruleset id / ruleset_id / node_id for audit and automation. See the 🔌 Cross-Module Contract.
flowchart TD
this["github_repository_ruleset.this<br/>name · repository · target · enforcement"]
rules["rules { } (required, 1)"]
conditions["conditions { } (dynamic)"]
bypass["bypass_actors [ ] (dynamic, default none)"]
this --> rules
this --> conditions
this --> bypass
rules --> toggles["scalar toggles<br/>creation · deletion · update<br/>non_fast_forward · required_signatures<br/>required_linear_history"]
rules --> pr["pull_request<br/>(reviews · merge methods · required_reviewers)"]
rules --> checks["required_status_checks<br/>(required_check[])"]
rules --> mq["merge_queue"]
rules --> deploy["required_deployments"]
rules --> ent["Enterprise-only<br/>*_pattern · required_code_scanning · copilot_code_review"]
rules --> push["push-target rules<br/>file_path / file_extension / max_file_size / max_file_path_length"]
conditions --> refname["ref_name { include / exclude }"]
bypass --> actor["actor_id · actor_type · bypass_mode"]
style this fill:#8957E5,color:#fff
Resource inventory
github_repository_ruleset.this— the single primary resource. Although the module manages exactly onegithub_*resource, it is treated as composite because that resource carries deeply-nested repeating blocks (rules,conditions,bypass_actors) rendered asdynamicblocks, demanding the composite typing discipline.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
integrations/github |
~> 6.0 (validated against 6.12.1) |
ℹ️ The provider source is
integrations/github— never the deprecatedhashicorp/github. If you have old state on the deprecated provider, migrate first withterraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.
Schema notes that bite:
- 🏢 Enterprise-only rules are silently meaningful only on GitHub Enterprise: the
*_patternrules,required_code_scanning,copilot_code_review, and certain bypass-actor types. They validate fine offline but fail (or no-op) on apply against a non-Enterprise org. - 🧮
enforcement = "evaluate"is only supported by the provider for organization-owned targets. For repository rulesets use"active"or"disabled".
tf-mod-github-repository-ruleset/
├── providers.tf # terraform{} + required_providers (integrations/github ~> 6.0)
├── variables.tf # name, repository, target, enforcement, rules, conditions, bypass_actors
├── main.tf # github_repository_ruleset.this — dynamic-block projection of var.rules
├── outputs.tf # id, ruleset_id, node_id, etag, name, repository
├── SCOPE.md # scope contract: consumes/emits, token scopes, prerequisites
└── README.md # this file
The smallest call that protects the default branch of a repository created by the keystone module:
module "repository" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
name = "payments-api"
}
module "default_branch_ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-default-branch"
repository = module.repository.id # the keystone's id IS the repo name
conditions = {
ref_name = {
include = ["~DEFAULT_BRANCH"]
}
}
rules = {
deletion = true # only bypassers may delete
non_fast_forward = true # block force-push
pull_request = {} # secure PR defaults kick in (1 approval, code-owner review, dismiss stale)
}
}| Input | Type | Source |
|---|---|---|
repository |
string (repo name) | tf-mod-github-repository → id output |
bypass_actors[].actor_id |
number | tf-mod-github-team → id (Team bypass), a GitHub App / Integration id, or a custom org-role id |
| Output | Description | Consumed by |
|---|---|---|
id |
Ruleset node ID (primary reference) | Audit / reporting modules |
ruleset_id |
Numeric ruleset ID | GitHub REST API automation |
node_id |
GraphQL global node ID | GraphQL automation |
etag |
Resource etag | Change detection |
name |
Ruleset name (echo of input) | Audit / reporting |
repository |
Repository the ruleset applies to (echo of input) | Audit / reporting |
ℹ️ Sourced directly from
SCOPE.md. Repository rulesets have no ARN, no slug, no full_name — the cross-module surface is the nodeidplus the numericruleset_id.
1️⃣ Minimal — protect the default branch
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-default-branch"
repository = "payments-api"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = { deletion = true, non_fast_forward = true }
}2️⃣ Repository wired from tf-mod-github-repository
module "repository" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
name = "ledger-core"
}
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-default-branch"
repository = module.repository.id # id IS the repo name
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
deletion = true
non_fast_forward = true
pull_request = {} # secure defaults
}
}💡 Always wire
repositoryfrom the keystone'sidso the ruleset is created after the repo and tracks renames in plan order.
3️⃣ Pull-request review protection
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "require-reviews"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
pull_request = {
required_approving_review_count = 2
require_code_owner_review = true
dismiss_stale_reviews_on_push = true
require_last_push_approval = true
required_review_thread_resolution = true
allowed_merge_methods = ["squash"]
}
}
}4️⃣ Required status checks
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "require-ci"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
required_status_checks = {
strict_required_status_checks_policy = true
required_check = [
{ context = "build" },
{ context = "unit-tests" },
{ context = "terraform-validate" },
]
}
}
}
⚠️ A checkcontextwill only ever pass if a CI run actually reports that exact name. A typo'd context blocks every merge silently.
5️⃣ Signed commits + linear history
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "verified-linear"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
required_signatures = true
required_linear_history = true
non_fast_forward = true
}
}6️⃣ Merge queue
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "merge-queue"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
merge_queue = {
merge_method = "SQUASH"
grouping_strategy = "ALLGREEN"
max_entries_to_merge = 5
min_entries_to_merge = 1
check_response_timeout_minutes = 60
}
}
}7️⃣ Tag protection (target = "tag")
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-release-tags"
repository = "ledger-core"
target = "tag"
conditions = { ref_name = { include = ["refs/tags/v*"] } }
rules = {
deletion = true
update = true
non_fast_forward = true
}
}8️⃣ Push protection (target = "push") — file restrictions
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "block-large-and-secrets"
repository = "ledger-core"
target = "push"
# NOTE: push targets must NOT set conditions.ref_name — leave conditions null.
rules = {
max_file_size = { max_file_size = 100 } # MB
max_file_path_length = { max_file_path_length = 255 }
file_extension_restriction = { restricted_file_extensions = ["exe", "dll", "pem", "key"] }
file_path_restriction = { restricted_file_paths = ["secrets/**", "**/*.env"] }
}
}
⚠️ Push rulesets are Enterprise-org-only in some editions. See GitHub Prerequisites.
9️⃣ Bypass actors — least-privilege exceptions
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-with-breakglass"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = { deletion = true, non_fast_forward = true, pull_request = {} }
bypass_actors = [
# Org admins may bypass only via a pull request (no direct push)
{ actor_type = "OrganizationAdmin", actor_id = 1, bypass_mode = "pull_request" },
# A release-automation Team always bypasses
{ actor_type = "Team", actor_id = module.release_team.id, bypass_mode = "always" },
]
}🔒
bypass_actorsdefaults to[]— nobody bypasses. Grant explicitly and sparingly.actor_idsemantics depend onactor_type(App id vs Team id vs Role id).
🔟 Include / exclude ref patterns
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "protect-release-branches"
repository = "ledger-core"
conditions = {
ref_name = {
include = ["refs/heads/release/*", "~DEFAULT_BRANCH"]
exclude = ["refs/heads/release/experimental-*"]
}
}
rules = { non_fast_forward = true, pull_request = {} }
}1️⃣1️⃣ Required deployments (environment gates)
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "require-staging-first"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
required_deployments = {
required_deployment_environments = ["staging"]
}
}
}1️⃣2️⃣ 🏢 Enterprise — code scanning + commit-message pattern
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "enterprise-hardening"
repository = "ledger-core"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
commit_message_pattern = {
operator = "regex"
pattern = "^(feat|fix|chore|docs)(\\(.+\\))?:.+"
name = "conventional-commits"
}
required_code_scanning = {
required_code_scanning_tool = [
{
tool = "CodeQL"
alerts_threshold = "errors"
security_alerts_threshold = "high_or_higher"
}
]
}
}
}🏢
*_patternandrequired_code_scanningrules require GitHub Enterprise (and Advanced Security for code scanning). They will fail on apply against a non-Enterprise org.
1️⃣3️⃣ 🔒 Secure / hardened variant — the default for a protected branch
module "hardened_ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "casey-hardened-default-branch"
repository = module.repository.id
enforcement = "active"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
# Only bypassers may create/delete/force-push the protected branch
creation = true
deletion = true
update = true
non_fast_forward = true
required_linear_history = true
required_signatures = true
pull_request = {
required_approving_review_count = 2
require_code_owner_review = true
dismiss_stale_reviews_on_push = true
require_last_push_approval = true
required_review_thread_resolution = true
allowed_merge_methods = ["squash"]
}
required_status_checks = {
strict_required_status_checks_policy = true
required_check = [
{ context = "build" },
{ context = "test" },
{ context = "security-scan" },
]
}
}
# No bypass actors — nobody is exempt on the most sensitive branch.
bypass_actors = []
}🔒 This is the recommended baseline for any regulated repository: enforced, signed, reviewed, CI-gated, and zero bypass.
1️⃣4️⃣ for_each at scale — one ruleset per repo from a map(object)
variable "rulesets" {
type = map(object({
repository = string
reviews = optional(number, 1)
checks = optional(list(string), [])
}))
default = {
"payments-api" = { repository = "payments-api", reviews = 2, checks = ["build", "test"] }
"ledger-core" = { repository = "ledger-core", reviews = 2, checks = ["build"] }
"web-portal" = { repository = "web-portal", reviews = 1 }
}
}
module "rulesets" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
for_each = var.rulesets
name = "protect-default-branch"
repository = each.value.repository
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
non_fast_forward = true
deletion = true
pull_request = { required_approving_review_count = each.value.reviews }
required_status_checks = length(each.value.checks) > 0 ? {
required_check = [for c in each.value.checks : { context = c }]
} : null
}
}
⚠️ Bulkfor_eachover many repos can trip GitHub's secondary rate limits. See Troubleshooting.
1️⃣5️⃣ 🏗️ End-to-end composition (mandatory) — full suite wired outputs → inputs
# 1) The keystone repository
module "repository" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
name = "regulated-service"
visibility = "private"
vulnerability_alerts = true
}
# 2) A team that owns the repo (and is a trusted bypass actor)
module "platform_team" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-team?ref=v1.0.0"
name = "platform-engineering"
}
# 3) THIS module — the enforced ruleset, wired entirely from sibling outputs
module "ruleset" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository-ruleset?ref=v1.0.0"
name = "regulated-default-branch"
repository = module.repository.id # <- from keystone
enforcement = "active"
conditions = { ref_name = { include = ["~DEFAULT_BRANCH"] } }
rules = {
deletion = true
non_fast_forward = true
required_linear_history = true
required_signatures = true
pull_request = {
required_approving_review_count = 2
require_code_owner_review = true
dismiss_stale_reviews_on_push = true
}
required_status_checks = {
required_check = [{ context = "build" }, { context = "test" }]
}
}
bypass_actors = [
{ actor_type = "Team", actor_id = module.platform_team.id, bypass_mode = "pull_request" }, # <- from team module
]
}
output "ruleset_id" {
value = module.ruleset.id
}💡 This is the canonical pattern:
tf-mod-github-repositoryemits the repo name,tf-mod-github-teamemits the Team id, andtf-mod-github-repository-rulesetcomposes both into a single enforced boundary.
Identity & target
name(string, required) — the ruleset name. Must be non-empty.repository(string, required) — repository name (not numeric id, notowner/name). Wire fromtf-mod-github-repository.id.target(string, default"branch") —branch|tag|push.enforcement(string, default"active") —active|evaluate|disabled. (evaluateis org-targets-only.)
Policy
rules(object, required) — the protections enforced. Scalar toggles plus block rules (PR review, status checks, merge queue, deployments, push file restrictions, Enterprise pattern / code-scanning / Copilot rules).
Targeting & exceptions
conditions(object, defaultnull) —ref_name.include / exclude. Required forbranch/tag; must benullforpush.bypass_actors(list(object), default[]) — who may bypass; least-privilege empty by default.
Full object schemas (rules / conditions / bypass_actors)
rules = object({
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
update_allows_fetch_and_merge = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
# Enterprise-only pattern rules. operator: starts_with|ends_with|contains|regex
branch_name_pattern = optional(object({ operator, pattern, name?, negate? }))
commit_author_email_pattern = optional(object({ operator, pattern, name?, negate? }))
commit_message_pattern = optional(object({ operator, pattern, name?, negate? }))
committer_email_pattern = optional(object({ operator, pattern, name?, negate? }))
tag_name_pattern = optional(object({ operator, pattern, name?, negate? }))
copilot_code_review = optional(object({ review_on_push?, review_draft_pull_requests? }))
required_deployments = optional(object({ required_deployment_environments = list(string) }))
merge_queue = optional(object({
check_response_timeout_minutes = optional(number, 60)
grouping_strategy = optional(string, "ALLGREEN") # ALLGREEN | HEADGREEN
max_entries_to_build = optional(number, 5)
max_entries_to_merge = optional(number, 5)
merge_method = optional(string, "MERGE") # MERGE | SQUASH | REBASE
min_entries_to_merge = optional(number, 1)
min_entries_to_merge_wait_minutes = optional(number, 5)
}))
pull_request = optional(object({
allowed_merge_methods = optional(list(string)) # merge|squash|rebase
dismiss_stale_reviews_on_push = optional(bool, true)
require_code_owner_review = optional(bool, true)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number, 1)
required_review_thread_resolution = optional(bool)
required_reviewers = optional(list(object({
file_patterns = list(string)
minimum_approvals = number
reviewer = object({ id = number, type = optional(string, "Team") })
})), [])
}))
required_status_checks = optional(object({
strict_required_status_checks_policy = optional(bool, true)
do_not_enforce_on_create = optional(bool)
required_check = optional(list(object({
context = string
integration_id = optional(number)
})), [])
}))
required_code_scanning = optional(object({ # Enterprise Cloud
required_code_scanning_tool = list(object({
tool = string
alerts_threshold = string # none|errors|errors_and_warnings|all
security_alerts_threshold = string # none|critical|high_or_higher|medium_or_higher|all
}))
}))
# push-target rules only
file_path_restriction = optional(object({ restricted_file_paths = list(string) }))
file_extension_restriction = optional(object({ restricted_file_extensions = list(string) }))
max_file_size = optional(object({ max_file_size = number })) # 1-100 MB
max_file_path_length = optional(object({ max_file_path_length = number }))
})
conditions = object({
ref_name = optional(object({
include = optional(list(string), []) # ref patterns / ~ALL / ~DEFAULT_BRANCH
exclude = optional(list(string), [])
}))
})
bypass_actors = list(object({
actor_id = optional(number) # Team / App / RepositoryRole id (omit for id-less types)
actor_type = string # RepositoryRole | Team | Integration | OrganizationAdmin | DeployKey
bypass_mode = optional(string, "always") # always | pull_request | exempt
}))| Output | Description |
|---|---|
id |
Ruleset node ID — the primary cross-module reference. |
ruleset_id |
Numeric ruleset ID — for GitHub REST API automation. |
node_id |
GraphQL global node ID — for GraphQL automation. |
etag |
Resource etag — for change detection. |
name |
Ruleset name (echo of input). |
repository |
Repository the ruleset applies to (echo of input). |
ℹ️ No
arn, noslug, nofull_name— repository rulesets do not expose them. None of these outputs are sensitive.
idvsruleset_idvsnode_id.idis the provider's resource id (the GraphQL node id), the primary reference.ruleset_idis the numeric id GitHub's REST API uses (/repos/{owner}/{repo}/rulesets/{id}).node_idis the GraphQL global node id. Rulesets have no slug or full_name — don't expect them.- Rulesets vs branch protection. Prefer this module over
tf-mod-github-branch-protectionfor all new work. Branch protection is the legacy v3 API; rulesets are the current model, are layerable (org + repo rulesets both apply, most-restrictive wins), and support tags and push targets that branch protection cannot. enforcementsemantics. Defaults toactive— rulesets actually enforce by default.evaluate(dry-run reporting) is supported only for organization-owned targets; for a repo ruleset useactiveordisabled.- ForceNew / immutability.
repositoryandtargetare effectively the resource's identity — changing them forces a destroy/recreate of the ruleset. Plan accordingly when renaming repos. bypass_actors.actor_idis type-dependent. The number means different things peractor_type: a Team id forTeam, a GitHub App/Integration id forIntegration, a role id forRepositoryRole(maintain 2, write 4, admin 5),1forOrganizationAdmin.DeployKeycarries no id — omitactor_id.- Authoritative resource.
github_repository_rulesetis authoritative for the ruleset it names — it fully owns that named ruleset's contents, but it does not clobber other rulesets on the same repo. Org rulesets remain independent. - No sensitive inputs. Unlike Actions/Dependabot secret modules, a ruleset carries no secret values — nothing here is marked
sensitive. - Eventual consistency & secondary rate limits. The GitHub REST API is eventually consistent and enforces secondary rate limits on bursts. Large
for_eachrollouts of rulesets can intermittently 403/429 mid-apply; re-runningterraform applyconverges.
- 🔒 Enforced by default —
enforcement = "active", notevaluate/disabled. - ✅ Secure PR defaults —
dismiss_stale_reviews_on_push = true,require_code_owner_review = true,required_approving_review_count = 1, strict status-check policy on. - 🚫 Force-push & deletion blocked on protected branches via
non_fast_forward/deletion. - 🧬 Deeply-typed schema — no
any, no loose maps; every enum and closed value set has avalidation {}block. - 🪪 Least-privilege bypass —
bypass_actorsdefaults to empty; exceptions are explicit and validated. - 🧩 Provider-owned auth — no
owner/token/app_authvariables; the caller's provider block configures identity and org.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output
⚠️ Always pin the module source to a tag —?ref=v1.0.0, never a branch. A moving branch ref turns an unrelated upstream change into an unreviewed change to your protected branches.
The offline proof gate (no live org needed):
terraform fmt -check # zero formatting differences
terraform validate # zero errors
tflint # core rules — no dedicated GitHub ruleset existsℹ️
plan/applyrequire live GitHub credentials (PAT, GitHub CLI token, or GitHub App) configured in the provider, not the module.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "RRS_lACq_examplenodeid"
name = "protect-default-branch"
node_id = "RRS_lACq_examplenodeid"
repository = "payments-api"
ruleset_id = 1234567
etag = "W/\"abc123...\""
| Symptom | Cause | Resolution |
|---|---|---|
403: Resource not accessible by integration on apply |
Token/App lacks Administration: write | Grant repo (classic) or fine-grained Administration r/w + Metadata r. |
enforcement "evaluate" not supported |
evaluate used on a repo ruleset |
Use active or disabled; evaluate is org-targets-only. |
Validation error: push target with conditions |
conditions.ref_name set while target = "push" |
Leave conditions = null for push rulesets. |
| Pattern / code-scanning / Copilot rule fails on apply | Org is not GitHub Enterprise (+ Advanced Security) | Remove the Enterprise-only rule or move the repo to an Enterprise org. |
422 on a status-check context |
context name doesn't match any reported check |
Use the exact check name CI reports; check casing. |
Intermittent 429 / secondary rate limit during bulk for_each |
Too many ruleset writes in a burst | Re-run apply (converges); reduce parallelism with -parallelism=2. |
| Bypass actor has no effect | Wrong actor_id for the actor_type |
Verify the id type: Team id vs App id vs Role id vs 1 for OrganizationAdmin. |
tf-mod-github-repository— the keystone that emits therepositoryname.tf-mod-github-team— emits Team ids used as bypass actors.tf-mod-github-organization-ruleset— the org-scope sibling (most-restrictive-wins layering).tf-mod-github-branch-protection— the legacy v3 alternative.- integrations/github provider —
github_repository_rulesetresource reference. - GitHub Docs — "About rulesets" and "Managing rulesets for a repository."