feat(org): org-level push policy enforced in the pre-receive hook (#727) #730
Reference in New Issue
Block a user
Delete Branch "feat/org-push-policy"
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?
Part of the org-governance series (#728, #729). Adds a single per-org push policy cascaded to every repo and enforced in the pre-receive hook.
What it enforces
The key safety property: the two content checks (blocked-paths, max-size) walk git objects in the hot push path, so a bug in code I can't compile could be catastrophic. They fail open — on any parse/command error they log and allow the push. Worst case they silently don't enforce (safe); they can never wedge every push in the org. Naming (trivial string match) is fail-closed.
How
models/git/org_push_policy.go:OrgPushPolicy(one row/org) + CRUD + name/blocked-path matchers +GetOrgPushPolicyForRepo. Migration 364.GET/PATCH/DELETE /orgs/{org}/push_policy—routers/api/v1/org/push_policy.go, DTOs inmodules/structs/org_push_policy.go, wired inapi.go.hook_pre_receive.go(branch: naming + blocked-paths + max-size viagit ls-tree --long; tag: naming) andservices/security/orchestrator.go(secret-block mandate overridesScanPushForSecrets' repo-config gate).Deferred / caveats
gofmt'd/tested. Hand-verified: gofmt (tabs, no blank-in-block), the\n/\tescapes in the ls-tree parser (a real trap here), imports used, migration contiguous, fail-open on content checks. CI must validate build + format + tests. Integration testing of the pre-receive path (push a too-large file, a badly-named branch, a blocked path, a secret to a mandated-block org) is the critical manual check before merge — this is the one PR that touches the push gate.https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
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.Adds org-level tag protection as a parallel to org-level branch protection. An org tag rule is {NamePattern, AllowlistTeamIDs}; it cascades to every repo in the org and layers on top of the repo's own protected tags — a tag is controllable (push/delete) only if allowed at BOTH levels (fail-closed). - models/git/org_protected_tag.go: OrgProtectedTag model + CRUD + ToProtectedTag() (reuses the ProtectedTag matcher/allowlist logic) + IsUserAllowedToControlTagInRepo() which ANDs the repo decision with the org decision. Migration 363. - API: /orgs/{org}/tag_protections CRUD (routers/api/v1/org/tag_protection.go, DTOs in modules/structs/org_tag.go, wired in api.go). - Enforcement: the git push/delete hook (hook_pre_receive.go) and the two release paths (release.go create/delete) now call the layered check, so no per-site tag logic changes beyond swapping the helper. - View: the repo Tag settings page lists inherited org tag rules read-only. Stacked on #728 (branch-protection PR) for migration ordering — merge #728 first. Swagger annotations omitted (can't regenerate the swagger JSON without the toolchain); routes still register. Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here. Hand-verified: gofmt (tabs, no blank-in-block, struct alignment), template nesting balances, all .Rule fields exist on OrgProtectedTag, all locale keys defined, JSON valid, migration contiguous (363). Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KTAdds a single per-org push policy that cascades to every repo of the org and is enforced in the pre-receive hook: - Branch/tag name conventions (glob) — a pushed ref name must match. Fail-closed. - Mandatory secret-scanning block-on-push — org can force secret blocking that a repo cannot disable (overrides the per-repo scanner config in the orchestrator). - Max pushed-file size — rejects a tip tree containing a blob over the limit. - Blocked file-path patterns — rejects pushes changing matching paths (reuses pull_service.CheckFileProtection). The two content checks (blocked paths, max size) FAIL OPEN on any error so a policy/parsing bug can never wedge all pushes; naming is fail-closed. - models/git/org_push_policy.go: OrgPushPolicy model + CRUD + matchers + GetOrgPushPolicyForRepo. Migration 364. - API: GET/PATCH/DELETE /orgs/{org}/push_policy (routers/api/v1/org/push_policy.go, DTOs in modules/structs/org_push_policy.go, wired in api.go). - Enforcement: routers/private/hook_pre_receive.go (branch: naming + blocked paths + max size; tag: naming) and services/security/orchestrator.go (secret mandate). Deferred: a repo-facing read-only view of the org push policy (it is an org-wide config, not per-repo overlay rules; readable via the API for now). Stacked on #729/#728 for migration ordering (this = 364). Swagger annotations omitted (can't regenerate without the toolchain). Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here. Hand-verified: gofmt (tabs, no blank-in-block), escape sequences in the ls-tree parser, imports used, migration contiguous (364), fail-open on content checks. Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KTa75338c9a4to3aac1b456c