Skip to content

microsoftexpert/tf-mod-github-actions-repository

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ™ GitHub Actions (Repository) Terraform Module

Repository-scoped GitHub Actions governance in one boundary β€” the Actions permissions policy (allowed actions + SHA-pinning), for_each collections of encrypted secrets and variables, and an optional reusable-workflow access level β€” all secure-by-default. Built for integrations/github v6.x.

Terraform GitHub provider module type resources


🧩 Overview

This composite module wraps the six tightly-coupled github_* resources that govern GitHub Actions for a single repository behind one boundary:

  • πŸ›‘οΈ Actions permissions policy (this) β€” controls which actions may run (all / local_only / selected) and enforces SHA-pinning to block mutable-tag supply-chain drift. Always applied, with least-privilege defaults.
  • πŸ” Actions secrets β€” a for_each map keyed by secret name; prefers pre-encrypted values (value_encrypted + key_id) so plaintext never enters code.
  • πŸ”§ Actions variables β€” a for_each map of non-secret vars.<NAME> values exposed to workflows.
  • 🌐 Reusable-workflow access level β€” an optional singleton controlling where this repo's actions/reusable workflows may be consumed (none / user / organization / enterprise).
  • πŸ”‘ Default workflow token scope β€” an optional singleton pinning the auto-provisioned GITHUB_TOKEN to read-only (workflow_permissions), so workflows must opt into write access explicitly. Distinct from the allowed-actions policy above.
  • πŸͺͺ OIDC subject-claim template β€” an optional singleton that hardens keyless OIDC federation (oidc_subject_claim_template) by scoping the sub claim workflows present to cloud providers (Azure, AWS).

πŸ’‘ Why it matters: Actions is the source-supply-chain execution plane. One module call gives a repository a locked-down, allow-listed, SHA-pinned Actions posture with its secrets and variables managed as code β€” no console clicks, no plaintext drift, no accidental Marketplace exposure.


❀️ 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

flowchart LR
 repo["tf-mod-github-repository<br/>(keystone)"]
 actrepo["tf-mod-github-actions-repository<br/>(this module)"]
 actorg["tf-mod-github-actions-organization"]
 runners["tf-mod-github-actions-runner-group"]
 env["tf-mod-github-repository-environment"]
 dep["tf-mod-github-dependabot"]

 repo -->|"repository (name)"| actrepo
 repo -->|repository| env
 repo -->|repository| dep
 actorg -.->|org Actions policy ceiling| actrepo
 runners -.->|runner availability| actrepo
 actrepo -->|secret_names / variable_names| env

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

This module consumes the repository name from the keystone tf-mod-github-repository (id output) and emits the policy id, secret/variable inventories, and the access level. See the Cross-Module Contract. The dashed edges are policy ceilings, not Terraform dependencies β€” tf-mod-github-actions-organization and tf-mod-github-actions-runner-group govern org-wide limits this repo-scoped module operates within.


🧬 What this module builds

flowchart TD
 input["var.repository<br/>(from tf-mod-github-repository)"]
 this["github_actions_repository_permissions.this<br/>PRIMARY β€” Actions policy"]
 secrets["github_actions_secret.secrets<br/>for_each secret_name"]
 vars["github_actions_variable.variables<br/>for_each variable_name"]
 access["github_actions_repository_access_level.access<br/>optional singleton"]
 workflow["github_workflow_repository_permissions.workflow<br/>optional β€” default GITHUB_TOKEN scope"]
 oidc["github_actions_repository_oidc_subject_claim_customization_template.oidc<br/>optional β€” OIDC subject-claim template"]

 input --> this
 input --> secrets
 input --> vars
 input --> access
 input --> workflow
 input --> oidc

 style this fill:#8957E5,color:#fff
 style input fill:#24292F,color:#fff
Loading

Resource inventory:

  • πŸ›‘οΈ github_actions_repository_permissions.this β€” the primary policy this module is named for. Always created; sets enabled, allowed_actions, sha_pinning_required, and (when allowed_actions = "selected") the allowed_actions_config allow-list.
  • πŸ” github_actions_secret.secrets β€” for_each over the secrets map; one encrypted Actions secret per key.
  • πŸ”§ github_actions_variable.variables β€” for_each over the variables map; one non-secret Actions variable per key.
  • 🌐 github_actions_repository_access_level.access β€” optional singleton, created only when actions_access_level is non-null.
  • πŸ”‘ github_workflow_repository_permissions.workflow β€” optional singleton; sets the default GITHUB_TOKEN scope (read/write) and the PR-approval flag. Created only when workflow_permissions is non-null.
  • πŸͺͺ github_actions_repository_oidc_subject_claim_customization_template.oidc β€” optional singleton; customizes the OIDC sub claim for keyless federation. Created only when oidc_subject_claim_template is non-null.

βœ… Provider / Versions

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

⚠️ Provider source is integrations/github β€” never the deprecated hashicorp/github. Migrate existing state with terraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.

ℹ️ Schema notes: sha_pinning_required is a v6-era field on github_actions_repository_permissions. The allowed_actions_config block is honored only when allowed_actions = "selected" β€” the provider rejects it otherwise, so the module gates it with a dynamic block. github_workflow_repository_permissions and github_actions_repository_oidc_subject_claim_customization_template both require admin access to the repo, and the OIDC template forbids include_claim_keys when use_default = true.


πŸ“ Module Structure

tf-mod-github-actions-repository/
β”œβ”€β”€ providers.tf # terraform{} + required_providers (integrations/github ~> 6.0)
β”œβ”€β”€ variables.tf # repository, permissions, workflow_permissions, actions_access_level, oidc_subject_claim_template, secrets, variables
β”œβ”€β”€ main.tf # permissions.this + secrets/variables for_each + access/workflow/oidc singletons
β”œβ”€β”€ outputs.tf # id, repository, allowed_actions, secret_*, variable_*, access_level, default_workflow_permissions, oidc_subject_claim_template
β”œβ”€β”€ SCOPE.md # in/out-of-scope, consumes/emits, token scopes, prerequisites
└── README.md # this file

βš™οΈ Quick Start

module "actions_repository" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"

  # Wire the repo name from the keystone module's id output.
  repository = module.repository.id
}

That single call applies the secure-default policy: Actions enabled, allowed_actions = "selected" (GitHub-owned + verified-creator actions only), and sha_pinning_required = true β€” with no secrets, variables, or access level managed.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
repository string (repo name) tf-mod-github-repository (id output)

Emits

Output Description Consumed by
id ID of the Actions permissions policy (= repository name) β€” primary reference Composition / dependency ordering
repository Repo name (echoed) Composition convenience
allowed_actions Resolved policy (all / local_only / selected) Audit / reporting
secret_names Set of managed Actions secret names (values never emitted) Audit / reporting
secret_ids Map of secret name => <repo>:<secret_name> ID Reporting
variable_names Map of variable name => value (non-secret) tf-mod-github-repository-environment / workflow config
variable_ids Map of variable name => <repo>:<variable_name> ID Reporting
access_level Resolved access level, or null when unmanaged Reporting
default_workflow_permissions Resolved default GITHUB_TOKEN scope (read / write), or null when unmanaged Audit / reporting
oidc_subject_claim_template Resolved OIDC subject-claim ({ use_default, include_claim_keys }), or null when unmanaged Audit / cloud federation policy

ℹ️ Sourced directly from SCOPE.md. Auth and the target org (owner / GITHUB_OWNER) are provider concerns β€” never module inputs.


πŸ“š Example Library

1️⃣ Minimal β€” secure defaults only
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"
}

Applies allowed_actions = "selected", GitHub-owned + verified actions, SHA-pinning on.

2️⃣ Repository 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 "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = module.repository.id # id == repo name
}

πŸ’‘ Wiring repository = module.repository.id makes Terraform create the repo before its Actions policy β€” no manual depends_on needed.

3️⃣ Allow-list extra trusted actions (selected + patterns)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  permissions = {
    allowed_actions = "selected"
    allowed_actions_config = {
      github_owned_allowed = true
      verified_allowed     = true
      patterns_allowed     = ["FinancialPartners/*", "hashicorp/setup-terraform@*"]
    }
  }
}
4️⃣ Local actions only (most restrictive runnable policy)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "internal-tooling"

  permissions = {
    allowed_actions = "local_only" # only actions defined in this repo may run
  }
}

πŸ”’ local_only blocks all Marketplace and third-party actions β€” strongest supply-chain stance for repos that ship their own actions.

