feat(org): org-level tag protection, layered with per-repo protected tags (#727) #729
Reference in New Issue
Block a user
Delete Branch "feat/org-tag-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?
Part of the org-governance series (see #728). Adds org-level tag protection as a parallel to org-level branch protection.
What it does
An org tag rule is
{NamePattern, AllowlistTeamIDs}(much simpler than branch rules — tags have no booleans). 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 AND).How
models/git/org_protected_tag.go:OrgProtectedTag+ CRUD +ToProtectedTag()(reuses the existingProtectedTagname-matcher/allowlist) +IsUserAllowedToControlTagInRepo()which ANDs the repo decision with the org decision. Migration 363./orgs/{org}/tag_protections(list/get/create/edit/delete) —routers/api/v1/org/tag_protection.go, DTOs inmodules/structs/org_tag.go, wired inapi.gobesidebranch_protections.hook_pre_receive.go) and the two release paths (release.gocreate/delete) now call the layered helper. No per-site tag logic changed beyond swapping the function.Caveats
gofmt'd/tested here. Hand-verified: gofmt (tabs, no blank-in-block, struct-field alignment), template nesting balances, every.Rulefield exists onOrgProtectedTag, all locale keys defined, JSON valid, migration contiguous.templates/swagger/v1_json.tmplwithout the toolchain, and the existing org-branch handlers already reference undefined#/responses/...). Routes register fine; docs can be regenerated later.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_01Wsno14cxE49MstXFs9G5KT