Enforces one consistent issue-label taxonomy and milestone scheme across a whole fleet of repositories β from a single module call. Authoritative labels with the standard set baked in (GitHub defaults retained, plus priority / security / compliance), and
for_eachmilestones for release cadence. Config & governance only β never issue content. Built for integrations/github v6.x.
This composite module standardizes the two pieces of issue infrastructure that every repository should share:
- π·οΈ Authoritative issue labels β
github_issue_labels, one for_each entry per repository. A single canonical taxonomy (the standard set) applied fleet-wide, with per-repo additions or full overrides where needed. - ποΈ Milestones β
github_repository_milestone, a for_each collection keyed by a stable handle, for release cadence and planning. - π§± The standard taxonomy baked in β the nine GitHub built-ins (retained on purpose) plus governance labels (
priority: *,security,compliance,blocked,tech-debt, β¦). - π« Issue content deliberately excluded β
github_issueis runtime/operational data, not infrastructure, and is never managed here.
π‘ Why it matters: When every repository carries the same labels and milestone scheme, triage, reporting, and automation work the same way everywhere. One call enforces the standard; drift is visible; and a regulated FI gets first-class
security/compliancelabels in every repo from day one.
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!
flowchart LR
repo["tf-mod-github-repository<br/>(keystone β emits id = repo name)"]
issue["tf-mod-github-issue-standards<br/>(THIS module)"]
ruleset["tf-mod-github-repository-ruleset"]
collab["tf-mod-github-repository-collaborators"]
files["tf-mod-github-repository-files"]
repo -->|"repositories keys = id"| issue
repo -->|"milestones[].repository = id"| issue
repo -->|"repository = id"| ruleset
repo -->|"repository = id"| collab
repo -->|"repository = id"| files
style issue fill:#8957E5,color:#fff
style repo fill:#24292F,color:#fff
This module consumes repository names from tf-mod-github-repository and emits label-set ids, the managed-repository set, and milestone id/number maps for audit, governance reporting, and release automation. See the Cross-Module Contract.
flowchart TD
dl["var.default_labels<br/>standard taxonomy"]
rp["var.repositories<br/>keyed by repo name"]
msin["var.milestones<br/>keyed by handle"]
this["github_issue_labels.this<br/>for_each repo β AUTHORITATIVE label set"]
msr["github_repository_milestone.milestones<br/>for_each handle"]
data["data.github_repository.milestone_repos<br/>for_each repo β derives owner"]
dl --> this
rp --> this
msin --> msr
msin --> data
data -->|"owner = split full_name"| msr
style this fill:#8957E5,color:#fff
Resource inventory:
github_issue_labels.thisβ the primary resource: the authoritative label set for each repository (for_eachovervar.repositories, keyed by repo name). It rebuilds each repo's entire label list from the computed final set.github_repository_milestone.milestonesβfor_eachmap of milestones (keyed by a stable handle);title,description,due_date,state.data.github_repository.milestone_reposβ read-only helper, keyed by milestone handle, used only to derive each milestone's requiredownerfrom the repo'sfull_name(soowneris never a module variable). NeedsMetadata: read.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
integrations/github |
~> 6.0 (current 6.12.1) |
β οΈ Useintegrations/github, never the deprecatedhashicorp/github. Migrate old state withterraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.
βΉοΈ Authoritative plural vs. additive singular. This module uses
github_issue_labels(plural) β it owns a repository's entire label list. Do not also manage labels on the same repo withgithub_issue_label(singular, additive); the two fight over policy. Use the singular only on repos this module does not manage.
βΉοΈ Milestone
ownerquirk.github_repository_milestoneis the rare GitHub resource that requiresownerand does not default it to the provider β this module derives it for you (see Architecture Notes). Works on any plan (Free / Team / Enterprise).
tf-mod-github-issue-standards/
βββ providers.tf # terraform block + integrations/github ~> 6.0 (no provider block)
βββ variables.tf # default_labels (taxonomy), repositories, milestones
βββ main.tf # github_issue_labels.this + github_repository_milestone.milestones + owner data source
βββ outputs.tf # label_set_ids, managed_label_repositories, label_names, milestone_* maps
βββ SCOPE.md # design contract: in/out-of-scope, consumes/emits, token scopes, prerequisites
βββ README.md # this file
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {} # gets the full standard taxonomy (incl. the GitHub defaults)
}
}π The defaults are secure-and-safe: the baked-in taxonomy includes the GitHub built-in labels, so this authoritative set never silently deletes
bug/enhancement/documentation/ β¦
| Input | Type | Source |
|---|---|---|
repositories (map keys) |
string β bare repo name, known at plan |
tf-mod-github-repository (id / name) |
milestones[].repository |
string β bare repo name (may be a module output) |
tf-mod-github-repository (id output) |
βΉοΈ A milestone's required
owneris not consumed β it is derived inside the module from the repo'sfull_namevia a read-only data source, soownernever appears as a variable.
| Output | Description | Consumed by |
|---|---|---|
label_set_ids |
Map: repo β github_issue_labels id (the repo name) |
Audit / drift detection |
managed_label_repositories |
Set of repos whose taxonomy is managed here | Governance reporting |
label_names |
Map: repo β sorted list of managed label names | Governance / audit |
milestone_ids |
Map: handle β id (owner/repository/number) |
Release tooling, audit |
milestone_numbers |
Map: handle β GitHub milestone number | Workflow / API automation |
milestone_repositories |
Map: handle β repository name | Downstream wiring |
Sourced from this repo's
SCOPE.md.
1οΈβ£ Minimal β one repo, the full standard taxonomy
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {} # empty object => receives var.default_labels as-is
}
}The repository gets the entire standard set (GitHub defaults + governance labels) with zero configuration.
2οΈβ£ Minimal explicit β a small custom set that RETAINS the GitHub defaults
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"sandbox" = {
labels = [
# github_issue_labels is AUTHORITATIVE β list the defaults you want to keep
{ name = "bug", color = "d73a4a", description = "Something isn't working" },
{ name = "enhancement", color = "a2eeef", description = "New feature or request" },
{ name = "question", color = "d876e3", description = "Further information is requested" },
#...plus your own
{ name = "spike", color = "5319e7", description = "Time-boxed investigation" },
]
}
}
}
β οΈ A fulllabelsoverride replaces the repo's entire label list β anything not listed (including GitHub defaults) is deleted.
3οΈβ£ The standard taxonomy across several repositories
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {}
"ledger-service" = {}
"auth-gateway" = {}
}
}All three repositories are standardized on the identical taxonomy from one call.
4οΈβ£ Standard taxonomy + extra_labels per repo
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {
extra_labels = [
{ name = "area: payments", color = "1d76db" },
{ name = "pci", color = "ee0701", description = "Touches the cardholder data environment" },
]
}
"ledger-service" = {
extra_labels = [
{ name = "area: ledger", color = "1d76db" },
]
}
}
}π‘
extra_labelsare appended on top ofvar.default_labels. The final set must have unique names (case-insensitive) β the module validates this.
5οΈβ£ Full per-repo override (bespoke set, defaults ignored)
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"marketing-site" = {
labels = [
{ name = "content", color = "0e8a16" },
{ name = "design", color = "5319e7" },
{ name = "bug", color = "d73a4a", description = "Something isn't working" },
]
}
}
}When labels is set, var.default_labels is ignored for that repo β it gets exactly this list.
6οΈβ£ Replace the org-wide default taxonomy
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
# Override the baked-in taxonomy with your own org standard.
default_labels = [
{ name = "bug", color = "d73a4a", description = "Something isn't working" }, # keep a GitHub default
{ name = "enhancement", color = "a2eeef", description = "New feature or request" }, # keep a GitHub default
{ name = "security", color = "ee0701", description = "Security-sensitive" },
{ name = "compliance", color = "5319e7", description = "Regulatory review required" },
{ name = "priority: high", color = "d93f0b" },
]
repositories = {
"payments-api" = {}
"ledger-service" = {}
}
}
β οΈ Overridingdefault_labelsdrops any GitHub built-ins you don't re-list β include the ones you want to keep.
7οΈβ£ A single milestone
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
milestones = {
"payments-api/v1.1" = {
repository = "payments-api"
title = "v1.1.0"
description = "Hardening + ISO-20022 message support"
due_date = "2026-09-30"
}
}
}8οΈβ£ Milestones for_each β a quarterly release cadence
locals {
cadence = {
"payments-api/2026-q3" = { title = "2026 Q3", due_date = "2026-09-30" }
"payments-api/2026-q4" = { title = "2026 Q4", due_date = "2026-12-31" }
"payments-api/2027-q1" = { title = "2027 Q1", due_date = "2027-03-31" }
}
}
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
milestones = {
for handle, m in local.cadence : handle => {
repository = "payments-api"
title = m.title
due_date = m.due_date
}
}
}π‘ Keep handle keys stable β they key every milestone output map. Renaming a handle churns that entry.
9οΈβ£ Repositories & milestones wired from tf-mod-github-repository
module "repository" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
name = "payments-api"
visibility = "private"
}
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {} # β static key = repo name (must be known at plan)
}
milestones = {
"payments-api/v1.1" = {
repository = module.repository.id # β value MAY be a module output (read deferred to apply)
title = "v1.1.0"
due_date = "2026-09-30"
}
}
depends_on = [module.repository] # labels have no implicit ref β order them after the repo
}π‘ Label map keys must be known at plan, so use the literal repo name (the same value
module.repository.idresolves to) and adddepends_on. A milestone'srepositoryis a value, so it can be wired straight frommodule.repository.id.
π Secure / governance variant β enforced priority & risk labels π
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
# The baked-in default_labels already include these; shown explicitly to make
# the enforced governance taxonomy auditable in one place.
default_labels = [
{ name = "bug", color = "d73a4a", description = "Something isn't working" },
{ name = "documentation", color = "0075ca", description = "Docs" },
{ name = "enhancement", color = "a2eeef", description = "New feature or request" },
{ name = "priority: critical", color = "b60205", description = "Production-impacting or a regulatory deadline" },
{ name = "priority: high", color = "d93f0b" },
{ name = "priority: medium", color = "fbca04" },
{ name = "priority: low", color = "0e8a16" },
{ name = "security", color = "ee0701", description = "Security-sensitive β do not include PII in public detail" },
{ name = "compliance", color = "5319e7", description = "Regulatory / compliance review required (privacy-regulation / ECOA / regulatory)" },
]
repositories = {
"payments-api" = {}
"ledger-service" = {}
"auth-gateway" = {}
}
}π Standardizing
securityandcompliancelabels across every repo makes regulated-data and regulatory-review issues consistently filterable for audit.
1οΈβ£1οΈβ£ Labels and milestones for the same repository
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = {
extra_labels = [{ name = "area: payments", color = "1d76db" }]
}
}
milestones = {
"payments-api/v1.1" = { repository = "payments-api", title = "v1.1.0", due_date = "2026-09-30" }
"payments-api/v1.2" = { repository = "payments-api", title = "v1.2.0" }
}
}1οΈβ£2οΈβ£ Standardize a fleet at scale (maps for both collections)
locals {
repos = ["payments-api", "ledger-service", "auth-gateway", "notifications", "reporting"]
}
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = { for r in local.repos : r => {} }
milestones = {
for r in local.repos : "${r}/2026-q3" => {
repository = r
title = "2026 Q3"
due_date = "2026-09-30"
}
}
}
β οΈ Bulkfor_eachacross many repos Γ labels can trip GitHub's secondary rate limits β see Troubleshooting.
1οΈβ£3οΈβ£ Strip a repo's labels entirely (use with care)
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"archived-poc" = {
labels = [] # AUTHORITATIVE empty set => deletes ALL labels on the repo
}
}
}
β οΈ An emptylabelslist deletes every label on the repository. This is intentional and irreversible for those labels β confirm before applying.
ποΈ End-to-end composition (full suite, outputs β inputs)
# 1 Β· Keystone repository
module "repository" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
name = "payments-api"
visibility = "private"
vulnerability_alerts = true
# has_issues defaults true β required for labels & milestones to apply
}
# 2 Β· Standardized labels + release milestones, wired from the repository
module "issue_standards" {
source = "git::https://github.com/microsoftexpert/tf-mod-github-issue-standards?ref=v1.0.0"
repositories = {
"payments-api" = { # static key = repo name (plan-known)
extra_labels = [
{ name = "area: payments", color = "1d76db" },
{ name = "pci", color = "ee0701", description = "Touches the cardholder data environment" },
]
}
}
milestones = {
"payments-api/v1.1" = {
repository = module.repository.id # β repository.id (read deferred to apply)
title = "v1.1.0"
due_date = "2026-09-30"
}
"payments-api/v1.2" = {
repository = module.repository.id
title = "v1.2.0"
}
}
depends_on = [module.repository] # ensure the repo exists before labels/milestones
}
output "managed_repos" { value = module.issue_standards.managed_label_repositories }
output "milestone_ids" { value = module.issue_standards.milestone_ids }ποΈ The canonical wiring: the keystone creates the repo; this module standardizes its labels (static key +
depends_on) and milestones (repository = module.repository.id, deferred read derives the owner). Every cross-module edge in the family DAG is exercised.
Shared taxonomy
default_labels(list(object), default = the standard taxonomy) β the shared label set applied to every repo that doesn't fully override it. Ships the 9 GitHub built-ins plus governance labels. Colors are 6-hex without#.
Collections
repositories(map(object), default{}) β keyed by bare repo name; each value is{ labels?, extra_labels? }.labelsis a full override;extra_labelsappends todefault_labels.milestones(map(object), default{}) β keyed by a stable handle; each value is{ repository, title, description?, due_date?, state? }.
Full object schemas
default_labels = list(object({
name = string # unique within the set (case-insensitive)
color = optional(string) # 6 hex chars, no '#'; defaults to "ededed"
description = optional(string)
}))
repositories = map(object({ # key = bare repository name
labels = optional(list(object({ # FULL override; when set, default_labels is ignored for this repo
name = string
color = optional(string) # defaults to "ededed"
description = optional(string)
})))
extra_labels = optional(list(object({ # appended to default_labels (ignored when labels is set)
name = string
color = optional(string) # defaults to "ededed"
description = optional(string)
})), [])
}))
milestones = map(object({ # key = stable handle (keys every milestone output)
repository = string # BARE repo name (no 'owner/' prefix)
title = string
description = optional(string)
due_date = optional(string) # "yyyy-mm-dd"
state = optional(string, "open") # "open" | "closed"
}))| Output | Description |
|---|---|
label_set_ids |
Map of repository name β github_issue_labels id (the repo name). |
managed_label_repositories |
Set of repository names whose label taxonomy is managed here. |
label_names |
Map of repository name β sorted list of managed label names. |
milestone_ids |
Map of milestone handle β id (owner/repository/number). |
milestone_numbers |
Map of milestone handle β GitHub-assigned milestone number. |
milestone_repositories |
Map of milestone handle β repository name. |
βΉοΈ No issue content is emitted β this module manages label/milestone configuration only.
- π¨
github_issue_labelsis AUTHORITATIVE β this is the single biggest surprise. It replaces a repository's entire label list and deletes any label not in the rendered set, including GitHub's built-in defaults (bug,enhancement,documentation, β¦). How to keep them: the baked-invar.default_labelsalready includes the GitHub defaults β leave it as-is, or, if you overridedefault_labels/ a repo'slabels, re-list every default you want to retain. An empty set (labels = []) deletes all labels on the repo. - π Authoritative plural vs. additive singular. Prefer
github_issue_labels(this module) when you own a repo's whole taxonomy. Usegithub_issue_label(singular, additive) only to add a label to a repo you do not standardize here. Never point both at the same repo β they fight. - π« Why
github_issueis excluded. Issues are operational content, created and closed continuously by people and automation. Managing them as Terraform state would mean drift on every comment and constant churn β so the boundary is drawn at standards (labels + milestones), not content. - πͺͺ Milestone
owneris derived, never a variable.github_repository_milestoneuniquely requiresownerand does not default it to the provider. The module reads each milestone's repo (data.github_repository, keyed by milestone handle) and takesowner = split("/", full_name)[0]. Keying the data source by handle (not by a set of repo names) is deliberate: it keepsfor_eachkeys plan-known sorepository = module.repository.idworks (the read defers to apply). A repo used by N milestones is read N times (no dedup) β the trade-off for that robustness. - π Milestone id format.
github_repository_milestone.idisowner/repository/number(3-part slash; e.g.FinancialPartnerscasey/payments-api/3), andnumberis assigned by GitHub. (Import isowner/repository/number.) - β»οΈ ForceNew. Changing a milestone's
repository(and therefore its derivedowner) recreates the milestone;title/description/due_date/stateupdate in place. - π¨ Label color. A 6-character hex code without the leading
#(e.g.d73a4a). The module validates this on every label. - ποΈ
for_eachkeys must be plan-known. A repository's label set is keyed by the repo name as a map key, so keys must be literals known at plan β you cannot key on a not-yet-created repo's output. Use the literal name anddepends_on = [module.repository]for ordering. (Milestonerepositoryis a value, so it may be a module output.) - π·οΈ No tags, no timeouts. GitHub has neither a resource-tagging concept nor a
timeoutsblock on these resources β there is intentionally notags/timeoutstail.
- π·οΈ Standardize by default. One taxonomy and milestone scheme, applied fleet-wide from a single call.
- π Safe authoritative. The baked-in defaults include the GitHub built-ins, so the authoritative set never silently deletes them β opt out explicitly and visibly.
- π§ Explicit overrides, no surprise merges. Per-repo
labels(full override) orextra_labels(additive) β the final set is computed predictably and validated for uniqueness. - ποΈ Governance for a regulated FI.
securityandcompliancelabels are part of the standard set so regulated-data and regulatory-review issues are consistently filterable. - πͺͺ Owner stays a provider concern. Derived from provider context, never a module variable.
- π« Out of scope by design. Issue content (
github_issue) is operational data, not IaC.
terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output
β οΈ Always pin the module source to a tag β?ref=v1.0.0β never a branch. A moving branch ref makes applies non-reproducible.
The offline proof gate (no live org needed):
terraform fmt -check # zero formatting differences
terraform validate # zero errors
tflint # core rules β no dedicated GitHub ruleset existsplan / apply require a configured integrations/github provider (PAT / GitHub App) with the token scopes below and the target owner / GITHUB_OWNER set. Labels and milestones require Issues enabled on each target repository.
| Identity | Grant |
|---|---|
| Classic PAT | repo |
| Fine-grained PAT / GitHub App | Repository β Issues: read/write (labels & milestones) + Metadata: read (the owner-derivation data source) |
- Plan/edition: any (Free / Team / Enterprise) β no Enterprise-only features.
- Issues must be enabled on each target repository (
has_issues = trueontf-mod-github-repository). Labels and milestones fail on repos with Issues disabled. - Secondary rate limits: standardizing many repositories via
for_eachis many API writes β watch GitHub's secondary rate limits on large fleets.
label_set_ids = {
"ledger-service" = "ledger-service"
"payments-api" = "payments-api"
}
managed_label_repositories = ["ledger-service", "payments-api"]
label_names = {
"payments-api" = ["area: payments", "blocked", "bug", "compliance", "dependencies",
"documentation", "duplicate", "enhancement", "good first issue",
"help wanted", "invalid", "needs-triage", "pci", "priority: critical",
"priority: high", "priority: low", "priority: medium", "question",
"security", "tech-debt", "wontfix"]
}
milestone_ids = {
"payments-api/v1.1" = "FinancialPartnerscasey/payments-api/3"
"payments-api/v1.2" = "FinancialPartnerscasey/payments-api/4"
}
milestone_numbers = { "payments-api/v1.1" = 3, "payments-api/v1.2" = 4 }
milestone_repositories = { "payments-api/v1.1" = "payments-api", "payments-api/v1.2" = "payments-api" }
| Symptom | Cause | Resolution |
|---|---|---|
GitHub default labels (bug, enhancement, β¦) disappeared |
github_issue_labels is authoritative and your set omitted them |
Keep the baked-in default_labels, or re-list the defaults in your override |
404 / 422 on label or milestone apply |
Issues disabled on the repo | Enable Issues (has_issues = true on tf-mod-github-repository) |
Invalid for_each argument β¦ keys β¦ not known until apply |
Used module.repository.id as a repositories map key |
Use the literal repo name as the key; add depends_on = [module.repository] |
403 secondary rate limit on bulk apply |
Too many writes across many repos Γ labels | Re-run apply (idempotent); reduce -parallelism; stagger large fleets |
every β¦ color must be a 6-character hex code β¦ |
Color had a leading # or wrong length |
Use 6 hex chars, no # (e.g. d73a4a) |
β¦ due_date must be in "yyyy-mm-dd" format |
Wrong date format | Use yyyy-mm-dd (e.g. 2026-09-30) |
the final label set β¦ must have unique names |
default_labels collides with a repo's extra_labels |
Remove the duplicate, or use a full labels override |
| Labels "fight" / flip every apply | A github_issue_label (singular) also targets the same repo |
Manage the repo's labels in exactly one place β this module owns the whole set |
Milestone created with the wrong owner / Not Found |
Static repo name for a repo that doesn't exist yet | Wire repository = module.repository.id (deferred read) or ensure the repo exists first |
tf-mod-github-repositoryβ keystone module emittingid(repo name) andfull_nameintegrations/githubprovider reference β issue labels, repository milestone, and the repository data source- GitHub REST API documentation β labels, milestones, and secondary rate limits
- GitHub documentation β managing labels and milestones for issues and pull requests