Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🐙 GitHub Repository Terraform Module

The keystone of the GitHub module library — provisions a github_repository and the tightly-coupled resources that define its identity (default branch, authoritative topics, autolink references, Dependabot security updates) behind one composable boundary. Private-by-default, vulnerability alerts on, archive-on-destroy on. Built for integrations/github v6.x.

Terraform GitHub provider module type resources


🧩 Overview

  • 🏛️ Creates a github_repository with deeply-typed, validated inputs for visibility, merge strategy, feature toggles, Pages, security & analysis, and template provisioning.
  • 🌿 Owns the default branch authoritatively via github_branch_default (switch or rename) — never guessed.
  • 🏷️ Owns the topic list authoritatively via github_repository_topics — one source of truth, no drift against inline topics.
  • 🔗 Maps autolink references (JIRA- → ticket URL) from a caller-keyed map via github_repository_autolink_reference.
  • 🛡️ Toggles automated Dependabot security-fix PRs via github_repository_dependabot_security_updates, wired to vulnerability_alerts.
  • 🔒 Secure by default — visibility = "private", vulnerability_alerts = true, dependabot_security_updates = true, delete_branch_on_merge = true, archive_on_destroy = true.

💡 Why it matters: Nearly every other modaz_github_* module wires repository = module.repository.id. Getting the repository's identity, default branch, and security posture right once, here, is what every ruleset, collaborator set, webhook, environment, and Actions config downstream depends on.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

This module is the keystone — siblings consume its outputs; it consumes nothing from them.

flowchart LR
 repo["modaz_github_repository<br/>(keystone · THIS module)"]

 subgraph Repository
 ruleset["modaz_github_repository_ruleset"]
 bp["modaz_github_branch_protection"]
 collab["modaz_github_repository_collaborators"]
 hook["modaz_github_repository_webhook"]
 files["modaz_github_repository_files"]
 env["modaz_github_repository_environment"]
 end

 subgraph ActionsCICD["Actions / CI-CD"]
 actrepo["modaz_github_actions_repository"]
 actorg["modaz_github_actions_organization"]
 dependabot["modaz_github_dependabot"]
 end

 subgraph Teams
 team["modaz_github_team"]
 end

 repo -->|id / default_branch| ruleset
 repo -->|id / default_branch| bp
 repo -->|id| collab
 repo -->|id| hook
 repo -->|id| files
 repo -->|id| env
 repo -->|id / repo_id| actrepo
 repo -->|repo_id| actorg
 repo -->|repo_id| dependabot
 repo -->|id| team

 style repo fill:#8957E5,color:#fff,stroke:#24292F,stroke-width:3px
Loading

🔌 Consumes: nothing from sibling modules (optionally an existing template repo, caller-supplied). Emits: id, node_id, repo_id, full_name, default_branch, clone URLs, and more — see the Cross-Module Contract.


🧬 What this module builds

flowchart TD
 this["github_repository.this<br/>(primary)"]

 this --> def["github_branch_default.default<br/>for_each · 0..1 · var.default_branch"]
 this --> topics["github_repository_topics.topics<br/>for_each · 0..1 · authoritative topic list"]
 this --> auto["github_repository_autolink_reference.autolinks<br/>for_each · map(object) · var.autolinks"]
 this --> sec["github_repository_dependabot_security_updates.security<br/>for_each · 0..1 · requires vulnerability_alerts"]
 this --> branches["github_branch.branches<br/>for_each · map(object) · var.branches"]
 this --> keys["github_repository_deploy_key.deploy_keys<br/>for_each · map(object) · var.deploy_keys (read_only default)"]
 this --> pages["github_repository_pages.pages<br/>for_each · 0..1 · var.pages_site (standalone)"]

 style this fill:#8957E5,color:#fff,stroke:#24292F,stroke-width:3px
Loading

Resource inventory

  • 🏛️ github_repository.this — the primary repository (the resource this module is named for).
  • 🌿 github_branch_default.default — sets/renames the default branch when var.default_branch is supplied (singleton via for_each).
  • 🏷️ github_repository_topics.topics — authoritative topic list; overwrites all topics (singleton via for_each).
  • 🔗 github_repository_autolink_reference.autolinks — for_each map of autolink references keyed by a caller string.
  • 🛡️ github_repository_dependabot_security_updates.security — toggles automated Dependabot security-fix PRs (singleton via for_each).
  • 🌱 github_branch.branches — for_each map of additional branches created beyond the default (keyed by branch name by convention).
  • 🔑 github_repository_deploy_key.deploy_keys — for_each map of repository deploy keys (SSH); read_only defaults to true (write keys are an explicit opt-in).
  • 📄 github_repository_pages.pages — standalone GitHub Pages site, rendered only when var.pages_site is set (singleton via for_each).

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
integrations/github ~> 6.0 (current 6.12.1)

⚠️ Provider source is integrations/github — never hashicorp/github (deprecated). Migrate old state with terraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.

ℹ️ v5 → v6 schema notes that bite: security_and_analysis sub-blocks (advanced_security, secret_scanning*) require GitHub Advanced Security (Enterprise / licensed). The repository's default_branch attribute is deprecated in favour of github_branch_default — this module reads through the managed branch first and falls back only when unmanaged.


📁 Module Structure

modaz_github_repository/
├── providers.tf # terraform{} + required_providers (integrations/github ~> 6.0); no provider{} block
├── variables.tf # name → optional repo config → optional blocks → child collections
├── main.tf # github_repository.this + 4 role-named supporting resources
├── outputs.tf # id, node_id, repo_id, full_name, clone URLs, default_branch, …
├── SCOPE.md # design contract: in/out-of-scope, consumes/emits, token scopes, prerequisites
└── README.md # this file

⚙️ Quick Start

module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name        = "loan-origination-api"
  description = "Loan origination service API"
  visibility  = "private"

  auto_init      = true # produce an initial commit so default_branch can be managed
  default_branch = { branch = "main" }

  topics = ["terraform-managed", "lending", "api"]
}

💡 auto_init = true creates an initial main branch so default_branch and other branch-scoped siblings have something to target. Without it (or a template/push), github_branch_default has no branch to set.


🔌 Cross-Module Contract

Consumes

Input Type Source
template.owner / template.repository string (optional) an existing template repository — caller-supplied, not a sibling module

ℹ️ The keystone consumes nothing from sibling modules — that is what makes it the keystone.

Emits

Output Description Consumed by
id Repository name — the primary cross-module reference Nearly every modaz_github_* module (repository = module.repository.id)
name Repository name Reporting, sibling wiring
node_id GraphQL global node ID Ruleset bypass actors, GraphQL automation
repo_id Numeric database ID modaz_github_actions_organization / modaz_github_dependabot (selected_repository_ids), runner groups
full_name owner/name Reporting, CODEOWNERS tooling
html_url Web URL Documentation, dashboards
http_clone_url / ssh_clone_url / git_clone_url Clone URLs CI bootstrap, downstream automation
default_branch Resolved default branch name modaz_github_repository_ruleset / modaz_github_branch_protection targeting
visibility Effective visibility (public/private/internal) Governance reporting
topics Managed authoritative topic set (null when unmanaged) Inventory, search tooling
autolink_ids Map of autolink reference IDs keyed by var.autolinks keys Audit, drift reporting

📚 Example Library

1️⃣ Minimal — the smallest call that produces a real repository
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "internal-tooling"
}

🔒 Even minimal, this is private, with vulnerability_alerts, dependabot_security_updates, delete_branch_on_merge, and archive_on_destroy all on by default.

2️⃣ Default branch management
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name      = "service-mesh-config"
  auto_init = true

  default_branch = {
    branch = "main"
  }
}

⚠️ The target branch must already exist. Use auto_init = true, a template, or push a branch before applying default_branch.

3️⃣ Rename the default branch
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "legacy-app"

  default_branch = {
    branch = "main"
    rename = true # rename the current default (e.g. "master") to "main"
  }
}
4️⃣ Authoritative topics
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name   = "data-pipeline"
  topics = ["terraform-managed", "etl", "data-platform"]
}

⚠️ github_repository_topics overwrites the entire topic list. Do not also set topics inline elsewhere — this resource owns them. Set topics = [] to clear all topics; leave null to leave them unmanaged.

5️⃣ Autolink references (Jira / ticketing)
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "core-banking"

  autolinks = {
    jira = {
      key_prefix          = "JIRA-"
      target_url_template = "https://jira.financialpartners.com/browse/JIRA-<num>"
    }
    incident = {
      key_prefix          = "INC-"
      target_url_template = "https://servicenow.financialpartners.com/incident/<num>"
      is_alphanumeric     = false
    }
  }
}

💡 autolink_ids is emitted keyed by the same map keys (jira, incident) so callers can index them.

6️⃣ Merge strategy & PR hygiene
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "frontend-app"

  allow_merge_commit  = false
  allow_rebase_merge  = false
  allow_squash_merge  = true
  allow_auto_merge    = true
  allow_update_branch = true

  squash_merge_commit_title   = "PR_TITLE"
  squash_merge_commit_message = "PR_BODY"

  delete_branch_on_merge = true # secure default — kept explicit here for clarity
}
7️⃣ GitHub Pages (workflow build)
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "engineering-docs"

  pages_site = {
    build_type = "workflow"
    cname      = "docs.financialpartners.com"
  }
}
8️⃣ GitHub Pages (legacy branch build)
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "marketing-site"

  pages_site = {
    build_type = "legacy"
    source = {
      branch = "gh-pages"
      path   = "/docs"
    }
  }
}
9️⃣ Provision from a template repository
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "new-microservice"

  template = {
    owner                = "FinancialPartners"
    repository           = "service-template"
    include_all_branches = false
  }
}

⚠️ template is create-time only (ForceNew) — changing it later forces replacement of the repository.

🔟 GitHub Advanced Security (Enterprise / licensed)
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "payments-core"

  security_and_analysis = {
    advanced_security               = "enabled"
    secret_scanning                 = "enabled"
    secret_scanning_push_protection = "enabled"
  }
}

🔒 Requires a GitHub Advanced Security license (Enterprise). See GitHub Prerequisites. Not supported on public repos via this block (GHAS is automatic there).

1️⃣1️⃣ 🔒 Secure / hardened variant
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name        = "regulated-ledger"
  description = "privacy-regulation-scoped ledger service — locked down"
  visibility  = "private"

  # Hygiene
  auto_init              = true
  delete_branch_on_merge = true
  allow_merge_commit     = false
  allow_rebase_merge     = false
  allow_squash_merge     = true
  allow_forking          = false

  # Sign-off + supply chain
  web_commit_signoff_required = true
  vulnerability_alerts        = true
  dependabot_security_updates = true

  # Advanced Security (Enterprise / licensed)
  security_and_analysis = {
    advanced_security               = "enabled"
    secret_scanning                 = "enabled"
    secret_scanning_push_protection = "enabled"
  }

  # Never hard-delete history
  archive_on_destroy = true

  default_branch = { branch = "main" }
  topics         = ["terraform-managed", "regulated", "ledger"]
}

🔒 Pair this with modaz_github_repository_ruleset for enforced PR review, required status checks, and no force-push on main.

1️⃣2️⃣ for_each at scale — fleet of repos from a map(object)
variable "repositories" {
  type = map(object({
    description = optional(string)
    topics      = optional(list(string))
    visibility  = optional(string, "private")
  }))
  default = {
    "loan-api"      = { description = "Loan API", topics = ["api", "lending"] }
    "loan-worker"   = { description = "Loan worker", topics = ["worker", "lending"] }
    "loan-frontend" = { description = "Loan UI", topics = ["frontend", "lending"] }
  }
}

module "repository" {
  source   = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"
  for_each = var.repositories

  name        = each.key
  description = each.value.description
  visibility  = each.value.visibility
  topics      = each.value.topics

  auto_init      = true
  default_branch = { branch = "main" }
}

⚠️ Bulk creation hits the REST API per repo (and per autolink). Watch secondary rate limits — see Troubleshooting.

1️⃣3️⃣ Integration — wiring a ruleset to this repository
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name           = "service-a"
  auto_init      = true
  default_branch = { branch = "main" }
}

module "repository_ruleset" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-ruleset?ref=v1.0.0"

  repository  = module.repository.id # ← keystone output
  name        = "protect-default-branch"
  enforcement = "active"
  # target the resolved default branch emitted by the keystone
  target_branch = module.repository.default_branch
}
1️⃣4️⃣ Integration — wiring repo_id into Actions org / Dependabot selected repositories
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "shared-actions-consumer"
}

