Agency-level Infrastructure as Code — GitHub automation, secrets management, and AI-powered workflows, fully managed with OpenTofu.
| Layer | Tool | Purpose |
|---|---|---|
| IaC engine | OpenTofu >= 1.9 |
Plan + apply all infrastructure |
| Remote backend | HCP Terraform | Remote execution, encrypted state, workspace secrets |
| Auth | GitHub App (ephemeral tokens) | Scoped GitHub API access — no long-lived PATs |
| Secrets | Doppler | Project-level secrets synced across all repos |
| CI/CD | GitHub Actions | Automated plan on PR, apply on merge |
| AI | GitHub Models API (free, GITHUB_TOKEN) |
PR review, issue triage, changelog generation |
| Scanning | Trivy + Checkov + OSSF Scorecard | IaC misconfig, policy enforcement, supply-chain scoring |
| Testing | tofu test (mock) + Terratest (integration) |
Unit + real-API coverage |
.
├── .devcontainer/ # VS Code dev container (ready to use)
├── .github/
│ ├── CODEOWNERS
│ ├── SECURITY.md
│ ├── pull_request_template.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature.yml
│ ├── dependabot.yml # Weekly dependency updates
│ ├── copilot-instructions.md # GitHub Copilot context
│ ├── scripts/ # Python automation (stdlib only — no pip)
│ │ ├── github_models.py # Shared Models API + GitHub REST client
│ │ ├── ai_issue_triage.py # Labels + triage comment on issue open
│ │ ├── ai_pr_review.py # Code review on IaC PRs
│ │ └── ai_changelog.py # Release notes on tag creation
│ └── workflows/
│ ├── _tofu-plan.yml # Reusable: fmt-check → init → validate → plan → PR comment
│ ├── _tofu-apply.yml # Reusable: init → apply -auto-approve
│ ├── tofu-github-plan.yml # Caller: github stack — plan on PR
│ ├── tofu-github-apply.yml # Caller: github stack — apply on merge
│ ├── tofu-doppler-plan.yml # Caller: doppler stack — plan on PR
│ ├── tofu-doppler-apply.yml # Caller: doppler stack — apply on merge
│ ├── tofu-test.yml # tofu test (mock_provider) on module changes
│ ├── integration-test.yml # Terratest weekly (real GitHub API)
│ ├── security-scan.yml # Trivy + Checkov → GitHub Security tab
│ ├── scorecard.yml # OSSF Scorecard weekly
│ ├── ai-issue-triage.yml # AI: triage on issue open
│ ├── ai-pr-review.yml # AI: code review on IaC PR
│ ├── ai-changelog.yml # AI: release notes on tag
│ ├── auto-merge.yml # Dependabot patch/minor auto-merge
│ ├── docs.yml # terraform-docs enforce (PR) + auto-commit (main)
│ ├── stale.yml # Close stale issues/PRs
│ └── release.yml # Manual semver bump + GitHub release
│
├── modules/
│ └── github/
│ └── repository/ # Module: repo + branch ruleset + environments + deploy keys
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── README.md # Auto-generated by terraform-docs
│ ├── templates/ # Files injected into managed repos
│ └── tests/
│ └── defaults.tftest.hcl # 11 mock_provider unit tests
│
├── scripts/
│ └── new-stack.sh # Generator: scaffold a new provider stack + 2 GHA workflows
│
├── tests/
│ ├── go.mod
│ └── github_repository_test.go # Terratest integration tests (real API)
│
└── terraform/
├── github/ # Stack: repos, labels, keys, files, Actions vars
└── doppler/ # Stack: Doppler projects, configs, service tokens
PR open / update
├── tofu-github-plan → fmt-check + init + validate + plan → comment on PR
├── tofu-test → mock_provider unit tests (modules/**/tests/)
├── ai-pr-review → AI review on terraform/** + modules/** changes
└── security-scan → Trivy + Checkov → GitHub Security tab
Merge to main
├── tofu-github-apply → tofu apply -auto-approve
└── tofu-doppler-apply → tofu apply -auto-approve
Weekly (scheduled)
├── scorecard → OSSF Scorecard → Security tab
├── integration-test → Terratest real-API tests (Sunday 02:00 UTC)
└── stale → Mark/close inactive issues + PRs (Monday 08:00 UTC)
Issue opened → ai-issue-triage (labels + comment)
Release created → ai-changelog (AI-written release notes)
Dependabot PR opened → auto-merge (patch/minor merged, major labelled priority:high)
Manual (workflow_dispatch)
└── release → semver bump → create GitHub release
| Tool | Install | Purpose |
|---|---|---|
| mise | curl https://mise.run | sh |
Go + Python version manager |
| tenv | brew install tofuutils/tap/tenv |
OpenTofu version manager |
| just | brew install just |
Task runner (see justfile) |
| pre-commit | pip install pre-commit |
Git hooks (fmt, validate, scan) |
| Trivy | brew install trivy |
IaC misconfiguration scanner |
| Checkov | pip install checkov |
Policy-as-code scanner |
| tflint | brew install tflint |
HCL linter |
| terraform-docs | brew install terraform-docs |
Module documentation |
mise install # Install Go 1.23 + Python 3.12 (reads .mise.toml)
just setup # Install OpenTofu, pre-commit hooks, Go depsjust fmt # Format all HCL in-place
just validate # Validate all stacks
just lint # tflint on all stacks
just test # mock_provider unit tests (no API calls)
just security # Trivy + Checkov scans
just check # All of the above — mirrors CI
just plan github # Plan the github stack
just plan doppler # Plan the doppler stack
just apply github # Apply (prompts for confirmation)
just new-stack stripe # Scaffold a new provider stack
just docs # Regenerate terraform-docs for all modules
just version # Show all tool versions
just test-integration # Run Terratest (creates real GitHub resources)
just test-integration TestMyTest # Run a specific Terratest functionGitHub → Settings → Developer settings → GitHub Apps → New GitHub App
| Permission | Access | Required for |
|---|---|---|
| Administration | Read & Write | Create + configure repositories |
| Contents | Read & Write | Inject .github/ files into repos |
| Issues | Read & Write | AI triage labels + comments |
| Metadata | Read | Repository metadata |
| Pull requests | Read & Write | Plan comments, auto-merge |
| Workflows | Read & Write | Repository Actions variables |
After creation:
- Note the App ID
- Generate a private key → download
.pemfile - Install the App on your GitHub account → note the Installation ID
- Sign in at app.terraform.io
- Create an organization and replace
"YOUR_TFC_ORG"in bothterraform/*/terraform.tffiles - Create two workspaces (execution mode: Remote):
Workspace github
| Variable | Type | Sensitive | Value |
|---|---|---|---|
github_owner |
Terraform | No | Your GitHub username |
github_app_id |
Terraform | Yes | App ID from step 1 |
github_app_installation_id |
Terraform | Yes | Installation ID from step 1 |
github_app_pem_file |
Terraform | Yes | Full contents of .pem file |
Workspace doppler
| Variable | Type | Sensitive | Value |
|---|---|---|---|
doppler_token |
Terraform | Yes | Doppler service token (account scope) |
- Create a Team API token → add as GitHub Actions secret
TF_API_TOKEN - Add the following GitHub Actions repository variable (Settings → Variables → Actions):
| Variable | Value | Used by |
|---|---|---|
TF_DOPPLER_WORKSPACE_ID |
HCP Terraform workspace ID for the doppler workspace (e.g. ws-xxxx) |
tofu-github-apply.yml pre-step that enables global remote state |
just plan github # Review the import of existing repos
just apply github # Import + configure all repos
just plan doppler # Preview Doppler project creation
just apply doppler # Create Doppler projects + service tokensAfter a successful apply, the import blocks in terraform/github/imports.tf can be removed.
terraform/github/repos.tf references the modules/github/repository module via a pinned git tag:
source = "git::https://github.com/GregoireF/iac.git//modules/github/repository?ref=v1.0.0&depth=1"After any change to modules/github/repository/:
- Merge the module change to
main - Run Actions → release → Run workflow — choose
patch,minor, ormajor - The workflow creates a new tag (e.g.
v1.1.0) and a GitHub Release - Open a PR to update the
ref=interraform/github/repos.tfto the new tag
Edit terraform/github/locals.tf and add an entry to local.repositories:
my-new-repo = {
description = "My new repository."
topics = ["example"]
visibility = "public"
has_issues = true
has_wiki = false
has_projects = false
allow_merge_commit = false
allow_squash_merge = true
allow_rebase_merge = false
delete_branch_on_merge = true
archived = false
allow_auto_merge = true
inject_standard_files = true
branch_protection = {
enabled = true
required_status_checks = []
enforce_conventional_commits = true
}
}Open a PR → plan comment posted automatically → merge → repo created.
just new-stack cloudflareThis creates:
terraform/cloudflare/withterraform.tf,providers.tf,variables.tf,locals.tf,outputs.tf.github/workflows/tofu-cloudflare-plan.yml.github/workflows/tofu-cloudflare-apply.yml
Then:
- Edit
terraform/cloudflare/terraform.tf— addrequired_providers+ set TFC org name - Edit
terraform/cloudflare/providers.tf— configure provider - Create HCP Terraform workspace
cloudflare+ set variables - Open a PR → automated plan → merge → apply
just test
# runs tofu init -backend=false && tofu test for every module with a tests/ directoryexport GITHUB_TOKEN=<pat-with-repo-scope>
export GITHUB_OWNER=<your-github-username>
just test-integrationThese run on a weekly schedule in CI (Sunday 02:00 UTC). They create temporary repos with a random suffix and destroy them with defer terraform.Destroy().
| Control | Detail |
|---|---|
| No long-lived credentials | GitHub App generates ephemeral tokens per run |
| Secrets never in code | GitHub App PEM + all secrets live in HCP Terraform workspace variables |
| CI gets minimum access | Workflows receive GITHUB_TOKEN only; TF_API_TOKEN scoped to HCP Terraform |
| Doppler | Non-sensitive workspace config via service tokens; secrets stay encrypted in Doppler |
| Pre-commit gates | detect-private-key, tofu fmt, Trivy, Checkov run before every push |
| PR gates | tofu validate, tofu test, security scan on every PR |
| OSSF Scorecard | Weekly scoring pushed to GitHub Security tab |
| Branch protection | Conventional commits enforced via ruleset; no force-push to main |
| Dependabot | Patch/minor auto-merged; major gets priority:high and blocks merge |
All AI features use the GitHub Models API with the built-in GITHUB_TOKEN — no external API key, no cost.
| Workflow | Trigger | Model | What it does |
|---|---|---|---|
ai-issue-triage |
Issue opened | gpt-4o |
Suggests labels + posts triage comment |
ai-pr-review |
PR on terraform/** |
gpt-4o |
Reviews diff for security, best practices, correctness |
ai-changelog |
Release created | gpt-4o-mini |
Writes structured release notes from commits |
Scripts are in .github/scripts/ and use Python stdlib only — no pip install, no container.
| Decision | Rationale |
|---|---|
| OpenTofu over Terraform | MPL-2 licence (BSL-free), mock_provider in tests, community momentum |
| HCP Terraform over self-hosted state | Remote execution, encrypted state, free tier sufficient |
| GitHub App over PAT | Ephemeral tokens, scoped permissions, no expiry surprises |
| Doppler over SOPS/Vault | Free tier, native GitHub Actions sync, zero infra to manage |
| GitHub Models over OpenAI | Free with GITHUB_TOKEN, no external key, sufficient quality |
justfile over Makefile |
POSIX-safe, better syntax, self-documenting (just --list) |
mise over asdf/pyenv |
Unified Go + Python version management, faster, .mise.toml is clean |
| Dependabot over Renovate | Zero config, free, sufficient for 2 stacks; Renovate is a future option for better grouping |
Security vulnerabilities should be reported according to the security policy.
Supply-chain security is continuously monitored via OSSF Scorecard.
Results are published to the GitHub Security tab after every push to main.
MIT — Copyright © 2026 GregoireF
No requirements.
No providers.
No modules.
No resources.
No inputs.
No outputs.