5️⃣ Disable Actions entirely
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "archived-service"

  permissions = {
    enabled = false # no workflows run at all
  }
}
6️⃣ Actions secrets β€” pre-encrypted (preferred)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  secrets = {
    NPM_TOKEN = {
      value_encrypted = var.npm_token_encrypted # base64, sealed with the repo public key
      key_id          = var.repo_public_key_id
    }
  }
}

πŸ”’ Prefer value_encrypted (+ key_id) so plaintext never enters Terraform code. Note that both forms still land in state β€” treat state as sensitive regardless.

7️⃣ Actions secrets β€” plaintext (provider encrypts on upload)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  secrets = {
    SONAR_TOKEN = { value = var.sonar_token } # sensitive; encrypted by the provider
  }
}

⚠️ Provide exactly one of value or value_encrypted per secret β€” the module validates this and fails the plan otherwise.

8️⃣ Actions variables (non-secret vars.*)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  variables = {
    DEPLOY_REGION = { value = "us-east-1" }
    NODE_VERSION  = { value = "20" }
  }
}

⚠️ Variables are not secret β€” never put credentials here; use secrets.

9️⃣ Reusable-workflow access level (share within the org)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "shared-workflows"

  actions_access_level = "organization" # any repo in the org may call these reusable workflows
}

⚠️ Requires a private/internal repo on an org plan. organization/enterprise levels require the matching GitHub plan; public repos reject this resource.

πŸ”Ÿ Combined β€” policy + secrets + variables together
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  permissions = {
    allowed_actions = "selected"
    allowed_actions_config = {
      patterns_allowed = ["FinancialPartners/*"]
    }
  }

  secrets = {
    NPM_TOKEN = { value_encrypted = var.npm_token_encrypted, key_id = var.repo_public_key_id }
  }

  variables = {
    DEPLOY_REGION = { value = "us-east-1" }
  }
}
1️⃣1️⃣ πŸ”’ Secure / hardened variant
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = module.repository.id

  permissions = {
    enabled              = true
    allowed_actions      = "selected"
    sha_pinning_required = true # block mutable tags β€” pin every action to a commit SHA
    allowed_actions_config = {
      github_owned_allowed = true
      verified_allowed     = false # tighten: GitHub-owned only, no Marketplace verified
      patterns_allowed     = ["FinancialPartners/*"]
    }
  }

  workflow_permissions = {
    default_workflow_permissions     = "read" # read-only GITHUB_TOKEN
    can_approve_pull_request_reviews = false
  }

  oidc_subject_claim_template = {
    use_default        = false
    include_claim_keys = ["repository", "context", "ref"] # scope keyless OIDC federation
  }

  actions_access_level = "none" # do not let other repos consume these workflows

  secrets = {
    DEPLOY_KEY = { value_encrypted = var.deploy_key_encrypted, key_id = var.repo_public_key_id }
  }
}

πŸ”’ The locked-down posture: GitHub-owned + first-party allow-list only, SHA-pinning enforced, a read-only GITHUB_TOKEN, a scoped OIDC subject claim, reusable workflows not shared, and secrets pre-encrypted.

1️⃣2️⃣ Read-only workflow GITHUB_TOKEN (default token scope)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  workflow_permissions = {
    default_workflow_permissions     = "read" # least-privilege GITHUB_TOKEN
    can_approve_pull_request_reviews = false  # block Actions self-approving PRs
  }
}

πŸ”’ Pins the auto-provisioned GITHUB_TOKEN to read-only β€” workflows that push commits, publish packages, or open PRs must request elevated permissions: in their YAML explicitly. This is the repo analog of the org default-token policy and is distinct from the allowed-actions policy in permissions (which controls which actions run, not token scope).

1️⃣3️⃣ πŸͺͺ Hardened OIDC subject claim (keyless federation, e.g. to Azure)
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = "payments-api"

  oidc_subject_claim_template = {
    use_default        = false
    include_claim_keys = ["repository", "context", "ref"]
  }
}

πŸ”’ Scoping the OIDC sub claim to repository + context + ref lets a cloud federation policy (an Azure federated credential or AWS IAM trust) bind trust to a precise repo, environment, and branch instead of accepting any token from the org β€” a high-value supply-chain control for keyless deployment. Set use_default = true to fall back to GitHub's default claim (the module rejects include_claim_keys alongside use_default = true).