module "actions_organization" {
  source = "git::https://github.com/microsoftexpert/terraform-github-actions-organization?ref=v1.0.0"

  # org-level Actions secret scoped to selected repositories by numeric id
  selected_repository_ids = [module.repository.repo_id] # ← repo_id, not id
}

💡 Org-scoped resources select repositories by numeric repo_id, not by name. That is why the keystone emits both id (name) and repo_id.

1️⃣5️⃣ Additional branches via for_each
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name      = "service-with-gitflow"
  auto_init = true # ensures a base branch ("main") exists to branch from

  branches = {
    develop = { branch = "develop" }                                # off the default branch ("main")
    release = { branch = "release/1.0", source_branch = "develop" } # chained off "develop"
  }
}

💡 Keys are the branch names by convention. Every branch needs a base branch to start from — source_branch defaults to the repository default (main); point it at another branch to chain (as release/1.0 does off develop). Outputs branch_names and branch_refs are keyed by these same map keys.

⚠️ The base branch must already exist (auto_init, a template, or a prior push) or branch creation fails.

1️⃣6️⃣ 🔒 Read-only deploy key
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name = "ci-consumer"

  deploy_keys = {
    ci_readonly = {
      title = "CI read-only checkout"
      key   = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID...example... ci@financialpartners.com"
      # read_only defaults to true — omit it for a read-only key
    }
  }
}

🔒 read_only defaults to true. A write-capable deploy key (read_only = false) is an explicit, audited opt-in — prefer a scoped GitHub App or fine-grained token over a write deploy key wherever possible. Supply only the public key here; keep the private half in a secrets manager and never commit it. deploy_key_ids is emitted keyed by these map keys (ci_readonly).

⚠️ All deploy-key fields are ForceNew — rotating key/title/read_only re-creates the key.

1️⃣7️⃣ GitHub Pages (standalone resource)
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name      = "engineering-handbook"
  auto_init = true

  # Standalone github_repository_pages resource
  pages_site = {
    build_type = "legacy"
    cname      = "handbook.financialpartners.com"
    source = {
      branch = "gh-pages"
      path   = "/"
    }
  }
}

ℹ️ GitHub Pages is managed exclusively through pages_site (this standalone github_repository_pages resource). pages_url is emitted (the rendered site URL) only when pages_site is set.

🏗️ 1️⃣8️⃣ End-to-end composition (full suite wired outputs → inputs)
# ── Keystone repository ───────────────────────────────────────────────
module "repository" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository?ref=v1.0.0"

  name        = "lending-platform"
  description = "Lending platform mono-service"
  visibility  = "private"

  auto_init                   = true
  default_branch              = { branch = "main" }
  vulnerability_alerts        = true
  dependabot_security_updates = true
  archive_on_destroy          = true

  topics = ["terraform-managed", "lending", "platform"]

  autolinks = {
    jira = {
      key_prefix          = "JIRA-"
      target_url_template = "https://jira.financialpartners.com/browse/JIRA-<num>"
    }
  }
}

# ── Enforced ruleset on the default branch ────────────────────────────
module "repository_ruleset" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-ruleset?ref=v1.0.0"

  repository    = module.repository.id
  target_branch = module.repository.default_branch
  enforcement   = "active"
}

# ── Least-privilege collaborators ─────────────────────────────────────
module "repository_collaborators" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-collaborators?ref=v1.0.0"

  repository = module.repository.id
}

# ── CI/CD webhook ─────────────────────────────────────────────────────
module "repository_webhook" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-webhook?ref=v1.0.0"

  repository = module.repository.id
}

# ── Deployment environment ────────────────────────────────────────────
module "repository_environment" {
  source = "git::https://github.com/microsoftexpert/terraform-github-repository-environment?ref=v1.0.0"

  repository = module.repository.id
}

# ── Org-level Actions secret scoped to this repo by numeric id ─────────
module "actions_organization" {
  source = "git::https://github.com/microsoftexpert/terraform-github-actions-organization?ref=v1.0.0"

  selected_repository_ids = [module.repository.repo_id]
}

💡 Every sibling references the keystone by output. Change the repo name once and every wire breaks — treat a rename as a breaking change.


