Skip to content

Kendall04/platform-github-repo

Repository files navigation

platform-github-repo

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.

Why This Exists

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

What It Creates

  • github_repository
  • github_branch_default
  • github_branch for each non-default protected branch
  • github_repository_ruleset for protected branches
  • github_repository_ruleset for release tags
  • optional github_repository_ruleset for push guardrails
  • github_repository_vulnerability_alerts
  • github_repository_dependabot_security_updates
  • github_issue_label or github_issue_labels
  • github_repository_file for optional .github/* content

Provider Requirements

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    github = {
      source  = "integrations/github"
      version = ">= 6.12.1, < 7.0.0"
    }
  }
}

Quickstart

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 apply

See:

  • examples/minimal
  • examples/basic
  • examples/github-free-private
  • examples/public-portfolio
  • examples/production
  • examples/existing-repo

Quickstart Paths

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.

Production Defaults

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

Repository Visibility

Set:

visibility = "private" # private, internal, or public

Notes:

  • internal requires GitHub Enterprise.
  • security and analysis settings are not managed for public repositories by this module because GitHub always enables Advanced Security on public repositories.
  • push rulesets are intended for private/internal repositories.

GitHub Plan Compatibility

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        = false

See examples/github-free-private for a copyable root module.

Repository Lifecycle

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.

Branch Governance

protected_branches defines long-lived branches governed by the branch ruleset:

protected_branches = ["main", "stage", "dev"]

The module:

  • requires main in protected_branches
  • creates non-default protected branches from main
  • targets main with GitHub's ~DEFAULT_BRANCH ruleset 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 Status Checks

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 = true

Use this only after accepting the provider drift risk.

Bypass Actors

Branch, tag, and push governance can use bypass actors:

branch_ruleset_bypass_actors = [
  {
    actor_id    = 12345
    actor_type  = "Integration"
    bypass_mode = "always"
  }
]

Validation rules:

  • OrganizationAdmin requires actor_id = 1
  • DeployKey omits actor_id
  • RepositoryRole, Team, and Integration require actor_id

For production, a GitHub App used by Terraform is usually cleaner than a personal token.

Release Tags

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.

Optional Push Guardrails

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       = 255

The module requires at least one push rule input when manage_push_ruleset = true.

Security Model

Always-on defaults:

  • manage_vulnerability_alerts = true
  • enable_vulnerability_alerts = true
  • manage_automated_security_fixes = true
  • enable_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 = true

Leave manage_security_and_analysis = false when organization policy, split licensing, or GitHub plan constraints centrally control these settings.

Labels

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.

Managed Files

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.

Dependabot Version Updates

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"
YAML

Importing Existing Repositories

Use 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:develop

Apply only after settings changes like Wiki, Projects, merge methods, and ruleset enforcement are understood.

Important Inputs

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.

Design Decisions

  • 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_id by 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.

Testing

Local verification:

terraform fmt -check -recursive
terraform init -backend=false
terraform validate
terraform test -no-color

Example 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)
done

The CI workflow in .github/workflows/terraform.yml runs the same checks.

Inputs And Outputs

See:

  • variables.tf
  • outputs.tf

Provider And GitHub Limitations

  • Required status check names must match GitHub check contexts exactly.
  • Pull request authors cannot satisfy required approvals with their own reviews.
  • require_last_push_approval requires someone other than the latest pusher to approve.
  • Branch and tag rulesets on private repositories require GitHub Pro, Team, or Enterprise Cloud.
  • Ruleset evaluate mode 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_file updates 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.

Primary References

About

Terraform module for managing a GitHub repository.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages