Skip to content

feat: add the validate-min-terraform action (ENG-8760) - #1

Merged
johnsca merged 10 commits into
mainfrom
johnsca/fix/ENG-8760/min-terraform-version
Oct 13, 2026
Merged

johnsca merged 10 commits into
mainfrom
johnsca/fix/ENG-8760/min-terraform-version

Conversation

@johnsca

@johnsca johnsca commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

ENG-8760

what

Adds validate-min-terraform, a composite action that reads a module's Terraform floor out of required_version and runs terraform validate at that release, with the parse in its own script and a test suite over it. Lays out the repository for the tools that follow it: one directory per tool, each carrying an action.yml and a README.md, alongside a lint workflow, a prek config, a justfile and a README recording the layout.

why

Five of the AWS and Azure onboarding module repositories need this check, and the version of it that ENG-8760 wrote carries enough shell to be worth one copy rather than five. Those repositories validate at the release pinned in .tool-versions, which sits near the top of the supported range, so nothing covered the bottom of it. That is how required_version = ">= 1" survived in a module that no release below 1.8 can parse.

The action derives the floor instead of taking it as an input, so editing the constraint moves what CI tests in the same commit. It runs terraform-docs to parse the configuration rather than matching a regex against one file. The regex approach is what the published actions do, and it is why they misread a second .tf, a .tf.json, or a constraint split across lines, the last of which yields an empty string that semver then reads as "any version".

The terraform-docs image is pinned by digest, not by tag. The project publishes no immutable releases, so a tag and the checksum published beside a release can both change underneath us, while a digest is content addressed and cannot.

The release the action installs comes out of the configuration under test, so on a pull request the branch decides which Terraform runs in CI. minimum-floor puts a bound under that, defaulting to 1.14.0. HashiCorp publishes no support window for the community CLI, and its support policy routes a version to a regime by release date, which puts 1.14 in the band still receiving fixes and security patches. That reading is a judgement call, recorded in the README alongside the number, and the default wants revisiting whenever a new minor lands. Callers can raise it to their own support floor.

Two things the clamp does not solve, and the action cannot. A pull request still chooses which providers init installs, and a provider plugin is a binary that validate runs to read its schema. So the job must hold no secrets and no write permission, which is the shape the usage example and all five callers have. The same is already true of the terraform_validate pre-commit hook these repos run today, so this is not new exposure, but it is worth stating where someone will read it.

The parse lives in read_floor.py rather than inline in the action. Inline, nothing linted it: the pre-commit actionlint hook only reads .github/workflows/, and a composite action.yml is not a workflow. In a script a linter reads it and a test can run it without a runner.

It is Python rather than shell because the work is parsing and comparing. One regex per term replaces a case statement, a tr, and two greps that all had to agree, and the version comparison is a tuple rather than a sort -V idiom. Splitting the derivation from the terraform-docs call is what makes the constraint cases unit tests: fourteen of the sixteen no longer start a container.

Callers pin by commit SHA, because this repository publishes no release tags and a floating ref would put five repositories' CI under whatever lands here next. If the repository grows enough tools to want versioned releases, that is the point to add tags and switch the callers over.

testing

  • Ran the floor derivation against a module carrying three constraints at once, one in a .tf.json and one split across lines. It reports the highest lower bound, which is the one Terraform's own init enforces.
  • Ran it against all five onboarding modules. Each resolves to the floor its required_version states.
  • Checked the operator handling across a matrix of constraints. >= 1.14.0, >= 1.14.0, < 2.0.0 and ~> 1.14 resolve, while !=, a bare version and > are rejected with the term that could not be read, and an upper bound on its own reports that the constraint states no lower bound. Review on the module PRs raised the first of those.
  • Checked that a container which fails to run prints docker's own error, rather than surfacing further down as an empty parse. Review raised this one too.
  • just test covers every constraint form the parser accepts or rejects, a constraint split across lines where that is the deciding bound, the three-constraint fixture, a .terraform-docs.yml that hides the requirements section, a module with no constraint at all, a floor under the clamp, the step output written to GITHUB_OUTPUT, and a container that cannot run. CI runs the same command. The suite takes under two seconds, since most cases call the parser directly and start no container.
  • Review caught four defects, all now covered by that suite. A single-component bound such as >= 1 was rejected outright, which is the constraint this ticket started from. A config hiding the requirements section made a module that declares a constraint look like one that declares none. The multiline fixture could not fail, because a higher bound in the sibling .tf.json decided the result either way. The clamp defaulted to a release nobody has patched in years.
  • just lint clean, and zizmor re-run under a token to confirm its five online audits schedule rather than skip. The prek release that job installs now comes from .tool-versions rather than being resolved at run time, so CI and a local run agree on it. zizmor audits action.yml as well as the workflow.
  • Confirm the action runs end to end once this merges and the five module PRs point at it. Their uses: pin names the commit on this branch, so it resolves as soon as the branch exists on GitHub, but it wants repointing at the commit on main after this merges.

