Why This Matters
If you’ve ever tried to explain to your team that “we can’t create a new repo right now, I’m at dinner,” you understand why GitHub management should be code. Every infrastructure change in my homelab goes through code review — including how we manage GitHub itself.
The problem: GitHub’s web UI is fine for 3 repos, painful for 30+. You can’t track who changed what, can’t enforce naming conventions, and can’t ensure consistency across repositories.
The solution: treat your GitHub organization like database infrastructure. Define everything in YAML, let Terraform handle the drift, and sleep better at night.
Scale: This setup manages 40+ repositories across my organization with full configuration, teams, secrets, and custom properties.
All Terraform resources support lifecycle { create_before_destroy = true } for zero-downtime deployments.
Architecture
The github-management-plane repository manages my GitHub organization through Terraform — the same configuration-driven pattern as my homelab infrastructure:
graph TB
G[github-management-plane]
R[Repository Module]
S[Secrets/Variables Module]
O[Organization Custom Properties]
G --> R
G --> S
G --> O
Repos["All Repositories"]
Vars["Actions Variables"]
Secs["Actions Secrets"]
Repos --> tf-infra-homelab["tf-infra-homelab"]
Repos --> tf-module-proxmox-talos["tf-module-proxmox-talos"]
Repos --> applications-homelab["applications-homelab"]
Repos --> ...["24+ repositories"]
What This Module Does
- Repository management — create and configure all repositories via YAML
- Team management — define teams and members
- Organization custom properties — classify repositories
- Actions secrets/variables — manage organization-wide secrets
- Issue labels — standardize labels across repositories
Quick Start
module "repositories" {
source = "./modules/repository"
for_each = local.filtered_repo_configurations
configuration = each.value
organization = var.github_organization
}Repository Management
All repositories are defined as YAML configurations:
# configurations/repository/tf-infra-homelab.yaml
name: tf-infra-homelab
description: A terraform infrastructure repository for managing my homelab environment
enabled: true
archived: false
visibility: private
type: terraform-infrastructure
topics:
- homelab
- proxmox
enabled_features:
vulnerability_alerts: true
issues: true
wiki: false
projects: false
discussions: falseRepository Types
The module supports different repository types with defaults:
locals {
repository_types = {
terraform-infrastructure = {
license_template = "mit"
auto_init = true
topics = ["terraform", "homelab"]
}
terraform-module = {
license_template = "mit"
auto_init = true
topics = ["terraform", "proxmox"]
}
python-docker-application = {
license_template = "mit"
auto_init = true
topics = ["python", "docker"]
}
generic = {
auto_init = true
}
}
}Repository Resource
resource "github_repository" "this" {
name = var.configuration.name
description = var.configuration.description
visibility = var.configuration.visibility
allow_rebase_merge = true
allow_squash_merge = true
delete_branch_on_merge = true
vulnerability_alerts = var.configuration.enabled_features.vulnerability_alerts
has_discussions = var.configuration.enabled_features.discussions
has_issues = var.configuration.enabled_features.issues
has_projects = var.configuration.enabled_features.projects
has_wiki = var.configuration.enabled_features.wiki
}Organization Custom Properties
Custom properties allow classification and filtering:
locals {
organization_custom_properties = {
"can-be-public" = {
description = "To indicate whether the repository can be made public"
value_type = "single_select"
required = true
allowed_values = ["true", "false"]
default_value = "false"
}
"managed-by" = {
description = "To identify who manages the repository"
value_type = "single_select"
required = true
allowed_values = [
"github-management-plane",
"manual-management",
]
default_value = "manual-management"
}
"repository-type" = {
description = "To indicate the type of repository"
value_type = "single_select"
required = true
allowed_values = [
"generic",
"repository-template",
"golang-linux-package",
"golang-docker-application",
"python-docker-application",
"python-package",
"terraform-infrastructure",
"terraform-module",
]
}
}
}Apply them:
resource "github_organization_custom_properties" "managed_properties" {
for_each = local.organization_custom_properties
property_name = each.key
value_type = each.value.value_type
required = each.value.required
description = each.value.description
default_value = each.value.default_value
allowed_values = each.value.allowed_values
}Secrets and Variables
Actions secrets and variables are managed through Bitwarden:
# configurations/secrets_variables/global.yaml
variables:
- name: RUNNER
value: self-hosted
visibility: all
secrets:
- name: BWS_ACCESS_TOKEN
is_manual: true
visibility: privateVariable Resource
resource "github_actions_organization_variable" "managed_variables" {
for_each = { for v in var.configuration.variables : v.name => v }
variable_name = each.value.name
visibility = each.value.visibility
value = each.value.value
}Secret Resource
For secrets, two patterns:
# Manual secrets (value set outside Terraform)
resource "github_actions_organization_secret" "managed_secrets_manual" {
for_each = { for v in var.configuration.secrets : v.name => v if v.is_manual }
secret_name = each.value.name
visibility = each.value.visibility
plaintext_value = "NONE"
}
# Synced secrets (from Bitwarden)
resource "github_actions_organization_secret" "managed_secrets_sync" {
for_each = { for v in var.configuration.secrets : v.name => v if !v.is_manual }
secret_name = each.value.name
visibility = each.value.visibility
plaintext_value = data.bitwarden-secrets_secret.secrets[each.value.name].value
}Get secrets from Bitwarden:
data "bitwarden-secrets_secret" "secrets" {
for_each = { for v in local.all_secrets : v.name => v if !v.is_manual }
id = each.value.bw_secret_id
}Team Management
Teams are defined in the main module:
resource "github_team" "organization_administrators" {
name = "organization-administrators"
description = "Team with administrative access to the organization"
privacy = "closed"
}
resource "github_team_members" "organization_administrators_members" {
team_id = github_team.organization_administrators.id
members = {
"your-username" = {
role = "maintainer"
}
}
}Issue Labels
Labels are standardized across repositories:
locals {
shared_labels = {
"bug" = {
color = "d73a4a"
description = "Bug report"
}
"enhancement" = {
color = "a2eeef"
description = "New feature"
}
"documentation" = {
color = "0075ca"
description = "Documentation improvements"
}
}
}
resource "github_issue_labels" "this" {
repository = var.configuration.name
label = merge(
local.shared_labels,
try(var.configuration.labels, {})
)
}My Repository Configuration
Here’s the list of repositories managed:
configurations/repository/
├── tf-infra-homelab.yaml # Homelab infrastructure
├── tf-infra-github-management-plane.yaml # GitHub management
├── tf-module-proxmox-lxc.yaml # LXC module
├── tf-module-proxmox-vm.yaml # VM module
├── tf-module-proxmox-talos.yaml # Talos module
├── tf-module-proxmox-docker.yaml # Docker module
├── applications-homelab.yaml # Kustomize apps
├── cf-worker-terraform-registry.yaml # TF registry worker
├── cf-worker-apt-repository.yaml # APT repository worker
├── template-terraform-basic.yaml # Module template
├── template-cloudflare-worker-python.yaml # Worker template
└── ... (24 total)Each is just a YAML file — adding a new repository is adding a file.
Configuration Structure
github-management-plane/
├── configurations/
│ ├── repository/ # Repository definitions
│ ├── secrets_variables/ # Actions secrets/variables
│ └── rulesets/ # Branch protection (future)
├── modules/
│ ├── repository/ # Repository module
│ ├── secrets_variables/ # Secrets module
│ └── organization/ # Org properties module
├── main.tf # Orchestration
├── locals.tf # Configuration loading
└── providers.tf # Provider setupThis mirrors the homelab infrastructure structure.
Outputs
The module doesn’t have specific outputs since it manages the organization passively.
What’s Next
- Branch protection rulesets — via rulesets API (when Terraform provider supports it)
- Repository invitations — managing outside collaborators
- Security advisories — automated security scanning
What Most People Get Wrong
-
“GitHub Terraform is just for repos” — It’s org-wide: teams, custom properties, secrets, variables. The whole thing.
-
“One Terraform run is enough” — Git rate limits apply. Use
ratenodeprovider or caching. -
“Manual changes are fine if you revert” — Terraform drift will catch you. Enable
logentoor runterraform refreshregularly.
When to Use / When NOT to Use
| Use GitHub Management Plane | Do it manually |
|---|---|
| 20+ repositories | 1-5 repos |
| Team collaboration | Personal projects |
| Audit requirements | Quick experiments |
| Secret/variable management | Ad-hoc scripts only |
This makes GitHub management declarative — every change goes through code review.