1️⃣4️⃣ for_each at scale β€” secrets and variables from map(object)
locals {
  app_secrets = {
    NPM_TOKEN   = { value_encrypted = var.npm_enc, key_id = var.kid }
    SONAR_TOKEN = { value_encrypted = var.sonar_enc, key_id = var.kid }
    DEPLOY_KEY  = { value_encrypted = var.deploy_enc, key_id = var.kid }
  }
  app_vars = {
    DEPLOY_REGION = { value = "us-east-1" }
    NODE_VERSION  = { value = "20" }
    LOG_LEVEL     = { value = "info" }
  }
}

module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = module.repository.id

  secrets   = local.app_secrets
  variables = local.app_vars
}

⚠️ Each secret/variable is at least one API call (plus a public-key fetch for encryption). Large maps can trip secondary rate limits β€” see Troubleshooting.

1️⃣5️⃣ Fleet of repos β€” module for_each over many repositories
module "actions_repository" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  for_each = module.repositories # map of repo modules

  repository = each.value.id
  variables = {
    DEPLOY_REGION = { value = "us-east-1" }
  }
}
1️⃣6️⃣ πŸ—οΈ End-to-end composition (mandatory) β€” full suite wired 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"
}

# 2) Org-wide Actions ceiling (policy this repo operates within)
module "actions_organization" {
  source = "git::https://github.com/microsoftexpert/tf-mod-github-actions-organization?ref=v1.0.0"
  #... org-level allowed_actions / enabled_repositories...
}

# 3) Repository-scoped Actions config β€” THIS module
module "actions_repository" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-actions-repository?ref=v1.0.0"
  repository = module.repository.id

  permissions = {
    allowed_actions        = "selected"
    allowed_actions_config = { patterns_allowed = ["FinancialPartners/*"] }
  }

  workflow_permissions = {
    default_workflow_permissions = "read" # read-only GITHUB_TOKEN
  }

  oidc_subject_claim_template = {
    use_default        = false
    include_claim_keys = ["repository", "context", "ref"]
  }

  secrets = {
    NPM_TOKEN = { value_encrypted = var.npm_token_encrypted, key_id = var.repo_public_key_id }
  }
  variables = {
    DEPLOY_REGION = { value = "us-east-1" }
  }
}

# 4) Environment consuming this module's emitted inventory
module "repository_environment" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-github-repository-environment?ref=v1.0.0"
  repository = module.repository.id
  # downstream workflow config can reference module.actions_repository.variable_names
}

πŸ’‘ The wiring direction is repository.id β†’ actions_repository.repository, and actions_repository.variable_names β†’ environment β€” outputs flowing into the next module's inputs, never the reverse.


πŸ“₯ Inputs

Identifying

  • repository (string, required) β€” bare repository name (org supplied by the provider).

Permissions policy

  • permissions (object, default secure) β€” enabled, allowed_actions, sha_pinning_required, and allowed_actions_config (honored only for selected).

Default token scope

  • workflow_permissions (object, default null) β€” default_workflow_permissions (read/write) and can_approve_pull_request_reviews; unmanaged (repo/org default applies) when null.

Sharing

  • actions_access_level (string, default null) β€” none / user / organization / enterprise; unmanaged when null.

OIDC federation

  • oidc_subject_claim_template (object, default null) β€” use_default and include_claim_keys; unmanaged (provider/org default applies) when null.

Collections

  • secrets (map(object), sensitive, default {}) β€” keyed by secret name.
  • variables (map(object), default {}) β€” keyed by variable name.
Full nested object schemas
permissions = object({
  enabled              = optional(bool, true)
  allowed_actions      = optional(string, "selected") # "all" | "local_only" | "selected"
  sha_pinning_required = optional(bool, true)
  allowed_actions_config = optional(object({
    github_owned_allowed = optional(bool, true)
    verified_allowed     = optional(bool, true)
    patterns_allowed     = optional(list(string), [])
  }), {})
})

workflow_permissions = object({
  default_workflow_permissions     = optional(string, "read") # "read" | "write"
  can_approve_pull_request_reviews = optional(bool, false)
}) # null = unmanaged

oidc_subject_claim_template = object({
  use_default        = optional(bool, false)
  include_claim_keys = optional(list(string), [])
}) # null = unmanaged

secrets = map(object({
  value           = optional(string) # plaintext (sensitive); provider encrypts on upload
  value_encrypted = optional(string) # base64, sealed with repo public key (PREFERRED)
  key_id          = optional(string) # required with value_encrypted
}))