docs

README.md lists the tools and says how to add one. The action carries its own README.md with usage, inputs, outputs and the one case it cannot catch: terraform validate against a module directory treats every root variable as unknown and so never evaluates validation blocks, which leaves a floor that is only wrong inside one still passing.

This repository is not described in cloud-infra, so its settings and required checks are unmanaged. Worth adding there separately.

🤖 Generated with Claude Code

[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

What
----

- Adds `validate-min-terraform`, a composite action that reads a module's
  Terraform floor out of `required_version` and runs `terraform validate` at
  that release.
- Sets the repository up for the tools that follow: one directory per tool,
  each with an `action.yml` and a `README.md`, plus a lint workflow, a prek
  config, a justfile and a README that records the layout.

Why
----

Five of the AWS and Azure onboarding module repositories need this job, and the
version of it that ENG-8760 wrote carries enough shell to be worth maintaining
in one place. Each repository validates at the release pinned in
`.tool-versions`, which sits near the top of the supported range, so nothing
covered the bottom of it. That is how `required_version = ">= 1"` survived in a
module that no release below 1.8 can even parse.

The action derives the floor rather than taking it as an input, so editing the
constraint moves what CI tests in the same commit. It runs terraform-docs to
parse the configuration instead of matching a regex against one file, which is
what the published actions do and what makes them wrong on a second `.tf`, a
`.tf.json`, or a constraint split across lines. The terraform-docs image is
pinned by digest because the project publishes no immutable releases, so both a
tag and a release checksum can change underneath us.

Callers pin by commit SHA. This repository publishes no release tags, and a
floating ref would put the module repositories' CI under whatever lands here
next.

Testing
----

- [x] Ran the floor derivation against a module carrying three constraints at
      once, one of them in a `.tf.json` and one split across lines: it reports
      the highest lower bound, where the regex-based action reports an empty
      string that `semver` then reads as "any version".
- [x] Ran it against all five onboarding modules, each of which resolves to the
      floor its `required_version` states.
- [x] Checked the operator handling across a matrix: `>= 1.14.0`,
      `>= 1.14.0, < 2.0.0` and `~> 1.14` resolve; `!=`, a bare version and `>`
      are rejected; an upper bound on its own reports that the constraint
      states no lower bound.
- [x] Checked that a container that fails to run prints docker's own error,
      rather than surfacing as an empty parse further down.
- [x] `just lint` clean, with zizmor re-run under a token to confirm the five
      online audits schedule rather than skip. zizmor audits `action.yml` as
      well as the workflow.
- [ ] Confirm the action runs end to end once this merges and the module
      repositories point at it.

Docs
----

`README.md` lists the tools and says how to add one. The action carries its own
`README.md` with usage, inputs, outputs and a note on the one case it cannot
catch: `terraform validate` never evaluates `validation` blocks, so a floor
that is only wrong inside one still passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnsca

johnsca commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai please review

johnsca and others added 2 commits October 6, 2026 18:11
`>= 1` is the constraint this ticket started from, and the parser rejected it
outright. Terraform reads a short bound as the lowest release matching it, so
it now pads to three components rather than failing.

`--show requirements` overrides a `.terraform-docs.yml` that hides the
requirements section, which would otherwise report a module with a constraint
as declaring none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review raised three things the inline shell could not answer. The parse now
lives in `read-floor.sh`, so shellcheck reads it and a test can run it without
a runner. `test-read-floor.sh` covers every constraint form the parser accepts
or rejects, and CI runs it.

The floor now has a floor. The release this installs comes out of the
configuration under test, so without a clamp a branch picks which Terraform
runs in CI, and Terraform below 1.2.2 bundles a go-getter affected by
HCSEC-2022-13. `minimum-floor` defaults there and callers can raise it.

The parse container also drops to a read-only mount with no network, neither of
which terraform-docs needs to read a configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnsca
johnsca marked this pull request as ready for review October 6, 2026 20:24
The parse was three mechanisms doing one job: a case statement for the
operator, tr to strip whitespace, and two greps to validate what was left.
Extending it meant touching all three. It is now one regex per term, and the
version comparison is a tuple rather than a sort -V idiom.

Splitting the derivation from the terraform-docs call makes the constraint
cases unit tests. Fourteen of the sixteen cases no longer start a container,
so the suite runs in a second and a new case costs nothing.

It also retires a trap. The old loop read from a heredoc so that it ran in the
current shell and its accumulator survived; rewriting it as a pipe would have
silently emptied the result and failed every valid module.

ruff replaces shellcheck, since no shell is left to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 16 files

Architecture diagram
sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub Actions
    participant Action as validate-min-terraform Action
    participant TD as terraform-docs Container
    participant Script as read_floor.py
    participant TF as Terraform CLI
    participant Runner as CI Runner

    Note over Dev,Runner: NEW: validate-min-terraform composite action flow

    Dev->>GH: Push/PR to main
    GH->>Runner: Trigger CI workflow
    
    Note over Runner: CI workflow runs lint + test jobs
    Runner->>Runner: lint: prek (actionlint, zizmor, ruff)
    Runner->>Runner: test: unittest discover

    alt Test job
        Runner->>Script: Run read_floor.py tests
        Script->>Script: Parse constraint cases (no container)
        Script->>TD: Docker run for fixture tests
        TD-->>Script: Parse requirements
        Script-->>Runner: Test results
    end

    Note over GH,Runner: Real module validation flow

    Runner->>Action: Composite action starts
    Action->>Action: Set working-directory
    
    Note over Action: Step 1: Read floor
    Action->>Script: Execute read_floor.py
    Script->>Script: Read required_version from module
    Script->>TD: Docker: terraform-docs json --show requirements
    TD->>TD: Parse all .tf/.tf.json files
    TD-->>Script: requirements JSON
    Script->>Script: Derive highest lower bound
    alt Valid floor found
        Script->>Script: Check minimum-floor clamp
        alt Floor >= minimum (1.2.2)
            Script-->>Action: version output
        else Floor < minimum
            Script-->>Action: Error + exit 1
        end
    else No constraint or unparseable
        Script-->>Action: Error + exit 1
    end

    Action->>Action: Write version to GITHUB_OUTPUT

    Note over Action: Step 2: Setup Terraform
    Action->>TF: Install terraform (version from floor)
    TF-->>Action: terraform ready

    Note over Action: Step 3: Validate
    Action->>TF: terraform init -backend=false
    TF->>TF: Install providers, fetch modules
    Action->>TF: terraform validate
    TF-->>Action: Validation results
    Action-->>Runner: Step completed

    Note over Dev,Runner: SECURITY: no secrets, no write permissions
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread README.md Outdated
Comment thread justfile Outdated
Comment thread validate-min-terraform/testdata/several-files/main.tf
Comment thread validate-min-terraform/README.md Outdated
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnsca
johnsca force-pushed the johnsca/fix/ENG-8760/min-terraform-version branch from 145094a to 34049ab Compare October 6, 2026 22:13

@jtroup jtroup left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comments inline aren't blocking, but please either address or respond to the Cubic feedback. Thanks @johnsca

# including one in a .tf.json or split across lines.
- name: Read the declared Terraform floor
id: floor
shell: bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@johnsca Hmm, why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hold-over from pre-refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A composite step has to declare a shell, so shell: bash itself cannot go. The hold-over was the logic on that line.

read_floor.py now writes version= to $GITHUB_OUTPUT itself, so the step is a bare invocation of the script. A test covers the file contents.

Filed by Claude on Cory's behalf.

Comment thread validate-min-terraform/README.md Outdated

Runs `terraform validate` against a module at the oldest Terraform release the module says it supports.

A repository's lint job validates at the release pinned in `.tool-versions`, which is near the top of the supported range. Nothing covers the bottom of the range, so a module can declare `required_version = ">= 1.0"` and use syntax that no release below 1.8 parses. This action closes that gap.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@johnsca to be honest, given https://endoflife.date/terraform and terraform's relatively short support lifecycle, I'm left wondering whether we might not be better off simply enforcing a modern terraform minimum floor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The official support info from HashiCorp has 1.14.x getting fixes and security patches though August of next year, per the Support Lifecycle Addendum. Which is a bit complicated, so we could instead just follow the 2 year primary LTS window.

Should I change the action to figure out the minimum version based on the 2-year LTS window and validate with that and that the constraint falls within that LTS window instead? That would force us to keep it up-to-date instead of letting it drift like we had.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping it a literal rather than deriving the window at run time.

Deriving it means a network call to HashiCorp on every run, a new failure mode when that endpoint moves or rate-limits, and a check whose result changes with no commit to point at. A module that passes on Monday fails on Tuesday because a window rolled.

The literal is 1.14.0, with the reasoning and its ambiguity in the README beside it. If drift is the worry, a scheduled job that opens a pull request raising the default keeps the change reviewable.

Filed by Claude on Cory's behalf.

Comment thread validate-min-terraform/README.md Outdated
| Name | Default | Description |
| --- | --- | --- |
| `working-directory` | `.` | Directory holding the module to validate. |
| `minimum-floor` | `1.2.2` | Refuse to run below this release. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@johnsca Shouldn't the minimum floor be something supported and modern? The version you mention has been end of life for over 3 years and has a ton of security problems.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and changed. The default is now 1.14.0 rather than 1.2.2.

1.2.2 cleared one advisory, the go-getter issues reachable while init fetches modules. Nothing else recommended it.

1.14 rests on a judgement call. HashiCorp publishes no support window for the community CLI. Its support policy routes a version to a regime by release date, and 1.14 lands in the band still receiving fixes and security patches. The Support Lifecycle Addendum that band points at lists only Self-Managed products, so no row names a CLI line. We read the routing table as the applicable statement, which puts 1.14 at the oldest release with a published claim to fixes. If you read it differently, the input exists and the number is one line.

The README records that reasoning, and says the default wants revisiting whenever a new minor lands.

Filed by Claude on Cory's behalf.

Comment thread .tool-versions Outdated
@@ -0,0 +1,3 @@
just 1.40.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@johnsca this is very old - you may as well update it before shipping this? same with uv.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated here and in the five module repositories: just 1.58.0, uv 0.12.23, and in the modules terraform 1.16.5 and tflint 0.64.0. just lint passes at all of them.

terraform-docs stays at 0.20.0. The docs job runs terraform-docs/gh-actions v1.4.1, which builds FROM quay.io/terraform-docs/terraform-docs:0.20.0. Moving it would have CI check the README against a different renderer than just docs uses.

The image this action parses with stays there too. 0.20.0 is the last release published as a multi-arch index, and later tags are per-architecture, so a digest pin would have to pick one.

Filed by Claude on Cory's behalf.

Comment thread validate-min-terraform/read_floor.py Outdated
johnsca and others added 4 commits October 7, 2026 23:53
The three-constraint fixture could not fail on the constraint split across
lines: the one in the .tf.json is higher, so dropping the multiline file
leaves the expected floor unchanged. A second fixture makes the multiline
form the deciding bound, where ignoring it yields 1.3.0 rather than 1.14.0.

The tool table said the action validates at the declared floor without saying
it refuses one below the configured minimum, and the note on `>` did not say
why an exclusive bound names nothing to install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clamp defaulted to 1.2.2, which closed one advisory and otherwise had
nothing to recommend it: that line stopped receiving fixes years ago. It now
defaults to 1.14.0, the oldest release still inside HashiCorp's support
window, and the README records the rule and the review it needs when a new
minor lands.

The docker invocation is one shlex.split of an interpolated string rather
than twenty lines of one argument each. read_floor.py writes the step output
itself, so the action step is a bare invocation with no shell logic.

just and uv in .tool-versions were both months behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`= 1.14.0` and a bare `1.14.0` both name a definite minimum, and Terraform
reads them as the same constraint, so rejecting either was a gap rather than a
guard. `!=` stays rejected, since it can rule out the bound another term
states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`>= 1.14.0, != 1.14.2` resolves to 1.14.0, since the exclusion does not touch
the bound. `>= 1.14.0, != 1.14.0` still fails, because finding the next release
above an excluded floor needs the release list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnsca
johnsca force-pushed the johnsca/fix/ENG-8760/min-terraform-version branch from a52f36f to e47cbf9 Compare October 8, 2026 16:13
@johnsca

johnsca commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai please re-review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 9, 2026

Copy link
Copy Markdown

@cubic-dev-ai please re-review

@johnsca I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 18 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant U as User Module Repo
    participant W as GitHub Workflow
    participant A as validate-min-terraform Action
    participant P as read_floor.py
    participant D as terraform-docs Container
    participant T as Terraform CLI
    participant CI as CI Pipeline

    Note over U,CI: Composite Action: Validate Terraform Floor

    U->>W: Trigger CI (push/PR)
    W->>A: Invoke composite action
    A->>P: Execute read_floor.py
    P->>D: docker run (parse .tf files)
    D->>D: Extract required_version constraints
    D-->>P: JSON with requirements
    P->>P: Parse constraints, derive floor
    alt Floor below minimum
        P-->>A: Error: below minimum floor
        A-->>W: Fail action
    else Valid floor
        P->>P: Write version to GITHUB_OUTPUT
        P-->>A: Return version
        A->>T: Setup Terraform at floor version
        A->>T: Run terraform init -backend=false
        T-->>A: Init complete
        A->>T: Run terraform validate
        T-->>A: Validation result
        A-->>W: Complete action
    end
    W-->>U: CI result (pass/fail)

    Note over U,CI: Tests run locally via unittest
    P->>P: Execute unit tests (no container)
    P->>D: End-to-end tests with Docker
Loading

Re-trigger cubic

Branch protection sets require_code_owner_reviews on a public repo, and with
no CODEOWNERS file that setting matches nobody. The other five repos in this
change carry the same one-line file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@johnsca
johnsca merged commit 3865043 into main Oct 13, 2026
6 checks passed
@johnsca
johnsca deleted the johnsca/fix/ENG-8760/min-terraform-version branch October 13, 2026 14:06
johnsca added a commit to stacklet/terraform-azure-stacklet-relay that referenced this pull request Oct 13, 2026
…#29)

[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

### what

Raises the declared Terraform floor to a version the module can actually
plan on, moves the CI pin to the current release, adds a job that
validates at whatever floor is declared, and adds a docs job to keep the
generated README tables honest. The lint job also gains a token for
zizmor. The docs job reads `.terraform-docs.yml`, which the action
ignores by default, and the config file is renamed from
`.terraform-docs.yaml` so all five repos spell it the same way.

### why

The ticket is about a sibling that declared a floor it could not run on.
This module turned out to have the same problem, which the ticket did
not predict.

The declared 1.9 was defensible on paper, since two variable validations
reach outside their own variable and that needs 1.9. What nobody had
checked is whether the module plans there. It does not. The blank-topic
guards added in ENG-8756 read `x == null || trimspace(x) != ""`, and
Terraform did not short-circuit `||` until 1.12, so below that it
evaluated `trimspace(null)` and failed the plan outright. Both topic
arguments default to null, so this hit every consumer on 1.9 through
1.11 who did not attach an existing topic.

Rewriting the guards as conditionals would have fixed it in place.
Raising the floor is the better trade, because 1.13 and below no longer
receive security fixes and there is no reason to keep supporting them.
The floor lands at 1.14, the CI pin moves to 1.16.5, and the guards stay
as written.

That bug is also worth knowing about when reading the new job.
`terraform validate` against a module directory treats every variable as
unknown, so it never evaluates a validation block, and it reported this
module as fine on 1.9. Only a plan from a root module, with the defaults
actually applied, exposed it. The job therefore covers syntax and
provider schema at the floor, not variable validation. Closing that gap
with `terraform test` and `mock_provider` is worth a follow-up.

The docs job is unrelated to floors but adjacent. This repo generates
its README tables and had nothing checking they stayed current, which is
precisely the state a sibling was already in, with its tables claiming
the module had no requirements at all.

The job itself lives in `stacklet/github-tooling`, because five module
repositories need the same check and it carries enough shell to be worth
one copy rather than five. This PR references that action by commit SHA,
naming the commit on github-tooling's `main`.

### testing

- [x] Sandbox deploy. Planned from a root module against the sandbox
Azure subscription with a service principal, at the declared floor of
1.14.0 and again at 1.16.4. Both produce the same 34 resources, and no
resource, variable or output changed. The plan running at all is the
thing to watch, since this is the change that decides whether the module
evaluates on a given CLI.
- [x] The old floor disproved rather than assumed. The module fails on
1.9.0, 1.10.5, 1.11.0 and 1.11.4, and passes from 1.12.0 up, which
matches where Terraform started short-circuiting `||`.
- [x] The README confirmed already identical to freshly generated
output, so the new docs job passes on arrival rather than going red.
- [x] The extraction checked against a module carrying three constraints
at once, one of them in a `.tf.json` and one split across lines. It
returns all three, and Terraform's own `init` agrees on which one binds.
The action it replaces returns an empty string for that module.
- [x] The docs job confirmed to load `.terraform-docs.yml`, which it was
silently ignoring.
- [x] `just lint` clean at the new pin, and zizmor re-run with a token
to confirm its five online audits schedule rather than skip. The prek
release that job installs now comes from `.tool-versions` rather than
being resolved at run time, so CI and a local run agree on it.
- [x] Repointed the `uses:` pin at `3865043`, the commit
github-tooling's `main` carries now that
stacklet/github-tooling#1 merged. The action
resolves there, so this job runs for the first time on this PR.
- [x] Refreshed the pinned tools and re-ran `just lint` at them:
terraform 1.16.5, tflint 0.64.0, just 1.58.0, uv 0.12.23. terraform-docs
stays at 0.20.0, the version `terraform-docs/gh-actions` v1.4.1 bundles,
so the docs job and `just docs` keep rendering with the same binary. The
plans above ran at 1.16.4, the pin at the time.

### docs

No documentation change beyond the generated Requirements table. The new
job enforces the existing generated output rather than altering it.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
johnsca added a commit to stacklet/terraform-azure-stacklet-cost-setup that referenced this pull request Oct 13, 2026
[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

### what

Adds a CI job that validates this module at the Terraform floor it
declares, raises that floor to a supported line, and adds a docs job to
keep the generated README tables honest. The lint job also gains a token
for zizmor. The docs job reads `.terraform-docs.yml`, which the action
ignores by default, and the config file is renamed from
`.terraform-docs.yaml` so all five repos spell it the same way.

### why

The ticket is about a sibling that declared a floor it could not run on,
undetected because the pinned toolchain sat far above it. This module's
floor was not wrong, but it was equally untested, so the job goes
everywhere rather than only where the bug happened to surface.

The floor moves from 1.9 to 1.14. The 1.9 was accurate, since the
cross-variable validations here need it, but accurate is not the same as
worth supporting: 1.13 and everything below it no longer receives
security fixes. The CI pin moves to 1.16.5 at the same time, so the
floor and the pin stay distinct and the new job covers an end that lint
does not.

This repo already had half the idea. The existing job pins providers to
their declared minimums and validates against the pinned Terraform. The
new one pins Terraform to its declared minimum and validates against
whatever providers resolve. The combination a consumer honoring both
advertised floors actually runs is still covered by neither. Closing
that needs a third run and its own verification, so it is left as a
follow-up rather than bolted on here.

The docs job is unrelated to floors but adjacent. This repo generates
its README tables and had nothing checking they stayed current, which is
precisely the state a sibling was already in, with its tables claiming
the module had no requirements at all.

The job itself lives in `stacklet/github-tooling`, because five module
repositories need the same check and it carries enough shell to be worth
one copy rather than five. This PR references that action by commit SHA,
naming the commit on github-tooling's `main`.

### testing

- [x] Sandbox deploy. Planned from a root module against the sandbox
Azure subscription with a service principal, at the declared floor of
1.14.0 and again at 1.16.4. Both produce the same 7 resources, and no
resource, variable or output changed.
- [x] The README confirmed already identical to freshly generated
output, so the new docs job passes on arrival rather than going red.
- [x] The extraction checked against a module carrying three constraints
at once, one of them in a `.tf.json` and one split across lines. It
returns all three, and Terraform's own `init` agrees on which one binds.
The action it replaces returns an empty string for that module.
- [x] The docs job confirmed to load `.terraform-docs.yml`, which it was
silently ignoring.
- [x] `just lint` clean at the new pin, and zizmor re-run with a token
to confirm its five online audits schedule rather than skip. The prek
release that job installs now comes from `.tool-versions` rather than
being resolved at run time, so CI and a local run agree on it.
- [x] Repointed the `uses:` pin at `3865043`, the commit
github-tooling's `main` carries now that
stacklet/github-tooling#1 merged. The action
resolves there, so this job runs for the first time on this PR.
- [x] Refreshed the pinned tools and re-ran `just lint` at them:
terraform 1.16.5, tflint 0.64.0, just 1.58.0, uv 0.12.23. terraform-docs
stays at 0.20.0, the version `terraform-docs/gh-actions` v1.4.1 bundles,
so the docs job and `just docs` keep rendering with the same binary. The
plans above ran at 1.16.4, the pin at the time.

### docs

No documentation change. The new job enforces the existing generated
output rather than altering it.

The azurerm deprecation warning visible in a local `validate` here is
pre-existing and unrelated, and the `provider.tf` header already
explains it.


[ENG-8760]:
https://stacklet.atlassian.net/browse/ENG-8760?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
johnsca added a commit to stacklet/terraform-aws-stacklet-cur-setup that referenced this pull request Oct 13, 2026
[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

### what

Adds a CI job that validates this module at the floor it declares,
raises that floor to a supported Terraform line, restores the README's
generated tables, and adds the terraform-docs tooling the sibling repos
already carry. Also stops the repo un-ignoring its provider lock file,
and gives zizmor a token. The docs job also reads `.terraform-docs.yml`,
which the action ignores by default.

### why

The ticket is about a sibling that declared a floor it could not run on.
This module's floor was not wrong, but it was equally untested, so the
job goes everywhere rather than only where the bug happened to surface.

The floor moves from 1.13.5 to 1.14 because 1.13 no longer receives
security fixes. Previously the floor was simply the pinned toolchain
version, which made the new job repeat what lint already does. The CI
pin moves to 1.16.5 at the same time, so the two ends are now distinct
and the job covers something lint does not.

The README is the substantive fix. Its tables claimed the module has no
requirements at all, while the constraints have been in `provider.tf`
since CI was first set up here. Nothing caught it because this is the
one repo of the set with no terraform-docs config, no pin, no `just`
target and no docs job, so CI was about to start enforcing a floor the
module's public documentation denied existed. Regenerating alone would
not hold, which is why the tooling comes with it.

The lock file matters for the same reason. Every sibling ignores it
deliberately, because committing one in a published module pins
consumers to its provider versions, and this repo did the opposite. The
new job runs `init`, which makes producing that file routine, so an
un-ignore that was previously harmless is now one careless `git add`
from doing real damage.

The job itself lives in `stacklet/github-tooling`, because five module
repositories need the same check and it carries enough shell to be worth
one copy rather than five. This PR references that action by commit SHA,
naming the commit on github-tooling's `main`.

### testing

- [x] Sandbox deploy. Planned from a root module against the sandbox dev
account at the declared floor of 1.14.0, and again at 1.16.4. Both
produce the same 6 resources, and no resource, variable or output
changed.
- [x] The README regenerated with the pinned terraform-docs and
confirmed stable on a second run, so the new docs job passes on arrival
rather than going red.
- [x] The extraction checked against a module carrying three constraints
at once, one of them in a `.tf.json` and one split across lines. It
returns all three, and Terraform's own `init` agrees on which one binds.
The action it replaces returns an empty string for that module.
- [x] The docs job confirmed to load `.terraform-docs.yml`, which it was
silently ignoring.
- [x] `just lint` clean at the new pin, and zizmor re-run with a token
to confirm its five online audits schedule rather than skip. The prek
release that job installs now comes from `.tool-versions` rather than
being resolved at run time, so CI and a local run agree on it.
- [x] Repointed the `uses:` pin at `3865043`, the commit
github-tooling's `main` carries now that
stacklet/github-tooling#1 merged. The action
resolves there, so this job runs for the first time on this PR.
- [x] Refreshed the pinned tools and re-ran `just lint` at them:
terraform 1.16.5, tflint 0.64.0, just 1.58.0, uv 0.12.23. terraform-docs
stays at 0.20.0, the version `terraform-docs/gh-actions` v1.4.1 bundles,
so the docs job and `just docs` keep rendering with the same binary. The
plans above ran at 1.16.4, the pin at the time.

### docs

The README's Requirements and Providers tables now match `provider.tf`,
and the tooling added alongside is what keeps them matching.

### one thing to know before reading the diff

Planning this module surfaces two deprecation warnings that predate this
change and are left alone here. `data.aws_region.current.name` is
deprecated in AWS provider 6.x in favour of `.region`, and the empty
`provider "aws" {}` block is the old proxy-provider form that no sibling
uses. Both are worth a follow-up ticket rather than a drive-by fix
inside a change about version floors.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
johnsca added a commit to stacklet/terraform-aws-onboarding-org-read that referenced this pull request Oct 13, 2026
[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

### what

Replaces a copied, meaningless Terraform floor with a real one, moves
the CI pin to the current release, and adds a job that validates at
whatever floor is declared, so the constraint is tested rather than
asserted. The lint job also gains a token for zizmor. Also points the
docs job at `.terraform-docs.yml`, which it was ignoring.

### why

This module carried the same `>= 1` as its sibling. Here it was never
actively wrong, only untrue, since the `iam_path` validation has always
needed 1.3.

The floor lands at 1.14 rather than the 1.3 the ticket implies.
Declaring the oldest version that happens to work means standing behind
Terraform lines that no longer receive security fixes, and nothing in
this module needs one. This narrows the supported range for customers a
long way, which is a deliberate trade rather than a side effect.

The ticket asks for a version matrix. A matrix needs a second axis that
does not exist here, because the lint job already covers the pinned end
and the floor was the only untested one. The job derives its version
from `required_version` instead of hardcoding it, so editing the
constraint moves what CI tests in the same commit. The CI pin moves to
1.16.5 in the same change, so the two ends stay distinct.

The floor is read with terraform-docs rather than with the
terraform-min-max action this started on. That action finds the
constraint by regex, takes the first match per file, ignores `.tf.json`,
and then picks a file by shortest path, and a guard step written to
compensate turned out to be the same regex with the same blind spots. 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 constraint in the module, so there is no
longer a question of which file holds the real one, and no guard. It is
pinned by digest rather than by version, because terraform-docs
publishes no immutable releases, which leaves both a tag and the
checksum published beside a release able to change underneath us.

The job itself lives in `stacklet/github-tooling`, because five module
repositories need the same check and it carries enough shell to be worth
one copy rather than five. This PR references that action by commit SHA,
naming the commit on github-tooling's `main`.

### testing

- [x] Sandbox deploy. Planned from a root module against the sandbox dev
account at the declared floor of 1.14.0, and again at 1.16.4. Both
produce the same 3 resources, and no resource, variable or output
changed. Confirmed explicitly, because a `provider.tf` edit is the kind
that looks inert and is not.
- [x] The job confirmed to have teeth here, by relaxing the constraint
and watching `validate` reject the module below 1.3. That was not
obvious, since the gated functions sit inside a variable validation
block.
- [x] The extraction checked against a module carrying three constraints
at once, one of them in a `.tf.json` and one split across lines. It
returns all three, and Terraform's own `init` agrees on which one binds.
The action it replaces returns an empty string for that module.
- [x] `just lint` clean at the new pin, and zizmor re-run with a token
to confirm its five online audits schedule rather than skip. The prek
release that job installs now comes from `.tool-versions` rather than
being resolved at run time, so CI and a local run agree on it.
- [x] Repointed the `uses:` pin at `3865043`, the commit
github-tooling's `main` carries now that
stacklet/github-tooling#1 merged. The action
resolves there, so this job runs for the first time on this PR.
- [x] Refreshed the pinned tools and re-ran `just lint` at them:
terraform 1.16.5, tflint 0.64.0, just 1.58.0, uv 0.12.23. terraform-docs
stays at 0.20.0, the version `terraform-docs/gh-actions` v1.4.1 bundles,
so the docs job and `just docs` keep rendering with the same binary. The
plans above ran at 1.16.4, the pin at the time.

### docs

The README's generated Requirements table follows `provider.tf`.

The narrowed range is worth a release note, stated as the support
decision it is rather than left for a customer to discover at `init`.


[ENG-8760]:
https://stacklet.atlassian.net/browse/ENG-8760?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
johnsca added a commit to stacklet/terraform-aws-onboarding-account that referenced this pull request Oct 13, 2026
[ENG-8760](https://stacklet.atlassian.net/browse/ENG-8760)

### what

Raises the declared Terraform floor to a version the module can actually
run on, moves the CI pin to the current release, and adds a job that
validates at whatever floor is declared, so the constraint is tested
rather than asserted. The lint job also gains a token for zizmor. Also
points the docs job at `.terraform-docs.yml`, which it was ignoring.

### why

The old constraint admitted versions that cannot parse this module at
all. Provider-defined functions are a parser change, so the failure
lands at `init` as a syntax error naming neither the function nor a
version, which is worse for a customer than a plain version mismatch and
is the reason the ticket exists.

The floor is 1.14, well above the 1.8 the ticket proposes and above the
oldest release that can run the module. Declaring the oldest workable
version means standing behind Terraform lines that no longer receive
security fixes, and nothing here needs one. The CI pin moves to 1.16.5
at the same time, so the floor and the pin stay distinct and the new job
covers an end that lint does not.

The ticket asks for a version matrix. A matrix needs a second axis that
does not exist here, because the lint job already covers the pinned end
and the floor was the only untested one. The job derives its version
from `required_version` instead of hardcoding it, so editing the
constraint moves what CI tests in the same commit.

The floor is read with terraform-docs rather than with the
terraform-min-max action this started on. That action finds the
constraint by regex, takes the first match per file, ignores `.tf.json`,
and then picks a file by shortest path, and a guard step written to
compensate turned out to be the same regex with the same blind spots. 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 constraint in the module, so there is no
longer a question of which file holds the real one, and no guard. It is
pinned by digest rather than by version, because terraform-docs
publishes no immutable releases, which leaves both a tag and the
checksum published beside a release able to change underneath us.

zizmor's online audits skip silently without a token and its summary
line reads the same either way, so CI reported success while the
supply-chain audits never ran. The lint job also pins the prek release
it installs. Without a pin it took whatever prek released most recently,
so a hostile release would have reached CI with no change here.

The job itself lives in `stacklet/github-tooling`, because five module
repositories need the same check and it carries enough shell to be worth
one copy rather than five. This PR references that action by commit SHA,
naming the commit on github-tooling's `main`.

### testing

- [x] Sandbox deploy. Planned from a root module against the sandbox dev
account at the declared floor of 1.14.0, and again at 1.16.4. Both
produce the same 15 resources, and no resource, variable or output
changed. Confirmed explicitly, because a `provider.tf` edit is the kind
that looks inert and is not.
- [x] The plan exercised the provider-defined functions, which only run
when the target account differs from the caller's.
`provider::aws::arn_build` resolved to the expected event bus ARNs in
both regions.
- [x] The old floor checked against real binaries before it moved. The
module fails at 1.7.5, so the original constraint was wrong rather than
merely loose.
- [x] The resolving action run locally against this module, confirming
CI installs the version the constraint names.
- [x] The extraction checked against a module carrying three constraints
at once, one of them in a `.tf.json` and one split across lines. It
returns all three, and Terraform's own `init` agrees on which one binds.
The action it replaces returns an empty string for that module.
- [x] `just lint` clean at the new pin, and zizmor re-run with a token
to confirm its five online audits schedule rather than skip. The prek
release that job installs now comes from `.tool-versions` rather than
being resolved at run time, so CI and a local run agree on it.
- [x] Repointed the `uses:` pin at `3865043`, the commit
github-tooling's `main` carries now that
stacklet/github-tooling#1 merged. The action
resolves there, so this job runs for the first time on this PR.
- [x] Refreshed the pinned tools and re-ran `just lint` at them:
terraform 1.16.5, tflint 0.64.0, just 1.58.0, uv 0.12.23. terraform-docs
stays at 0.20.0, the version `terraform-docs/gh-actions` v1.4.1 bundles,
so the docs job and `just docs` keep rendering with the same binary. The
plans above ran at 1.16.4, the pin at the time.

### docs

The README's generated Requirements table follows `provider.tf`.

Raising the floor on a published module is a visible contract change:
customers below it now get a clear version error where they used to get
a syntax error. That is the intent, but it deserves a release note, and
1.14 rules out more customers than 1.8 would have.


[ENG-8760]:
https://stacklet.atlassian.net/browse/ENG-8760?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants