Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions modules/gh-repo-secrets-section/.terraform-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
formatter: "markdown"

version: ""

header-from: docs/header.md
footer-from: docs/footer.md

recursive:
enabled: false
path: modules
include-main: true

sections:
hide: []
show: []

content: ""

output:
file: "README.md"
mode: inject
template: |-
<!-- BEGIN_TF_DOCS -->
{{ .Content }}
<!-- END_TF_DOCS -->

output-values:
enabled: false
from: ""

sort:
enabled: true
by: name

settings:
anchor: true
color: true
default: true
description: false
escape: true
hide-empty: false
html: true
indent: 2
lockfile: true
read-comments: true
required: true
sensitive: true
type: true
112 changes: 112 additions & 0 deletions modules/gh-repo-secrets-section/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!-- BEGIN_TF_DOCS -->
# **GitHub Repository Secrets Terraform Module**

## Overview

This module manages GitHub repository secrets for **Actions**, **Codespaces**, and **Dependabot** using a single strongly-typed `config` object.

**Important**:
The `encryptedValue` values passed in the `config` **must already be encrypted** using **libsodium** against the target repository’s public key (GitHub’s recommended method).
**Terraform does not perform any encryption**. It simply forwards the pre-encrypted value to the GitHub API.

This approach is the most secure for automated pipelines (Prefapp IDP, GitHub Actions, etc.).

## Key Features

- **Single complex object**: All secrets are defined in one `config` variable.
- **Pre-encrypted values**: `encryptedValue` must be provided already encrypted with libsodium.
- **Three secret types**: Actions, Codespaces, and Dependabot supported in the same module.
- **Lifecycle protection**: `ignore_changes` on `encrypted_value` to prevent unnecessary drift.
- **Full validation**: Enforces required fields and non-empty values.
- **JSON-native**: Works seamlessly with `terraform.tfvars.json` generated by external programs.
Comment on lines +8 to +21
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README refers to encryptedValue and claims “Full validation…non-empty values”, but the module uses encrypted_value strings in the maps and does not validate non-empty secret values. Please update the README to match the actual input schema (or implement the validations described).

Copilot uses AI. Check for mistakes.

## Basic Usage

### Using `terraform.tfvars.json` (recommended)

```hcl
module "repo_secrets" {
source = "git::https://github.com/prefapp/tfm.git//modules/github-repository-secrets"

config = var.config # Terraform automatically loads terraform.tfvars.json
Comment on lines +27 to +31
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README’s first HCL snippet is not closed (missing a terminating ```), which causes the remainder of the document to render as code. Also the source points to `modules/github-repository-secrets`, but this module directory is `modules/gh-repo-secrets-section`.

Copilot uses AI. Check for mistakes.
}

### Inline example

```hcl
module "repo\_secrets" {
source = "git::https://github.com/prefapp/tfm.git//modules/github-repository-secrets"

config = {
actions = {
SECRET\_A = {
secretName = "SECRET\_A"
repository = "component\_a"
encryptedValue = "r+RFBGIn8U7z2Opm5RN7PXKdgFzefXiV91IpG3O2DrClZl9dkTJBfhRZbi2uV2nu4ijn5yUfZ9O1eqjaXL2dWByFV+T2swZCQVQdDGmDlF24MPvEFh2ZbQ=="
}
}
codespaces = {}
dependabot = {}
}
}

## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_github"></a> [github](#requirement\_github) | ~> 6.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_github"></a> [github](#provider\_github) | ~> 6.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [github_actions_secret.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_secret) | resource |
| [github_codespaces_secret.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/codespaces_secret) | resource |
| [github_dependabot_secret.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/dependabot_secret) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_config"></a> [config](#input\_config) | GitHub repository secrets configuration as a single complex object.<br/><br/>IMPORTANT:<br/>- encryptedValue must be ALREADY encrypted with libsodium using the target repository's public key.<br/>- Terraform does NOT encrypt anything — it only passes the pre-encrypted value. | <pre>object({<br/> actions = optional(map(object({<br/> secretName = string<br/> repository = string<br/> encryptedValue = string<br/> })), {})<br/><br/> codespaces = optional(map(object({<br/> secretName = string<br/> repository = string<br/> encryptedValue = string<br/> })), {})<br/><br/> dependabot = optional(map(object({<br/> secretName = string<br/> repository = string<br/> encryptedValue = string<br/> })), {})<br/> })</pre> | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_actions_secrets"></a> [actions\_secrets](#output\_actions\_secrets) | List of Action secrets created |
| <a name="output_all_secret_names"></a> [all\_secret\_names](#output\_all\_secret\_names) | Combined list of all secret names |
| <a name="output_codespaces_secrets"></a> [codespaces\_secrets](#output\_codespaces\_secrets) | List of Codespaces secrets created |
| <a name="output_dependabot_secrets"></a> [dependabot\_secrets](#output\_dependabot\_secrets) | List of Dependabot secrets created |

### 3. `docs/footer.md`
```markdown
## Examples

