The keystone of the GitHub module library — provisions a
github_repositoryand 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.
- 🏛️ Creates a
github_repositorywith 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 viagithub_repository_autolink_reference. - 🛡️ Toggles automated Dependabot security-fix PRs via
github_repository_dependabot_security_updates, wired tovulnerability_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 wiresrepository = 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
🔌 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.
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
Resource inventory
- 🏛️
github_repository.this— the primary repository (the resource this module is named for). - 🌿
github_branch_default.default— sets/renames the default branch whenvar.default_branchis supplied (singleton viafor_each). - 🏷️
github_repository_topics.topics— authoritative topic list; overwrites all topics (singleton viafor_each). - 🔗
github_repository_autolink_reference.autolinks—for_eachmap of autolink references keyed by a caller string. - 🛡️
github_repository_dependabot_security_updates.security— toggles automated Dependabot security-fix PRs (singleton viafor_each). - 🌱
github_branch.branches—for_eachmap of additional branches created beyond the default (keyed by branch name by convention). - 🔑
github_repository_deploy_key.deploy_keys—for_eachmap of repository deploy keys (SSH);read_onlydefaults totrue(write keys are an explicit opt-in). - 📄
github_repository_pages.pages— standalone GitHub Pages site, rendered only whenvar.pages_siteis set (singleton viafor_each).
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
integrations/github |
~> 6.0 (current 6.12.1) |
⚠️ Provider source isintegrations/github— neverhashicorp/github(deprecated). Migrate old state withterraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.ℹ️ v5 → v6 schema notes that bite:
security_and_analysissub-blocks (advanced_security,secret_scanning*) require GitHub Advanced Security (Enterprise / licensed). The repository'sdefault_branchattribute is deprecated in favour ofgithub_branch_default— this module reads through the managed branch first and falls back only when unmanaged.
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
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 = truecreates an initialmainbranch sodefault_branchand other branch-scoped siblings have something to target. Without it (or a template/push),github_branch_defaulthas no branch to set.
| 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.
| 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 |
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, withvulnerability_alerts,dependabot_security_updates,delete_branch_on_merge, andarchive_on_destroyall 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. Useauto_init = true, atemplate, or push a branch before applyingdefault_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_topicsoverwrites the entire topic list. Do not also set topics inline elsewhere — this resource owns them. Settopics = []to clear all topics; leavenullto 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_idsis 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
}
}
⚠️ templateis 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_rulesetfor enforced PR review, required status checks, and no force-push onmain.
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 bothid(name) andrepo_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_branchdefaults to the repository default (main); point it at another branch to chain (asrelease/1.0does offdevelop). Outputsbranch_namesandbranch_refsare keyed by these same map keys.
⚠️ The base branch must already exist (auto_init, atemplate, 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_onlydefaults totrue. 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_idsis emitted keyed by these map keys (ci_readonly).
⚠️ All deploy-key fields are ForceNew — rotatingkey/title/read_onlyre-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 standalonegithub_repository_pagesresource).pages_urlis emitted (the rendered site URL) only whenpages_siteis 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
nameonce and every wire breaks — treat a rename as a breaking change.
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, defaultprivate) - 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(defaulttrue), and the four*_commit_title/*_commit_messageenums (all validated) - Create-time (ForceNew):
auto_init,gitignore_template,license_template - Lifecycle / security:
archived,archive_on_destroy(defaulttrue),vulnerability_alerts(defaulttrue),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(defaulttrue; requiresvulnerability_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_onlydefaulttrue; 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, "/")
}))
})| 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, notags_all— GitHub has neither. No outputs aresensitive(this module manages no secrets).
id/namesemantics. Forgithub_repository,idis the repository name — that is whyrepository = module.repository.idworks everywhere.full_nameadds theowner/prefix.node_idvsrepo_id.node_idis the GraphQL global node ID (ruleset bypass actors, GraphQL automation);repo_idis the numeric REST database ID used by org-scopedselected_repository_idsand 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.nameis a rename (in-place) but a breaking change for downstream wires. - Authoritative vs additive.
github_repository_topicsis authoritative — it overwrites the entire topic list, so topics are not set inline ongithub_repository.this(they would fight).default_branchis likewise owned here when supplied. - Default-branch read path. The repository's own
default_branchattribute is deprecated in v6 (reads steer throughgithub_branch_default). Thedefault_branchoutput 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 legacymodaz_github_branch_protection(v3 API) for new work; both consumeid+default_branchfrom here. - No secret handling here. This module owns no Actions/Dependabot/Codespaces secrets — those live in the Actions/CI-CD family and are marked
sensitivethere. - 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_onlyby default and fully ForceNew.var.deploy_keysdefaults each key toread_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_pagesresource (var.pages_site), andpages_urlis emitted only for that path. When something outside this module manages the repo's inline pages, addlifecycle { ignore_changes = [pages] }there. vulnerability_alertsstays inline — no standalone resource. Vulnerability alerts are managed by thevulnerability_alertsargument ongithub_repository.this(secure defaulttrue). The module deliberately does not also declare the standalonegithub_repository_vulnerability_alertsresource — doing so would double-manage the same setting and cause perpetual drift between the two.- Extra branches need a base branch.
github_branchcreates a branch fromsource_branch(defaultmain); the repository must already have that base branch (viaauto_init, atemplate, or a prior push) or branch creation fails. Chain branches by pointing one branch'ssource_branchat another.source_sha, when set, overridessource_branch.
- 🔒 Private by default —
visibility = "private";internal/publicare explicit opt-ins. - 🛡️ Vulnerability surface on —
vulnerability_alerts = trueanddependabot_security_updates = trueby default. - 🧹 Clean by default —
delete_branch_on_merge = truekeeps the branch list tidy. - 🪦 Safe destroy —
archive_on_destroy = trueprotects 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_keysdefault toread_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_authvariables; auth and target org are the caller's provider block. - 📦 No GitHub-absent concepts — no
tags, notags_all, notimeoutstail, no ARNs.
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.
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/applyrequire a livegithubprovider (GITHUB_OWNER+ a PAT / GitHub App).init/validate/fmtrun fully offline.
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
| 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 |
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.
- Plan/edition: GitHub Free works for private repos. Some settings (
security_and_analysisblocks like secret scanning,advanced_security) require GitHub Advanced Security (Enterprise / licensed).internalvisibility requires the org to belong to a GitHub Enterprise. - Dependabot:
github_repository_dependabot_security_updatesrequiresvulnerability_alerts = trueon the repository (the module wires this dependency). - Org settings:
members_can_create_repositories(and its public/private/internal sub-flags) must permit the chosenvisibility. - Rate limits: bulk
for_eachrepository and autolink creation hits the REST API per item — watch secondary rate limits on large applies.
integrations/githubprovider —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