variables = map(object({
  value = string
}))

🧾 Outputs

Output Description
id ID of the Actions permissions policy (= repository name). Primary reference.
repository Repo name this config is bound to (echoed).
allowed_actions Resolved policy (all / local_only / selected).
secret_names Set of managed secret names. Values never emitted.
secret_ids Map of secret name => <repo>:<secret_name> resource ID.
variable_names Map of variable name => value (non-secret).
variable_ids Map of variable name => <repo>:<variable_name> resource ID.
access_level Resolved access level, or null when the access-level resource is unmanaged.
default_workflow_permissions Resolved default GITHUB_TOKEN scope (read / write), or null when workflow_permissions is unmanaged.
oidc_subject_claim_template Resolved OIDC subject-claim ({ use_default, include_claim_keys }), or null when oidc_subject_claim_template is unmanaged.

πŸ”’ No secret values are ever emitted β€” only names and IDs β€” so all outputs are non-sensitive and safe to log.


🧠 Architecture Notes

  • id/identity semantics. GitHub Actions resources have no node_id/slug. The policy id equals the repository name; secret/variable IDs are <repository>:<name>. There are no ARNs (GitHub has none).
  • Why this is the permissions policy. It is the resource that governs everything else here β€” secrets/variables/access are role-named for_each collections under it.
  • sensitive + for_each. var.secrets is sensitive, and for_each forbids sensitive values. The module iterates over nonsensitive(toset(keys(var.secrets))) and looks each value up inside the block β€” the keys (secret names) are not secret, the values are.
  • Write-only secret values β†’ no drift detection. GitHub never returns a secret's value, so the provider cannot detect remote drift. Rotating a secret outside Terraform will not show in a plan; rotate through the module.
  • allowed_actions_config is conditional. The provider rejects the block unless allowed_actions = "selected", so it is wrapped in a dynamic block gated on that value.
  • Authoritative vs additive. github_actions_repository_permissions is authoritative for the repo's policy β€” it owns the whole setting. Secrets/variables are managed per named key; keys you don't declare are left untouched, but a key you remove from the map is deleted.
  • Repo-scope vs org-scope. This module is the repository-scoped counterpart to tf-mod-github-actions-organization. The org policy is a ceiling β€” a repo cannot grant itself more than the org allows.
  • Two distinct "permissions" resources β€” both matter. github_actions_repository_permissions (this, via permissions) governs WHICH actions may run (the allowed-actions policy); github_workflow_repository_permissions (via workflow_permissions) governs the read/write SCOPE of the auto-provisioned GITHUB_TOKEN. They are separate API objects, set independently β€” locking down one does nothing for the other. A hardened repo wants BOTH: a selected allow-list and a read-only default token.
  • Org policy is a ceiling for the token scope too. If the org's default workflow-permissions policy enforces read-only, a repo cannot grant itself write β€” the effective scope is the intersection. Relax the org policy (tf-mod-github-actions-organization) before a repo write default takes effect.
  • OIDC subject-claim customization. oidc_subject_claim_template shapes the sub claim in the OIDC tokens this repo's workflows present to cloud providers. A scoped claim (["repository", "context", "ref"]) lets cloud-side trust policies bind to a precise repo + environment + branch β€” the foundation of least-privilege keyless federation. use_default = true forbids include_claim_keys (the module validates this); leaving both unset inherits the org (or GitHub) default.
  • Eventual consistency. The Actions API is eventually consistent; a freshly created repo may briefly 404 on its Actions endpoints. Wiring repository = module.repository.id orders creation correctly and avoids this in practice.

🧱 Design Principles

  • πŸ”’ Secure by default β€” allowed_actions = "selected", GitHub-owned + verified only, sha_pinning_required = true. Broadening to all is an explicit opt-out.
  • πŸ” Least privilege β€” the narrowest runnable Actions policy by default; none access level available to prevent reusable-workflow sharing.
  • 🀫 Secrets are sensitive β€” value/value_encrypted marked sensitive, value_encrypted preferred, and no secret value ever emitted as output.
  • βœ… Closed value sets validated β€” allowed_actions and actions_access_level enums, and the exactly-one-of secret rule, are enforced with validation {} blocks.
  • 🧩 Provider-owned auth β€” no owner / token / app_auth variables; no provider {} block.
  • 🚫 No tags, no timeouts β€” GitHub has neither.

