From fb6aa33001fa849f41ac1e48616511f75ec4f04e Mon Sep 17 00:00:00 2001 From: Bryce Lynn Date: Fri, 31 Jul 2026 11:29:50 -0700 Subject: [PATCH 1/2] ci: verify each module's declared Terraform version floor Runs the version-floor guard after `make lint`, so a module whose required_version claims support for a version it cannot actually be loaded with fails its own PR rather than a consumer's apply. The guard itself ships via launch-terraform-skeleton, so it will not exist in a repository until that update propagates. The step therefore skips when the script is absent -- but emits a workflow notice while doing so, so a skipped check is visible rather than looking like a pass. Requires launch-terraform-skeleton#38. Co-authored-by: Cursor --- .github/workflows/reusable-terraform-check.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/reusable-terraform-check.yml b/.github/workflows/reusable-terraform-check.yml index 86e6f20..4af51ed 100644 --- a/.github/workflows/reusable-terraform-check.yml +++ b/.github/workflows/reusable-terraform-check.yml @@ -195,6 +195,22 @@ jobs: run: | make lint + - id: version-floor + name: "Verify declared Terraform version floor" + # Confirms required_version names a version the module can actually be + # loaded with, by initializing it with the oldest version the constraint + # admits. The guard ships via launch-terraform-skeleton, so it is absent + # until a repo has picked up that update; skip loudly rather than fail, + # and rather than pass silently. + shell: bash + run: | + guard=".github/scripts/check-terraform-version-floor.sh" + if [[ ! -f "${guard}" ]]; then + echo "::notice title=Version floor check skipped::${guard} is not present in this repository yet. It arrives with the next launch-terraform-skeleton update." + exit 0 + fi + make tfmodule/check-version-floor + - id: update-lint-status name: "Update Terraform Lint status" if: always() && steps.lint.outcome != 'skipped' From 031e2cb14bca911a6a213f6dd1e174e724ad941b Mon Sep 17 00:00:00 2001 From: Bryce Lynn Date: Fri, 31 Jul 2026 13:51:13 -0700 Subject: [PATCH 2/2] ci: cache the floor Terraform and report it in the lint status Two corrections found while reviewing this PR against itself. The check installs the oldest Terraform its constraint admits, but nothing was caching that binary. The asdf tool cache is keyed on .tool-versions and only saved on a miss, so a version installed after that restore is discarded -- meaning every affected run re-downloaded it. Resolving the floor first (via the guard's --print-floor mode, which exists for this) allows a cache entry keyed on the resolved version. The resolve step emits an empty value when the guard has not propagated yet or when the constraint declares no lower bound, so the cache is skipped and the real diagnostic still comes from the check itself. The posted "Terraform Lint" status keyed only on make lint, so a version-floor failure showed as "Terraform Lint: success" beside a red job. Merge was still blocked -- the legacy required check is gated through `needs` on the lint job -- but the status was misleading. It now reflects both steps. Step ids use underscores so they can be referenced in expressions without bracket syntax. Co-authored-by: Cursor --- .../workflows/reusable-terraform-check.yml | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-terraform-check.yml b/.github/workflows/reusable-terraform-check.yml index 4af51ed..ace1cfb 100644 --- a/.github/workflows/reusable-terraform-check.yml +++ b/.github/workflows/reusable-terraform-check.yml @@ -195,7 +195,35 @@ jobs: run: | make lint - - id: version-floor + - id: version_floor_resolve + name: "Resolve declared Terraform version floor" + # Resolved before the check runs so the floor's toolchain can be cached + # by version. Emits an empty value in the two cases where there is + # nothing to cache -- the guard has not propagated to this repo yet, or + # the constraint declares no lower bound -- leaving the real diagnostic + # to the check step rather than failing here with something cryptic. + shell: bash + run: | + guard=".github/scripts/check-terraform-version-floor.sh" + version="" + if [[ -f "${guard}" ]]; then + version="$(bash "${guard}" --print-floor 2>/dev/null || true)" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + + - id: version_floor_cache + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 + # The asdf tool cache is keyed on .tool-versions and only saved on a + # miss, so a Terraform installed after that restore is never persisted. + # This entry is keyed on the resolved floor instead, so the floor's + # binary is downloaded once per version rather than on every run. + if: steps.version_floor_resolve.outputs.version != '' + name: Cache floor-version Terraform + with: + path: ~/.asdf/installs/terraform/${{ steps.version_floor_resolve.outputs.version }} + key: ${{ runner.os }}-tf-floor-${{ steps.version_floor_resolve.outputs.version }} + + - id: version_floor name: "Verify declared Terraform version floor" # Confirms required_version names a version the module can actually be # loaded with, by initializing it with the oldest version the constraint @@ -213,13 +241,17 @@ jobs: - id: update-lint-status name: "Update Terraform Lint status" + # Reflects the version-floor check as well as make lint, so this status + # cannot report success while the job is red. if: always() && steps.lint.outcome != 'skipped' uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@0.15.0 with: check_name: "Terraform Lint" - status: ${{ steps.lint.outcome == 'success' && 'success' || steps.lint.outcome - == 'failure' && 'failure' || 'error' }} - description: "Terraform lint ${{ steps.lint.outcome }}" + status: ${{ (steps.lint.outcome == 'success' && steps.version_floor.outcome + != 'failure') && 'success' || (steps.lint.outcome == 'failure' || steps.version_floor.outcome + == 'failure') && 'failure' || 'error' }} + description: "Terraform lint ${{ steps.lint.outcome }}, version floor ${{ + steps.version_floor.outcome }}" target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"