Skip to content

microsoftexpert/tf-mod-github-app-installation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🐙 GitHub App Installation Repositories Terraform Module

Authoritatively governs which repositories an already-installed GitHub App can access — declares the complete selected-repository allow-list for one App installation behind a single, least-privilege module boundary. Built for integrations/github v6.x.

Terraform GitHub provider module type resources


🧩 Overview

This module wraps a single github_app_installation_repositories resource (this) and gives you:

  • 🔐 Authoritative repo access for an installed App — declare the exact set of repositories an existing GitHub App installation may reach; the resource owns the whole set.
  • 🧹 Drift-free reconciliation — any repository the App can currently reach but absent from selected_repositories is removed on apply. State becomes the access model.
  • 🎯 Set-typed, order-independentselected_repositories is a set(string) of bare repo names, so reordering never churns the plan and adding/removing one name is a minimal diff.
  • 🔗 Composable by name — wire repository names straight from tf-mod-github-repository (module.repository.id), no numeric ids to look up.
  • Fail-fast validationinstallation_id must be the numeric installation id (digits only); every repo entry must be a bare name (no owner/repo, no ids); the set must be non-empty.
  • 🚧 Clear boundary — installs nothing; it only governs an installation that already exists. App installation is a one-time, out-of-band action.

💡 Why it matters: A GitHub App with broad repo access is a high-value path into your source supply chain. Making the App's repository reach an authoritative, reviewed allow-list means the Terraform diff is the access review — no repo is silently in scope.


❤️ 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 governs access an already-installed GitHub App has, not the repository itself — it consumes repository names from the keystone repository module and is a terminal leaf: nothing in this suite wires from its outputs.

flowchart LR
 repo["tf-mod-github-repository<br/>(keystone)"]
 appinst["tf-mod-github-app-installation<br/>(THIS MODULE)"]

 repo -->|"id (repo name)"| appinst

 style appinst fill:#8957E5,color:#fff
 style repo fill:#24292F,color:#fff
Loading

This module consumes bare repository names (the id output of tf-mod-github-repository) into selected_repositories; it emits id / selected_repositories for access-review and compliance tooling — see the Typical wiring section. It is a terminal grant with no downstream module consumers in this suite.


🧬 What this module builds

A single, flat, authoritative repository-access association for an already-installed GitHub App — both arguments are required scalars/sets, so there are no nested or dynamic blocks.

flowchart TD
 this["github_app_installation_repositories.this<br/>(keystone)<br/>authoritative repo allow-list for an installed App"]

 style this fill:#8957E5,color:#fff
Loading

📁 Module Structure

tf-mod-github-app-installation/
├── providers.tf # terraform >= 1.12.0; integrations/github ~> 6.0
├── variables.tf # installation_id, selected_repositories
├── main.tf # github_app_installation_repositories.this
├── outputs.tf # id, installation_id, selected_repositories
├── SCOPE.md # resource scope, token scopes, prerequisites, emits, gotchas
└── README.md # this file

⚙️ Quick Start

module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678" # numeric installation id — NOT the app id / client id / slug
  selected_repositories = ["platform-api", "platform-web"]
}

⚠️ The owner/organization and authentication (PAT / OAuth / GitHub App) are provider concerns — configure them on the caller's provider "github" block, never as module inputs. The provider identity must be an organization owner.


📋 GitHub Prerequisites

  • The App must already be installed. This module does not install the GitHub App — installation is a one-time action via the GitHub UI or an App manifest, performed out of band. The module governs only which repositories an existing installation can reach. You supply the installation's id; you never create the installation here.
  • You need the numeric installation_id — not the App id, OAuth client id, or App slug. Find it at Settings → Installed GitHub Apps → your app → Configure; it is the number in the resulting URL (.../installations/<installation_id>). Or read it programmatically from the github_organization_app_installations data source (installations[*].id) or the REST API (see the Example Library).
  • Provider identity must be an organization owner. Governing another App's repository access is an org-admin operation — a repo-scoped token returns 403 / Resource not accessible.
  • Installation should use "only select repositories" access. This resource manages the selected repository set; confirm the installation's repository_selection is selected (not all) before applying. Converting an "all repositories" installation to a managed selected set is a meaningful change — make sure it is intended.
  • Authoritative / overwrite behavior. On apply the module sets the installation's repository access to exactly selected_repositories. Any repository the App can currently reach but omitted here is removed. Inventory the installation's current access before the first apply.
  • Plan / edition: any org plan that supports GitHub Apps (Free / Team / Enterprise). No Enterprise-only features are used by this module.

