Reusable Terraform module for managing one GitHub repository with production-oriented defaults for repository lifecycle, branch governance, merge policy, security controls, labels, and optional repository-standard files.
The module supports private, internal, and public repositories. It defaults to private.
Teams often create repositories by hand, which leads to inconsistent merge policies, missing security settings, unmanaged labels, and branch protection that is hard to audit. This module codifies repository standards as Terraform so new repositories can start with safe defaults and existing repositories can be adopted gradually.
The module is designed for platform teams and solo maintainers who want:
- repeatable repository settings
- branch and release governance
- security defaults that are explicit but plan-aware
- additive labels and optional repository-standard files
- a documented import path for existing repositories
github_repositorygithub_branch_defaultgithub_branchfor each non-default protected branchgithub_repository_rulesetfor protected branchesgithub_repository_rulesetfor release tags- optional
github_repository_rulesetfor push guardrails github_repository_vulnerability_alertsgithub_repository_dependabot_security_updatesgithub_issue_labelorgithub_issue_labelsgithub_repository_filefor optional.github/*content
terraform {
required_version = ">= 1.6.0"
required_providers {
github = {
source = "integrations/github"
version = ">= 6.12.1, < 7.0.0"
}
}
}provider "github" {
owner = var.github_owner
}
module "platform_github_repo" {
source = "./platform-github-repo"
repository_name = "payments-service"
description = "Private repository for the payments service."
visibility = "private"
}Run:
terraform init
terraform plan
terraform applySee:
examples/minimalexamples/basicexamples/github-free-privateexamples/public-portfolioexamples/productionexamples/existing-repo
Use the example that matches the repository lifecycle:
| Scenario | Start Here | Notes |
|---|---|---|
| New private repository with safe defaults | examples/basic |
Requires GitHub Pro/Team/Enterprise for private repository rulesets. |
| New private repository on GitHub Free | examples/github-free-private |
Disables branch and tag rulesets so apply can succeed on the free plan. |
| New minimal bootstrap | examples/minimal |
Smallest useful root module. |
| Existing repository adoption | examples/existing-repo |
Import repository, default branch, non-default protected branches, rulesets, and security resources before apply. |
| Public portfolio/demo repository | examples/public-portfolio |
Conservative adoption defaults for public repos with existing workflows. |
| Stricter production governance | examples/production |
CODEOWNERS, required checks, long-lived environment branches, and push guardrails. |
The module is intentionally conservative:
- repositories are private by default
- repositories archive instead of deleting on
terraform destroy - only squash merge is enabled
- non-default protected branches use
prevent_destroy - vulnerability alerts and Dependabot security updates are managed through dedicated resources
- premium security settings are not managed unless explicitly opted in
- required status checks default to empty so new repositories can bootstrap cleanly
- standard labels are managed additively by default
Set:
visibility = "private" # private, internal, or publicNotes:
internalrequires GitHub Enterprise.- security and analysis settings are not managed for
publicrepositories by this module because GitHub always enables Advanced Security on public repositories. - push rulesets are intended for private/internal repositories.
Repository rulesets are available for public repositories on GitHub Free. Private repositories require GitHub Pro, GitHub Team, or GitHub Enterprise Cloud for branch and tag rulesets. Protected branches follow the same public/free and private/paid availability pattern. Push rulesets are more restrictive and are intended for paid private/internal repository use.
For private repositories on GitHub Free, disable rulesets:
visibility = "private"
manage_branch_ruleset = false
manage_release_tag_ruleset = false
manage_push_ruleset = falseSee examples/github-free-private for a copyable root module.
archive_on_destroy = true is the default. This means Terraform archives the repository instead of deleting it when the resource is destroyed.
Non-default protected branches are also created with prevent_destroy = true.
If you intentionally need to delete a repository or retire a protected branch, handle that as an explicit operational change. See docs/operations.md.
protected_branches defines long-lived branches governed by the branch ruleset:
protected_branches = ["main", "stage", "dev"]The module:
- requires
maininprotected_branches - creates non-default protected branches from
main - targets
mainwith GitHub's~DEFAULT_BRANCHruleset pattern - requires pull requests for governed branches
- blocks deletion and non-fast-forward updates
- enforces linear history
- allows only squash merge
Admins remain subject to rules unless bypass actors are configured.
Required checks are object-based and require integration_id by default. This avoids a known provider/GitHub drift problem where unpinned ruleset checks can appear as integration_id = 0 or later drift to the GitHub App ID returned by GitHub.
required_status_checks = [
{
context = "ci/test"
integration_id = 15368
},
]Leave the list empty during initial bootstrap. Add check names only after CI has run and the exact GitHub check contexts are stable.
If you intentionally want context-only checks, opt in explicitly:
allow_unpinned_required_status_checks = trueUse this only after accepting the provider drift risk.
Branch, tag, and push governance can use bypass actors:
branch_ruleset_bypass_actors = [
{
actor_id = 12345
actor_type = "Integration"
bypass_mode = "always"
}
]Validation rules:
OrganizationAdminrequiresactor_id = 1DeployKeyomitsactor_idRepositoryRole,Team, andIntegrationrequireactor_id
For production, a GitHub App used by Terraform is usually cleaner than a personal token.
The module creates a v* tag ruleset by default:
- tag creation remains open by default
- updates are blocked
- deletion is blocked
- non-fast-forward updates are blocked
This lets release automation publish new tags while keeping published release tags immutable.
Enable push guardrails only when your GitHub plan supports push rulesets:
manage_push_ruleset = true
push_restricted_file_paths = [".env", ".env.*", "secrets/**"]
push_restricted_file_extensions = [".pem", ".key"]
push_max_file_size_mb = 50
push_max_file_path_length = 255The module requires at least one push rule input when manage_push_ruleset = true.
Always-on defaults:
manage_vulnerability_alerts = trueenable_vulnerability_alerts = truemanage_automated_security_fixes = trueenable_automated_security_fixes = true
Premium security settings are separate:
manage_security_and_analysis = true
enable_premium_security_features = true
enable_advanced_security = true
enable_code_security = true
enable_secret_scanning = true
enable_secret_scanning_push_protection = trueLeave manage_security_and_analysis = false when organization policy, split licensing, or GitHub plan constraints centrally control these settings.
By default the module manages standard labels in additive mode:
labels_mode = "additive"Additive mode creates or updates only declared labels.
Authoritative mode replaces the repository label policy with the module's effective labels:
labels_mode = "authoritative"Use authoritative mode carefully because labels outside the declared set can be removed.
Optional files:
.github/pull_request_template.md.github/ISSUE_TEMPLATE/*.md.github/CODEOWNERS.github/dependabot.yml
Managed files are committed before the branch ruleset is created, which helps first apply on a new repository.
If Terraform later updates managed files on a protected branch, the Terraform identity needs a ruleset bypass actor or the apply may fail.
Version updates require .github/dependabot.yml:
manage_dependabot_file = true
dependabot_yaml_content = <<-YAML
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
YAMLUse examples/existing-repo and docs/operations.md.
Start with content management disabled, import the repository, default branch, existing non-default protected branches, security resources, and existing rulesets, run terraform plan, then gradually enable stricter controls.
If terraform plan wants to create a non-default branch that already exists:
terraform import 'module.platform_github_repo.github_branch.protected["develop"]' payments-service:developApply only after settings changes like Wiki, Projects, merge methods, and ruleset enforcement are understood.
| Input | Default | When To Change |
|---|---|---|
visibility |
private |
Set public for public portfolio/open-source repos, or internal on Enterprise. |
protected_branches |
["main"] |
Add long-lived branches such as develop, stage, or prod. Import them first when adopting existing repos. |
manage_branch_ruleset |
true |
Disable for private repos on GitHub Free or during staged adoption. |
manage_release_tag_ruleset |
true |
Disable when release tags are managed elsewhere or private rulesets are unavailable. |
required_status_checks |
[] |
Add after CI has run and check contexts plus integration IDs are known. |
labels_mode |
additive |
Use authoritative only when Terraform owns the full label taxonomy. |
manage_security_and_analysis |
false |
Enable only when plan, licensing, and org policy allow Terraform to manage premium security settings. |
manage_pull_request_template |
true |
Disable when adopting repos that already own PR templates. |
manage_issue_templates |
true |
Disable when adopting repos that already own issue templates. |
manage_dependabot_file |
false |
Enable when Terraform should own Dependabot version update config. |
archive_on_destroy |
true |
Set false only for an intentional hard delete workflow. |
- Repositories archive on destroy by default to avoid accidental data loss.
- Required status checks default to empty so new repos can bootstrap before CI exists.
- Required checks use
integration_idby default to reduce provider/GitHub drift. - Labels are additive by default so existing label taxonomies are not removed unexpectedly.
- Premium security settings are opt-in because GitHub plans, split licensing, and org policies vary.
- Managed files are committed before branch rulesets are created to make first apply work on new repos.
- Existing repositories have a dedicated adoption path because imports and drift review are safer than direct apply.
Local verification:
terraform fmt -check -recursive
terraform init -backend=false
terraform validate
terraform test -no-colorExample validation:
for dir in examples/basic examples/minimal examples/github-free-private examples/public-portfolio examples/production examples/existing-repo; do
(cd "$dir" && terraform init -backend=false && terraform validate)
doneThe CI workflow in .github/workflows/terraform.yml runs the same checks.
See:
variables.tfoutputs.tf
- Required status check names must match GitHub check contexts exactly.
- Pull request authors cannot satisfy required approvals with their own reviews.
require_last_push_approvalrequires someone other than the latest pusher to approve.- Branch and tag rulesets on private repositories require GitHub Pro, Team, or Enterprise Cloud.
- Ruleset
evaluatemode is available only for organization-owned repositories. - Premium security controls may require GitHub Advanced Security, Code Security, Secret Protection, split licensing, or organization-level enablement.
- Security settings may be rejected if organization policy centrally manages them.
github_repository_fileupdates on protected branches need bypass or bootstrap sequencing.- Push rulesets require plan support and are intended for private/internal repositories.
See docs/troubleshooting.md for common plan and apply failures.
- Terraform GitHub provider:
integrations/github - GitHub repository rulesets: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets
- GitHub protected branches: https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches
- GitHub security and analysis settings
- GitHub Dependabot version updates
- Terraform module structure, dependency lock files, and tests