Skip to content

Manage every repository's default-branch ruleset - #3

Merged
oskarbarcz merged 1 commit into
mainfrom
unify-branch-rulesets
Nov 10, 2026
Merged

oskarbarcz merged 1 commit into
mainfrom
unify-branch-rulesets

Conversation

@oskarbarcz

Copy link
Copy Markdown
Member

The default-branch rulesets were made by hand, one repository at a time, and had drifted apart. Four of them still require the old matrix check names (code (seatmap), code (layouts), code (pull), code (city)), which the flattened function repositories no longer report, so mypreflight/aerolopa-provider#14, mypreflight/osm-provider#12 and mypreflight/postcard-generator#14 can't merge. oceanic-tracks-provider had no ruleset at all.

Every repository now gets the same Main branch protection ruleset from infra/rulesets.tf. The branch can't be deleted or force-pushed. Changes arrive through pull requests with every conversation resolved and only the merge methods the repository allows. The checks in required_checks must pass, reported by GitHub Actions on a branch that is up to date. required_checks defaults to [files, code] in config/repositories.yaml. github-iac sets [plan], product [markdownlint] and transponder-app [files, code, executable]. .github sets branch_protection: false and stays unprotected, as it is today.

infra/imports.tf adopts the nine existing rulesets, and a moved block turns github-iac's "Require a passing plan" into its entry in the new resource. Only oceanic-tracks-provider's is created. The README describes the keys.

What the apply changes

  • Every ruleset:
    • Name: Main branch protection (product's was main, github-iac's Require a passing plan).
    • Check source: every required check is pinned to GitHub Actions; adsb-receiver-api, app and landing had unpinned ones.
    • Up-to-date branch: required everywhere; landing didn't require it.
    • New branches: the checks also apply to newly created branches; aerolopa-provider and app exempted them.
  • aerolopa-provider, osm-provider, postcard-generator: files and code replace the matrix checks.
  • Pull request rule: added to aerolopa-provider, postcard-generator, adsb-receiver-api, app and github-iac. osm-provider now requires conversations to be resolved.
  • api: gains the deletion rule. github-iac: gains deletion and force-push protection.
  • Dropped:
    • required_signatures, which only aerolopa-provider had.
    • The code quality rule on aerolopa-provider, osm-provider, adsb-receiver-api and landing. The provider (6.13) has no such rule type, and none of the four has a code-quality analysis for it to check.

After merging

The apply makes the three function pull requests above mergeable. Then remove infra/imports.tf in a follow-up, as in #1; its plan should show no changes.

Verification

  • tofu fmt -check and tofu validate pass.
  • On a copy without the backend, local.protected_repositories lists the 11 repositories with the checks and merge methods above.
  • The plan on this pull request is the real check. It should show 9 imports, 1 move, 1 create and in-place updates only.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@oskarbarcz oskarbarcz self-assigned this Nov 10, 2026
@github-actions

Copy link
Copy Markdown

OpenTofu plan for infra

Commit 34013d3 · Plan: 9 to import, 1 to add, 9 to change, 0 to destroy.

Show plan
 OpenTofu used the selected providers to generate the following execution
 plan. Resource actions are indicated with the following symbols:
+  + create
!  ~ update in-place (current -> planned)
 
 OpenTofu will perform the following actions:
 
   # github_repository_ruleset.default_branch["adsb-receiver-api"] will be updated in-place
   # (imported from "adsb-receiver-api:20952365")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"9dfa07cc77840b83a35a1df0771497355b073751ea07420a5cdb4ea84f650ea3\""
         id          = "20952365"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec48eHgRzgE_tS0"
         repository  = "adsb-receiver-api"
         ruleset_id  = 20952365
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
                 do_not_enforce_on_create             = false
                 strict_required_status_checks_policy = true
 
-              - required_check {
-                  - context        = "code" -> null
-                  - integration_id = 0 -> null
                 }