📥 Inputs

Identifying name

  • name (required) — repository name; the primary cross-module reference. A rename is a GitHub rename (not a destroy) but breaks downstream wires.

Scalar repository config

  • Description / discovery: description, homepage_url, visibility (validated: public/private/internal, default private)
  • Features: has_issues, has_discussions, has_projects, has_wiki, is_template, allow_forking
  • Merge strategy: allow_merge_commit, allow_squash_merge, allow_rebase_merge, allow_auto_merge, allow_update_branch, delete_branch_on_merge (default true), and the four *_commit_title / *_commit_message enums (all validated)
  • Create-time (ForceNew): auto_init, gitignore_template, license_template
  • Lifecycle / security: archived, archive_on_destroy (default true), vulnerability_alerts (default true), ignore_vulnerability_alerts_during_read, web_commit_signoff_required

Optional blocks

  • security_and_analysis — object({ advanced_security, code_security, secret_scanning, … }) (GHAS-gated)
  • template — object({ owner, repository, include_all_branches }) (ForceNew)

Child-resource collections

  • topics — list(string) (authoritative; null = unmanaged, [] = clear)
  • default_branch — object({ branch, rename })
  • autolinks — map(object({ key_prefix, target_url_template, is_alphanumeric }))
  • dependabot_security_updates — bool (default true; requires vulnerability_alerts = true)
  • branches — map(object({ branch, source_branch, source_sha })) (extra branches; needs an existing base branch to start from)
  • deploy_keys — map(object({ title, key, read_only })) (read_only default true; supply the public SSH key; all fields ForceNew)
  • pages_site — object({ build_type, cname, source }) (standalone GitHub Pages site)
Full object schemas for nested inputs
# security_and_analysis (each field "enabled"/"disabled")
object({
 advanced_security = optional(string) # GHAS — Enterprise/licensed
 code_security = optional(string)
 secret_scanning = optional(string) # GHAS — Enterprise/licensed
 secret_scanning_ai_detection = optional(string)
 secret_scanning_non_provider_patterns = optional(string)
 secret_scanning_push_protection = optional(string) # GHAS — Enterprise/licensed
})

# template (ForceNew)
object({
 owner = string
 repository = string
 include_all_branches = optional(bool, false)
})

# default_branch
object({
 branch = string
 rename = optional(bool, false)
})

# autolinks
map(object({
 key_prefix = string # e.g. "JIRA-"
 target_url_template = string # must contain <num>
 is_alphanumeric = optional(bool, true)
}))

# branches
map(object({
 branch = string # branch name to create, e.g. "develop"
 source_branch = optional(string) # branch to start from (provider default: "main")
 source_sha = optional(string) # commit SHA to start from; overrides source_branch
}))

# deploy_keys (all fields ForceNew)
map(object({
 title = string # human-readable label
 key = string # the PUBLIC SSH key
 read_only = optional(bool, true) # secure default true; false = write-capable
}))

# pages_site (standalone github_repository_pages)
object({
 build_type = optional(string) # "legacy" | "workflow"
 cname = optional(string)
 source = optional(object({ # required when build_type = "legacy"
 branch = string
 path = optional(string, "/")
 }))
})

🧾 Outputs

Output Description
id Repository name — primary cross-module reference
name Repository name
node_id GraphQL global node ID
repo_id Numeric database ID
full_name owner/name
html_url Web URL
http_clone_url HTTPS clone URL
ssh_clone_url SSH clone URL
git_clone_url Git protocol clone URL
default_branch Resolved default branch (managed value, else repository's computed default)
visibility Effective visibility
topics Managed authoritative topic set — ⚠️ null when topics are unmanaged
autolink_ids Map of autolink reference IDs keyed by var.autolinks keys — empty map when none
branch_names Map of created branch names keyed by var.branches keys — empty map when none
branch_refs Map of branch refs (refs/heads/<branch>) keyed by var.branches keys — empty map when none
deploy_key_ids Map of deploy key IDs (repository:key_id) keyed by var.deploy_keys keys — empty map when none
pages_url Rendered GitHub Pages site URL — ⚠️ null unless var.pages_site is set

ℹ️ No arn, no tags_all — GitHub has neither. No outputs are sensitive (this module manages no secrets).


🧠 Architecture Notes

  • id / name semantics. For github_repository, id is the repository name — that is why repository = module.repository.id works everywhere. full_name adds the owner/ prefix.
  • node_id vs repo_id. node_id is the GraphQL global node ID (ruleset bypass actors, GraphQL automation); repo_id is the numeric REST database ID used by org-scoped selected_repository_ids and runner groups. Emit both — siblings need different ones.
  • ForceNew fields. auto_init, template {}, and effectively the create-time templates are immutable — flipping them forces replacement. name is a rename (in-place) but a breaking change for downstream wires.
  • Authoritative vs additive. github_repository_topics is authoritative — it overwrites the entire topic list, so topics are not set inline on github_repository.this (they would fight). default_branch is likewise owned here when supplied.
  • Default-branch read path. The repository's own default_branch attribute is deprecated in v6 (reads steer through github_branch_default). The default_branch output prefers the managed branch and falls back to the computed attribute only when unmanaged, keeping the emit meaningful either way.
  • Rulesets vs branch protection. Prefer modaz_github_repository_ruleset (modern Rules API) over the legacy modaz_github_branch_protection (v3 API) for new work; both consume id + default_branch from here.
  • No secret handling here. This module owns no Actions/Dependabot/Codespaces secrets — those live in the Actions/CI-CD family and are marked sensitive there.
  • Eventual consistency & secondary rate limits. The GitHub REST API is eventually consistent and enforces secondary rate limits on rapid writes. Bulk for_each (many repos, many autolinks, branches, or deploy keys) can trip them — see Troubleshooting.
  • Deploy keys are read_only by default and fully ForceNew. var.deploy_keys defaults each key to read_only = true; a write-capable key is an explicit opt-in. The provider treats every field (title / key / read_only) as immutable — any change re-creates the key (a brief access gap on rotation). Supply only the public key; the private half never touches Terraform. The provider field is required, so the module always passes the concrete (defaulted) value.
  • GitHub Pages: standalone resource only. Pages is managed exclusively via the standalone github_repository_pages resource (var.pages_site), and pages_url is emitted only for that path. When something outside this module manages the repo's inline pages, add lifecycle { ignore_changes = [pages] } there.
  • vulnerability_alerts stays inline — no standalone resource. Vulnerability alerts are managed by the vulnerability_alerts argument on github_repository.this (secure default true). The module deliberately does not also declare the standalone github_repository_vulnerability_alerts resource — doing so would double-manage the same setting and cause perpetual drift between the two.
  • Extra branches need a base branch. github_branch creates a branch from source_branch (default main); the repository must already have that base branch (via auto_init, a template, or a prior push) or branch creation fails. Chain branches by pointing one branch's source_branch at another. source_sha, when set, overrides source_branch.

🧱 Design Principles

  • 🔒 Private by default — visibility = "private"; internal/public are explicit opt-ins.
  • 🛡️ Vulnerability surface on — vulnerability_alerts = true and dependabot_security_updates = true by default.
  • 🧹 Clean by default — delete_branch_on_merge = true keeps the branch list tidy.
  • 🪦 Safe destroy — archive_on_destroy = true protects history from accidental hard deletes.
  • 🎯 One source of truth — topics and default branch are owned authoritatively, never split between inline and child resources.
  • 🔑 Least-privilege deploy keys — deploy_keys default to read_only = true; write-capable keys are an explicit opt-in. Only the public key is ever supplied.
  • 📄 One Pages owner — Pages is managed by exactly one path: the standalone pages_site (github_repository_pages).
  • 🚫 No provider concerns leak in — no owner / token / app_auth variables; auth and target org are the caller's provider block.
  • 📦 No GitHub-absent concepts — no tags, no tags_all, no timeouts tail, no ARNs.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires a configured github provider (GITHUB_OWNER + auth)
terraform apply
terraform output

⚠️ Always pin the module source to a tag — ?ref=v1.0.0, never a branch. Branches move; releases are immutable.


🧪 Testing

Offline proof gate (no live org required):

terraform fmt -check # zero formatting differences
terraform validate # zero errors
tflint # core rules — no dedicated GitHub ruleset exists

ℹ️ terraform plan/apply require a live github provider (GITHUB_OWNER + a PAT / GitHub App). init/validate/fmt run fully offline.


💬 Example Output

id = "lending-platform"
name = "lending-platform"
node_id = "R_kgDOL1a2b3"
repo_id = 789456123
full_name = "FinancialPartners/lending-platform"
html_url = "https://github.com/FinancialPartners/lending-platform"
http_clone_url = "https://github.com/FinancialPartners/lending-platform.git"
ssh_clone_url = "git@github.com:FinancialPartners/lending-platform.git"
git_clone_url = "git://github.com/FinancialPartners/lending-platform.git"
default_branch = "main"
visibility = "private"
topics = ["lending", "platform", "terraform-managed"]
autolink_ids = { "jira" = "12345678" }
branch_names = { "develop" = "develop" }
branch_refs = { "develop" = "refs/heads/develop" }
deploy_key_ids = { "ci_readonly" = "lending-platform:87654321" }
pages_url = null

🔍 Troubleshooting

Symptom Likely cause Resolution
403 / Resource not accessible by integration on create Token missing repo (classic) or Administration: write (fine-grained) Grant the scopes in Required token scopes
default_branch apply fails — branch not found Branch doesn't exist yet Set auto_init = true, use a template, or push the branch first
secret_scanning/advanced_security apply error GitHub Advanced Security not licensed, or set on a public repo License GHAS (Enterprise) / remove the block on public repos
Topics keep reverting / drift Topics also set inline elsewhere Let this module own them exclusively (github_repository_topics is authoritative)
dependabot_security_updates won't enable vulnerability_alerts = false Set vulnerability_alerts = true (the module wires the dependency)
You have exceeded a secondary rate limit on bulk apply Too many rapid REST writes (many repos/autolinks) Reduce -parallelism, apply in smaller batches, retry with backoff
internal visibility rejected Org not part of a GitHub Enterprise Use private, or move the org under Enterprise
Provider plugin not found / wrong provider hashicorp/github referenced somewhere Use integrations/github; migrate state with terraform state replace-provider
github_branch create fails — base branch not found The source_branch (default main) doesn't exist yet Set auto_init = true, use a template, or push the base branch before adding branches
Deploy key apply error / key already in use The same public key is attached to another repo, or the key is malformed Use a unique keypair per repo; supply the full public key line (ssh-ed25519 AAAA…)
Every plan wants to replace a deploy key A field changed — all deploy-key fields are ForceNew Expected; rotating key / title / read_only re-creates it. Plan rotations during a maintenance window
GitHub Pages won't enable Pages unavailable for the repo's plan/visibility, or a legacy build's source.branch is missing Enable Pages for the org/plan; ensure source.branch exists; a workflow build needs no source

🔗 Required token scopes / GitHub App permissions

Classic PAT scopes

  • repo — full control of repositories (create, settings, topics, default branch, security updates toggle)

Fine-grained PAT / GitHub App permissions

  • Administration: read/write — repo create, settings, default branch, Dependabot security-updates toggle
  • Contents: read/write — default branch, topics
  • Metadata: read — always required

⚠️ Auth and the target org (owner / GITHUB_OWNER) are provider concerns — never module variables.


🧷 GitHub Prerequisites

  • Plan/edition: GitHub Free works for private repos. Some settings (security_and_analysis blocks like secret scanning, advanced_security) require GitHub Advanced Security (Enterprise / licensed). internal visibility requires the org to belong to a GitHub Enterprise.
  • Dependabot: github_repository_dependabot_security_updates requires vulnerability_alerts = true on the repository (the module wires this dependency).
  • Org settings: members_can_create_repositories (and its public/private/internal sub-flags) must permit the chosen visibility.
  • Rate limits: bulk for_each repository and autolink creation hits the REST API per item — watch secondary rate limits on large applies.

🔗 Related Docs

  • integrations/github provider — github_repository, github_branch_default, github_repository_topics, github_repository_autolink_reference, github_repository_dependabot_security_updates
  • Sibling modules — modaz_github_repository_ruleset, modaz_github_branch_protection, modaz_github_repository_collaborators, modaz_github_repository_webhook, modaz_github_repository_files, modaz_github_repository_environment
  • This module's SCOPE.md — the authoritative design contract

About

Terraform module: terraform-github-repository

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages