Repository navigation
Commit 3865043
feat: add the validate-min-terraform action (ENG-8760) (#1)
[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, 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
- [x] 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.
- [x] Ran it against all five onboarding modules. Each resolves to the
floor its `required_version` states.
- [x] 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.
- [x] 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.
- [x] `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.
- [x] 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.
- [x] `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](https://claude.com/claude-code)
[ENG-8760]:
https://stacklet.atlassian.net/browse/ENG-8760?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent 8530322 commit 3865043
19 files changed
Lines changed: 585 additions & 1 deletion
File tree
- .github
- workflows
- validate-min-terraform
- testdata
- multiline-constraint
- no-constraint
- several-files
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
0 commit comments