Repository-scoped GitHub Actions governance in one boundary β the Actions permissions policy (allowed actions + SHA-pinning),
for_eachcollections of encrypted secrets and variables, and an optional reusable-workflow access level β all secure-by-default. Built for integrations/github v6.x.
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_eachmap keyed by secret name; prefers pre-encrypted values (value_encrypted+key_id) so plaintext never enters code. - π§ Actions variables β a
for_eachmap of non-secretvars.<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_TOKENto 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 thesubclaim 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.
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)"]
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
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.
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
Resource inventory:
- π‘οΈ
github_actions_repository_permissions.thisβ the primary policy this module is named for. Always created; setsenabled,allowed_actions,sha_pinning_required, and (whenallowed_actions = "selected") theallowed_actions_configallow-list. - π
github_actions_secret.secretsβfor_eachover thesecretsmap; one encrypted Actions secret per key. - π§
github_actions_variable.variablesβfor_eachover thevariablesmap; one non-secret Actions variable per key. - π
github_actions_repository_access_level.accessβ optional singleton, created only whenactions_access_levelis non-null. - π
github_workflow_repository_permissions.workflowβ optional singleton; sets the defaultGITHUB_TOKENscope (read/write) and the PR-approval flag. Created only whenworkflow_permissionsis non-null. - πͺͺ
github_actions_repository_oidc_subject_claim_customization_template.oidcβ optional singleton; customizes the OIDCsubclaim for keyless federation. Created only whenoidc_subject_claim_templateis non-null.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
integrations/github |
~> 6.0 (current 6.12.1) |
β οΈ Provider source isintegrations/githubβ never the deprecatedhashicorp/github. Migrate existing state withterraform state replace-provider registry.terraform.io/hashicorp/github registry.terraform.io/integrations/github.
βΉοΈ Schema notes:
sha_pinning_requiredis a v6-era field ongithub_actions_repository_permissions. Theallowed_actions_configblock is honored only whenallowed_actions = "selected"β the provider rejects it otherwise, so the module gates it with adynamicblock.github_workflow_repository_permissionsandgithub_actions_repository_oidc_subject_claim_customization_templateboth require admin access to the repo, and the OIDC template forbidsinclude_claim_keyswhenuse_default = true.
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
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.
| Input | Type | Source |
|---|---|---|
repository |
string (repo name) |
tf-mod-github-repository (id output) |
| 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.
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.idmakes Terraform create the repo before its Actions policy β no manualdepends_onneeded.
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_onlyblocks 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 ofvalueorvalue_encryptedper 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; usesecrets.
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/enterpriselevels 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_TOKENto read-only β workflows that push commits, publish packages, or open PRs must request elevatedpermissions:in their YAML explicitly. This is the repo analog of the org default-token policy and is distinct from the allowed-actions policy inpermissions(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
subclaim torepository+context+reflets 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. Setuse_default = trueto fall back to GitHub's default claim (the module rejectsinclude_claim_keysalongsideuse_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, andactions_repository.variable_names β environmentβ outputs flowing into the next module's inputs, never the reverse.
Identifying
repository(string, required) β bare repository name (org supplied by the provider).
Permissions policy
permissions(object, default secure) βenabled,allowed_actions,sha_pinning_required, andallowed_actions_config(honored only forselected).
Default token scope
workflow_permissions(object, defaultnull) βdefault_workflow_permissions(read/write) andcan_approve_pull_request_reviews; unmanaged (repo/org default applies) when null.
Sharing
actions_access_level(string, defaultnull) βnone/user/organization/enterprise; unmanaged when null.
OIDC federation
oidc_subject_claim_template(object, defaultnull) βuse_defaultandinclude_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
}))| 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.
id/identity semantics. GitHub Actions resources have no node_id/slug. The policyidequals the repository name; secret/variable IDs are<repository>:<name>. There are no ARNs (GitHub has none).- Why
thisis the permissions policy. It is the resource that governs everything else here β secrets/variables/access are role-namedfor_eachcollections under it. sensitive+for_each.var.secretsissensitive, andfor_eachforbids sensitive values. The module iterates overnonsensitive(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_configis conditional. The provider rejects the block unlessallowed_actions = "selected", so it is wrapped in adynamicblock gated on that value.- Authoritative vs additive.
github_actions_repository_permissionsis 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, viapermissions) governs WHICH actions may run (the allowed-actions policy);github_workflow_repository_permissions(viaworkflow_permissions) governs the read/write SCOPE of the auto-provisionedGITHUB_TOKEN. They are separate API objects, set independently β locking down one does nothing for the other. A hardened repo wants BOTH: aselectedallow-list and aread-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 repowritedefault takes effect. - OIDC subject-claim customization.
oidc_subject_claim_templateshapes thesubclaim 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 = trueforbidsinclude_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.idorders creation correctly and avoids this in practice.
- π Secure by default β
allowed_actions = "selected", GitHub-owned + verified only,sha_pinning_required = true. Broadening toallis an explicit opt-out. - π Least privilege β the narrowest runnable Actions policy by default;
noneaccess level available to prevent reusable-workflow sharing. - π€« Secrets are sensitive β
value/value_encryptedmarked sensitive,value_encryptedpreferred, and no secret value ever emitted as output. - β
Closed value sets validated β
allowed_actionsandactions_access_levelenums, and the exactly-one-of secret rule, are enforced withvalidation {}blocks. - π§© Provider-owned auth β no
owner/token/app_authvariables; noprovider {}block. - π« No tags, no timeouts β GitHub has neither.
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.
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)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
}
| 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). |
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
- Actions enabled for the repo/org (org policy can disable it).
actions_access_levelrequires a private/internal repo on an org plan;organization/enterpriselevels 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_permissionsandoidc_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_templateonly shapes the token'ssubclaim; the trust relationship itself lives on the cloud side (an Azure federated credential / AWS IAM role).
- 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/githubprovider β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."