diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..188f8bf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Summary + + + +## Risk + + + +## Test plan + +- [ ] `make ci` passes locally +- [ ] PR Validation green in CI +- [ ] Security Scan green in CI +- [ ] Documentation reflects the change (when applicable) diff --git a/.github/workflows/auto-merge.yaml b/.github/workflows/auto-merge.yaml new file mode 100644 index 0000000..a0e1fba --- /dev/null +++ b/.github/workflows/auto-merge.yaml @@ -0,0 +1,24 @@ +name: Auto-Merge +# Caller for nwarila/terraform-template reusable-auto-merge workflow. +# Enables GitHub auto-merge on trusted-bot PRs (Renovate, template-sync, +# release-please) once required status checks pass. Human-authored PRs are +# never auto-merged. Renovate manages the SHA pin. + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review, labeled] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: auto-merge-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + auto-merge: + permissions: + contents: write + pull-requests: write + uses: NWarila/terraform-template/.github/workflows/reusable-auto-merge.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..d56f9fe --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,25 @@ +name: CodeQL Analysis + +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + schedule: + - cron: "30 6 * * 0" + workflow_dispatch: + +permissions: {} + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + permissions: + contents: read + security-events: write + actions: read + uses: NWarila/terraform-template/.github/workflows/reusable-codeql.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/org-adr-sync.yaml b/.github/workflows/org-adr-sync.yaml new file mode 100644 index 0000000..c6ba906 --- /dev/null +++ b/.github/workflows/org-adr-sync.yaml @@ -0,0 +1,26 @@ +name: Org ADR Sync +# Caller for nwarila/terraform-template reusable-org-adr-sync workflow. +# Verifies that org-baseline ADRs mirrored under docs/decision-records/org/ +# match upstream /.github byte-for-byte. Runs on every PR so the +# check is always available as a required status check in branch +# protection. Renovate manages the SHA pin. + +on: + pull_request: + branches: [main] + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: org-adr-sync-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + permissions: + contents: read + uses: NWarila/terraform-template/.github/workflows/reusable-org-adr-sync.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml new file mode 100644 index 0000000..afa3720 --- /dev/null +++ b/.github/workflows/pr-validation.yaml @@ -0,0 +1,37 @@ +name: PR Validation + +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pr-validation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate: + uses: NWarila/terraform-template/.github/workflows/reusable-terraform-validation.yaml@5a9279e0514ab054d89430a4453409213f9f351f + with: + # renovate: datasource=github-releases depName=hashicorp/terraform extractVersion=^v(?.*)$ versioning=hashicorp + terraform_version: "1.15.1" + # renovate: datasource=github-releases depName=terraform-linters/tflint extractVersion=^v(?.*)$ + tflint_version: "0.59.1" + # renovate: datasource=github-releases depName=terraform-docs/terraform-docs extractVersion=^v(?.*)$ + terraform_docs_version: "0.20.0" + # renovate: datasource=github-releases depName=open-policy-agent/opa extractVersion=^v(?.*)$ + opa_version: "1.10.0" + template_ref: 5a9279e0514ab054d89430a4453409213f9f351f + mode: runner + framework_repo: nwarila-platform/github-terraform-framework + # Renovate keeps this in lockstep with terraform-deploy.yaml. + framework_ref: 2fe1bceb4f2aadfab703244f899e378fa738d1d2 + overlay_paths: | + repos/public/=>terraform/repos/public/ + tests/fixtures/repos/private/=>terraform/repos/private/ diff --git a/.github/workflows/release-evidence.yaml b/.github/workflows/release-evidence.yaml new file mode 100644 index 0000000..e0523c2 --- /dev/null +++ b/.github/workflows/release-evidence.yaml @@ -0,0 +1,29 @@ +name: Release Evidence +# Caller for nwarila/terraform-template reusable-release-evidence workflow. +# Generates a uniform evidence bundle on every release and attaches it to +# the GitHub release. Renovate manages the SHA pin. +# +# Dispatched explicitly by reusable-release-please.yaml (via `gh workflow +# run`) on every release. The release-published trigger is included as a +# defensive fallback for manual releases. + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: release-evidence-${{ github.ref }} + cancel-in-progress: false + +jobs: + evidence: + permissions: + contents: write + uses: NWarila/terraform-template/.github/workflows/reusable-release-evidence.yaml@5a9279e0514ab054d89430a4453409213f9f351f + with: + # renovate: datasource=github-releases depName=hashicorp/terraform extractVersion=^v(?.*)$ versioning=hashicorp + terraform_version: "1.15.1" diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 0000000..ebca077 --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,26 @@ +name: Release Please +# Caller for nwarila/terraform-template reusable-release-please workflow. +# Renovate manages the SHA pin. The reusable always dispatches +# release-evidence.yaml on every release; no per-repo configuration is +# needed. + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release: + permissions: + contents: write + pull-requests: write + issues: write + actions: write + uses: NWarila/terraform-template/.github/workflows/reusable-release-please.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml new file mode 100644 index 0000000..a973010 --- /dev/null +++ b/.github/workflows/scorecard.yaml @@ -0,0 +1,24 @@ +name: Scorecard + +on: + branch_protection_rule: + schedule: + - cron: "17 6 * * 2" + push: + branches: [main] + workflow_dispatch: + +permissions: read-all + +concurrency: + group: scorecard + cancel-in-progress: false + +jobs: + analysis: + permissions: + security-events: write + id-token: write + actions: read + contents: read + uses: NWarila/terraform-template/.github/workflows/reusable-scorecard.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..10e06bf --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,25 @@ +name: Security Scan + +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + schedule: + - cron: "0 8 * * 1" + workflow_dispatch: + +permissions: {} + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +jobs: + scan: + permissions: + contents: read + security-events: write + actions: read + uses: NWarila/terraform-template/.github/workflows/reusable-iac-security.yaml@5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml new file mode 100644 index 0000000..6f2c6d6 --- /dev/null +++ b/.github/workflows/template-sync.yaml @@ -0,0 +1,22 @@ +name: Template Sync +# Caller for nwarila/terraform-template reusable-template-sync workflow. +# Renovate manages the SHA pin (both `uses:` and `template_ref:`). + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: template-sync + cancel-in-progress: false + +jobs: + sync: + uses: NWarila/terraform-template/.github/workflows/reusable-template-sync.yaml@5a9279e0514ab054d89430a4453409213f9f351f + with: + template_ref: 5a9279e0514ab054d89430a4453409213f9f351f diff --git a/.github/workflows/terraform-deploy.yaml b/.github/workflows/terraform-deploy.yaml new file mode 100644 index 0000000..a326382 --- /dev/null +++ b/.github/workflows/terraform-deploy.yaml @@ -0,0 +1,51 @@ +name: Deploy GitHub Terraform + +# Calls nwarila-platform/github-terraform-framework reusable deploy +# workflow. This file is byte-identical across all three github-terraform- +# runner repos; per-runner specifics live in repo Variables and Secrets: +# +# secrets.AWS_ROLE_TO_ASSUME full IAM role ARN for OIDC +# secrets.AWS_REGION e.g. us-east-1 +# secrets.AWS_S3_BUCKET tfstate backend + private-repo source bucket +# secrets.FINE_GRAINED_PERSONAL_ACCESS_TOKEN GH PAT for Terraform +# +# vars.PRIVATE_REPOS_FILES newline-separated list of private-repo +# filenames to download from S3 before deploy. +# Each line is a bare filename (e.g. +# `Personal.yml`); the reusable derives the S3 +# URL via the convention +# `s3://///repos/`. +# Leave empty to skip S3 fetch entirely (the +# committed `repos/private/` is still overlaid). +# +# github_owner is derived from ${{ github.repository_owner }} so the file +# itself stays identical across the three runners. + +on: + push: + branches: [main] + paths: + - "repos/**" + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: tf-${{ github.event.repository.name }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + deploy: + uses: nwarila-platform/github-terraform-framework/.github/workflows/reusable-terraform-deploy.yaml@40d21e78004ae11e3a2cb4b2d9319643e114743b + with: + github_owner: ${{ github.repository_owner }} + # renovate: datasource=github-releases depName=hashicorp/terraform extractVersion=^v(?.*)$ versioning=hashicorp + terraform_version: "1.15.1" + private_repos_files: ${{ vars.PRIVATE_REPOS_FILES }} + secrets: + aws_role_arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws_region: ${{ secrets.AWS_REGION }} + backend_bucket: ${{ secrets.AWS_S3_BUCKET }} + github_token: ${{ secrets.FINE_GRAINED_PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml deleted file mode 100644 index 8968255..0000000 --- a/.github/workflows/terraform.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: 'Deploy GitHub Terraform' - -on: - workflow_dispatch: - push: - branches: [main] - paths: - - "repos/**" - -permissions: - contents: read - id-token: write - -concurrency: - group: tf-${{ github.event.repository.name }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - terraform: - runs-on: ubuntu-latest - env: - TF_VAR_github_owner: "nwarila-platform" - TF_VAR_github_token: "${{ secrets.FINE_GRAINED_PERSONAL_ACCESS_TOKEN }}" - - steps: - - name: 'Initialize Temporary AWS Credentials (via OIDC)' - uses: 'aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7' # v6.0.0 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - mask-aws-account-id: true - - - name: 'Checkout This Repository' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # v5.0.0 - with: - repository: "${{ github.repository }}" - ref: '${{ github.sha }}' - path: '${{ github.event.repository.name }}' - - - name: "Checkout The 'GitHub Terraform Framework' Repository" - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # v5.0.0 - with: - repository: 'nwarila-platform/github-terraform-framework' - ref: 'main' - path: 'github-terraform-framework' - - - name: 'Download Private Repo Definitions from S3' - run: | - # Sync every *.yml / *.yaml under the private-repos prefix. Keeping - # repo names out of this workflow is intentional — the names are - # private metadata and live only in the private S3 prefix. New repo - # YAMLs are picked up automatically without touching this file. - # Non-YAML objects in the prefix are filtered out. - DEST="${{ github.workspace }}/${{ github.event.repository.name }}/repos/private" - SRC="s3://${{ secrets.AWS_S3_BUCKET }}/nwarila-platform/${{ github.event.repository.name }}/repos" - mkdir -p "$DEST" - aws s3 sync "$SRC/" "$DEST/" \ - --exclude "*" \ - --include "*.yml" \ - --include "*.yaml" \ - --only-show-errors - echo "Downloaded $(ls -1 "$DEST" 2>/dev/null | wc -l) private repo definitions." - - - name: 'Assemble Workspace' - run: | - cp -r "${{ github.workspace }}/${{ github.event.repository.name }}/repos/public/"* "${{ github.workspace }}/github-terraform-framework/terraform/repos/public/" 2>/dev/null || true - cp -r "${{ github.workspace }}/${{ github.event.repository.name }}/repos/private/"* "${{ github.workspace }}/github-terraform-framework/terraform/repos/private/" 2>/dev/null || true - - - name: 'Setup Terraform' - uses: 'hashicorp/setup-terraform@v3' - with: - terraform_version: "1.14.3" - - - name: 'Terraform Init' - working-directory: "${{ github.workspace }}/github-terraform-framework/terraform" - run: | - terraform init \ - -backend-config="bucket=${{ secrets.AWS_S3_BUCKET }}" \ - -backend-config="encrypt=true" \ - -backend-config="key=nwarila-platform/${{ github.event.repository.name }}/terraform.tfstate" \ - -backend-config="region=${{ secrets.AWS_REGION }}" - - - name: 'Terraform Validate' - working-directory: "${{ github.workspace }}/github-terraform-framework/terraform" - run: terraform validate - - - name: 'Terraform Plan' - working-directory: "${{ github.workspace }}/github-terraform-framework/terraform" - run: | - terraform plan -out=tfplan - terraform show -no-color tfplan > plan-output.txt - - - name: 'Archive Plan Output' - uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # v4.6.2 - if: always() - with: - name: terraform-plan-${{ github.sha }} - path: "${{ github.workspace }}/github-terraform-framework/terraform/plan-output.txt" - retention-days: 90 - - - name: 'Terraform Apply' - working-directory: "${{ github.workspace }}/github-terraform-framework/terraform" - run: terraform apply -auto-approve tfplan - - - name: 'Cleanup Workspace' - if: always() - run: | - rm -f "${{ github.workspace }}/github-terraform-framework/terraform/tfplan" - rm -rf "${{ github.workspace }}/${{ github.event.repository.name }}" - rm -rf "${{ github.workspace }}/github-terraform-framework" diff --git a/.gitignore b/.gitignore index b915180..3e88095 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,116 @@ -# Ignore everything by default. -** - -# Keep core repo files. -!/.gitignore -!/.github/ -!/.github/** - -# Keep repo definition YAMLs (public only; private YAMLs live in S3). -!/repos/ -!/repos/public/ -!/repos/public/** -!/repos/private/ -!/repos/private/.gitkeep +# Default-deny: every tracked file must be explicitly allowlisted below. +# No wildcard allows. Adding a tracked file requires adding its exact path +# here too — keeps the tracked surface auditable at a glance. +** + +# ----- [ Repository self-management ] ------------------------------------------------------------- + +!/.gitignore +!/.gitattributes +!/.editorconfig +!/.markdownlint-cli2.jsonc +!/.pre-commit-config.yaml +!/.template-type +!/.terraform-docs.yml +!/.tflint.hcl +!/Makefile + +# ----- [ Top-level documentation ] ---------------------------------------------------------------- + +!/README.md +!/LICENSE +!/CHANGELOG.md + +# ----- [ Release automation ] --------------------------------------------------------------------- + +!/release-please-config.json +!/.release-please-manifest.json + +# ----- [ GitHub configuration ] ------------------------------------------------------------------- + +!/.github/ +!/.github/CODEOWNERS +!/.github/PULL_REQUEST_TEMPLATE.md +!/.github/renovate.json5 +!/.github/workflows/ +!/.github/workflows/auto-merge.yaml +!/.github/workflows/release-evidence.yaml +!/.github/workflows/org-adr-sync.yaml +!/.github/workflows/codeql.yaml +!/.github/workflows/pr-validation.yaml +!/.github/workflows/release-please.yaml +!/.github/workflows/scorecard.yaml +!/.github/workflows/security.yaml +!/.github/workflows/template-sync.yaml +!/.github/workflows/terraform-deploy.yaml + +# ----- [ Documentation (Diátaxis) ] --------------------------------------------------------------- + +!/docs/ +!/docs/README.md +!/docs/explanation/ +!/docs/explanation/architecture.md +!/docs/explanation/testing-strategy.md +!/docs/explanation/threat-model.md +!/docs/reference/ +!/docs/reference/invariants.md +!/docs/reference/release-gates.md +!/docs/how-to/ +!/docs/how-to/develop-this-module.md +!/docs/decision-records/ +!/docs/decision-records/README.md +!/docs/decision-records/org/ +!/docs/decision-records/org/0001-use-architecture-decision-records.md +!/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md +!/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md +!/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md +!/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md + +# ----- [ Tooling & policies ] --------------------------------------------------------------------- + +!/tools/ +!/tools/check_docs_layout.py +!/policies/ +!/policies/opa/ +!/policies/opa/.gitkeep + +# ----- [ Repo definitions ] ----------------------------------------------------------------------- +# Public YAMLs are checked in and overlaid onto the framework at deploy time. +# Private YAMLs live in S3 (private metadata); only .gitkeep is tracked. + +!/repos/ +!/repos/public/ +!/repos/public/.gitkeep +!/repos/public/.github.yml +!/repos/public/ansible-framework.yml +!/repos/public/aws-terraform-framework.yml +!/repos/public/batch-powershell-polyglot.yml +!/repos/public/chisel-releases.yml +!/repos/public/compliance-baseline-reference.yml +!/repos/public/deploy-hello-world.yml +!/repos/public/deploy-shellshockable.yml +!/repos/public/deploy-whoami.yml +!/repos/public/github-terraform-framework.yml +!/repos/public/github-terraform-runner.yml +!/repos/public/packer-required-version.yml +!/repos/public/proxmox-iso-manager.yml +!/repos/public/proxmox-packer-framework.yml +!/repos/public/proxmox-terraform-framework.yml +!/repos/public/secure-packer-bootstrapper.yml +!/repos/public/secure-rockylinux9-template.yml +!/repos/public/start-uninstaller.yml +!/repos/public/talos-cluster.yml +!/repos/public/tdahq-sim00.yml +!/repos/public/terraform-proxmox-iso-manager-framework.yml +!/repos/private/ +!/repos/private/.gitkeep + +# ----- [ Test fixtures (public-safe; used by pr-validation) ] ------------------------------------- + +!/tests/ +!/tests/fixtures/ +!/tests/fixtures/repos/ +!/tests/fixtures/repos/public/ +!/tests/fixtures/repos/public/example.yml +!/tests/fixtures/repos/private/ +!/tests/fixtures/repos/private/example.yml diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..053b68a --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/.template-type b/.template-type new file mode 100644 index 0000000..e73132c --- /dev/null +++ b/.template-type @@ -0,0 +1 @@ +runner diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..edcb5e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +Generated by release-please. Do not edit by hand. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d431807 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Smarter > Harder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c988c4f --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# nwarila-platform/github-terraform-runner + +GitHub-as-code deployer for the +[nwarila-platform](https://github.com/nwarila-platform) GitHub organization. +Owns the inventory of repositories under `repos/` and delegates the actual +`terraform apply` to the +[github-terraform-framework](https://github.com/nwarila-platform/github-terraform-framework) +reusable workflow. + +This repository is a *runner* under the +[NWarila/terraform-template](https://github.com/NWarila/terraform-template) +contract. It contains no Terraform module code of its own; every gate +(validation, security scan, CodeQL, scorecard, sync, release, auto-merge) +runs through reusable workflows from terraform-template, and the deploy +runs through `nwarila-platform/github-terraform-framework`'s +`reusable-terraform-deploy` workflow. + +## Layout + +``` +repos/ + public/ YAML definitions for public repos in nwarila-platform + private/ Empty in-repo (gitkeep only); fetched from S3 at deploy time +tests/ + fixtures/ Public-safe fixtures used by pr-validation +.github/workflows/ + pr-validation.yaml end-to-end CI: checks out the framework at the + pinned SHA, overlays this runner's repos/, runs + `make ci` against the assembled tree + terraform-deploy.yaml the apply path: plans and applies on push to main, + with private repo definitions s3-sync'd at runtime + ... universal callers (security, codeql, scorecard, + release-please, auto-merge, template-sync) +``` + +## Private repo definitions + +Names of private repos are private metadata. The YAMLs that describe them +live in S3 (`s3://${AWS_S3_BUCKET}/nwarila-platform//repos/`) and are +synced into `repos/private/` during the deploy job. Adding a new private +repo is a matter of dropping a YAML into the S3 prefix — no code change +needed in this runner. + +## How a change lands + +1. Edit a YAML under `repos/public/` (or upload one to S3 for private). +2. PR Validation runs end-to-end: framework + this PR's data + the + public-safe `tests/fixtures/` private overlay → must pass contract, + lint, security, and `terraform plan`. +3. After merge, `terraform-deploy.yaml` applies on `main`. + +Renovate keeps `framework_ref` and the deploy-reusable SHA in lockstep with +the framework's `main`. Trusted-bot PRs auto-merge once required checks +pass; human PRs follow normal review. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5f29afd --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# Documentation + +Documentation for this repository follows the [Diátaxis framework](https://diataxis.fr/) +per [org ADR-0002](decision-records/org/0002-adopt-diataxis-documentation-framework.md). + +| Quadrant | Path | Purpose | +| ------------ | --------------------- | ------------------------------------ | +| Explanation | `explanation/` | Architecture, threat model, testing | +| Reference | `reference/` | Generated terraform docs, invariants | +| How-to | `how-to/` | Task-oriented guides | +| Decisions | `decision-records/` | ADRs (org-mirrored + repo-specific) | diff --git a/docs/decision-records/README.md b/docs/decision-records/README.md new file mode 100644 index 0000000..ed8dede --- /dev/null +++ b/docs/decision-records/README.md @@ -0,0 +1,13 @@ +# Architecture Decision Records + +This directory contains the Architecture Decision Records (ADRs) for this +repository. + +ADRs are organized into two scopes per +[org ADR-0001](https://github.com/nwarila-platform/.github/blob/main/docs/decision-records/0001-use-architecture-decision-records.md): + +- `org/` — byte-identical mirrors of org-baseline ADRs. +- `repo/` — repository-specific ADRs. + +When this repo gains its first decision, copy the org ADR-0001 file as +the ADR template and place new entries in `repo/NNNN-short-kebab-title.md`. diff --git a/docs/decision-records/org/0001-use-architecture-decision-records.md b/docs/decision-records/org/0001-use-architecture-decision-records.md new file mode 100644 index 0000000..4708e03 --- /dev/null +++ b/docs/decision-records/org/0001-use-architecture-decision-records.md @@ -0,0 +1,259 @@ + + +# ADR-0001: Use Architecture Decision Records to Document Design Rationale + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-04-22 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | N/A (Accepted) | + +## TL;DR + +We will use `docs/decision-records/` as the conventional home for architecturally significant decisions across the `nwarila-platform` organization. ADRs are organized into two scopes: **org-baseline** ADRs whose master copies live in this `nwarila-platform/.github` repository at `docs/decision-records/` and are mirrored into every adopting child repository at `docs/decision-records/org/`; and **repository-specific** ADRs that live only in their owning repository at `docs/decision-records/repo/`. The format is MADR 4.0-aligned but uses a visible Markdown metadata table, adds explicit reversibility, implementing-PR links, and a conservative compliance-notes crosswalk, and uses the more readable `decision-records` directory name in place of MADR's conventional `adr/`. This gives the organization a single source of truth for org-level governance that travels alongside the code in every adopting repository, plus a place for each repository to record its own architectural choices without conflicting with the shared baseline. + +## Context and Problem Statement + +Every nontrivial repository accumulates architectural choices. Why is authentication handled one way and not another? Why was one platform or workflow chosen over another? Why does CI enforce one supply-chain posture instead of a weaker one? A year later, the code can still show *what* exists, but it rarely explains *why* it exists. + +This `nwarila-platform/.github` repository is the canonical home for org-level governance artifacts. ADRs that establish org-wide conventions (this one, the documentation framework, the source-control hygiene policy, etc.) are authored here and replicated into every adopting child repository. Repositories that participate inherit the org baseline by syncing those mirrored copies into their own `docs/decision-records/org/` directory, and they may add their own repository-scoped ADRs at `docs/decision-records/repo/` for decisions that affect only that repository. + +Three audiences matter here: + +1. **Future maintainers**, trying to determine whether a past decision still makes sense. +2. **Prospective collaborators, students, and hiring managers**, trying to understand the quality of judgment behind the work. +3. **Reviewers and auditors**, who may need source-controlled rationale for security-relevant or compliance-relevant design choices. + +A wiki, a Notion page, a README section, or a folder of ad hoc design notes fails at least one of those audiences. External tools drift from code, READMEs get crowded with user-facing content, and loosely managed documents often disappear or become misleading as ownership changes. + +Architecture Decision Records (ADRs) solve this well: they are lightweight, source-controlled, and widely understood. Michael Nygard introduced the pattern in 2011. ThoughtWorks later recommended lightweight ADRs in source control instead of a wiki or website, and MADR 4.0.0, released on 2024-09-17, provides a well-known community template that is easy to adapt. + +The remaining question is not whether to keep a decision log. It is which format to use, how much structure to require, and where those records should live. + +## Decision Drivers + +The following forces shaped this decision. Subsequent ADRs in repositories that adopt this baseline should name the drivers relevant to their own scope in this same section: + +1. **Reader-first clarity.** A reader without deep software-architecture vocabulary should be able to open any ADR and follow the reasoning. +2. **Portfolio-grade professionalism.** The format should read as serious and disciplined without becoming performatively bureaucratic. +3. **Clone-and-use friendliness.** Another developer should be able to fork a repository and use its ADR structure immediately, with no proprietary tools or dashboards. +4. **Traceability.** Readers should be able to connect a decision to the code and pull requests that put it into effect. +5. **Compliance support without overclaim.** Security-relevant ADRs should make it easier to assemble review evidence without pretending that the document alone proves compliance. +6. **Durability.** The format should remain readable even if tools, vendors, or hosting platforms change. +7. **Low authoring friction.** If the format is painful to write, it will not be used consistently. + +## Considered Options + +1. **No formal decision documentation.** Rely on the README, git history, and commit messages. +2. **Wiki-hosted decision log.** Use GitHub Wiki, Confluence, Notion, or similar. +3. **Canonical Nygard ADRs.** Use the classic five-section ADR format. +4. **Vanilla MADR 4.0.** Use the community standard with its default structure and conventions. +5. **Custom MADR 4.0-aligned format with portfolio-specific extensions.** Keep MADR's core shape, but use a readable `decision-records` directory name, a visible Markdown metadata table, and add reversibility, implementing-PR traceability, and compliance notes. + +## Decision Outcome + +Chosen option: **Option 5, a MADR 4.0-aligned Markdown template with small portfolio-specific extensions.** + +ADRs are organized into two scopes with independent four-digit numbering namespaces: + +- **Org-baseline ADRs** capture decisions that apply to the entire `nwarila-platform` organization. Their master copies live in this `nwarila-platform/.github` repository at `docs/decision-records/NNNN-short-kebab-title.md`. Every adopting child repository mirrors them at `docs/decision-records/org/NNNN-short-kebab-title.md` (identical content, copied byte-for-byte from the master). Numbers in the org namespace are allocated monotonically and never reused. + +- **Repository-specific ADRs** capture decisions that apply only to one repository. They live in that repository at `docs/decision-records/repo/NNNN-short-kebab-title.md` and are not mirrored to any other repository or to the org `.github` repo. Numbers in a repository's `repo` namespace are independent of the org namespace; the same number can appear once in `org/` and once in `repo/` without conflict because they are in different directories. + +Within both scopes, `NNNN` is the next unused four-digit number, allocated monotonically and never reused. The directory name is `decision-records` because it is immediately understandable to readers who do not already know the acronym. The subdirectory split (`org/` vs `repo/`) keeps the two scopes visually and structurally distinct, so a reader scanning a repository can immediately see which decisions were inherited from the organization and which were made locally. + +ADRs follow the structure demonstrated by this file itself, in this order: metadata table, TL;DR, Context and Problem Statement, Decision Drivers, Considered Options, Decision Outcome, Pros and Cons of the Options, Confirmation, Consequences (Positive / Negative / Neutral), Assumptions, Supersedes, Superseded by, Implementing PRs, Related ADRs, and Compliance Notes. Sections that genuinely do not apply are kept and filled with "None." or "N/A (reason)." so readers can distinguish "not applicable" from "forgotten." + +A decision is **architecturally significant** and warrants an ADR when any of the following are true: + +- It has multiple serious alternatives with nontrivial trade-offs. +- It shapes how future work in the repository will be done, not just one implementation task. +- It materially affects security, compliance posture, or supply-chain posture. +- A reader six months from now would reasonably ask "why did we choose X over Y?" and the answer will not be obvious from the code alone. + +Decisions that are **not** ADR-worthy include forced choices with no practical alternatives, style-level preferences with negligible downstream impact, runbook procedures, and single-PR implementation details. + +This ADR is the canonical example for this baseline and the starting point for participating repositories in the portfolio. When another repository seeds its own `ADR-0001` from this file, it must rewrite the metadata, context, consequences, and compliance notes so the record is true for that repository. + +## Pros and Cons of the Options + +### Option 1: No formal decision documentation + +- **Good, because** it has effectively zero authoring overhead. +- **Good, because** it requires no new conventions or templates. +- **Bad, because** reasoning behind important choices is quickly lost. +- **Bad, because** it leaves reviewers and future maintainers to reverse-engineer intent from code and commit messages. +- **Bad, because** it provides no durable source-controlled artifact for security-relevant design rationale. + +### Option 2: Wiki-hosted decision log + +- **Good, because** wikis are easy to edit and cross-link in a browser. +- **Good, because** they support long-form documentation well. +- **Bad, because** wiki or website content can drift away from the code it describes; ThoughtWorks explicitly recommends source control instead. +- **Bad, because** GitHub wikis are stored and cloned separately from the main repository, which weakens the "docs travel with the code" property. +- **Bad, because** externally hosted tools create additional access, lifecycle, and vendor dependencies. + +### Option 3: Canonical Nygard ADRs + +- **Good, because** the format is widely recognized and easy to explain. +- **Good, because** its five-section shape is approachable for non-specialist readers. +- **Neutral, because** its minimalism is a strength until stronger traceability or reviewability is needed. +- **Bad, because** it does not natively prompt explicit decision drivers, option-by-option trade-off analysis, or confirmation criteria. +- **Bad, because** reversibility, implementing-PR traceability, and compliance notes would all need to be reinvented locally as ad hoc extensions. + +### Option 4: Vanilla MADR 4.0 + +- **Good, because** MADR 4.0.0 is a well-known and maintained community standard. +- **Good, because** Decision Drivers, Considered Options, option-level pros and cons, and Confirmation add rigor beyond the classic Nygard structure. +- **Good, because** YAML front matter and existing tooling make machine processing feasible. +- **Neutral, because** YAML front matter is slightly less approachable to some readers than a visible Markdown metadata table. +- **Bad, because** the default conventions do not capture this portfolio's preference for the clearer `decision-records` directory name. +- **Bad, because** reversibility, implementing PRs, and conservative compliance mapping still need local conventions. + +### Option 5: Custom MADR 4.0-aligned format with portfolio-specific extensions (chosen) + +- **Good, because** it stays close to MADR 4.0 while remaining easy to read in plain GitHub Markdown. +- **Good, because** the explicit `decision-records` directory name is clearer for first-time readers. +- **Good, because** a visible Markdown metadata table keeps key governance facts readable in rendered and raw form. +- **Good, because** an explicit **Reversibility** field encourages better judgment about how expensive it will be to change course later. +- **Good, because** **Implementing PRs** and supersession links improve traceability between rationale and code. +- **Good, because** **Compliance Notes** creates a place to record how a decision may support external review frameworks without claiming that the ADR alone proves compliance. +- **Neutral, because** the additional fields only need short entries when a decision is simple. +- **Bad, because** any future automation must target this exact schema rather than stock MADR. +- **Bad, because** readers familiar with vanilla MADR may need a brief orientation to the differences. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Org-baseline mirror check.** A child repository that adopts this baseline MUST contain `docs/decision-records/org/` populated with byte-identical copies of every accepted org-baseline ADR from `nwarila-platform/.github/docs/decision-records/`. A CI job or `pre-commit` hook MAY fail a pull request that adds an `org/` file that does not match the master, removes a master that still exists upstream, or omits a master that has been added upstream. +2. **Repo-scope check.** Repository-specific ADRs MUST live at `docs/decision-records/repo/NNNN-short-kebab-title.md`. They MUST NOT appear in `docs/decision-records/org/` or be promoted to the org namespace without first being authored as a new ADR in `nwarila-platform/.github`. A CI script MAY assert this directory split. +3. **Schema check.** A CI script SHOULD verify that every file matching `docs/decision-records/{org,repo}/[0-9][0-9][0-9][0-9]-*.md` contains the required section headings from this template: `## TL;DR`, `## Context and Problem Statement`, `## Decision Drivers`, `## Considered Options`, `## Decision Outcome`, `## Pros and Cons of the Options`, `## Confirmation`, `## Consequences`, `## Assumptions`, `## Supersedes`, `## Superseded by`, `## Implementing PRs`, `## Related ADRs`, and `## Compliance Notes`. `## Considered Options` and `## Pros and Cons of the Options` are especially important because they preserve rejected alternatives and trade-offs. +4. **Index check.** A repository that has any ADRs MUST contain `docs/decision-records/README.md` listing every ADR (org-mirrored and repo-specific, in clearly separated sections) with its current Status and Summary. A CI script SHOULD diff the directory listing against the index and fail on drift. +5. **Human review.** Every pull request that introduces a new ADR MUST be reviewed. Every pull request that materially contradicts an Accepted ADR SHOULD either update the code to comply, supersede the ADR, or explain why the ADR never actually applied to the change in question. +6. **Editorial rule.** After acceptance, edits MAY correct typos, broken links, formatting, Status, supersession fields, or `Implementing PRs`, but they MUST NOT silently change the decision, its scope, or its rationale. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository or a compliance-critical repository SHOULD automate at least the presence, schema, and index checks. + +## Consequences + +### Positive + +- Decisions are explained, findable, and version-controlled alongside the code they govern. +- The explicit `decision-records` directory name is easier for first-time readers to understand. +- Reviewers, contributors, and hiring audiences can reconstruct the reasoning behind important architectural choices without a synchronous conversation. +- Security-relevant ADRs can contribute reusable evidence for reviews, assessments, and compliance preparation. +- The format is self-documenting: this ADR both adopts the format and demonstrates how to use it. + +### Negative + +- Every architecturally significant change now carries some documentation overhead. +- The format is custom enough that future automation and linting will likely need repository-specific support. +- If an adopting repository copies this ADR mechanically instead of rewriting repository-specific content, it can create a polished but false record. +- Without enforcement tooling, ADRs can still drift from the code they describe. + +### Neutral + +- The directory layout `docs/decision-records/{org,repo}/` and the filename pattern `NNNN-short-kebab-title.md` are established conventions for the organization. +- Each adopting repository's `docs/decision-records/README.md` becomes the canonical local index for its full ADR set (org-mirrored plus repo-specific) and is the single page to read to understand the repository's decision posture. +- Future repositories may introduce carefully scoped local extensions, but those should be documented in their own repo-specific ADRs (under `docs/decision-records/repo/`) rather than by silently mutating this baseline. +- Org-baseline ADRs are duplicated content (master in `nwarila-platform/.github`; mirrors in every adopting child repo). The duplication is deliberate — it keeps governance content traveling with the code that implements it — but it means org-ADR amendments require a coordinated update across all adopting repositories. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. GitHub, or an equivalent Git-hosting service, remains the primary home for source control in repositories that adopt this baseline. +2. Markdown remains a widely supported, human-readable plain-text format. +3. Participating repositories continue to benefit from keeping governance artifacts near the code instead of in a separate knowledge base. +4. Repositories using this template have a clear decision-making path. In a solo-maintainer repository that may be the maintainer; in a team repository it may be a designated approver or architecture owner. + +## Supersedes + +None. This is the inaugural ADR. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself; that is already discoverable from version control. This is one of the few sections expected to gain entries after acceptance. + +None yet. The primary expected follow-ons for this ADR are enforcement checks for presence, schema, and index drift, plus propagation of the structure into participating repositories that adopt this baseline. + +## Related ADRs + +None at this time. Subsequent ADRs that depend on this one, refine this format, or add repository-specific extensions should back-link here in their own `Related ADRs` sections. + +## Compliance Notes + +This ADR establishes a documentation mechanism, not a deployed security control. Its value is evidentiary: later ADRs can capture rationale, alternatives, and security trade-offs in a form that is reusable during reviews. The table below indicates where such evidence may help; it is illustrative rather than exhaustive, and it is not a claim that a repository is compliant merely because ADRs exist. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SA-17 (Developer Security and Privacy Architecture and Design) | ADRs can document the architecture and design rationale considered during development. | +| NIST SP 800-53 Rev. 5 | PL-8 (Security and Privacy Architectures) | ADRs can support architecture narratives and link design choices to source-controlled artifacts. | +| NIST SP 800-53 Rev. 5 | SA-8 (Security and Privacy Engineering Principles) | Security-relevant ADRs can record how engineering principles shaped specific choices. | +| NIST SP 800-218 (SSDF) | PW.1 (Design Software to Meet Security Requirements and Mitigate Security Risks) | Security-focused ADRs can record identified risks, planned mitigations, and why a requirement was accepted, relaxed, or judged out of scope. | +| FedRAMP SSP artifacts | System-description and architecture narratives | ADRs can provide reusable source material and traceability for SSP drafting, but they do not replace the SSP or the assessment evidence set. | + +Subsequent ADRs should keep only the rows that genuinely apply to the decision at hand and should describe the relationship conservatively. diff --git a/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md b/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md new file mode 100644 index 0000000..7416a9a --- /dev/null +++ b/docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md @@ -0,0 +1,193 @@ +# ADR-0002: Adopt Diátaxis as the Documentation Framework + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-04-24 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | N/A (Accepted) | + +## TL;DR + +We will use the [Diátaxis](https://diataxis.fr) documentation framework for all non-ADR documentation in repositories that adopt this baseline. Each adopting repository organizes long-form documentation into the four Diátaxis quadrants — **tutorials**, **how-to guides**, **reference**, and **explanation** — under a `docs/` directory whose immediate subdirectories mirror those quadrant names. ADRs themselves remain governed by [ADR-0001](0001-use-architecture-decision-records.md) and live in their own subtree at `docs/decision-records/{org,repo}/` (org-mirrored or repository-specific). This gives every repository a consistent, reader-first information architecture that is easy to navigate, easy to maintain, and easy for new contributors to understand. + +## Context and Problem Statement + +[ADR-0001](0001-use-architecture-decision-records.md) established a format for *decisions*, but a repository's documentation surface is much larger than its decision log. Repositories accumulate setup procedures, permission matrices, troubleshooting guides, conceptual explainers, contributor onboarding material, and operational runbooks. Without a shared organizing principle, that material lands wherever the original author thought to put it — a `README.md` section, a long monolithic design document, a folder of unstructured Markdown files, or worse, an out-of-band tool. + +Three failure modes consistently follow from ad-hoc documentation organization: + +1. **Drift.** Reference material, procedural material, and conceptual material accumulate in the same file. Updates to one type silently invalidate the others, and readers cannot tell which sentences are authoritative reference and which are narrative explanation. +2. **Findability collapse.** A reader who needs a specific permission, a specific command, or a specific definition cannot predict where to look. Repositories develop "you have to know to look in `DESIGN.md` §15.2" tribal knowledge. +3. **Authoring paralysis.** Without a framework that names what kind of document is needed, every contributor reinvents structure from scratch. Some attempts produce comprehensive prose explainers when a one-page reference would suffice; others produce bare command listings when readers actually need conceptual grounding. + +This portfolio is solo-maintained today but is built to be reviewable, hireable-from, and potentially shared with collaborators. Documentation that fails the three modes above is invisible work: it costs time to write, costs time to maintain, and produces little durable value. + +The remaining question is which documentation framework to adopt. The choice has to balance authoring cost, reader experience, durability against the framework's own future, and accessibility for solo and small-team maintainers. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Reader-first organization.** A reader's first question is rarely "what topic is this?" — it is usually "what am I trying to do?" The framework should organize by user need, not by author convenience. +2. **Authorial clarity.** A contributor sitting down to write should know what kind of document they are producing before they begin. A framework that names doc types reduces the "what should this be?" decision to a one-step lookup. +3. **Findability.** A reader who has visited the documentation once should be able to predict the location of any subsequent piece of information from the framework's conventions alone. +4. **Active community and durability.** The framework's own home should still be active in five years. Frameworks whose flagship implementations are archived create future migration cost. +5. **Named-adopter signal.** Adoption by recognized organizations is empirical evidence that the framework survives contact with real engineering teams. +6. **Accessibility.** A solo maintainer must be able to produce a useful first draft without building tooling, hiring a writer, or reading a 200-page manual on documentation theory. +7. **Co-existence with ADR-0001.** Whatever framework is chosen must accommodate ADRs as a distinct, separately governed artifact rather than competing with them. +8. **Co-existence with operational reality.** Some real-world documentation is composite by necessity — runbooks naturally combine reference, procedure, and troubleshooting. The framework must accommodate composite documents without forcing artificial fragmentation. + +## Considered Options + +1. **No formal documentation framework.** Continue with ad-hoc structure: `README.md`, monolithic `DESIGN.md`, scattered notes. +2. **POSIX `man(7)` format.** Use the Unix manual-page convention with `NAME / SYNOPSIS / DESCRIPTION / OPTIONS / EXAMPLES / SEE ALSO`. +3. **The Good Docs Project templates.** Use the community-maintained per-doc-type Markdown templates. +4. **Single-doc-per-topic runbook structure.** Use one Markdown file per topic, internally structured along Google SRE / Atlassian / PagerDuty runbook conventions (`Purpose / Prerequisites / Procedure / Verification / Rollback / Troubleshooting`). +5. **Diátaxis.** Organize all documentation into four quadrants (tutorials, how-to guides, reference, explanation) with each document classified into exactly one quadrant. +6. **Custom in-house framework.** Define a bespoke documentation taxonomy specific to this portfolio. + +## Decision Outcome + +Chosen option: **Option 5, Diátaxis.** + +In a repository that adopts this baseline, all non-ADR documentation lives under `docs/` with subdirectories named exactly `tutorials/`, `how-to/`, `reference/`, and `explanation/`. Every Markdown file under those four subdirectories (other than an index `README.md`) lives in exactly one of them and is authored to one Diátaxis purpose. ADRs live in their own sibling subtree at `docs/decision-records/{org,repo}/` as established by ADR-0001 and are not subject to the Diátaxis quadrant rule. + +A repository is not required to populate every quadrant. A repository that has no learning-oriented onboarding need not create `docs/tutorials/`. A repository may begin by populating only the quadrants that solve current pain (commonly `docs/reference/` and `docs/how-to/`) and grow into the others over time. A repository's `docs/README.md` MAY serve as the index across populated quadrants and SHOULD label each linked document with its quadrant. + +Composite operational documents — runbooks, troubleshooting guides, and other artifacts that combine reference, procedural, and explanatory content by necessity — are treated as belonging to the **how-to** quadrant when their primary purpose is to walk an operator through a goal-directed task, and to the **reference** quadrant when their primary purpose is lookup. Composite documents MUST use explicit second-level section headings labelled with the quadrant they touch (`## Reference`, `## How to ...`, `## Why ...`) so readers can predict where the lookup material ends and the procedural material begins. Composite documents are an explicit accommodation of operational reality and are not an exception that is allowed to expand silently into general non-operational docs. + +The directory name `docs/` is preferred over alternatives such as `documentation/` or `book/` for terseness and broad community familiarity. The subdirectory names match Diátaxis terminology exactly: `tutorials`, `how-to`, `reference`, `explanation`. Plural for `tutorials` matches Diátaxis usage. Hyphenated `how-to` matches Diátaxis usage and avoids an awkward `how_to` or `howto`. The plural `references` is explicitly avoided because Diátaxis uses the mass noun `reference`. + +## Pros and Cons of the Options + +### Option 1: No formal documentation framework + +- **Good, because** it has zero authoring overhead at the framework level. +- **Good, because** it imposes no structure on contributors who already know what they want to write. +- **Bad, because** it allows reference, procedural, and conceptual material to accumulate in the same files, where one type silently invalidates the others. +- **Bad, because** readers cannot predict the location of new information without reading the whole repository. +- **Bad, because** every contributor reinvents the wheel; the result reads as a collage rather than a documentation set. + +### Option 2: POSIX `man(7)` format + +- **Good, because** the format is one of the most stable in software history, with effectively 100% adoption in Unix-derived CLI tooling for over fifty years. +- **Good, because** the structure is well-defined and predictable: `NAME / SYNOPSIS / DESCRIPTION / OPTIONS / EXAMPLES / SEE ALSO`. +- **Bad, because** the format is designed for CLI program reference, not for operational documentation, conceptual explainers, onboarding tutorials, or repository-level governance docs. +- **Bad, because** it has no equivalent of how-to guides, tutorials, or explanation, so it would force every non-reference document into an awkward shape. +- **Bad, because** community familiarity outside CLI tooling is limited; readers expect this format only for executable manuals. + +### Option 3: The Good Docs Project templates + +- **Good, because** it provides explicit Markdown templates for several distinct document types. +- **Good, because** templates lower the barrier to authoring, especially for non-writers. +- **Bad, because** the project's flagship templates repository on GitHub was archived on 2022-09-24, and active development has migrated to a less-discoverable GitLab home; the public signal of momentum has weakened. +- **Bad, because** the project has no publicly named enterprise adopters of comparable visibility to the framework chosen below. +- **Bad, because** it is a templates-bundle rather than a documentation philosophy; it tells contributors what template to use but not why a particular reader needs a particular type of document. + +### Option 4: Single-doc-per-topic runbook structure + +- **Good, because** it is the most accessible structure when the constraint is "one URL per topic for a hurried operator." +- **Good, because** it matches the mental model of operations and SRE teams, who already think in runbooks. +- **Good, because** it accommodates reference, procedure, and troubleshooting in a single file, which simplifies cross-linking. +- **Neutral, because** it is a structure pattern rather than a documentation framework; it does not categorize non-operational documentation at all. +- **Bad, because** it encourages exactly the drift problem this ADR is trying to prevent: reference, how-to, and explanation accumulate in one file with no enforced separation. +- **Bad, because** it has no taxonomy for tutorials, conceptual explainers, or repository-level governance, so non-operational docs end up unfiled. +- **Bad, because** there is no canonical runbook standard; choosing this option requires also choosing one of several competing runbook templates and maintaining it as a local convention. + +### Option 5: Diátaxis (chosen) + +- **Good, because** it is the most-adopted explicit documentation methodology in modern software-engineering practice, with public adopters including Canonical, Cloudflare, Gatsby, LangChain, Vonage, Sequin, and StreamingFast, and an active framework repository at over a thousand stars. +- **Good, because** it is reader-first: it organizes documentation by what the reader is trying to do, not by what topic it is about. +- **Good, because** the four quadrants are mutually exclusive and collectively exhaustive in everyday practice; an authoring contributor knows in seconds which quadrant their document belongs in. +- **Good, because** it is a *philosophy* not just a *template bundle*; it explains why a particular reader needs a particular type of document, which is more durable than any single template. +- **Good, because** it accommodates ADR-0001 cleanly: ADRs are a specialized governance artifact, not a Diátaxis category, and live in their own directory governed by their own ADR. +- **Good, because** repositories may adopt it incrementally, populating only the quadrants they currently need. +- **Neutral, because** composite operational documents (runbooks, troubleshooting guides) span quadrant lines and require explicit accommodation; this ADR provides that accommodation in the Decision Outcome. +- **Bad, because** topics that today live in a single file may need to be split across two or more files when adopted, creating short-term re-shelving cost. +- **Bad, because** it requires authors to classify each new document, which is a small but nonzero per-doc decision. + +### Option 6: Custom in-house framework + +- **Good, because** it could be tailored to this portfolio's exact needs. +- **Bad, because** the maintenance and onboarding cost of a bespoke framework is unjustified when an actively maintained external framework already covers the same ground. +- **Bad, because** it provides no signal of community recognition to external readers, contributors, or hiring audiences. +- **Bad, because** it directly contradicts ADR-0001's preference for established community standards over local invention. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Layout check.** A repository that adopts this baseline MUST place all non-ADR documentation under `docs/`. Where any quadrant is populated, it MUST be in a top-level subdirectory named exactly `tutorials`, `how-to`, `reference`, or `explanation`. A CI script or `pre-commit` hook MAY fail a pull request that adds a Markdown file under `docs/` outside one of the four quadrant subdirectories. +2. **Quadrant-purity check.** Each non-composite document under `docs/` SHOULD address exactly one Diátaxis quadrant. Composite operational documents (runbooks, troubleshooting guides) MAY span quadrants but MUST mark each section with its quadrant via an explicit second-level heading (`## Reference`, `## How to ...`, `## Why ...`). +3. **Index check.** A repository's `docs/README.md`, if present, SHOULD link to every populated quadrant and SHOULD label each linked document with the quadrant it belongs to. A CI script MAY diff the directory listing against the index and fail on drift. +4. **Co-existence check.** ADRs live at `docs/decision-records/{org,repo}/` (a sibling subtree, not a Diátaxis quadrant) and are not subject to the quadrant rule. A pull request that misfiles an ADR under one of the four quadrant directories SHOULD be rejected with a pointer to ADR-0001. +5. **Editorial rule.** After acceptance of this ADR, document re-shelving (moving an existing doc into the appropriate quadrant) is editorial, not architectural; it does not require its own ADR. A material *change* to the framework choice — adopting a different framework, abandoning Diátaxis, or extending the quadrant taxonomy — does require a superseding ADR. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository SHOULD automate at least the layout and index checks. + +## Consequences + +### Positive + +- New documentation has a predictable home from the moment it is written. +- Readers can navigate any adopting repository's `docs/` tree using the same mental model. +- Authoring decisions reduce to a one-step quadrant classification rather than open-ended structural design. +- Drift between reference, procedural, and conceptual material becomes harder, because each document has a single declared purpose. +- The choice carries community recognition: external readers, contributors, and hiring audiences encounter a framework they likely already know. + +### Negative + +- Existing monolithic documents (in particular `DESIGN.md` in `github-terraform-framework`, and any plan documents in other repositories) do not yet conform to Diátaxis. Re-shelving them is a non-trivial editorial pass that is deferred to per-repository follow-on work. +- Some topics that today live in a single file will need to be split across two or more files; readers who already know the old single-file layout will have a one-time re-orientation cost. +- Authors must perform a small classification step per document. + +### Neutral + +- The four Diátaxis quadrant names are now reserved at `docs/{tutorials,how-to,reference,explanation}/` in adopting repositories. Future ADRs that need additional top-level directories under `docs/` should reference this ADR explicitly. +- Composite operational documents are an explicit accommodation rather than a violation; their boundaries are codified in the Decision Outcome and Confirmation sections above. +- ADRs continue to be governed by ADR-0001 and are unaffected by this decision other than by cross-reference. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The Diátaxis framework remains actively maintained and documented at [diataxis.fr](https://diataxis.fr) or an equivalent successor URL. +2. The four-quadrant taxonomy continues to map cleanly to the documentation needs that arise in this portfolio. If a sustained category of need cannot be classified into one of the four quadrants, that pattern is itself evidence to reconsider. +3. Markdown remains the primary documentation format in adopting repositories. +4. Repositories prefer source-controlled documentation that travels with the code over externally hosted knowledge bases. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself. + +Pending. The first expected implementer is `github-terraform-framework`, which will adopt the Diátaxis layout for its initial PAT and AWS IAM documentation set, with `DESIGN.md` deferred for separate refactor. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the ADR convention itself. ADR-0001 governs `docs/decision-records/{org,repo}/`; this ADR governs the rest of `docs/` (everywhere else). + +## Compliance Notes + +This ADR establishes a documentation organization convention, not a deployed security control. The table below indicates where evidence produced under this convention may help during reviews; it is illustrative rather than exhaustive, and is not a claim that a repository is compliant merely because Diátaxis is adopted. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SA-5 (System Documentation) | A consistently structured `docs/` tree supports the system-documentation requirement by making operational, reference, and conceptual material findable. | +| NIST SP 800-53 Rev. 5 | AT-2 (Literacy Training and Awareness) | `docs/tutorials/` and `docs/how-to/` material supports onboarding and operational literacy. | +| NIST SP 800-218 (SSDF) | PO.3.2 (Document the security policies of the SDLC) | A standardized `docs/` location for security-relevant procedural and reference material reduces the assembly cost of evidence packages. | +| ISO/IEC 27001:2022 | A.5.37 (Documented operating procedures) | `docs/how-to/` and operational composite documents under `docs/reference/` provide a predictable location for documented procedures. | + +Subsequent repository-level ADRs that scope this convention to specific compliance contexts should keep only the rows that genuinely apply to their decision. diff --git a/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md b/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md new file mode 100644 index 0000000..624d1c4 --- /dev/null +++ b/docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md @@ -0,0 +1,202 @@ +# ADR-0003: Use a Deny-All `.gitignore` Strategy + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-04-25 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Cheap | +| Review-by | N/A (Accepted) | + +## TL;DR + +In repositories that adopt this baseline, `.gitignore` is structured as **deny-all by default** with an **explicit allowlist** of files and directories that are intended to be tracked. The first non-comment rule in `.gitignore` is `**` (ignore everything), followed by `!`-prefixed allowlist entries. New files are not tracked unless their path is explicitly added to the allowlist. This inverts the dominant `.gitignore` convention (allow-all with scattered denies) in exchange for default-safe behaviour: secrets, terraform state, build artifacts, IDE files, and any future class of accidentally-introduced sensitive content cannot enter the repository through `git add` alone. + +## Context and Problem Statement + +The default `.gitignore` convention across the open-source community is **allow-all with explicit denies**: a base of "track everything" with a list of patterns to ignore. Tooling reinforces this default — IDEs, scaffolding tools, and language ecosystems ship pre-built `.gitignore` files that enumerate known build artifacts, vendor directories, and temporary files for that ecosystem. + +The dominant convention has a structural failure mode: anything *not* in the deny list is tracked. New tooling, new build outputs, new artifact types, and new categories of sensitive file all default to tracked. The list of patterns to deny is open-ended and grows with every new tool a repository adopts. A repository that integrates a new tool without simultaneously updating `.gitignore` silently begins tracking that tool's outputs. + +For the categories of file that matter most to a security-aware portfolio, this failure mode is dangerous: + +1. **Credentials.** `.env`, `credentials.json`, `.aws/`, OAuth tokens, GitHub Personal Access Tokens accidentally written to disk by a script. +2. **Terraform state.** `terraform.tfstate` files contain decrypted secrets and resource metadata in plaintext after `apply`. A leaked tfstate is an immediate compromise of the infrastructure it describes. +3. **Build artifacts.** Compiled binaries, bundled JavaScript, container layers, and similar outputs that bloat the repository and add no source-of-truth value. +4. **Personal-environment leakage.** IDE configuration, editor swap files, OS-level metadata files (`.DS_Store`, `Thumbs.db`, `desktop.ini`). +5. **Tool-specific transient files.** `.terraform/` plugin caches, `node_modules/` (when the project intends to commit `package-lock.json` only), `__pycache__/`, `.coverage`, `.pytest_cache/`, and a long tail. + +A repository that uses the dominant convention defends against (5) and parts of (3) and (4) only because someone, at some point, added each pattern to `.gitignore`. The defence against (1) and (2) is implicit — *if* the repository's `.gitignore` was thoughtful enough to deny `*.env`, `*.tfstate`, etc., the file is excluded; otherwise it is tracked the moment someone runs `git add .`. There is no signal at the moment of failure: `git add` does not distinguish between intended commits and accidental commits, and `git status` shows the file as a normal addition. + +This portfolio's `github-terraform-framework` repository has carried a deny-all `.gitignore` since its initial commit, and the strategy has prevented multiple categories of accidental commit during normal development. The remaining question is whether to elevate that ad hoc choice into an explicit, named decision that other repositories in the portfolio inherit, and to document the trade-offs honestly so future contributors understand both the value and the friction. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Default-safe behaviour.** A new file should be ignored unless explicit action is taken to track it. The cost of one extra allowlist line is negligible; the cost of one accidentally committed credential is potentially catastrophic. +2. **Reviewability.** Every new tracked file should be visible in pull-request review as an `.gitignore` allowlist edit, not as a silent inclusion. This makes "what files does this PR start tracking?" a single grep, not a directory walk. +3. **Failure-mode visibility.** The strategy should fail in a way that is *visible*. A file that is silently ignored is bad if the contributor expected it to be tracked; the absence from `git status` should be diagnosable in seconds with documented tooling. +4. **Consistency across repositories.** Contributors who learn the pattern in one repository should encounter the same pattern in others. Mixing strategies across the portfolio costs cognitive overhead disproportionate to any per-repo optimisation. +5. **Reversibility.** A repository that adopts this strategy and later regrets it should be able to migrate back to allow-all-with-denies in a single PR. The choice should not lock the repository in. +6. **Compatibility with established tooling.** Pre-commit hooks, CI lint stages, and IDE integrations should not need to be retrained. The strategy must use only standard `.gitignore` syntax. +7. **Community familiarity.** The dominant convention is allow-all-with-denies. Choosing the opposite imposes a learning cost on contributors. The strategy should carry enough documentation that the cost is paid once, by the contributor's first encounter, not repeatedly. + +## Considered Options + +1. **No `.gitignore`.** Track every file in the working directory. +2. **Allow-all with explicit denies (community default).** Use a base of "track everything" with `.gitignore` entries that enumerate patterns to skip. +3. **Hybrid.** Allow-all base with periodic deny additions, supplemented by ad hoc per-directory `.gitignore` files. +4. **Deny-all with explicit allowlist (chosen).** First non-comment rule is `**` (ignore everything); subsequent `!`-prefixed entries explicitly allowlist tracked paths. +5. **`git add` discipline only.** No `.gitignore`; rely on contributors to use `git add ` rather than `git add .`. +6. **Sparse checkout / worktree partitioning.** Use git's sparse-checkout to limit which files are visible. + +## Decision Outcome + +Chosen option: **Option 4, deny-all with an explicit allowlist.** + +In a repository that adopts this baseline, `.gitignore` is organised as follows: + +1. The first non-comment rule is `**` (or an equivalent globstar that excludes the entire working tree). +2. Subsequent rules are `!`-prefixed allowlist entries that re-include specific files and directories. Allowlist entries are organised in groups corresponding to the categories of tracked content (source code, configuration, fixtures, documentation, CI workflows, license, README). +3. Each allowlist group is preceded by a `#`-prefixed comment that names the group and, where useful, explains why those entries are tracked. +4. New files added to the repository require an `.gitignore` allowlist edit. The allowlist edit and the new file MUST be in the same pull request and ideally in the same commit. A pull request that adds a file without allowlisting it is a reviewer-detectable defect: the new file will not appear in `git status` after `git add`. +5. Directories require **two** allowlist entries: one for the directory itself (`!/path/to/dir/`) and one for its contents (`!/path/to/dir/**`). A single entry of either form does not suffice in all git versions. + +The strategy applies recursively. A repository's top-level `.gitignore` SHOULD carry the deny-all rule and the full allowlist. Per-directory `.gitignore` files MAY exist but MUST NOT contradict the top-level deny: a per-directory file may add denials, never re-add allows. + +This baseline ADR establishes the default. Repositories that have a sustained reason to opt out — for example, a repository whose primary purpose is hosting a large generated artifact tree where allowlisting every file is impractical — MAY do so by recording a repository-level ADR that supersedes ADR-0003 in scope. An opt-out is itself an architectural decision and is treated as such. + +The strategy explicitly does not prescribe enforcement tooling. A pre-commit hook, CI check, or `git hook` MAY verify that the first non-comment line of `.gitignore` is the deny-all rule, that the allowlist contains no contradictions, or that every tracked file in the working tree is explicitly allowlisted. None of these checks are mandatory at acceptance time. Adopting repositories MAY add them as separate decisions. + +## Pros and Cons of the Options + +### Option 1: No `.gitignore` + +- **Good, because** it has zero authoring overhead and zero rule maintenance. +- **Bad, because** every transient file (build artifacts, IDE files, OS metadata, secrets) lands in pull requests by default. +- **Bad, because** it is not a viable strategy for any real-world repository and is included only for completeness. + +### Option 2: Allow-all with explicit denies (community default) + +- **Good, because** it is the dominant convention; contributors recognise it without explanation. +- **Good, because** language and tool ecosystems ship pre-built `.gitignore` files that contributors can drop in. +- **Good, because** small repositories with predictable noise (a single language, a single build system) can rely on community templates with minimal customisation. +- **Neutral, because** the rule list grows over time with every new tool the repository adopts. +- **Bad, because** anything *not* in the deny list is tracked by default. New file types slip through silently. +- **Bad, because** the failure mode for sensitive files (credentials, state, secrets) is "tracked unless explicitly denied." A forgotten deny is a security incident. +- **Bad, because** there is no review-time signal that a new tracked file was *intentionally* tracked rather than accidentally swept in by `git add .`. + +### Option 3: Hybrid + +- **Good, because** it allows incremental adoption: existing allow-all repositories can add per-directory denies without restructuring the top-level file. +- **Bad, because** it inherits all of Option 2's failure modes. +- **Bad, because** per-directory `.gitignore` files are easy to overlook; the strategy is implicitly distributed and hard to audit. +- **Bad, because** it picks the worst of both ends: contributors must understand both top-level and per-directory rules, but the safety properties of deny-all are not gained. + +### Option 4: Deny-all with explicit allowlist (chosen) + +- **Good, because** new files default to ignored; sensitive files (credentials, state, secrets) cannot enter the repository through `git add` alone. +- **Good, because** each new tracked file is an explicit, reviewable allowlist edit. "What files does this PR begin tracking?" reduces to a `.gitignore` diff. +- **Good, because** the inventory of intentionally tracked file paths *is* `.gitignore` itself; the file becomes self-documenting. +- **Good, because** the failure mode (a file not appearing in `git status` after `git add`) is detectable in seconds with `git check-ignore -v `, which names the rule that is excluding it. +- **Good, because** it requires only standard `.gitignore` syntax; pre-commit hooks, IDEs, and CI tooling work without modification. +- **Neutral, because** the initial setup of the allowlist for an existing repository is a one-time effort proportional to the breadth of currently tracked content. +- **Bad, because** it is unusual; contributors familiar only with the dominant convention can be surprised when a new file does not appear in `git status`. +- **Bad, because** a directory addition requires two allowlist entries (the directory and its contents); the most-elegant single-entry form `!/foo/**` does not allow the directory itself in all git versions. +- **Bad, because** repositories that legitimately track a very large number of file types (e.g., a generated documentation site with thousands of files) face high allowlist-maintenance overhead. Such repositories should consider opting out. + +### Option 5: `git add` discipline only + +- **Good, because** it imposes no rule maintenance. +- **Bad, because** human discipline is the weakest possible control. A single `git add .` undoes years of careful commits. +- **Bad, because** it provides no defence against IDE-driven file additions, automated tooling, or contributors unfamiliar with the discipline. +- **Bad, because** it provides no review-time signal for intentional vs. accidental tracking. + +### Option 6: Sparse checkout / worktree partitioning + +- **Good, because** it limits the surface area of `git add .` per checkout configuration. +- **Bad, because** sparse checkout is a *per-clone* setting, not a repository property. It does not protect contributors who do not configure it. +- **Bad, because** it is a checkout strategy, not a tracking strategy. Files outside the sparse area are still tracked in the repository if they were ever committed. +- **Bad, because** it adds significant tooling complexity that is disproportionate to the problem being solved. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Structural check.** A repository that adopts this baseline MUST contain a top-level `.gitignore` whose first non-comment, non-blank line is `**` (or an equivalent globstar excluding the entire working tree). A `pre-commit` hook or CI script MAY assert this. +2. **Allowlist-only-after-deny check.** All `!`-prefixed allowlist entries in the top-level `.gitignore` MUST appear *after* the deny-all rule. A reversed order silently negates the strategy. A CI script MAY assert this. +3. **No-orphan-tracked-files check.** Every file in `git ls-files` SHOULD have a corresponding `!`-prefixed allowlist entry. A CI script MAY enforce this; in practice it is reviewer-detectable because adding a file without allowlisting it produces `git status: nothing to commit` after `git add`. +4. **Per-directory file scope.** Per-directory `.gitignore` files MAY exist but MUST NOT contain `!`-prefixed entries that re-allow content that the top-level deny-all rule excludes. A reviewer SHOULD reject any per-directory allow that contradicts the top-level intent. +5. **PR review rule.** Pull requests that add new tracked files MUST modify `.gitignore` in the same change. A reviewer SHOULD reject a PR that introduces a new file without a corresponding allowlist edit; a CI check MAY automate the detection. +6. **Opt-out rule.** A repository that opts out of this strategy MUST record a repository-level ADR that names ADR-0003 in its `Supersedes` section (scoped to that repository) and explains why the trade-offs differ. + +Enforcement tooling is recommended but not mandatory at acceptance time. A solo-maintainer repository MAY rely on manual discipline; a team repository or a compliance-critical repository SHOULD automate at least the structural and allowlist-only-after-deny checks. + +## Consequences + +### Positive + +- New files default to ignored. Credentials, terraform state, build artifacts, and IDE files cannot enter the repository through `git add .` alone. +- Each new tracked file is an explicit allowlist edit, visible in pull-request review. +- `.gitignore` becomes the inventory of intentionally tracked content, making the repository's tracked surface auditable from a single file. +- The diagnostic for "why isn't this file showing up?" is `git check-ignore -v `, which names the rule responsible — fast and unambiguous. +- The strategy applies uniformly across the portfolio: contributors learn it once and recognise it everywhere it is adopted. + +### Negative + +- Initial allowlist setup is a one-time effort per repository. For an existing repository, this means walking `git ls-files` and producing the allowlist that matches it. +- Contributors unfamiliar with the pattern may be surprised when a new file does not appear in `git status`. This is mitigated by documentation but not eliminated. +- Each new directory requires two allowlist entries. The most-elegant single-entry form is not portable. +- Repositories that legitimately track very large generated trees may find the per-file allowlist impractical and should opt out. + +### Neutral + +- Repositories MAY adopt enforcement tooling (pre-commit hooks, CI checks). The strategy works without it but is more durable with it. +- Per-directory `.gitignore` files are not forbidden but are constrained to additive denies; they cannot re-allow content excluded at the top level. +- The dominant community convention (allow-all with denies) remains the default outside this portfolio. Contributors who work across both conventions must context-switch. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. Git's `**` and `!`-prefixed pattern semantics continue to behave as documented in `gitignore(5)`. +2. Adopting repositories' tooling (IDEs, pre-commit, CI, vendor-specific build systems) does not require allow-all `.gitignore` semantics to function. To date, none observed in this portfolio do. +3. Contributors to adopting repositories have access to this ADR and to the per-repository `.gitignore` documentation, so the unfamiliar pattern is learnable in seconds. +4. Repositories that legitimately need allow-all semantics are a minority and will opt out via repository-level ADR rather than pretending to adopt this baseline. + +## Supersedes + +None. + +## Superseded by + +None (current). + +## Implementing PRs + +This section lists downstream pull requests that implement or operationalize the decision described in this ADR. It does not need to list the pull request that introduced the ADR itself. + +`github-terraform-framework` already implements this strategy and predates the ADR; the implementation does not require a new PR. Subsequent adopting repositories will record their adoption in this section as they migrate. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the ADR convention. ADR-0003 is governed by ADR-0001's format and lifecycle rules. +- [ADR-0002](0002-adopt-diataxis-documentation-framework.md) — establishes the Diátaxis documentation framework. Companion documentation for ADR-0003 (an explanation of how the strategy works in practice and a how-to for adding allowlist entries) belongs in adopting repositories' `docs/explanation/` and `docs/how-to/` directories per ADR-0002. + +## Compliance Notes + +This ADR establishes a source-control hygiene practice that contributes to the prevention of accidental disclosure of sensitive information. The table below indicates where evidence produced under this convention may help during reviews; it is illustrative rather than exhaustive, and is not a claim that a repository is compliant merely because the strategy is adopted. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| NIST SP 800-53 Rev. 5 | SC-28 (Protection of Information at Rest) | A deny-all `.gitignore` reduces the risk that sensitive at-rest content (terraform state, credentials, environment files) enters version control through accidental `git add` operations. | +| NIST SP 800-53 Rev. 5 | SI-12 (Information Management and Retention) | Explicitly allowlisting tracked content provides a source-of-truth inventory of what data the repository retains. | +| NIST SP 800-53 Rev. 5 | IA-5 (Authenticator Management) | The strategy contributes to authenticator-management hygiene by blocking common credential-bearing files (`.env`, `credentials.json`, `*.pem`, `*.key`) from accidental commit. | +| NIST SP 800-218 (SSDF) | PS.1 (Protect All Forms of Code from Unauthorized Access and Tampering) | The pull-request-visible `.gitignore` allowlist edit creates a reviewable trail of what content is intentionally added to the source-controlled artifact set. | +| OWASP | A02:2021 — Cryptographic Failures | Default-blocking `.env`, `*.key`, `*.pem`, and similar files reduces the most common vector for cryptographic-material disclosure: accidental commit. | + +Subsequent repository-level ADRs that scope this convention to specific compliance contexts should keep only the rows that genuinely apply to their decision. diff --git a/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md b/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md new file mode 100644 index 0000000..7800a8d --- /dev/null +++ b/docs/decision-records/org/0004-use-renovate-for-dependency-updates.md @@ -0,0 +1,197 @@ +# ADR-0004: Use Renovate for Dependency Updates with Shared Org Baseline + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-05-05 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | N/A (Accepted) | + +## TL;DR + +All `nwarila-platform/*` repositories track dependency updates via [Renovate](https://docs.renovatebot.com/). Common settings — schedule, semantic-commit prefixes, dependency-dashboard, SHA-pin retention on GitHub Actions, PR concurrency caps — live in a single shared baseline at `nwarila-platform/.github/.github/renovate.json5`. Every adopting repository's local `.github/renovate.json5` extends `github>nwarila-platform/.github` and adds only the overrides specific to that repository's manager surface (e.g., `terraform.rangeStrategy: "bump"` for child modules, `"pin"` for root modules). Renovate replaces Dependabot at the org level because Dependabot does not update Terraform's `required_version` field and has incomplete coverage of pinned tool versions in adjacent tooling. The shared-baseline pattern keeps the policy DRY across the org while preserving per-repo override capability. + +## Context and Problem Statement + +Repositories under the `nwarila-platform` organization track several version-pin surfaces that need automated updates: + +- **GitHub Actions** referenced by full commit SHA in workflow files, per the org's SHA-pin policy. +- **Terraform** version constraints — `required_version` on the `terraform` block, and provider versions in `required_providers`. +- **Tool versions** in adjacent tooling such as `.tool-versions` (asdf), devcontainer feature inputs, the `terraform_version:` literal in `hashicorp/setup-terraform` workflow steps, Dockerfile `FROM` lines, and pre-commit `rev:` references. +- **Other ecosystems** as repos add language-specific tooling (npm, pip, etc.). + +Dependabot supports the GitHub Actions case well. It does **not** support Terraform's `required_version` field — Dependabot's Terraform updater scans `required_providers` but ignores the constraint on Terraform itself. Dependabot also has limited and inconsistent handling of pinned tool versions in adjacent tooling. As repositories grow to include any of those, Dependabot leaves silent drift. Adopting Dependabot for every new repository accepts that gap by default, which is misaligned with the org's secure-by-default posture. + +Renovate offers native managers for every one of those surfaces (`terraform`, `terraform-version`, `github-actions`, `pre-commit`, `asdf`, `dockerfile`, `devcontainer`, `npm`, `pip`, etc.) plus a `regex` manager for arbitrary version literals. It also rewrites the trailing tag comment on SHA-pinned Actions bumps (`# v6` → `# v6.1.0`), preserving the human-readability convention enforced in the org's Actions SHA-pin policy. + +A second concern beyond manager coverage is configuration drift. If every repository hand-rolls its Renovate config, common settings (schedule, semantic-commit prefixes, SHA-pin retention) diverge across repos and the org loses uniform behavior. Renovate's `extends` mechanism solves this: a single shared config at the org level is inherited by every consuming repo, with per-repo overrides limited to repo-specific concerns. + +The previous per-repo `.github/dependabot.yml` files covered only `github-actions`, at varying schedules. That coverage no longer matches the org's actual update surface, and the cadence drift produces avoidable PR churn. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Coverage of Terraform `required_version` and adjacent tooling.** Dependabot does not handle these; Renovate does. As repositories grow to pin Terraform CLI versions and other tool versions in adjacent tooling, the gap widens. +2. **SHA-pin retention on GitHub Actions.** The org's SHA-pin policy requires every `uses:` entry to be a 40-character commit SHA with a tag comment. The dependency-update tool must preserve this format on every bump. +3. **Conventional Commit emission.** Update PRs should emit Conventional Commit prefixes that release-please (where configured) categorises. +4. **Cross-repo consistency.** Common settings must be uniform across repos. Per-repo configs that drift are a maintenance liability. +5. **DRY (Inheritance over duplication).** Hand-copying the same settings into every new repo is error-prone. A shared baseline that consuming repos inherit reduces maintenance to one place. This aligns with the "Inheritance over duplication" principle in [ADR-0001 (org)](0001-use-architecture-decision-records.md). +6. **Per-repo override capability.** Some settings (e.g., `terraform.rangeStrategy: "bump"` vs `"pin"`) are repo-specific. The shared baseline must accommodate per-repo overrides without forcing a one-size-fits-all default that is wrong for half the repos. +7. **Reasonable PR cadence.** Daily PR creation produces noise; weekly cadence aligns with most repository review windows. + +## Considered Options + +1. **Stay on Dependabot org-wide.** Continue with per-repo `.github/dependabot.yml`, accepting the `required_version` gap and per-repo cadence drift. +2. **Adopt Renovate per-repo with no shared baseline.** Each repo maintains its own `.github/renovate.json5` independently. +3. **Adopt Renovate with a shared org baseline.** Common settings live in `nwarila-platform/.github/.github/renovate.json5`; consuming repos extend `github>nwarila-platform/.github` and override repo-specific concerns. +4. **Mix Dependabot for legacy repos and Renovate for new repos.** Run both tools depending on repo age. +5. **Hand-roll a scheduled GitHub Actions workflow that opens update PRs.** Custom maintenance pipeline. + +## Decision Outcome + +Chosen option: **Option 3, Renovate with a shared org baseline.** + +The baseline lives at `nwarila-platform/.github/.github/renovate.json5` and configures: + +- `extends: ["config:recommended"]` as the inherited Renovate baseline. +- `schedule: ["before 6am on monday"]` (weekly), the org cadence. +- `semanticCommits: "enabled"` so PRs use Conventional Commit prefixes. +- `dependencyDashboard: true` so each repo gets a single tracking issue rather than a flood of standalone PRs. +- `prConcurrentLimit: 5` and `prHourlyLimit: 2` to cap noise during update bursts. +- `github-actions.pinDigests: true` to preserve SHA-pin format and rewrite trailing tag comments. +- A `packageRules` entry that maps `github-actions` updates to `ci(actions): ...` Conventional Commit prefixes. + +Each adopting repository carries a minimal `.github/renovate.json5` that: + +- Inherits the baseline via `extends: ["github>nwarila-platform/.github"]`. +- Adds only the overrides that are genuinely repo-specific. The most common are: + - `terraform.rangeStrategy` — `"bump"` for child modules (so consumer-side compatibility is preserved), `"pin"` for root modules. + - Additional `packageRules` for managers the baseline does not cover (e.g., `terraform`, `pip`, `npm`). + +The `.github/dependabot.yml` file MUST NOT exist in any adopting repository. Repositories that previously contained one MUST remove it as part of their Renovate migration PR. + +Renovate enablement requires the Renovate GitHub App to be installed against each repository or against the entire org. Installation is a one-time operation outside the repo's git history and is the maintainer's responsibility. + +## Pros and Cons of the Options + +### Option 1: Stay on Dependabot org-wide + +- **Good, because** Dependabot is GitHub-native; no third-party app installation required. +- **Good, because** existing single-ecosystem configurations are already working for GitHub Actions in the repos that have them. +- **Bad, because** Dependabot cannot update Terraform's `required_version` field. Repositories with pinned Terraform versions accumulate untracked drift. +- **Bad, because** Dependabot's coverage of pinned tool versions in adjacent tooling is incomplete and inconsistent. +- **Bad, because** Dependabot's per-repo configuration provides no shared baseline; common settings drift across repos. + +### Option 2: Adopt Renovate per-repo with no shared baseline + +- **Good, because** every repo's behavior is fully self-contained and visible in one file. +- **Good, because** there is no implicit dependency on an external config repo at evaluation time. +- **Bad, because** common settings (schedule, semantic-commit prefixes, SHA-pin retention) drift across repos as new repos are bootstrapped from older templates. +- **Bad, because** changing an org-wide setting (e.g., shifting the cadence from weekly to bi-weekly) requires a coordinated PR across every repo. +- **Bad, because** it duplicates ~30 lines of identical config into every repository, contradicting the org's "Inheritance over duplication" principle. + +### Option 3: Adopt Renovate with a shared org baseline (chosen) + +- **Good, because** Renovate covers every update surface the org has now or is likely to grow into. +- **Good, because** `pinDigests: true` preserves SHA-pin format on GitHub Actions bumps and rewrites trailing tag comments in place. +- **Good, because** `semanticCommits` emits Conventional Commit prefixes that release-please categorises without per-PR rewriting. +- **Good, because** the shared baseline at `nwarila-platform/.github` keeps common settings uniform across the org. Changing a setting in one place updates every consuming repo on its next Renovate run. +- **Good, because** consuming repos remain free to override repo-specific concerns (e.g., `terraform.rangeStrategy`) without re-declaring the entire config. +- **Good, because** the dependency-dashboard issue surfaces pending updates without flooding the PR list. +- **Neutral, because** Renovate requires the GitHub App to be installed once per repository (or once per org). +- **Bad, because** the Renovate GitHub App is a third-party dependency in the supply chain (managed by Mend); operational burden of compromise is real. +- **Bad, because** the dependency-dashboard issue is opinionated; if not curated it can clutter the issue tracker. +- **Bad, because** an outage or breaking change in the shared baseline propagates to every consuming repo at once. Mitigation: changes to `nwarila-platform/.github/.github/renovate.json5` are reviewed in PR like any other org-baseline change. + +### Option 4: Mix Dependabot and Renovate + +- **Good, because** legacy repos avoid the migration cost. +- **Bad, because** the org loses uniform dependency-update behavior. New contributors must learn which repo uses which tool. +- **Bad, because** the `required_version` coverage gap remains for any repo still on Dependabot, partially defeating the value of switching at all. +- **Bad, because** mixed-tool environments accumulate inconsistencies (different cadences, different commit-message formats) that erode the value of having either tool. + +### Option 5: Hand-roll a scheduled GitHub Actions workflow + +- **Good, because** it provides full control over update logic, schedule, and PR template. +- **Bad, because** it imposes disproportionate maintenance burden for a personal-account org. +- **Bad, because** features like release-notes fetching, semver diffing, and dependency-graph awareness would all be reinvented. +- **Bad, because** a hand-rolled workflow is a single point of failure with no community support. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Tool-presence check.** Every adopting repository MUST contain `.github/renovate.json5`. A `.github/dependabot.yml` file MUST NOT exist; a CI script or `pre-commit` hook MAY assert its absence. +2. **Inheritance check.** Every adopting repository's `.github/renovate.json5` MUST include `github>nwarila-platform/.github` in its `extends` array, or document an explicit reason in a repo-specific superseding ADR for not doing so. +3. **SHA-pin retention check.** The shared baseline MUST set `github-actions.pinDigests: true`. A reviewer SHOULD reject a PR to the baseline that removes or disables this setting without a superseding ADR. +4. **Schedule check.** The shared baseline MUST schedule weekly or less-frequent runs. Daily or more-frequent schedules would produce avoidable PR churn across the org. +5. **Override discipline.** Repository-local overrides MUST be limited to repo-specific concerns. Settings that should apply org-wide MUST be added to the shared baseline rather than copy-pasted into every consuming repo. +6. **Editorial rule.** A change of dependency-update tool (back to Dependabot, or to a third option) is itself an architectural decision and MUST be recorded as a superseding ADR. + +Enforcement tooling is recommended but not mandatory at acceptance time. A repository MAY add CI scripts that verify (1)–(3); the org-wide adoption pattern MAY be enforced by the same `org-adr-sync` workflow that mirrors org ADRs into consuming repos. + +## Consequences + +### Positive + +- Terraform `required_version` updates are tracked automatically across the org; the Dependabot-shaped gap is closed. +- Action SHAs stay current with their tag comments rewritten in place across every repo, preserving the SHA-pin convention without manual intervention. +- Conventional Commit prefixes flow into release-please without per-PR rewriting. +- Common settings live in one place; changing the cadence or a packaging rule org-wide takes one PR rather than many. +- New repositories bootstrap with a ~6-line `renovate.json5` that inherits the org behavior automatically. +- Future managers (pre-commit, devcontainer features, mkdocs Python deps, Docker base images) can be enabled by editing the shared baseline rather than every consuming repo. + +### Negative + +- One additional GitHub App must be installed against the org (or per-repo). +- Renovate's dependency-dashboard issue is opinionated and clutters the issue tracker if not curated. +- The shared baseline is now a load-bearing artifact: an outage or breaking change at `nwarila-platform/.github/.github/renovate.json5` propagates to every consuming repo on the next Renovate run. +- Release-notes fetching adds latency to PR creation (negligible in practice). + +### Neutral + +- The `github>` extends syntax creates a runtime dependency on `nwarila-platform/.github` being reachable when Renovate evaluates a consuming repo. In practice this is reliable; if it becomes unreliable, repositories MAY temporarily inline the baseline. +- This ADR scopes the decision to the `nwarila-platform` organization. If `NWarila/*` user-account repos adopt Renovate later, they will reference this ADR as the canonical pattern but with their own user-level shared baseline. +- Repo-specific overrides remain permitted; this ADR is not a uniformity-at-all-costs mandate. The only constraint is that overrides MUST be repo-specific concerns. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The Renovate GitHub App remains free for personal-account organizations and continues to be actively maintained. +2. Renovate continues to support the `extends: ["github>org/.github"]` shared-config pattern. +3. The Renovate config schema remains compatible with the configuration shape used here. +4. The org continues to use Conventional Commits + release-please for repos that publish releases. A switch to a different release tool would require adjusting `semanticCommitType` overrides in the shared baseline. + +## Supersedes + +None — `.github/dependabot.yml` files in `nwarila-platform/*` repos were single-ecosystem configurations with no prior ADR documenting their adoption. This ADR replaces that pattern as a new decision rather than as a formal supersession. + +## Superseded by + +None (current). + +## Implementing PRs + +Pending. The first implementing PR ships in `terraform-proxmox-iso-manager-framework`, which migrates from `dependabot.yml` to the shared-baseline `.github/renovate.json5` pattern. Subsequent adopting repositories will be listed here. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the format and dual-scope structure of decision records. +- [ADR-0003](0003-use-deny-all-gitignore-strategy.md) — establishes the deny-all `.gitignore` strategy. Renovate config files are explicitly allowlisted in adopting repositories per ADR-0003. +- [ADR-0005](0005-pin-terraform-and-provider-versions-exactly.md) — refines this ADR's `rangeStrategy` guidance: instead of differentiating child vs root modules, all repos pin Terraform and provider versions exactly. The shared baseline at `nwarila-platform/.github/.github/renovate.json5` accordingly sets `terraform.rangeStrategy: "pin"`. + +## Compliance Notes + +This ADR preserves the SHA-pin policy (encoded in the shared baseline as `github-actions.pinDigests: true`). It does not modify branch-protection or PR-review requirements: every Renovate PR is subject to the same `main`-branch protections as a human-authored PR, including required status checks. Future ADRs that adopt additional managers (e.g., `pre-commit`, `pip`, `docker`) inherit this ADR's defaults and need only document scope-specific divergence in repo-local config. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| NIST SP 800-53 Rev. 5 | SI-2 (Flaw Remediation) | Renovate's automated update PRs contribute to the timely application of patches and security fixes across the org. | +| NIST SP 800-53 Rev. 5 | CM-3 (Configuration Change Control) | The shared-baseline pattern records org-wide dependency-management policy in source control with PR review history. | +| NIST SP 800-218 (SSDF) | PW.4 (Reuse Existing, Well-Secured Software When Feasible) | Tracking dependency updates with SHA-pin retention preserves the supply-chain integrity posture for reused software. | diff --git a/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md b/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md new file mode 100644 index 0000000..711b66c --- /dev/null +++ b/docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md @@ -0,0 +1,179 @@ +# ADR-0005: Pin Terraform and Provider Versions Exactly + +| Field | Value | +| -------------- | ---------------------------------------- | +| Status | Accepted | +| Date | 2026-05-05 | +| Authors | Nick Warila (@NWarila) | +| Decision-maker | Nick Warila (sole portfolio maintainer) | +| Consulted | None. | +| Informed | None. | +| Reversibility | Medium | +| Review-by | N/A (Accepted) | + +## TL;DR + +Every repository under `nwarila-platform` that contains Terraform configuration pins both the Terraform CLI version (via `terraform { required_version = "= X.Y.Z" }`) and every provider version in `required_providers` to an exact version using the `=` operator. Range constraints (`>=`, `~>`, etc.) are not used. Renovate keeps these exact pins current via `rangeStrategy: "pin"` configured in the shared org baseline at `nwarila-platform/.github/.github/renovate.json5`. Consumers of repos that publish Terraform modules MUST run the exact pinned Terraform CLI version; consumers running anything else hit `terraform init` failure immediately rather than discovering compatibility issues partway through `apply`. + +## Context and Problem Statement + +Terraform's `required_version` constraint on the `terraform { }` block is enforced by the CLI: every consumer of a configuration (root or child module) must satisfy the constraint, or `terraform init` aborts. The constraint uses the [HashiCorp version-constraint syntax](https://developer.hashicorp.com/terraform/language/expressions/version-constraints) and supports several operators: + +- `= X.Y.Z` — exact pin; only this version satisfies the constraint +- `>= X.Y` — minimum; any newer version satisfies +- `~> X.Y` — pessimistic; permits patch updates within X.Y +- Combinations like `>= 1.9, < 2.0` for explicit ranges + +The same operators apply to provider version constraints in `required_providers`. + +Two camps exist in the wider Terraform community: + +1. **Range constraints** (`>=`, `~>`). The argument is multi-module satisfiability: if a root module pulls in three child modules from independent authors, range constraints let all three coexist as long as one CLI version satisfies their union. This matters when consuming modules from authors you do not control. + +2. **Exact pins** (`=`). The argument is reproducibility and security: every consumer runs the exact CLI version the author tested with; behavior is deterministic; supply-chain integrity is stronger. + +The `nwarila-platform` portfolio sits squarely in the second context. Modules in this org are consumed almost exclusively by other repositories in the same org. The maintainer controls every published module and every consumer. Multi-module satisfiability across third-party authors is not a real concern here. What matters is: + +- Reproducibility: every consumer runs the exact Terraform CLI version we tested with +- Security: known-good versions; no surprise behavior changes from a consumer using a newer CLI +- Supply-chain consistency: the SHA-pin policy on GitHub Actions extends naturally to exact-pinning Terraform versions +- Predictable failure modes: `terraform init` fails fast on version mismatch, not partway through `apply` + +The previous default ([ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome) suggested `rangeStrategy: "bump"` for child modules and `"pin"` for root modules. That distinction is suitable for the wider community but is unnecessarily permissive for this org's consumption model. + +## Decision Drivers + +The following forces shaped this decision: + +1. **Reproducibility.** Every consumer should run the exact Terraform CLI and provider versions the author tested with. Version drift is a known source of "works on my machine" incidents. +2. **Security and supply-chain consistency.** Exact pins on Terraform and providers match the SHA-pin posture on GitHub Actions. The org's overall stance is "if it can be pinned exactly, pin it exactly." +3. **Failure-mode visibility.** `terraform init` aborting with "required Terraform version is X, you have Y" is unambiguous. Compatibility issues that surface partway through `terraform plan` or `terraform apply` are harder to diagnose and may leave partial state behind. +4. **Cross-module composability within the org.** Because the maintainer controls every module, exact-pinning all of them to the same Terraform version is straightforward; multi-module satisfiability concerns do not apply. +5. **Tested-and-proven posture.** Publishing a module that says "should work with Terraform >= 1.9" makes a claim the maintainer has not actually verified. Pinning `= 1.9.8` says only what has been tested. +6. **Renovate fitness.** Renovate's `rangeStrategy: "pin"` operates correctly on exact-pinned versions: it bumps the exact version on each update, requiring an explicit author-controlled PR for each change. + +## Considered Options + +1. **Exact pins for both Terraform and providers.** `terraform { required_version = "= X.Y.Z" }` and `required_providers` entries with `version = "= X.Y.Z"`. Renovate uses `rangeStrategy: "pin"`. +2. **Range constraints with pessimistic operator (`~>`).** Permits patch-level updates without re-running tests. +3. **Range constraints with minimum (`>=`).** Permits any newer version. +4. **Hybrid: exact-pin Terraform CLI, range-pin providers.** Lock the runtime, allow provider drift. +5. **No constraint at all.** Omit `required_version` and `required_providers` constraints; let consumers choose. + +## Decision Outcome + +Chosen option: **Option 1, exact pins for both Terraform and providers.** + +In every repository that adopts this baseline: + +- The `terraform { }` block in `versions.tf` MUST set `required_version = "= X.Y.Z"` using a single exact version. Range operators (`>=`, `~>`, etc.) MUST NOT be used. +- Every `required_providers` entry MUST set `version = "= X.Y.Z"` using a single exact version. +- The shared org Renovate baseline at `nwarila-platform/.github/.github/renovate.json5` sets `terraform.rangeStrategy: "pin"`. Every adopting repository inherits this via `extends: ["github>nwarila-platform/.github"]` per [ADR-0004](0004-use-renovate-for-dependency-updates.md). Repo-local Renovate configs MUST NOT override this to `"bump"`, `"replace"`, or `"widen"` without a superseding repo-level ADR. +- The README's "Provider Requirements" or equivalent table MUST display the exact pinned versions and explain that consumers must run that exact CLI version. +- When a repository updates either the Terraform CLI or a provider version, the update MUST be tested against the pinned version before merging the Renovate PR. A `terraform test` suite that runs on every PR satisfies this requirement. + +This refines the rangeStrategy guidance in [ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome, which previously suggested `"bump"` for child modules and `"pin"` for root modules. ADR-0005 narrows that to a single org-wide policy: **always pin exactly**. ADR-0004's mention of `"bump"` is superseded by this ADR for the rangeStrategy choice; the rest of ADR-0004 (Renovate over Dependabot, shared baseline at `nwarila-platform/.github`) remains in force. + +## Pros and Cons of the Options + +### Option 1: Exact pins for both Terraform and providers (chosen) + +- **Good, because** every consumer runs the exact CLI and provider version the author tested with; reproducibility is total. +- **Good, because** failure modes are predictable: `terraform init` fails fast on mismatch with a clear message. +- **Good, because** supply-chain posture is consistent with the org's SHA-pin policy on GitHub Actions. +- **Good, because** Renovate's `rangeStrategy: "pin"` operates cleanly on exact pins; each version bump is an explicit, reviewable PR. +- **Good, because** authors cannot accidentally publish a "should work with anything ≥ X" claim they have not actually verified. +- **Bad, because** consumers must update their Terraform CLI when a module bumps. For consumers using `tfenv` or `asdf`, this is a one-line `.tool-versions` change. For consumers without per-project version management, it is more friction. +- **Bad, because** in a hypothetical future with cross-org module consumption, an exact-pinned dependency tree is harder to satisfy than a range-pinned one. Mitigation: this is not a concern in the current org's consumption model. +- **Neutral, because** it imposes more discipline on the maintainer (every Renovate PR requires testing against the new exact version) but the discipline matches the org's overall posture. + +### Option 2: Pessimistic operator (`~> X.Y`) + +- **Good, because** it permits patch-level CLI/provider updates without per-update testing or PRs. +- **Good, because** it is the most common pattern in the wider Terraform community. +- **Bad, because** consumers may run a slightly different version from the author's tested version; subtle behavior differences slip through. +- **Bad, because** it weakens the reproducibility argument — "tested with 1.9.8, deployed with 1.9.12" is not the same as "tested and deployed with 1.9.8". +- **Bad, because** Renovate's behavior for `~>` versions is to bump the floor, not pin to the exact version, leaving the same drift surface. + +### Option 3: Minimum constraint (`>= X.Y`) + +- **Good, because** it is the most permissive option for consumers; any newer CLI works. +- **Bad, because** it makes a claim the author has not verified ("should work with anything ≥ X"). Future Terraform versions may break this assumption silently. +- **Bad, because** it provides no upper-bound protection; a consumer running a future Terraform major version might break in unpredictable ways with no constraint to catch it. + +### Option 4: Hybrid — exact CLI, range providers + +- **Good, because** it locks the runtime (the most volatile component) while permitting provider patches. +- **Bad, because** it introduces inconsistency: why pin some things and not others? The org's stance is "pin everything that can be pinned." +- **Bad, because** providers are equally susceptible to surprise behavior changes; locking the CLI but not the provider gives a false sense of reproducibility. + +### Option 5: No constraint + +- **Good, because** it imposes nothing on consumers. +- **Bad, because** it abandons the reproducibility and supply-chain arguments entirely. +- **Bad, because** Terraform itself recommends including `required_version` and `required_providers` for any non-trivial configuration; omitting them is a documentation deficit, not a permissive choice. + +## Confirmation + +Adherence to this ADR is confirmed by the following mechanisms. The wording `MUST`, `SHOULD`, and `MAY` follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) conventions. + +1. **Constraint operator check.** Every `required_version` and `required_providers[].version` value MUST use the `=` exact operator. A CI script or `tflint` rule MAY assert this; the regex `^=\s*[0-9]+\.[0-9]+\.[0-9]+$` is sufficient for the exact-pin shape. +2. **Renovate rangeStrategy check.** The shared org baseline at `nwarila-platform/.github/.github/renovate.json5` MUST set `terraform.rangeStrategy: "pin"`. Repo-local overrides MAY narrow this for a specific manager but MUST NOT widen it without a superseding repo-level ADR. A CI script MAY assert this. +3. **README documentation.** Repositories that publish Terraform modules MUST document the exact pinned Terraform CLI version and provider versions in the README's prerequisites or provider requirements section, with an explicit statement that consumers must run those exact versions. +4. **Test-before-bump rule.** A Renovate PR that bumps the Terraform CLI version or a pinned provider version SHOULD NOT be merged without the maintainer running the test suite against the new version. A CI workflow that runs `terraform test` on every PR satisfies this requirement automatically. +5. **Editorial rule.** A relaxation of the exact-pin policy (e.g., adopting `~>` for a specific repo) is an architectural decision and MUST be recorded as a repository-level superseding ADR. + +## Consequences + +### Positive + +- Reproducibility: every consumer runs the exact CLI and provider version the author tested with. +- Failure modes are predictable and fail-fast. +- Supply-chain posture is consistent across SHA-pinned Actions, exact-pinned Terraform, and exact-pinned providers. +- Each version update is an explicit, reviewable, testable Renovate PR rather than silent drift. + +### Negative + +- Consumers must update their local Terraform CLI on every CLI version bump in a depended-on module. For consumers using `tfenv` or `asdf` this is trivial; for others it is more friction. +- The org now ships modules that have a hard external dependency on a specific Terraform version. Switching modules to a newer Terraform requires a coordinated bump across every consumer. +- Renovate generates more frequent PRs against the org as Terraform and providers release. The maintainer absorbs the review burden. + +### Neutral + +- The exact-pin policy applies only inside `nwarila-platform`. Future external consumers (if any) inherit the strictness; if their context demands range constraints they fork or pin internally. +- Repositories that today have range constraints will be migrated to exact pins via the implementing PRs. The migration is a one-time editorial pass; ongoing maintenance is a single-line edit per Renovate PR. + +## Assumptions + +This decision rests on the following assumptions. If any becomes false, this ADR should be revisited: + +1. The `nwarila-platform` org continues to consume Terraform modules primarily from itself, not from third-party authors with conflicting version constraints. +2. Renovate's `rangeStrategy: "pin"` continues to behave as documented — converting ranges to exact pins on the next bump and bumping exact pins to newer exact versions thereafter. +3. Consumers of `nwarila-platform` Terraform modules are willing to accept the discipline of running the exact pinned CLI version. + +## Supersedes + +None. This ADR refines [ADR-0004](0004-use-renovate-for-dependency-updates.md) §Decision Outcome's rangeStrategy guidance but does not supersede ADR-0004 in full; ADR-0004's choice of Renovate over Dependabot and the shared-baseline pattern remain in force. + +## Superseded by + +None (current). + +## Implementing PRs + +Pending. The first implementing PR ships in `terraform-proxmox-iso-manager-framework`, which migrates `terraform/versions.tf` from `required_version = ">= 1.9"` to `= 1.9.8` and from `version = ">= 0.98.1"` to `= 0.98.1`, simplifies `.github/renovate.json5` to inherit the org baseline (which now sets `terraform.rangeStrategy: "pin"`), and adds `terraform test` coverage to enforce the test-before-bump rule. + +## Related ADRs + +- [ADR-0001](0001-use-architecture-decision-records.md) — establishes the format and dual-scope structure of decision records. +- [ADR-0004](0004-use-renovate-for-dependency-updates.md) — establishes Renovate as the org's dependency-update tool. ADR-0005 refines ADR-0004's rangeStrategy guidance to "always pin exactly". + +## Compliance Notes + +This ADR strengthens the supply-chain posture by ensuring that every Terraform configuration consumed in `nwarila-platform` runs against a known, tested set of CLI and provider versions. + +| Framework | Control / Practice ID | Potential Evidence Contribution | +| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| NIST SP 800-53 Rev. 5 | CM-2 (Baseline Configuration) | Exact-pinned Terraform and provider versions are part of the baseline configuration of every infrastructure deployment. | +| NIST SP 800-53 Rev. 5 | SI-7 (Software, Firmware, and Information Integrity) | Exact pins reduce the surface for unintentional or malicious version changes between author-tested and consumer-deployed. | +| NIST SP 800-218 (SSDF) | PS.2 (Provide a Mechanism for Verifying Software Release Integrity) | Combined with SHA-pinned Actions, exact-pinned Terraform contributes to release-integrity verification across the toolchain. | diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md new file mode 100644 index 0000000..b8cc229 --- /dev/null +++ b/docs/explanation/architecture.md @@ -0,0 +1,14 @@ +# Architecture + +## Module boundary + +Describe what this module owns and what consumers must provide. + +## Inputs and outputs + +Summarize the variable surface and the output surface. + +## External dependencies + +List external systems this module talks to and the trust assumptions made +about each. diff --git a/docs/explanation/testing-strategy.md b/docs/explanation/testing-strategy.md new file mode 100644 index 0000000..dd00004 --- /dev/null +++ b/docs/explanation/testing-strategy.md @@ -0,0 +1,11 @@ +# Testing Strategy + +## What the tests cover + +Describe the layers exercised by `terraform test` and what each layer +proves. + +## What the tests do NOT cover + +Be explicit. Document the gap between unit-level coverage and +integration/staging coverage so reviewers know what they are accepting. diff --git a/docs/explanation/threat-model.md b/docs/explanation/threat-model.md new file mode 100644 index 0000000..e6f94eb --- /dev/null +++ b/docs/explanation/threat-model.md @@ -0,0 +1,17 @@ +# Threat Model + +## Scope + +What this module guarantees: + +- TODO + +## Out of scope + +What this module does **not** guarantee: + +- TODO + +Cross-reference: `SECURITY.md` (in `nwarila/.github` or +`nwarila-platform/.github`) defines the org-level reporting channel and +the org-wide scope boundary. diff --git a/docs/how-to/develop-this-module.md b/docs/how-to/develop-this-module.md new file mode 100644 index 0000000..905c9d0 --- /dev/null +++ b/docs/how-to/develop-this-module.md @@ -0,0 +1,29 @@ +# Develop this module + +## Local setup + +Use the devcontainer in [`nwarila/terraform-template/.devcontainer`](https://github.com/NWarila/terraform-template/tree/main/.devcontainer) +or install the same pinned tools manually: + +- Terraform 1.15.1 +- TFLint 0.59.1 +- terraform-docs 0.20.0 +- OPA 1.10.0 +- Python 3.12 with `pyyaml`, `ruff`, `yamllint`, `zizmor` + +## The development loop + +```sh +make fmt # format Terraform +make ci # run every gate +make docs # regenerate docs/reference/terraform.md +``` + +## Before opening a PR + +```sh +make ci +``` + +If `make ci` is green locally, the reusable validation workflow will be +green in CI. diff --git a/docs/reference/invariants.md b/docs/reference/invariants.md new file mode 100644 index 0000000..da8f1d4 --- /dev/null +++ b/docs/reference/invariants.md @@ -0,0 +1,8 @@ +# Invariants + +Non-negotiable rules for this module. Violating one of these is a +breaking change at minimum. + +- TODO: enumerate invariants. Examples: + - "Outputs MUST remain stable across patch versions." + - "Resources MUST verify external content via SHA-256 before consuming." diff --git a/docs/reference/release-gates.md b/docs/reference/release-gates.md new file mode 100644 index 0000000..e3be0f5 --- /dev/null +++ b/docs/reference/release-gates.md @@ -0,0 +1,12 @@ +# Release Gates + +PRs to `main` must pass: + +- `make ci` (Terraform fmt/init/validate/test, TFLint, terraform-docs + diff, Diátaxis docs layout, OPA tests) +- Reusable lint gates (actionlint, shellcheck, yamllint, ruff, + markdownlint) +- Reusable IaC security gates (Trivy, Gitleaks, zizmor) + +All gates run via `NWarila/terraform-template` reusable workflows and +must be SHA-pinned per the contract. diff --git a/policies/opa/.gitkeep b/policies/opa/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..996fa96 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "terraform-module", + "bootstrap-sha": "0000000000000000000000000000000000000000", + "include-component-in-tag": false, + "include-v-in-tag": true, + "packages": { + ".": { + "package-name": "", + "changelog-path": "CHANGELOG.md" + } + } +} diff --git a/repos/private/.gitkeep b/repos/private/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/repos/public/.gitkeep b/repos/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/repos/public/proxmox-iso-manager.yml b/repos/public/proxmox-iso-manager.yml new file mode 100644 index 0000000..b4117cf --- /dev/null +++ b/repos/public/proxmox-iso-manager.yml @@ -0,0 +1,49 @@ +proxmox-iso-manager: + description: "Terraform runner that pins, downloads, verifies, and publishes release metadata for supported Rocky Linux and Ubuntu installer ISOs on Proxmox VE storage." + visibility: "public" + topics: + - proxmox + - terraform + - iso-management + - rocky-linux + - ubuntu + - packer + - vm-templates + - infrastructure-as-code + - devsecops + - gitops + - ci-cd + - security + + actions: + enabled: true + allowed_actions: selected + allowed_actions_config: + github_owned_allowed: true + verified_allowed: true + patterns_allowed: + - "actions/checkout@*" + - "actions/upload-artifact@*" + - "actions/download-artifact@*" + - "aws-actions/configure-aws-credentials@*" + - "hashicorp/setup-terraform@*" + + environments: + terraform-plan: + wait_timer: 0 + can_admins_bypass: false + prevent_self_review: false + deployment_branch_policy: + protected_branches: true + custom_branch_policies: false + + terraform-apply: + wait_timer: 0 + can_admins_bypass: false + prevent_self_review: false + reviewers: + users: + - 33955773 + deployment_branch_policy: + protected_branches: true + custom_branch_policies: false diff --git a/tests/fixtures/repos/private/example.yml b/tests/fixtures/repos/private/example.yml new file mode 100644 index 0000000..80bace3 --- /dev/null +++ b/tests/fixtures/repos/private/example.yml @@ -0,0 +1,5 @@ +# Public-safe fixture used by pr-validation only. +# In production, the actual private repo definitions are sourced from S3. +# Anything sensitive must NOT live here — this file is in the public repo. +name: example-private-repo +visibility: private diff --git a/tests/fixtures/repos/public/example.yml b/tests/fixtures/repos/public/example.yml new file mode 100644 index 0000000..f41a559 --- /dev/null +++ b/tests/fixtures/repos/public/example.yml @@ -0,0 +1,4 @@ +# Public-safe fixture used by pr-validation only. +# In production, this directory is populated from the runner's repos/public/. +name: example-public-repo +visibility: public