For detailed examples, refer to the [module examples](https://github.com/prefapp/tfm/tree/main/modules/github-repository-secrets/_examples):

- [basic](https://github.com/prefapp/tfm/tree/main/modules/github-repository-secrets/_examples/basic) - Full example with Actions, Codespaces, and Dependabot secrets

## Resources

- **github_actions_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_secret)
- **github_codespaces_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/codespaces_secret)
- **github_dependabot_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/dependabot_secret)
- **GitHub Terraform Provider**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs)

## Support

For issues, questions, or contributions related to this module, please visit the [repository's issue tracker](https://github.com/prefapp/tfm/issues).

```
<!-- END_TF_DOCS -->
5 changes: 5 additions & 0 deletions modules/gh-repo-secrets-section/_examples/basic/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module "repo_secrets" {
source = "git::https://github.com/prefapp/tfm.git//modules/github-repository-secrets"

config = var.config # Terraform automatically loads terraform.tfvars.json
}
Comment on lines +1 to +5
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example references var.config, but the example directory doesn’t declare a variable "config" anywhere, so terraform plan will fail. Either add a variables.tf declaring config (matching the module input type) or inline the config object in the module block.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example references var.config but the example directory doesn’t include a variables.tf (and it also omits a terraform/provider "github" setup), so it’s not runnable as a standalone example like the other module examples in this repo. Consider adding a minimal terraform block + required_providers and a provider "github" stub, and/or include a variables.tf showing how config is defined/loaded.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"config": {
"actions": {
Comment on lines +1 to +4
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example file is named secrets-config.json, but Terraform will only auto-load terraform.tfvars.json (or files passed via -var-file). Consider renaming it to terraform.tfvars.json or updating the example instructions to show how to load this file.

Copilot uses AI. Check for mistakes.
"SECRET_A": {
"secretName": "SECRET_A",
"repository": "component_a",
"encryptedValue": "r+RFBGIn8U7z2Opm5RN7PXKdgFzefXiV91IpG3O2DrClZl9dkTJBfhRZbi2uV2nu4ijn5yUfZ9O1eqjaXL2dWByFV+T2swZCQVQdDGmDlF24MPvEFh2ZbQ=="
},
"SECRET_B": {
"secretName": "SECRET_B",
"repository": "component_a",
"encryptedValue": "MkLxMAQDhWu0HotjWvNDzyquGJyCxf+n/Qh+eLtDi+Ci0U+M5CCYYLabH6Y0/+sx5aksP+PJPM9PxIYQImx82lOcy1MJ08Fi+JCtT2l2CixOP19McNu7XQ=="
}
},
"codespaces": {
"SECRET_C": {
"secretName": "SECRET_C",
"repository": "component_a",
"encryptedValue": "rJ0H0YfnvedPtMOD0Ete+Pl802OQXa8e7Ia6lbfeTEnIiFRnqjc3XMPGxk7VC8yMeM2znStVAP2eeI97XrfMzvM/N/289g8ZElsCXlLesn4JXY2+chmcCQ=="
}
},
"dependabot": {}
}
}
Comment on lines +1 to +14
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example file is named secrets-config.json, but the usage docs say Terraform will auto-load terraform.tfvars.json. As-is, the example won’t be picked up automatically. Consider renaming this to terraform.tfvars.json (or adjust the docs/example to show how to pass -var-file=secrets-config.json).

Copilot uses AI. Check for mistakes.
20 changes: 20 additions & 0 deletions modules/gh-repo-secrets-section/docs/footer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

### 3. `docs/footer.md`
```markdown
## Examples

For detailed examples, refer to the [module examples](https://github.com/prefapp/tfm/tree/main/modules/github-repository-secrets/_examples):

- [basic](https://github.com/prefapp/tfm/tree/main/modules/github-repository-secrets/_examples/basic) - Full example with Actions, Codespaces, and Dependabot secrets

## Resources

- **github_actions_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_secret)
- **github_codespaces_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/codespaces_secret)
- **github_dependabot_secret**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/dependabot_secret)
- **GitHub Terraform Provider**: [Official Documentation](https://registry.terraform.io/providers/integrations/github/latest/docs)

## Support

For issues, questions, or contributions related to this module, please visit the [repository's issue tracker](https://github.com/prefapp/tfm/issues).

50 changes: 50 additions & 0 deletions modules/gh-repo-secrets-section/docs/header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# **GitHub Repository Secrets Terraform Module**

## Overview

This module manages GitHub repository secrets for **Actions**, **Codespaces**, and **Dependabot** using a single strongly-typed `config` object.

**Important**:
The `encryptedValue` values passed in the `config` **must already be encrypted** using **libsodium** against the target repository’s public key (GitHub’s recommended method).
**Terraform does not perform any encryption**. It simply forwards the pre-encrypted value to the GitHub API.

This approach is the most secure for automated pipelines (Prefapp IDP, GitHub Actions, etc.).

## Key Features

- **Single complex object**: All secrets are defined in one `config` variable.
- **Pre-encrypted values**: `encryptedValue` must be provided already encrypted with libsodium.
- **Three secret types**: Actions, Codespaces, and Dependabot supported in the same module.
- **Lifecycle protection**: `ignore_changes` on `encrypted_value` to prevent unnecessary drift.
- **Full validation**: Enforces required fields and non-empty values.
- **JSON-native**: Works seamlessly with `terraform.tfvars.json` generated by external programs.

## Basic Usage

### Using `terraform.tfvars.json` (recommended)

```hcl
module "repo_secrets" {
source = "git::https://github.com/prefapp/tfm.git//modules/github-repository-secrets"

config = var.config # Terraform automatically loads terraform.tfvars.json
}

### Inline example

```hcl
module "repo_secrets" {
source = "git::https://github.com/prefapp/tfm.git//modules/github-repository-secrets"

config = {
actions = {
SECRET_A = {
secretName = "SECRET_A"
repository = "component_a"
encryptedValue = "r+RFBGIn8U7z2Opm5RN7PXKdgFzefXiV91IpG3O2DrClZl9dkTJBfhRZbi2uV2nu4ijn5yUfZ9O1eqjaXL2dWByFV+T2swZCQVQdDGmDlF24MPvEFh2ZbQ=="
}
}
codespaces = {}
dependabot = {}
}
}
50 changes: 50 additions & 0 deletions modules/gh-repo-secrets-section/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ─────────────────────────────────────────────────────────────
# GitHub Actions Secrets
# encrypted_value is ALREADY libsodium-encrypted against the repo public key
# Terraform does NOT perform any encryption
# ─────────────────────────────────────────────────────────────
resource "github_actions_secret" "this" {
for_each = var.config.actions

repository = each.value.repository
secret_name = each.value.secretName
encrypted_value = each.value.encryptedValue

lifecycle {
ignore_changes = ["encrypted_value"]
}
Comment on lines +16 to +20
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ignore_changes = [encrypted_value] prevents Terraform from ever updating/rotating a secret when the encrypted value changes. If rotation via Terraform is a desired capability, consider making this behavior configurable (e.g., a boolean to enable/disable ignore_changes) and document the rotation workflow.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore_changes = [encrypted_value] prevents Terraform from ever updating secret values after the initial create, which blocks intentional secret rotations via config changes. If the goal is to avoid perpetual diffs from re-encrypting, consider making this behavior configurable (e.g., a boolean variable) and/or clearly documenting the rotation workflow (taint/import/remove+recreate) so operators don’t assume updates will apply.

Copilot uses AI. Check for mistakes.
}

# ─────────────────────────────────────────────────────────────
# GitHub Codespaces Secrets
# encrypted_value is ALREADY libsodium-encrypted against the repo public key
# Terraform does NOT perform any encryption
# ─────────────────────────────────────────────────────────────
resource "github_codespaces_secret" "this" {
for_each = var.config.codespaces

repository = each.value.repository
secret_name = each.value.secretName
encrypted_value = each.value.encryptedValue

lifecycle {
ignore_changes = ["encrypted_value"]
}
}

# ─────────────────────────────────────────────────────────────
# GitHub Dependabot Secrets
# encrypted_value is ALREADY libsodium-encrypted against the repo public key
# Terraform does NOT perform any encryption
# ─────────────────────────────────────────────────────────────
resource "github_dependabot_secret" "this" {
for_each = var.config.dependabot

repository = each.value.repository
secret_name = each.value.secretName
encrypted_value = each.value.encryptedValue

lifecycle {
ignore_changes = ["encrypted_value"]
}
}
23 changes: 23 additions & 0 deletions modules/gh-repo-secrets-section/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
output "actions_secrets" {
description = "List of Action secrets created"
value = [for k, v in var.config.actions : v.secretName]
}

output "codespaces_secrets" {
description = "List of Codespaces secrets created"
value = [for k, v in var.config.codespaces : v.secretName]
}

output "dependabot_secrets" {
description = "List of Dependabot secrets created"
value = [for k, v in var.config.dependabot : v.secretName]
}

output "all_secret_names" {
description = "Combined list of all secret names"
value = concat(
[for v in var.config.actions : v.secretName],
[for v in var.config.codespaces : v.secretName],
[for v in var.config.dependabot : v.secretName]
)
}
55 changes: 55 additions & 0 deletions modules/gh-repo-secrets-section/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
variable "config" {
description = <<EOT
GitHub repository secrets configuration as a single complex object.

IMPORTANT:
- encryptedValue must be ALREADY encrypted with libsodium using the target repository's public key.
- Terraform does NOT encrypt anything — it only passes the pre-encrypted value.
EOT

type = object({
actions = optional(map(object({
secretName = string
repository = string
encryptedValue = string
})), {})

codespaces = optional(map(object({
secretName = string
repository = string
encryptedValue = string
})), {})

dependabot = optional(map(object({
secretName = string
repository = string
encryptedValue = string
})), {})
})

# validation {
# condition = length(var.config.actions) + length(var.config.codespaces) + length(var.config.dependabot) > 0
# error_message = "At least one secret must be defined in actions, codespaces or dependabot."
# }

validation {
condition = alltrue([
for k, v in var.config.actions : length(trimspace(v.secretName)) > 0 && length(trimspace(v.repository)) > 0
])
error_message = "Every action secret must have non-empty secretName and repository."
}

validation {
condition = alltrue([
for k, v in var.config.codespaces : length(trimspace(v.secretName)) > 0 && length(trimspace(v.repository)) > 0
])
error_message = "Every codespaces secret must have non-empty secretName and repository."
}

validation {
condition = alltrue([
for k, v in var.config.dependabot : length(trimspace(v.secretName)) > 0 && length(trimspace(v.repository)) > 0
])
error_message = "Every dependabot secret must have non-empty secretName and repository."
}
}
Loading