🔑 Required token scopes / GitHub App permissions

Scope / Permission Required for Notes
admin:org (classic PAT) Managing a GitHub App installation's repository access Org-owner-level scope.
Organization administration: Read & write (fine-grained PAT / GitHub App) The fine-grained equivalent of admin:org for this operation The provider identity must be an organization owner, or an org-wide App granted this permission.

⚠️ This is an org-admin operation — the running identity must be an organization owner. A repo-scoped token fails with 403 / Resource not accessible. ℹ️ This resource is not compatible with the GitHub App Installation authentication method. Authenticate the provider with a PAT or OAuth token belonging to an org owner — not an App installation token.


🔌 Typical wiring

Derived from SCOPE.md Emits — one row per output.

Output Type Typically consumed by
id string (installation id) Composition / depends_on ordering and audit. Equals the installation id and the terraform import id.
installation_id string Audit / reporting — echoes the governed installation id.
selected_repositories set(string) Access-review / compliance tooling — the exact repository access in effect after apply.

🧠 Architecture Notes

  • id is the installation id. github_app_installation_repositories uses the installation id as its resource id (and output.id returns exactly that). There is no node_id / slug / repo_id / html_url here because the resource manages an association set, not a first-class GitHub object — and there are no ARNs on GitHub. The import id is also the installation id (terraform import... 12345678).
  • Authoritative overwrite semantics. Applying sets the installation's selected repositories to exactly var.selected_repositories. Any repo the App reaches today but not listed here is removed on apply. This is the opposite of the additive singular resource (below): there is no "leave the others alone" mode. The trade-off is the strength — the state is the complete, reviewable access model.
  • installation_id is NOT the App id or client id. Three different numbers/strings live in GitHub's App world: the App id (identifies the App globally), the OAuth client id (Iv1.…-style, for OAuth flows), and the installation id (identifies this org's installation of the App). Only the installation id belongs here. Find it under Settings → Installed GitHub Apps → app → Configure (the number in the URL), or via the github_organization_app_installations data source (installations[*].id, matched on app_slug). The module validates it is digits-only to catch the common mistake of pasting a slug or client id.
  • installation_id is ForceNew (the identity). It is both the import id and the computed id, so changing it points the module at a different installation — Terraform destroys the old association and creates a new one. Treat a change as a re-target, not an in-place edit.
  • Modeled as a set(string) of bare names. The provider schema is a TypeSet of strings (despite the docs prose saying "list"), so order is irrelevant, duplicates collapse, and adding/removing one name yields a minimal, stable plan. Repos are referenced by name (e.g. platform-api), never owner/repo and never numeric ids — the owner is a provider concern.
  • Cannot drop to zero — and destroy leaves one repo behind. GitHub does not allow a "selected repositories" installation with zero repositories. The module enforces a non-empty set at plan time. For the same reason, destroying this resource leaves the App installed on one repository (a documented provider behavior) — finish removal in the GitHub UI, or switch the installation to "all repositories".
  • Authoritative plural vs additive singular — why the plural. uses the authoritative github_app_installation_repositories (this module) so the App's reach is declarative and drift-free, the same rationale as github_repository_collaborators over its singular. The additive singular github_app_installation_repository manages one repo at a time and leaves others untouched — useful only when per-repo grants must be delegated across separate states/owners. Documented alternative:
# DOCUMENTED ALTERNATIVE — not this module. Additive, one repo per resource,
# no authoritative reconcile (other repos the app reaches are left alone).
resource "github_app_installation_repository" "api" {
installation_id = "12345678"
repository = "platform-api"
}
  • No tags, no timeouts. GitHub has no resource-tagging concept and this resource exposes no timeouts block — there is deliberately no tags / tags_all / timeouts tail and no owner / token / app_auth inputs.
  • Eventual consistency & secondary rate limits. Installation/repository changes can take a moment to propagate; a re-plan shortly after apply may briefly show movement. Driving many installations via for_each issues many serial API writes and can trip GitHub's secondary (abuse) rate limit (403 with Retry-After) — apply in batches, lower -parallelism, or rely on the provider's back-off.

📚 Example Library

1 · Minimal — one installation, two repositories
module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = ["platform-api", "platform-web"]
}

ℹ️ The set must be non-empty — GitHub cannot install an App on zero selected repositories.

2 · Cross-module — wire a repo name from `tf-mod-github-repository`
module "repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
  name       = "platform-api"
  visibility = "private"
}

module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = [module.repository.id] # wire by id (== the repository name)
}
3 · Build the allow-list from a `for_each`'d repository module
module "repos" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
  for_each = toset(["platform-api", "platform-web", "data-pipeline"])

  name       = each.key
  visibility = "private"
}

module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = [for r in module.repos : r.id] # every repo the module created
}
4 · `for_each` at the module level — several installed Apps, each with its own allow-list
locals {
  app_access = {
    ci_bot = {
      installation_id = "12345678"
      repositories    = ["platform-api", "platform-web"]
    }
    security_scanner = {
      installation_id = "23456789"
      repositories    = ["platform-api", "data-pipeline", "regulated-loan-service"]
    }
  }
}

module "app_repos" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"
  for_each = local.app_access

  installation_id       = each.value.installation_id
  selected_repositories = each.value.repositories
}
5 · Two Apps over a shared repository group
locals {
  shared_repos = ["platform-api", "platform-web"]
  app_ids = {
    ci_bot           = "12345678"
    security_scanner = "23456789"
  }
}

module "app_repos" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"
  for_each = local.app_ids

  installation_id       = each.value
  selected_repositories = local.shared_repos
}

ℹ️ Each installation is authoritative only for itself — two Apps can both include the same repository without conflict. The module never reconciles one App's set against another's.

6 · Secure / hardened — explicit, reviewed allow-list
# Regulated repository set — the App's access is an explicit, reviewed allow-list.
# AUTHORITATIVE: applying this REMOVES the App from any repository not listed here.
module "scanner_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id = "23456789"
  selected_repositories = [
    "regulated-loan-service",
    "regulated-loan-service-docs",
  ]
}

🔒 Because the set is authoritative, anyone who grants the App another repository on GitHub.com has that grant revoked on the next apply — exactly what you want for a regulated source supply chain. Keep the list short and reviewed.

7 · Discover the `installation_id` instead of hard-coding it
data "github_organization_app_installations" "all" {}

locals {
  # Match the installation by the App's URL-friendly slug.
  ci_bot = one([
    for inst in data.github_organization_app_installations.all.installations :
    inst if inst.app_slug == "my-ci-bot"
  ])
}

module "ci_bot_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = local.ci_bot.id # the numeric installation id, discovered
  selected_repositories = ["platform-api", "platform-web"]
}

💡 one(...) returns the single matching installation (or errors if the slug is ambiguous/absent), so a typo fails the plan instead of silently targeting the wrong App.

8 · Revoke access authoritatively — drop a name
# There is no "remove" verb: you revoke an App's access to a repo by REMOVING
# its name from the set and letting the authoritative reconcile do the work.
# Before: ["platform-api", "platform-web", "legacy-service"]
# After: ["platform-api", "platform-web"] # legacy-service is revoked on apply
module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = ["platform-api", "platform-web"]
}

⚠️ The plan shows the dropped repository explicitly as a removal — review it; this is a real access revocation.

9 · End-to-end composition — repositories + App access
module "repository_api" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
  name       = "platform-api"
  visibility = "private"
}

module "repository_web" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-repository?ref=v1.0.0"
  name       = "platform-web"
  visibility = "private"
}

module "ci_app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = [module.repository_api.id, module.repository_web.id]
}
10 · Auditing output — expose the applied access across a fleet
module "app_repos" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"
  for_each = local.app_access

  installation_id       = each.value.installation_id
  selected_repositories = each.value.repositories
}

output "app_repo_access" {
  description = "Map of app key => repository names that App can access (for access review / compliance)."
  value       = { for k, m in module.app_repos : k => m.selected_repositories }
}
11 · Importing an existing installation's repository set
module "app_repos" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-app-installation?ref=v1.0.0"

  installation_id       = "12345678"
  selected_repositories = ["platform-api", "platform-web"]
}
# Then, once (the import id is the installation id):
# terraform import 'module.app_repos.github_app_installation_repositories.this' 12345678

⚠️ On the first plan after import, any repository the App can currently reach but missing from selected_repositories shows as a removal — review carefully before applying.

12 · Documented alternative — the additive singular resource
# NOT this module. github_app_installation_repository manages ONE repo's grant
# at a time and never reconciles the others — use it only when per-repo grants
# must be delegated across separate states or owners.
resource "github_app_installation_repository" "api" {
  installation_id = "12345678"
  repository      = "platform-api"
}

resource "github_app_installation_repository" "web" {
  installation_id = "12345678"
  repository      = "platform-web"
}

