From d87886755b9df77685079552af16bf07cbc9772d Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Wed, 30 Sep 2026 17:51:16 +0000 Subject: [PATCH 1/8] fix: test the declared Terraform floor, and document it (ENG-8760) [ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760) ### what Adds a CI job that validates this module at the floor it declares, restores the README's Requirements table, and adds the terraform-docs tooling the other four module repos already carry. ### why ENG-8760 found `terraform-aws-onboarding-account` declaring a floor it could not run on. This repo's floor is correct, but it was never tested: the job runs in all five module repos so an incorrect floor cannot sit in `main` the way the original one did. The floor here equals the `.tool-versions` pin today, so the job duplicates what the lint job already covers. That is the point at which it stops being free and starts being insurance: the moment the two diverge, the bottom of the constraint is covered without anyone remembering to add a job. The README is the substantive fix. Its Requirements table said `No requirements.` and `| aws | n/a |`, while `provider.tf` declares `>= 1.13.5` and `aws ~> 6.0`. The constraints arrived in a20d594, which never touched the README, and nothing here would have caught that: alone among the five repos, this one has no terraform-docs config, no terraform-docs pin, no `just docs` target and no `lint-docs` job. So CI was about to start enforcing a floor that the module's own public documentation denied existed. Adding the tooling is what stops it drifting again. `.gitignore` also stops un-ignoring `.terraform.lock.hcl`. The other four ignore it deliberately, because committing a lock file in a published module pins every consumer to its provider versions. The new job runs `terraform init`, which makes producing that file routine, so the un-ignore was one `git add -A` away from doing real damage. The lint job now passes a token to zizmor. Its five online audits skip silently without one, and the summary line reads identically either way. ### testing - [x] Ran the version-resolution action locally against this `provider.tf`: resolves `>= 1.13.5` to `1.13.5`, the version CI will install, and the module validates there. - [x] Regenerated the README with the pinned terraform-docs 0.20.0 and confirmed the result is stable on a second run, so the new `lint-docs` job passes. - [x] The guard step exercised both ways under bash and GNU grep: passes as-is, and fails when a second `required_version` is planted in another file. - [x] `just lint` clean, and zizmor re-run with a token to confirm the five online audits schedule rather than skip. - [ ] Sandbox deploy. Expect a no-op: no resource, variable or output changed. ### docs The README's Requirements and Providers tables now match `provider.tf`. The tooling added alongside is what keeps them matching. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 4 +-- .terraform-docs.yml | 7 +++++ .tool-versions | 1 + README.md | 7 +++-- justfile | 4 +++ 6 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 .terraform-docs.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb2e5f3..a695a02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,3 +38,64 @@ jobs: - name: Run pre-commit checks uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 + env: + # zizmor's five online audits skip silently without a token, and its + # summary line reads the same either way, so CI would look green with + # the supply-chain audits never having run. + GH_TOKEN: ${{ github.token }} + + lint-docs: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check documentation + uses: terraform-docs/gh-actions@6de6da0cefcc6b4b7a5cbea4d79d97060733093c # v1.4.1 + with: + fail-on-diff: true + + validate-min-terraform: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # terraform-min-max picks the constraint file by shortest filename, not by + # a canonical name, so a second required_version anywhere in the module + # would silently change which floor CI validates against. + - name: Assert a single declared floor + run: | + files=$(grep -rlE '^[[:space:]]*required_version[[:space:]]*=' --include='*.tf' . | sed 's|^\./||' | sort) + if [ "$files" != "provider.tf" ]; then + echo "expected exactly one required_version, in ./provider.tf; found:" + printf '%s\n' "$files" + exit 1 + fi + + # The floor is read out of required_version rather than written here, so + # that editing the constraint moves what CI tests in the same commit. + - name: Read the declared Terraform floor + id: min-max + uses: clowdhaus/terraform-min-max@5e40f8cf535d84fbd031571abd363ffd81dbfbfc # v3.0.1 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 + with: + terraform_version: ${{ steps.min-max.outputs.minVersion }} + + # The lint job validates at the pinned .tool-versions release. This run + # validates at the declared floor, so that whenever the two differ the + # bottom of the constraint is covered too. + - name: Validate at the declared floor + run: | + terraform init -backend=false + terraform validate diff --git a/.gitignore b/.gitignore index 414daf1..c36f6f7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ *.tfstate *.tfstate.* -#Terraform files to not ignore -!.terraform.lock.hcl \ No newline at end of file +# Reusable modules do not pin providers with a lock file +.terraform.lock.hcl \ No newline at end of file diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..cbffa1a --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,7 @@ +formatter: markdown + +output: + file: README.md + +settings: + lockfile: false diff --git a/.tool-versions b/.tool-versions index 19b155f..e05a8c6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,5 @@ just 1.40.0 terraform 1.13.5 +terraform-docs 0.20.0 tflint v0.61.0 uv 0.8.14 diff --git a/README.md b/README.md index 221c579..f28af97 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,16 @@ This setup ensures that the CUR is in the format required by Stacklet, and that ## Requirements -No requirements. +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.13.5 | +| [aws](#requirement\_aws) | ~> 6.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | n/a | +| [aws](#provider\_aws) | ~> 6.0 | ## Modules diff --git a/justfile b/justfile index f774cff..a9bfc23 100644 --- a/justfile +++ b/justfile @@ -8,3 +8,7 @@ format: # lint files lint: uvx prek run --all-files + +# update autogenerated documentation +docs: + terraform-docs . From 1d718170e740508fadc923af928ba696f01b9af1 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Wed, 30 Sep 2026 19:41:45 +0000 Subject: [PATCH 2/8] Raise the Terraform floor to a supported line The old floor was the oldest release that could run the module, which meant standing behind Terraform lines that no longer receive security fixes. Declare 1.14 instead, and move the CI pin to the current 1.16.4 so the floor and the pin stay distinct. Co-Authored-By: Claude Opus 5 --- .tool-versions | 2 +- README.md | 2 +- provider.tf | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.tool-versions b/.tool-versions index e05a8c6..680b1a4 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ just 1.40.0 -terraform 1.13.5 +terraform 1.16.4 terraform-docs 0.20.0 tflint v0.61.0 uv 0.8.14 diff --git a/README.md b/README.md index f28af97..4569eb2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This setup ensures that the CUR is in the format required by Stacklet, and that | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.13.5 | +| [terraform](#requirement\_terraform) | >= 1.14.0 | | [aws](#requirement\_aws) | ~> 6.0 | ## Providers diff --git a/provider.tf b/provider.tf index bd01208..62c9d36 100644 --- a/provider.tf +++ b/provider.tf @@ -1,5 +1,9 @@ terraform { - required_version = ">= 1.13.5" + # The floor is a Terraform line that still receives patches. This module uses + # nothing newer than the language has had for years, so the constraint is + # about support rather than features, and 1.13 and earlier no longer get + # security fixes. + required_version = ">= 1.14.0" required_providers { aws = { From 042cf7bca8f8f213bfba2cbb57dd5ecad82d3777 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Mon, 5 Oct 2026 15:35:00 +0000 Subject: [PATCH 3/8] Read the declared floor with terraform-docs, and drop the guard terraform-min-max finds the constraint by regex, takes the first match per file, ignores .tf.json, and picks a file by shortest path. The guard step added to compensate was the same regex with the same blind spots, so it could never be more correct than the thing it was guarding. On a required_version split across lines the action extracts an empty string, which semver reads as "any version", so the floor resolves to the oldest Terraform ever released. terraform-docs parses the configuration and reports every required_version in the module, which removes the question of which file holds the real one. The floor is then the highest declared lower bound, which is what Terraform applies. Pinned by digest because terraform-docs publishes no immutable releases, so neither a tag nor a release checksum is fixed. Also point the docs job at .terraform-docs.yml, which it was ignoring, and settle on that spelling in the two repos that used .terraform-docs.yaml. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a695a02..70cfb34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,10 @@ jobs: - name: Check documentation uses: terraform-docs/gh-actions@6de6da0cefcc6b4b7a5cbea4d79d97060733093c # v1.4.1 with: + # Without this the action ignores .terraform-docs.yml and runs its own + # defaults, so CI would check the docs against a different + # configuration than `just docs` generates them with. + config-file: .terraform-docs.yml fail-on-diff: true validate-min-terraform: @@ -69,28 +73,45 @@ jobs: with: persist-credentials: false - # terraform-min-max picks the constraint file by shortest filename, not by - # a canonical name, so a second required_version anywhere in the module - # would silently change which floor CI validates against. - - name: Assert a single declared floor - run: | - files=$(grep -rlE '^[[:space:]]*required_version[[:space:]]*=' --include='*.tf' . | sed 's|^\./||' | sort) - if [ "$files" != "provider.tf" ]; then - echo "expected exactly one required_version, in ./provider.tf; found:" - printf '%s\n' "$files" - exit 1 - fi - # The floor is read out of required_version rather than written here, so # that editing the constraint moves what CI tests in the same commit. + # terraform-docs parses the configuration instead of matching a regex + # against one file, so it reports every required_version in the module, + # including one in a .tf.json or split across lines. The image is pinned + # by digest because terraform-docs publishes no immutable releases, which + # leaves both a tag and a release checksum able to change underneath us. + # This digest is terraform-docs 0.20.0, the version in .tool-versions. - name: Read the declared Terraform floor - id: min-max - uses: clowdhaus/terraform-min-max@5e40f8cf535d84fbd031571abd363ffd81dbfbfc # v3.0.1 + id: floor + run: | + constraints=$(docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/wd" -w /wd \ + quay.io/terraform-docs/terraform-docs@sha256:37329e2dc2518e7f719a986a3954b10771c3fe000f50f83fd4d98d489df2eae2 \ + json . --output-file '' 2>/dev/null \ + | jq -r '.requirements[] | select(.name == "terraform") | .version') + if [ -z "$constraints" ]; then + echo "this module declares no required_version" >&2 + exit 1 + fi + # Terraform applies every required_version in the module at once, so + # the effective floor is the highest lower bound among them. Only >= + # and ~> state that bound literally. Resolving anything else needs the + # release list, so reject it rather than guess at it. + floor=$(printf '%s\n' "$constraints" \ + | grep -oE '(>=|~>)[[:space:]]*[0-9]+\.[0-9]+(\.[0-9]+)?' \ + | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' \ + | awk -F. 'NF==2 {print $0".0"; next} {print}' \ + | sort -V | tail -1) || true + if [ -z "$floor" ]; then + echo "required_version must state a lower bound with >= or ~>; found:" >&2 + printf ' %s\n' "$constraints" >&2 + exit 1 + fi + echo "version=$floor" >> "$GITHUB_OUTPUT" - name: Setup Terraform uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 with: - terraform_version: ${{ steps.min-max.outputs.minVersion }} + terraform_version: ${{ steps.floor.outputs.version }} # The lint job validates at the pinned .tool-versions release. This run # validates at the declared floor, so that whenever the two differ the From a20d66d5b1ae243c1a835d904eb69cdef264b7b3 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Tue, 6 Oct 2026 16:14:07 +0000 Subject: [PATCH 4/8] Move the floor check into the shared github-tooling action Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 54 ++++++---------------------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70cfb34..0733058 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,50 +73,10 @@ jobs: with: persist-credentials: false - # The floor is read out of required_version rather than written here, so - # that editing the constraint moves what CI tests in the same commit. - # terraform-docs parses the configuration instead of matching a regex - # against one file, so it reports every required_version in the module, - # including one in a .tf.json or split across lines. The image is pinned - # by digest because terraform-docs publishes no immutable releases, which - # leaves both a tag and a release checksum able to change underneath us. - # This digest is terraform-docs 0.20.0, the version in .tool-versions. - - name: Read the declared Terraform floor - id: floor - run: | - constraints=$(docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/wd" -w /wd \ - quay.io/terraform-docs/terraform-docs@sha256:37329e2dc2518e7f719a986a3954b10771c3fe000f50f83fd4d98d489df2eae2 \ - json . --output-file '' 2>/dev/null \ - | jq -r '.requirements[] | select(.name == "terraform") | .version') - if [ -z "$constraints" ]; then - echo "this module declares no required_version" >&2 - exit 1 - fi - # Terraform applies every required_version in the module at once, so - # the effective floor is the highest lower bound among them. Only >= - # and ~> state that bound literally. Resolving anything else needs the - # release list, so reject it rather than guess at it. - floor=$(printf '%s\n' "$constraints" \ - | grep -oE '(>=|~>)[[:space:]]*[0-9]+\.[0-9]+(\.[0-9]+)?' \ - | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' \ - | awk -F. 'NF==2 {print $0".0"; next} {print}' \ - | sort -V | tail -1) || true - if [ -z "$floor" ]; then - echo "required_version must state a lower bound with >= or ~>; found:" >&2 - printf ' %s\n' "$constraints" >&2 - exit 1 - fi - echo "version=$floor" >> "$GITHUB_OUTPUT" - - - name: Setup Terraform - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 - with: - terraform_version: ${{ steps.floor.outputs.version }} - - # The lint job validates at the pinned .tool-versions release. This run - # validates at the declared floor, so that whenever the two differ the - # bottom of the constraint is covered too. - - name: Validate at the declared floor - run: | - terraform init -backend=false - terraform validate + # The lint job validates at the release pinned in .tool-versions. This + # validates at the floor declared in required_version, so that whenever + # the two differ the bottom of the supported range is covered too. The + # action reads the floor back out of the constraint, so editing the + # constraint moves what CI tests in the same commit. + - name: Validate at the declared Terraform floor + uses: stacklet/github-tooling/validate-min-terraform@904e90a978529d3f81edc397d81eea4f5b54dd30 From 148a1bedc04d4868e8f7758b43ff2e8f562cc8fb Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Tue, 6 Oct 2026 21:26:45 +0000 Subject: [PATCH 5/8] Pin the prek version the lint job installs Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 5 +++++ .tool-versions | 1 + justfile | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0733058..4c57c74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,11 @@ jobs: - name: Run pre-commit checks uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 + with: + # Without a pin this installs whatever prek released most recently, + # so a hostile release reaches CI with no change here. The version + # comes from .tool-versions, so CI and a local run agree on it. + prek-version: ${{ env.PREK_TOOL_VERSION }} env: # zizmor's five online audits skip silently without a token, and its # summary line reads the same either way, so CI would look green with diff --git a/.tool-versions b/.tool-versions index 680b1a4..95a4961 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,5 @@ just 1.40.0 +prek 0.5.5 terraform 1.16.4 terraform-docs 0.20.0 tflint v0.61.0 diff --git a/justfile b/justfile index a9bfc23..82d3d23 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,7 @@ format: # lint files lint: - uvx prek run --all-files + prek run --all-files # update autogenerated documentation docs: From 2776febe794c9bdc1bf8be0908047681034b7f7c Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Thu, 8 Oct 2026 00:39:48 +0000 Subject: [PATCH 6/8] Refresh the pinned tools, and repin the shared action terraform-docs stays at 0.20.0: terraform-docs/gh-actions v1.4.1, which the docs job runs, bundles that exact version, so moving it here would have CI check the README against a different renderer than `just docs` uses. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 8 ++++---- .tool-versions | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c57c74..f624e3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,8 +45,8 @@ jobs: prek-version: ${{ env.PREK_TOOL_VERSION }} env: # zizmor's five online audits skip silently without a token, and its - # summary line reads the same either way, so CI would look green with - # the supply-chain audits never having run. + # summary line reads the same either way, so CI would pass while the + # supply-chain audits never ran. GH_TOKEN: ${{ github.token }} lint-docs: @@ -64,7 +64,7 @@ jobs: with: # Without this the action ignores .terraform-docs.yml and runs its own # defaults, so CI would check the docs against a different - # configuration than `just docs` generates them with. + # configuration than `just docs` uses. config-file: .terraform-docs.yml fail-on-diff: true @@ -84,4 +84,4 @@ jobs: # action reads the floor back out of the constraint, so editing the # constraint moves what CI tests in the same commit. - name: Validate at the declared Terraform floor - uses: stacklet/github-tooling/validate-min-terraform@904e90a978529d3f81edc397d81eea4f5b54dd30 + uses: stacklet/github-tooling/validate-min-terraform@c9cef9bf2460b6faf752da72f763656ea491a368 diff --git a/.tool-versions b/.tool-versions index 95a4961..9fa3528 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,6 +1,6 @@ -just 1.40.0 +just 1.58.0 prek 0.5.5 -terraform 1.16.4 +terraform 1.16.5 terraform-docs 0.20.0 -tflint v0.61.0 -uv 0.8.14 +tflint v0.64.0 +uv 0.12.23 From d0c72b923c77ee0d2470b79006ac66a67c6d52da Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Tue, 13 Oct 2026 14:12:08 +0000 Subject: [PATCH 7/8] repoint the action pin at github-tooling main Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f624e3e..7079dc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,4 +84,4 @@ jobs: # action reads the floor back out of the constraint, so editing the # constraint moves what CI tests in the same commit. - name: Validate at the declared Terraform floor - uses: stacklet/github-tooling/validate-min-terraform@c9cef9bf2460b6faf752da72f763656ea491a368 + uses: stacklet/github-tooling/validate-min-terraform@3865043f45564294b8e447f9e8ea03a57ccca51d From 48f68695c27af16c61054e56af8c773fb54e14e4 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Tue, 13 Oct 2026 15:10:25 +0000 Subject: [PATCH 8/8] shorten the required_version comment Co-Authored-By: Claude Opus 5 --- provider.tf | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/provider.tf b/provider.tf index 62c9d36..77e7075 100644 --- a/provider.tf +++ b/provider.tf @@ -1,8 +1,5 @@ terraform { - # The floor is a Terraform line that still receives patches. This module uses - # nothing newer than the language has had for years, so the constraint is - # about support rather than features, and 1.13 and earlier no longer get - # security fixes. + # Require a Terraform version that has had recent updates. required_version = ">= 1.14.0" required_providers {