-              - required_check {
-                  - context        = "files" -> null
-                  - integration_id = 0 -> null
                 }
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
+              + required_check {
+                  + context        = "files"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["aerolopa-provider"] will be updated in-place
   # (imported from "aerolopa-provider:21036225")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"94ffb4ad18f5a87549750c0e1608d7f3b4bf89aa52d349819a0ac12c292e8a0d\""
         id          = "21036225"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5Py16SzgFA_ME"
         repository  = "aerolopa-provider"
         ruleset_id  = 21036225
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
-          - required_signatures           = true -> null
             update                        = false
             update_allows_fetch_and_merge = false
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
!              ~ do_not_enforce_on_create             = true -> false
                 strict_required_status_checks_policy = true
 
-              - required_check {
-                  - context        = "code (layouts)" -> null
-                  - integration_id = 15368 -> null
                 }
-              - required_check {
-                  - context        = "code (seatmap)" -> null
-                  - integration_id = 15368 -> null
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["api"] will be updated in-place
   # (imported from "api:2856650")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"f2e93a98b61ea9346d6669f267d4fc53a862ef1097cde972dc287dcb8d5cdf46\""
         id          = "2856650"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec41kOwGzgArlso"
         repository  = "api"
         ruleset_id  = 2856650
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
!          ~ deletion                      = false -> true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
             pull_request {
                 allowed_merge_methods             = [
                     "squash",
                 ]
                 dismiss_stale_reviews_on_push     = false
                 require_code_owner_review         = false
                 require_last_push_approval        = false
                 required_approving_review_count   = 0
                 required_review_thread_resolution = true
             }
 
             required_status_checks {
                 do_not_enforce_on_create             = false
                 strict_required_status_checks_policy = true
 
                 required_check {
                     context        = "code"
                     integration_id = 15368
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["app"] will be updated in-place
   # (imported from "app:2843988")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"713de07251018ff8a925b4d554a2865f89815ccb18f2adc1a50366c9fe839f78\""
         id          = "2843988"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec41d5b1zgArZVQ"
         repository  = "app"
         ruleset_id  = 2843988
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
!              ~ do_not_enforce_on_create             = true -> false
                 strict_required_status_checks_policy = true
 
-              - required_check {
-                  - context        = "code" -> null
-                  - integration_id = 0 -> null
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["github-iac"] will be updated in-place
   # (moved from github_repository_ruleset.github_iac_plan)
!  ~ resource "github_repository_ruleset" "default_branch" {
         id          = "26844030"
!      ~ name        = "Require a passing plan" -> "Main branch protection"
         # (6 unchanged attributes hidden)
 
!      ~ rules {
!          ~ deletion                      = false -> true
!          ~ non_fast_forward              = false -> true
             # (5 unchanged attributes hidden)
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
             # (1 unchanged block hidden)
         }
 
         # (1 unchanged block hidden)
     }
 
   # github_repository_ruleset.default_branch["landing"] will be updated in-place
   # (imported from "landing:21995511")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"d650cb0a0bdc500bd0cc871cfa8f6f4cf4dd9d2eec02b51fa7d7ab03a9b4cc93\""
         id          = "21995511"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5QqaN6zgFPn_c"
         repository  = "landing"
         ruleset_id  = 21995511
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
             pull_request {
                 allowed_merge_methods             = [
                     "squash",
                 ]
                 dismiss_stale_reviews_on_push     = false
                 require_code_owner_review         = false
                 require_last_push_approval        = false
                 required_approving_review_count   = 0
                 required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
                 do_not_enforce_on_create             = false
!              ~ strict_required_status_checks_policy = false -> true
 
                 required_check {
                     context        = "code"
                     integration_id = 15368
                 }
-              - required_check {
-                  - context        = "files" -> null
-                  - integration_id = 0 -> null
                 }
+              + required_check {
+                  + context        = "files"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["oceanic-tracks-provider"] will be created
+  + resource "github_repository_ruleset" "default_branch" {
+      + enforcement = "active"
+      + etag        = (known after apply)
+      + id          = (known after apply)
+      + name        = "Main branch protection"
+      + node_id     = (known after apply)
+      + repository  = "oceanic-tracks-provider"
+      + ruleset_id  = (known after apply)
+      + target      = "branch"
 
+      + conditions {
+          + ref_name {
+              + exclude = []
+              + include = [
+                  + "~DEFAULT_BRANCH",
                 ]
             }
         }
 
+      + rules {
+          + deletion                      = true
+          + non_fast_forward              = true
+          + update_allows_fetch_and_merge = false
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
+          + required_status_checks {
+              + do_not_enforce_on_create             = false
+              + strict_required_status_checks_policy = true
 
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
+              + required_check {
+                  + context        = "files"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["osm-provider"] will be updated in-place
   # (imported from "osm-provider:20548043")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"20fe077f94ee26a80a6fc7403bc93265ecf09893c51469cedf43221c099fbd68\""
         id          = "20548043"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5PD3hvzgE5ics"
         repository  = "osm-provider"
         ruleset_id  = 20548043
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
!          ~ pull_request {
                 allowed_merge_methods             = [
                     "squash",
                 ]
                 dismiss_stale_reviews_on_push     = false
                 require_code_owner_review         = false
                 require_last_push_approval        = false
                 required_approving_review_count   = 0
!              ~ required_review_thread_resolution = false -> true
             }
 
!          ~ required_status_checks {
                 do_not_enforce_on_create             = false
                 strict_required_status_checks_policy = true
 
-              - required_check {
-                  - context        = "code (pull)" -> null
-                  - integration_id = 15368 -> null
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["postcard-generator"] will be updated in-place
   # (imported from "postcard-generator:21585090")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"b68d61e43b7d58f29d9288639b903b668e2a1f06cef452926be3f638a106047c\""
         id          = "21585090"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5QU5PqzgFJXMI"
         repository  = "postcard-generator"
         ruleset_id  = 21585090
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
+          + pull_request {
+              + allowed_merge_methods             = [
+                  + "squash",
                 ]
+              + dismiss_stale_reviews_on_push     = false
+              + require_code_owner_review         = false
+              + require_last_push_approval        = false
+              + required_approving_review_count   = 0
+              + required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
                 do_not_enforce_on_create             = false
                 strict_required_status_checks_policy = true
 
-              - required_check {
-                  - context        = "code (city)" -> null
-                  - integration_id = 15368 -> null
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
+              + required_check {
+                  + context        = "code"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["product"] will be updated in-place
   # (imported from "product:25468006")
!  ~ resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"a82394ee1bbf81851ce8a9d93c665bacc0c42e3c3d76f01926e6cc4578385f44\""
         id          = "25468006"
!      ~ name        = "main" -> "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5VC6TqzgGEnGY"
         repository  = "product"
         ruleset_id  = 25468006
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
!      ~ rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
!          ~ pull_request {
!              ~ allowed_merge_methods             = [
-                  - "merge",
                     "squash",
-                  - "rebase",
                 ]
                 dismiss_stale_reviews_on_push     = false
                 require_code_owner_review         = false
                 require_last_push_approval        = false
                 required_approving_review_count   = 0
                 required_review_thread_resolution = true
             }
 
!          ~ required_status_checks {
                 do_not_enforce_on_create             = false
!              ~ strict_required_status_checks_policy = false -> true
 
-              - required_check {
-                  - context        = "markdownlint" -> null
-                  - integration_id = 0 -> null
                 }
+              + required_check {
+                  + context        = "markdownlint"
+                  + integration_id = 15368
                 }
             }
         }
     }
 
   # github_repository_ruleset.default_branch["transponder-app"] will be imported
   # (imported from "transponder-app:21283082")
     resource "github_repository_ruleset" "default_branch" {
         enforcement = "active"
         etag        = "W/\"3f9b1ff15f06f9d2ff2506cb5407c07f79c44714311b6a7f5b67749cd5b5958d\""
         id          = "21283082"
         name        = "Main branch protection"
         node_id     = "RRS_lACqUmVwb3NpdG9yec5PetgSzgFEwQo"
         repository  = "transponder-app"
         ruleset_id  = 21283082
         target      = "branch"
 
         conditions {
             ref_name {
                 exclude = []
                 include = [
                     "~DEFAULT_BRANCH",
                 ]
             }
         }
 
         rules {
             creation                      = false
             deletion                      = true
             non_fast_forward              = true
             required_linear_history       = false
             required_signatures           = false
             update                        = false
             update_allows_fetch_and_merge = false
 
             pull_request {
                 allowed_merge_methods             = [
                     "squash",
                 ]
                 dismiss_stale_reviews_on_push     = false
                 require_code_owner_review         = false
                 require_last_push_approval        = false
                 required_approving_review_count   = 0
                 required_review_thread_resolution = true
             }
 
             required_status_checks {
                 do_not_enforce_on_create             = false
                 strict_required_status_checks_policy = true
 
                 required_check {
                     context        = "code"
                     integration_id = 15368
                 }
                 required_check {
                     context        = "executable"
                     integration_id = 15368
                 }
                 required_check {
                     context        = "files"
                     integration_id = 15368
                 }
             }
         }
     }
 
 Plan: 9 to import, 1 to add, 9 to change, 0 to destroy.

Workflow run

@oskarbarcz
oskarbarcz merged commit 1b135f1 into main Nov 10, 2026
1 check passed
@oskarbarcz
oskarbarcz deleted the unify-branch-rulesets branch November 10, 2026 16:55
oskarbarcz added a commit that referenced this pull request Nov 10, 2026
The apply of #3 adopted the default-branch rulesets into state (9
imported, 1 added, 9 changed, 0 destroyed), and moved github-iac's own
ruleset to `github_repository_ruleset.default_branch["github-iac"]`. The
import blocks and the `moved` block are no longer needed, so
`infra/imports.tf` goes. The plan on this PR should show no changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant