fix(org): layer org-level branch protection with repo rules — most-restrictive wins (#727) #728
Reference in New Issue
Block a user
Delete Branch "fix/727-materialize-org-branch-protection"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #727.
What's actually going on
Org-level branch protection is not inert as the issue states. The single enforcement choke point every push/merge/approval/status/force-push/delete check funnels through —
GetFirstMatchProtectedBranchRule(models/git/protected_branch_list.go) — already consults org rules. The issue reporter'sgrep OrgProtectedBranchmissed it because that file callsFindOrgBranchRuleForBranch/ToProtectedBranch(functions) without ever naming the type.But it only used the org rule as a fallback: if any repo rule matched the branch, the org rule was ignored entirely — so a repo could define a looser rule for a pattern and opt out of the org's protection.
Fix — layer, most-restrictive wins (fail-closed)
Change that one choke point to combine the repo rule and the org rule when both match, returning the stricter constraint per field. The org rule becomes a mandatory floor a repo can only tighten.
models/git/protected_branch_merge.go(new):mergeMostRestrictive+ helpers.Can*,*ActionsUser,*DeployKeys) → ANDEnable*,Block*,Require*) → ORRequiredApprovals→ maxmodels/git/protected_branch_list.go:GetFirstMatchProtectedBranchRulefetches both rules and merges; returns whichever exists when only one matches. Org lookup factored intogetFirstMatchOrgProtectedBranchRule.Because the choke point feeds all ~15 enforcement sites, no enforcement site needs editing and the existing checks run unchanged on the merged rule.
Safety
Fail-closed: every merge edge errs toward more protection (over-restrict) rather than less, so a mistake can lock people out (recoverable, visible) but cannot silently open a hole. Whitelist intersection across separate user/team OR-criteria can over-restrict in edge cases — documented in the merge function.
Existing tests unaffected: the unit test uses a user-owned fixture repo (no org rules), integration tests set up no org rules — layering only adds restriction when an org rule matches.
⚠️ Verification caveat
No Go toolchain in the authoring environment — not compiled,
gofmt'd, vet'd, or tested locally. Hand-verified: tab indentation, no helper-name collisions in packagegit, every merged field name against theProtectedBranchstruct, and that the reworked choke point preserves prior error-handling. CI must validate build + format + tests before merge, and integration testing of layered enforcement is recommended.https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
fix(org): materialize org-level branch protection into per-repo rules (#727)to fix(org): layer org-level branch protection with repo rules — most-restrictive wins (#727)Added a second commit: surface the inherited org rules in the repo's Branch Protection settings (read-only), like GitHub shows org rulesets in a repo.
ProtectedBranchRuleshandler loadsFindOrgProtectedBranchRulesfor org-owned repos →OrgProtectedBranches.templates/repo/settings/branches.tmpl: new read-only "Organization Branch Protection" section — each org rule shows an Organization badge, a lock/read-only marker, and compact indicators (required approvals, signed commits, status checks). No edit/delete controls (managed at the org level).Closes the visibility gap noted earlier: the org floor is enforced implicitly at the choke point, so admins had no way to see which org rules applied to their repo. +46 lines, 3 files.
Same caveat: no Go toolchain locally → not compiled/tested; CI must validate. Template/locale render errors surface at runtime, so a quick manual load of the branch-protection settings page on an org repo is worth doing.
Follow-up commit — richer read-only view plus a new capability:
1. Branch deletion is now an org-level ability.
OrgProtectedBranchgainedCanDelete/EnableDeleteAllowlist/DeleteAllowlistTeamIDs(migration 362),ToProtectedBranchmaps them, and the API surface (OrgBranchProtection,CreateOrgBranchProtectionOption,EditOrgBranchProtectionOption+ create/edit handlers) exposesenable_delete/enable_delete_allowlist/delete_allowlist_teams. The layering merge already combined the delete fields, so org delete-protection enforces as a floor once populated. (Deletion was previously not removed from the view — it's added as a real ability instead.)2. The inherited-rule view is now an expandable detail (
<details>) rather than three badges: direct push, force-push, branch deletion, merge restriction, required approvals, status checks, and protected-file patterns, with team IDs resolved to names. Still read-only.+203 lines across model, migration, API DTO/handlers, web handler, template, locale, changelog.
Same caveat: no Go toolchain locally. Hand-verified: struct-field gofmt alignment (name→30 / type→9 in DTO, →19 in model; literal values→col 31), template block-nesting balances, every
.Rule.*field exists onOrgProtectedBranch, all 34 template locale keys defined, JSON valid. CI + a manual load of an org repo's Branch Protection page still recommended before merge.LandingPageType.Mode defaults to "" (Go zero value), and the template renders the home radio as checked for an empty Mode. The initial radio fill would evaluate home.checked = ("home" === "") = false, unchecking the default on a fresh install. Skip assignment when the config value is empty so the server-rendered selection is preserved. Adds a test for the empty-value case.jmiller referenced this pull request2026-07-05 04:37:45 +00:00