diff --git a/modules/common_repository/main.tf b/modules/common_repository/main.tf index 630124d..dc5e95b 100644 --- a/modules/common_repository/main.tf +++ b/modules/common_repository/main.tf @@ -189,6 +189,63 @@ resource "github_repository_ruleset" "status_checks" { depends_on = [github_repository.repo] } +resource "github_repository_ruleset" "tag_creation" { + for_each = var.archived ? {} : { for r in var.tag_rulesets : r.name => r } + + name = "${each.value.name}-creation" + repository = github_repository.repo.name + target = "tag" + enforcement = "active" + + dynamic "bypass_actors" { + for_each = each.value.creation_bypass_app_ids + content { + actor_id = bypass_actors.value + actor_type = "Integration" + bypass_mode = "always" + } + } + + conditions { + ref_name { + include = each.value.ref_patterns + exclude = [] + } + } + + rules { + creation = true + } + + depends_on = [github_repository.repo] +} + +resource "github_repository_ruleset" "tag_immutable" { + for_each = var.archived ? {} : { for r in var.tag_rulesets : r.name => r } + + name = "${each.value.name}-immutable" + repository = github_repository.repo.name + target = "tag" + enforcement = "active" + + # Deliberately no bypass_actors -- updates/deletions are blocked for + # everyone, including the App(s) allowed to create matching tags above. + + conditions { + ref_name { + include = each.value.ref_patterns + exclude = [] + } + } + + rules { + update = true + deletion = true + } + + depends_on = [github_repository.repo] +} + resource "github_repository_environment" "env" { # Skip environments for archived repos (GitHub rejects writes with 409) for_each = var.archived ? {} : { diff --git a/modules/common_repository/variables.tf b/modules/common_repository/variables.tf index 689026b..1e078b4 100644 --- a/modules/common_repository/variables.tf +++ b/modules/common_repository/variables.tf @@ -213,3 +213,20 @@ variable "all_members_permission" { condition = contains(["pull", "push", "triage", "maintain", "admin"], var.all_members_permission) } } + +variable "tag_rulesets" { + description = <<-EOT + Tag protection rulesets. Each entry creates two ruleset resources for the + given ref patterns: one allowing tag creation only to the listed GitHub + App IDs (nobody, if left empty), and one blocking tag updates and + deletions for everyone unconditionally, with no bypass. Intended for + marker/automation tags that a specific trusted CI identity creates and + that should otherwise be immutable. + EOT + type = list(object({ + name = string + ref_patterns = list(string) + creation_bypass_app_ids = optional(list(number), []) + })) + default = [] +} diff --git a/repositories.tf b/repositories.tf index 76307d9..5fae23d 100644 --- a/repositories.tf +++ b/repositories.tf @@ -199,6 +199,18 @@ module "repo_osac" { github_team.all["infrastructure"].id, ] + # osac-release/* tags trigger a Konflux staging release (see + # .tekton/*-release.yaml and the trigger-konflux-staging-release job in + # osac-build-and-publish.yaml) -- only the osac-ci app may create them, + # and nobody may move or delete one once pushed. + tag_rulesets = [ + { + name = "osac-release-tag" + ref_patterns = ["refs/tags/osac-release/*"] + creation_bypass_app_ids = [4966101] # osac-ci + } + ] + merge_queue = { merge_method = "SQUASH" max_entries_to_build = 4