πŸš€ Runbook

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. Branches move; tags are immutable and reproducible.


πŸ§ͺ Testing

The offline proof gate (no GitHub API, no apply):

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

πŸ’¬ Example Output

id = "payments-api"
repository = "payments-api"
allowed_actions = "selected"
secret_names = toset([ "NPM_TOKEN" ])
secret_ids = { "NPM_TOKEN" = "payments-api:NPM_TOKEN" }
variable_names = { "DEPLOY_REGION" = "us-east-1" }
variable_ids = { "DEPLOY_REGION" = "payments-api:DEPLOY_REGION" }
access_level = null
default_workflow_permissions = "read"
oidc_subject_claim_template = {
 "include_claim_keys" = tolist([ "repository", "context", "ref" ])
 "use_default" = false
}

πŸ” Troubleshooting

Symptom Cause Resolution
403 / Resource not accessible by integration Token missing repo + workflow (classic) or Actions/Secrets/Administration write (fine-grained) Grant the scopes in Required token scopes.
allowed_actions_config rejected on apply Block set while allowed_actions β‰  "selected" The module gates this automatically β€” confirm allowed_actions = "selected" when configuring the allow-list.
Secret plan validation error Both/neither value and value_encrypted set Provide exactly one per secret; add key_id with value_encrypted.
Rotated secret not showing in plan Secret values are write-only β€” no drift detection Rotate through the module; out-of-band changes are invisible to Terraform.
404 on Actions endpoints right after repo creation Eventual consistency on a brand-new repo Wire repository = module.repository.id so the repo is created first; re-run if needed.
Secondary rate limit on large for_each Many secret/variable writes + public-key fetches in a burst Apply with lower -parallelism, split the map, or stagger applies.
Access-level apply fails Public repo, or organization/enterprise without the matching plan Use a private/internal repo on an org/enterprise plan; see Prerequisites.
GITHUB_TOKEN still has write access despite allowed_actions = "selected" Conflating the two resources β€” permissions is the allowed-actions policy, not the token scope Set workflow_permissions = { default_workflow_permissions = "read" } (a separate resource).
Repo default_workflow_permissions = "write" has no effect Org-level workflow-permissions policy enforces a stricter (read-only) default Org policy is a ceiling β€” relax it in tf-mod-github-actions-organization / org settings, then re-apply.
OIDC apply error: include_claim_keys set with use_default = true The provider forbids setting both The module validates this β€” use use_default = false for custom claims, or omit include_claim_keys.
403 on workflow-permissions or OIDC apply Token lacks admin on the repo (Administration: write) Both resources require repo admin β€” grant repo (classic) or Administration read/write (fine-grained).

Required token scopes / GitHub App permissions

Classic PAT scopes

  • repo + workflow

Fine-grained PAT / GitHub App permissions

  • Actions: read/write
  • Secrets: read/write
  • Variables: read/write
  • Administration: read/write (permissions policy, workflow permissions & OIDC template β€” all require repo admin)
  • Metadata: read

GitHub Prerequisites

  • Actions enabled for the repo/org (org policy can disable it).
  • actions_access_level requires a private/internal repo on an org plan; organization/enterprise levels require the matching GitHub plan/edition.
  • Rate limits: each secret/variable is one API call plus a public-key fetch for encryption β€” watch secondary rate limits on large maps.
  • Repo admin required for workflow_permissions and oidc_subject_claim_template (both call admin-only endpoints).
  • Org policy is a ceiling: an org-level workflow-permissions policy can override a repo's default_workflow_permissions β€” a repo cannot exceed the org default.
  • OIDC federation: oidc_subject_claim_template only shapes the token's sub claim; the trust relationship itself lives on the cloud side (an Azure federated credential / AWS IAM role).

πŸ”— Related Docs

  • module β€” GitHub Repository (tf-mod-github-repository, the keystone)
  • module β€” GitHub Actions (Organization) (tf-mod-github-actions-organization)
  • module β€” GitHub Repository Environment (tf-mod-github-repository-environment)
  • module β€” GitHub Dependabot (tf-mod-github-dependabot)
  • integrations/github provider β€” github_actions_repository_permissions, github_actions_secret, github_actions_variable, github_actions_repository_access_level, github_workflow_repository_permissions, github_actions_repository_oidc_subject_claim_customization_template
  • this module's SCOPE.md

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages