diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ebaf2..6369710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ pins that tag, so this file is the human-readable answer to "what's in v0.2.0?". - **Fixed** — bug fixes. - **Removed** — removed features (call out breaking changes loudly). +## [Unreleased] + +### Changed +- `github` — added per-repo `delete_branch_on_merge` (auto-deletes the head branch on merge, + default `true`) and an org-wide default-team grant: `default_team` (default `engineers`) + is granted `default_team_permission` (default `push`) on every managed repo via + `github_team_repository`; set `default_team = ""` to opt out. Adds a `team_grants` output. + The team grant requires `GITHUB_TOKEN` with `admin:org`. + ## [0.3.0] - 2026-06-08 ### Added diff --git a/github/README.md b/github/README.md index a0fddf7..d08fea6 100644 --- a/github/README.md +++ b/github/README.md @@ -14,16 +14,24 @@ it cannot run on the credential-free `TG_BACKEND=local` path. Because GitHub rep Per entry in `repositories`: -- `github_repository` — the repo (visibility, description, topics, `has_issues`, `auto_init`). +- `github_repository` — the repo (visibility, description, topics, `has_issues`, `auto_init`, + `delete_branch_on_merge`). Head branches are auto-deleted on merge by default. - `github_branch_default` — sets the default branch. - `github_branch_protection` — only when the entry includes a `branch_protection` block (required reviews, optional required status checks, enforce-admins). +Org-wide (one grant per repo, not configured per entry): + +- `github_team_repository` — grants `default_team` access to **every** managed repo at + `default_team_permission`. Skipped entirely when `default_team` is `""`. The team must + already exist in the org — this component grants access, it does not create teams. + ## Auth The provider reads `GITHUB_TOKEN` from the environment — a PAT or GitHub App token with at -least `repo` scope (`admin:org` if you manage org-internal settings). **No token is stored in -this module or in git.** Export it before running: +least `repo` scope. **`admin:org` is required** to manage the `default_team` grant (and any +org-internal settings); without it `plan` succeeds but `apply` fails on the team grant. **No +token is stored in this module or in git.** Export it before running: ```bash export GITHUB_TOKEN=ghp_... @@ -31,11 +39,13 @@ export GITHUB_TOKEN=ghp_... ## Inputs -| Name | Type | Default | Description | -| -------------- | ------------- | -------------- | ------------------------------------------------------------------ | -| `global` | object | — | Env-wide context. Accepted for convention only; **unused** here. | -| `github_owner` | string | `officialdad` | The GitHub org (or user) the provider operates on. | -| `repositories` | map(object) | `{}` | Repositories to manage, keyed by repo name (see shape below). | +| Name | Type | Default | Description | +| ------------------------- | ----------- | ------------- | ------------------------------------------------------------------ | +| `global` | object | — | Env-wide context. Accepted for convention only; **unused** here. | +| `github_owner` | string | `officialdad` | The GitHub org (or user) the provider operates on. | +| `default_team` | string | `engineers` | Team slug granted to every repo by default; `""` disables. | +| `default_team_permission` | string | `push` | Default team's access: `pull`/`triage`/`push`/`maintain`/`admin`. | +| `repositories` | map(object) | `{}` | Repositories to manage, keyed by repo name (see shape below). | ### `repositories` entry shape @@ -48,6 +58,8 @@ repositories = { default_branch = "main" has_issues = true + delete_branch_on_merge = true # auto-delete head branch on merge (default true) + # Omit this block entirely to leave the branch unprotected. branch_protection = { required_approving_review_count = 1 @@ -60,10 +72,11 @@ repositories = { ## Outputs -| Name | Description | -| ------------------ | -------------------------------------------- | -| `repository_names` | Map of key → full name (`owner/repo`). | -| `repository_urls` | Map of key → HTML URL. | +| Name | Description | +| ------------------ | ----------------------------------------------------- | +| `repository_names` | Map of key → full name (`owner/repo`). | +| `repository_urls` | Map of key → HTML URL. | +| `team_grants` | Map of key → `"team:permission"` from `default_team`. | ## Managing repos that already exist diff --git a/github/terraform/main.tf b/github/terraform/main.tf index 48c4faf..33eb7a8 100644 --- a/github/terraform/main.tf +++ b/github/terraform/main.tf @@ -12,11 +12,12 @@ provider "github" { resource "github_repository" "this" { for_each = var.repositories - name = each.key - description = each.value.description - visibility = each.value.visibility - topics = each.value.topics - has_issues = each.value.has_issues + name = each.key + description = each.value.description + visibility = each.value.visibility + topics = each.value.topics + has_issues = each.value.has_issues + delete_branch_on_merge = each.value.delete_branch_on_merge # Give a newly-created repo an initial commit + default branch so the branch_default and # branch_protection resources below have something to point at. No-op on imported repos. @@ -55,3 +56,14 @@ resource "github_branch_protection" "this" { } } } + +# Grants the org-wide default team access to every managed repo. Fanned out over the same +# repositories map. The whole block is skipped when var.default_team is empty, so the +# component still works in orgs that don't use a default team. +resource "github_team_repository" "default" { + for_each = var.default_team == "" ? {} : var.repositories + + team_id = var.default_team + repository = github_repository.this[each.key].name + permission = var.default_team_permission +} diff --git a/github/terraform/outputs.tf b/github/terraform/outputs.tf index a0c1554..87611b4 100644 --- a/github/terraform/outputs.tf +++ b/github/terraform/outputs.tf @@ -7,3 +7,8 @@ output "repository_urls" { value = { for k, r in github_repository.this : k => r.html_url } description = "Map of repository key -> HTML URL." } + +output "team_grants" { + value = { for k, g in github_team_repository.default : k => "${var.default_team}:${g.permission}" } + description = "Map of repository key -> \"team:permission\" granted by the default team." +} diff --git a/github/terraform/variables.tf b/github/terraform/variables.tf index 5f7642e..a81d7df 100644 --- a/github/terraform/variables.tf +++ b/github/terraform/variables.tf @@ -4,13 +4,31 @@ variable "github_owner" { default = "officialdad" } +variable "default_team" { + type = string + description = "Team slug granted access to every managed repo by default. Empty string disables the default grant. The team must already exist in the org." + default = "engineers" +} + +variable "default_team_permission" { + type = string + description = "Permission the default team receives on each repo." + default = "push" + + validation { + condition = contains(["pull", "triage", "push", "maintain", "admin"], var.default_team_permission) + error_message = "default_team_permission must be one of: pull, triage, push, maintain, admin." + } +} + variable "repositories" { type = map(object({ - description = optional(string, "") - visibility = optional(string, "private") - topics = optional(list(string), []) - default_branch = optional(string, "main") - has_issues = optional(bool, true) + description = optional(string, "") + visibility = optional(string, "private") + topics = optional(list(string), []) + default_branch = optional(string, "main") + has_issues = optional(bool, true) + delete_branch_on_merge = optional(bool, true) branch_protection = optional(object({ required_approving_review_count = optional(number, 1) required_status_checks = optional(list(string), [])