ℹ️ Prefer the authoritative plural (this module) for declarative, drift-free access. Reach for the singular only when you explicitly need additive, non-authoritative grants.


📦 Inputs (high-level)

Identity

  • installation_id (string, required) — the numeric GitHub App installation id (e.g. "12345678"). ForceNew (it is the resource id and import id). Not the App id, OAuth client id, or App slug. Validated digits-only.

Repository allow-list

  • selected_repositories (set(string), required) — the authoritative set of bare repository names the installation may access. Must be non-empty; each entry must be a bare name (letters, digits, ., _, -) — not owner/repo, not a URL, not a numeric id. Wire from tf-mod-github-repository (module.repository.id).

No tags, no timeouts, no owner / token / app_auth inputs — auth and the target organization are provider concerns.


🧾 Outputs

Output Description
id Resource id — the installation id (also the terraform import id). Primary cross-module reference.
installation_id The GitHub App installation id whose repository access is governed (echoed for audit/reporting).
selected_repositories The applied authoritative set of repository names the App can access — the exact access in effect after apply.

ℹ️ No sensitive outputs — an installation id and a set of repository names are not secrets.


🧱 Design Principles

  • 🔐 Authoritative = drift-free — the App's complete repository reach lives in state; out-of-band grants are reconciled away. The diff is the access review.
  • 🎯 Explicit least-privilege allow-list — every repository the App can touch is named and reviewable; nothing is implicitly in scope.
  • 🧱 Separation of duties — App installation (a one-time human/UI action) is deliberately kept separate from App repository access (this module, an org-admin/security concern with its own lifecycle and token scope).
  • 🏢 Provider owns auth & owner — no owner / token / app_auth inputs; the module is portable across orgs by swapping provider config.
  • Fail fast — digits-only installation_id, non-empty + bare-name validation on selected_repositories; invalid input fails at plan, not apply.
  • 🚫 No tags / timeouts tail — GitHub has neither; the module surface stays minimal.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output

⚠️ Review the plan before applying — because the set is authoritative, any repository dropped from selected_repositories (or attached out-of-band) is a real access revocation/grant. ⚠️ Pin the version. Always source at ?ref=v1.0.0 — never a branch. Branch sources drift silently and break reproducibility.


🔍 Troubleshooting

Symptom Cause Resolution
403 / Resource not accessible / Must have admin rights to Organization The provider identity is not an org owner / lacks the right scope Use a classic PAT with admin:org, or a fine-grained token / App with Organization administration: Read & write, as an organization owner.
Auth fails when using a GitHub App installation token This resource is not compatible with the GitHub App Installation auth method Authenticate the provider with a PAT or OAuth token belonging to an org owner.
A repository shows as a removal on every plan Authoritative reconcile — the repo was granted in the UI/out-of-band but is not in selected_repositories Add the name to selected_repositories, or accept the removal if the grant should not exist.
selected_repositories must contain at least one repository name Empty set passed GitHub cannot install an App on zero repositories — supply at least one name; to fully remove access, uninstall the App in the UI.
After destroy, the App is still on one repository Documented GitHub behavior — an installation cannot drop to zero repositories Finish removal in the GitHub UI, or switch the installation to "all repositories".
404 Not Found on the installation Wrong installation_id (e.g. the App id, client id, or slug was used) Use the numeric installation id from Installed GitHub Apps → app → Configure (URL), or the github_organization_app_installations data source.
Plan wants to replace the whole resource installation_id changed (it is ForceNew — the resource id) Expected when re-targeting a different installation; confirm the replace is intended.
Each selected_repositories entry must be a bare repository name… An entry used owner/repo, a URL, or a numeric id Pass repository names only; the owner/org is a provider concern.
installation_id must be the numeric GitHub App installation id (digits only) A slug or Iv1.… client id was passed Use the numeric installation id.
403 You have exceeded a secondary rate limit on bulk apply Many installations driven via for_each issue many serial API writes Apply in smaller batches, lower terraform apply -parallelism, or let the provider's back-off retry pace the run.

🔗 Related Docs

  • integrations/github provider — github_app_installation_repositories resource reference (the authoritative plural used here)
  • integrations/github provider — github_app_installation_repository resource reference (the additive singular alternative)
  • integrations/github provider — github_organization_app_installations data source (discovering installation ids)
  • Sibling modules — tf-mod-github-repository (keystone; emits the repo id/name wired into selected_repositories), tf-mod-github-repository-collaborators, tf-mod-github-team
  • GitHub docs — reviewing and managing installed GitHub Apps in an organization; selected vs. all repository access

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages