diff --git a/AGENTS.md b/AGENTS.md index 5c36f59d..a65b3de3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,6 +202,37 @@ The `availability` field controls publication state and access restrictions. mes --- + +## Provider Version Floor + +A `>=` constraint says "not older than this"; the floor says **how old is still acceptable +anywhere in the hub**. It is declared in exactly one place — the `PROVIDER_FLOOR` map at the top of +[`tools/scorecard/scorecard.mjs`](tools/scorecard/scorecard.mjs) — and the `provider_floor` +scorecard check enforces it across every tier a module owns: `meshstack_integration.tf`, +`backplane/`, `buildingblock/` and `e2e/` (including nested submodules). + +Two things fail the check: + +- a `required_providers` entry for a floor-managed provider with **no** `version` at all — that is + unbounded, so `tofu init` may resolve an arbitrarily old release +- a constraint whose lower bound is **below** the floor (`>= 0.21.0` against a 0.23.0 floor, or a + constraint with no lower bound at all such as `< 1.0.0`) + +**To raise the floor for the whole hub**: edit the one value in `PROVIDER_FLOOR`, then run +`node tools/scorecard/scorecard.mjs --category=core`. Every module still declaring less turns red +and the failure detail names the file and the current constraint — that list is the bump work item. +The check verifies rather than rewrites, because HCL `required_providers` `version` must be a string +literal (no interpolation), so there is nothing a module could reference; and because the constraint +is a deliberate, reviewable statement about what a module needs. + +Why the floor is load-bearing even though nothing pins in CI: the hub commits no +`.terraform.lock.hcl` (root configurations own locking), and the smoke-test workflow injects a +provider built from `main` through Terraform `dev_overrides`, which bypasses both +`required_providers` and any lockfile. The floor is therefore what protects **local** runs +(`task e2e:run`, `terraform validate`) from silently resolving a stale provider. + +--- + ## Scorecard The repository includes a scorecard tool that checks module maturity across four categories: diff --git a/modules/aks/github-connector/meshstack_integration.tf b/modules/aks/github-connector/meshstack_integration.tf index 3e66ad98..cbad684c 100644 --- a/modules/aks/github-connector/meshstack_integration.tf +++ b/modules/aks/github-connector/meshstack_integration.tf @@ -232,7 +232,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/aks/meshstack_integration.tf b/modules/aks/meshstack_integration.tf index b7b5c40f..493fc7c5 100644 --- a/modules/aks/meshstack_integration.tf +++ b/modules/aks/meshstack_integration.tf @@ -183,7 +183,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azuread = { source = "hashicorp/azuread" diff --git a/modules/aws/agentic-coding-sandbox/buildingblock/README.md b/modules/aws/agentic-coding-sandbox/buildingblock/README.md index 39f3df46..7a9fdb6e 100644 --- a/modules/aws/agentic-coding-sandbox/buildingblock/README.md +++ b/modules/aws/agentic-coding-sandbox/buildingblock/README.md @@ -77,7 +77,7 @@ End users provide: | Name | Version | |------|---------| -| [meshstack](#requirement\_meshstack) | >= 0.7.1 | +| [meshstack](#requirement\_meshstack) | >= 0.23.0 | ## Modules diff --git a/modules/aws/agentic-coding-sandbox/buildingblock/versions.tf b/modules/aws/agentic-coding-sandbox/buildingblock/versions.tf index 0cc33709..cfbecac3 100644 --- a/modules/aws/agentic-coding-sandbox/buildingblock/versions.tf +++ b/modules/aws/agentic-coding-sandbox/buildingblock/versions.tf @@ -2,7 +2,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.7.1" + version = ">= 0.23.0" } } } \ No newline at end of file diff --git a/modules/aws/route53-dns-alias-record/meshstack_integration.tf b/modules/aws/route53-dns-alias-record/meshstack_integration.tf index bc51db42..32134220 100644 --- a/modules/aws/route53-dns-alias-record/meshstack_integration.tf +++ b/modules/aws/route53-dns-alias-record/meshstack_integration.tf @@ -239,7 +239,7 @@ terraform { } meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/aws/route53-dns-record/meshstack_integration.tf b/modules/aws/route53-dns-record/meshstack_integration.tf index d8249ff2..1af995b7 100644 --- a/modules/aws/route53-dns-record/meshstack_integration.tf +++ b/modules/aws/route53-dns-record/meshstack_integration.tf @@ -233,7 +233,7 @@ terraform { } meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/aws/s3_bucket/meshstack_integration.tf b/modules/aws/s3_bucket/meshstack_integration.tf index 503b1404..78778244 100644 --- a/modules/aws/s3_bucket/meshstack_integration.tf +++ b/modules/aws/s3_bucket/meshstack_integration.tf @@ -191,7 +191,7 @@ terraform { } meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/azure/budget-alert/e2e/terraform.tf b/modules/azure/budget-alert/e2e/terraform.tf index 39e32bf6..8ef1256d 100644 --- a/modules/azure/budget-alert/e2e/terraform.tf +++ b/modules/azure/budget-alert/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/budget-alert/meshstack_integration.tf b/modules/azure/budget-alert/meshstack_integration.tf index 1ec281b4..8b8a41d6 100644 --- a/modules/azure/budget-alert/meshstack_integration.tf +++ b/modules/azure/budget-alert/meshstack_integration.tf @@ -230,7 +230,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/entra-id-groups/meshstack_integration.tf b/modules/azure/entra-id-groups/meshstack_integration.tf index 537fc3cd..4ffeba52 100644 --- a/modules/azure/entra-id-groups/meshstack_integration.tf +++ b/modules/azure/entra-id-groups/meshstack_integration.tf @@ -255,7 +255,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/meshstack_integration.tf b/modules/azure/meshstack_integration.tf index 540b168a..311afdee 100644 --- a/modules/azure/meshstack_integration.tf +++ b/modules/azure/meshstack_integration.tf @@ -249,7 +249,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/resource-group/e2e/terraform.tf b/modules/azure/resource-group/e2e/terraform.tf index 39e32bf6..8ef1256d 100644 --- a/modules/azure/resource-group/e2e/terraform.tf +++ b/modules/azure/resource-group/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/resource-group/meshstack_integration.tf b/modules/azure/resource-group/meshstack_integration.tf index e45c31e6..203afa2c 100644 --- a/modules/azure/resource-group/meshstack_integration.tf +++ b/modules/azure/resource-group/meshstack_integration.tf @@ -226,7 +226,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/service-principal/meshstack_integration.tf b/modules/azure/service-principal/meshstack_integration.tf index e3055a61..626cf4ab 100644 --- a/modules/azure/service-principal/meshstack_integration.tf +++ b/modules/azure/service-principal/meshstack_integration.tf @@ -258,7 +258,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/storage-account/e2e/terraform.tf b/modules/azure/storage-account/e2e/terraform.tf index 39e32bf6..8ef1256d 100644 --- a/modules/azure/storage-account/e2e/terraform.tf +++ b/modules/azure/storage-account/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/azure/storage-account/meshstack_integration.tf b/modules/azure/storage-account/meshstack_integration.tf index 5ab8aca8..5f4356b3 100644 --- a/modules/azure/storage-account/meshstack_integration.tf +++ b/modules/azure/storage-account/meshstack_integration.tf @@ -223,7 +223,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } azurerm = { source = "hashicorp/azurerm" diff --git a/modules/gcp/storage-bucket/meshstack_integration.tf b/modules/gcp/storage-bucket/meshstack_integration.tf index afee390a..ddec339d 100644 --- a/modules/gcp/storage-bucket/meshstack_integration.tf +++ b/modules/gcp/storage-bucket/meshstack_integration.tf @@ -203,7 +203,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } google = { source = "hashicorp/google" diff --git a/modules/github/repository/meshstack_integration.tf b/modules/github/repository/meshstack_integration.tf index e0a20223..5fa1a9c2 100644 --- a/modules/github/repository/meshstack_integration.tf +++ b/modules/github/repository/meshstack_integration.tf @@ -255,7 +255,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/kubernetes/manifest/meshstack_integration.tf b/modules/kubernetes/manifest/meshstack_integration.tf index 39009f89..f3a7db4b 100644 --- a/modules/kubernetes/manifest/meshstack_integration.tf +++ b/modules/kubernetes/manifest/meshstack_integration.tf @@ -205,7 +205,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/github-workflow/e2e/terraform.tf b/modules/meshstack/github-workflow/e2e/terraform.tf index 3d4bedc5..499dac0d 100644 --- a/modules/meshstack/github-workflow/e2e/terraform.tf +++ b/modules/meshstack/github-workflow/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/github-workflow/meshstack_integration.tf b/modules/meshstack/github-workflow/meshstack_integration.tf index 91e4ee04..511b99c8 100644 --- a/modules/meshstack/github-workflow/meshstack_integration.tf +++ b/modules/meshstack/github-workflow/meshstack_integration.tf @@ -219,7 +219,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/link/e2e/terraform.tf b/modules/meshstack/link/e2e/terraform.tf index 3d4bedc5..499dac0d 100644 --- a/modules/meshstack/link/e2e/terraform.tf +++ b/modules/meshstack/link/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/link/meshstack_integration.tf b/modules/meshstack/link/meshstack_integration.tf index 6d4b6cb3..c2691449 100644 --- a/modules/meshstack/link/meshstack_integration.tf +++ b/modules/meshstack/link/meshstack_integration.tf @@ -222,7 +222,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/manual/e2e/terraform.tf b/modules/meshstack/manual/e2e/terraform.tf index 3d4bedc5..499dac0d 100644 --- a/modules/meshstack/manual/e2e/terraform.tf +++ b/modules/meshstack/manual/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/manual/meshstack_integration.tf b/modules/meshstack/manual/meshstack_integration.tf index f9443320..f013ee6a 100644 --- a/modules/meshstack/manual/meshstack_integration.tf +++ b/modules/meshstack/manual/meshstack_integration.tf @@ -110,7 +110,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/noop/backplane/versions.tf b/modules/meshstack/noop/backplane/versions.tf index 91ce98f6..44897660 100644 --- a/modules/meshstack/noop/backplane/versions.tf +++ b/modules/meshstack/noop/backplane/versions.tf @@ -12,7 +12,7 @@ terraform { } meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } time = { source = "hashicorp/time" diff --git a/modules/meshstack/noop/e2e/env-audit/terraform.tf b/modules/meshstack/noop/e2e/env-audit/terraform.tf index 1e6fcb01..6de0a9e2 100644 --- a/modules/meshstack/noop/e2e/env-audit/terraform.tf +++ b/modules/meshstack/noop/e2e/env-audit/terraform.tf @@ -2,7 +2,8 @@ terraform { required_version = ">= 1.0" required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/noop/e2e/runner/terraform.tf b/modules/meshstack/noop/e2e/runner/terraform.tf index bd4e078f..18af6a0e 100644 --- a/modules/meshstack/noop/e2e/runner/terraform.tf +++ b/modules/meshstack/noop/e2e/runner/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } google = { source = "hashicorp/google" diff --git a/modules/meshstack/noop/e2e/terraform.tf b/modules/meshstack/noop/e2e/terraform.tf index eb95de8a..b1ecdd7e 100644 --- a/modules/meshstack/noop/e2e/terraform.tf +++ b/modules/meshstack/noop/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/noop/meshstack_integration.tf b/modules/meshstack/noop/meshstack_integration.tf index 3bf8abdb..0a7db55c 100644 --- a/modules/meshstack/noop/meshstack_integration.tf +++ b/modules/meshstack/noop/meshstack_integration.tf @@ -239,7 +239,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/meshstack/payment-method/buildingblock/README.md b/modules/meshstack/payment-method/buildingblock/README.md index 3bf4757e..1c661d97 100644 --- a/modules/meshstack/payment-method/buildingblock/README.md +++ b/modules/meshstack/payment-method/buildingblock/README.md @@ -63,7 +63,7 @@ module "payment_method" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [meshstack](#requirement\_meshstack) | >= 0.14.0 | +| [meshstack](#requirement\_meshstack) | >= 0.23.0 | ## Modules diff --git a/modules/meshstack/payment-method/buildingblock/versions.tf b/modules/meshstack/payment-method/buildingblock/versions.tf index 84dee37a..499dac0d 100644 --- a/modules/meshstack/payment-method/buildingblock/versions.tf +++ b/modules/meshstack/payment-method/buildingblock/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.14.0" + version = ">= 0.23.0" } } } diff --git a/modules/oci/application-compartment/buildingblock/README.md b/modules/oci/application-compartment/buildingblock/README.md index 0a6b04cf..7002fa25 100644 --- a/modules/oci/application-compartment/buildingblock/README.md +++ b/modules/oci/application-compartment/buildingblock/README.md @@ -161,7 +161,7 @@ The module automatically: | Name | Version | |------|---------| -| [meshstack](#requirement\_meshstack) | >= 0.21.0 | +| [meshstack](#requirement\_meshstack) | >= 0.23.0 | | [oci](#requirement\_oci) | 7.32.0 | ## Modules diff --git a/modules/oci/application-compartment/buildingblock/provider.tf b/modules/oci/application-compartment/buildingblock/provider.tf index 6007c065..980e4661 100644 --- a/modules/oci/application-compartment/buildingblock/provider.tf +++ b/modules/oci/application-compartment/buildingblock/provider.tf @@ -2,7 +2,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } oci = { source = "oracle/oci" diff --git a/modules/ske/forgejo-connector/meshstack_integration.tf b/modules/ske/forgejo-connector/meshstack_integration.tf index af739386..674422ba 100644 --- a/modules/ske/forgejo-connector/meshstack_integration.tf +++ b/modules/ske/forgejo-connector/meshstack_integration.tf @@ -269,7 +269,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/ske/ske-starterkit/e2e/meshstack_kubernetes_platform/terraform.tf b/modules/ske/ske-starterkit/e2e/meshstack_kubernetes_platform/terraform.tf index 6b9b271a..901cb8a1 100644 --- a/modules/ske/ske-starterkit/e2e/meshstack_kubernetes_platform/terraform.tf +++ b/modules/ske/ske-starterkit/e2e/meshstack_kubernetes_platform/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/modules/ske/ske-starterkit/e2e/terraform.tf b/modules/ske/ske-starterkit/e2e/terraform.tf index 46994e51..01aeae49 100644 --- a/modules/ske/ske-starterkit/e2e/terraform.tf +++ b/modules/ske/ske-starterkit/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/modules/stackit/git-repository/e2e/terraform.tf b/modules/stackit/git-repository/e2e/terraform.tf index 3d4bedc5..499dac0d 100644 --- a/modules/stackit/git-repository/e2e/terraform.tf +++ b/modules/stackit/git-repository/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } } } diff --git a/modules/stackit/git-repository/meshstack_integration.tf b/modules/stackit/git-repository/meshstack_integration.tf index e1c94449..7c96a646 100644 --- a/modules/stackit/git-repository/meshstack_integration.tf +++ b/modules/stackit/git-repository/meshstack_integration.tf @@ -281,7 +281,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/stackit/network-area/meshstack_integration.tf b/modules/stackit/network-area/meshstack_integration.tf index f0e59748..e62c1d9b 100644 --- a/modules/stackit/network-area/meshstack_integration.tf +++ b/modules/stackit/network-area/meshstack_integration.tf @@ -272,7 +272,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } stackit = { source = "stackitcloud/stackit" diff --git a/modules/stackit/network/meshstack_integration.tf b/modules/stackit/network/meshstack_integration.tf index 1e2201bc..3678428c 100644 --- a/modules/stackit/network/meshstack_integration.tf +++ b/modules/stackit/network/meshstack_integration.tf @@ -236,7 +236,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } stackit = { source = "stackitcloud/stackit" diff --git a/modules/stackit/project/buildingblock/README.md b/modules/stackit/project/buildingblock/README.md index 262c4f4b..cce6edda 100644 --- a/modules/stackit/project/buildingblock/README.md +++ b/modules/stackit/project/buildingblock/README.md @@ -44,7 +44,7 @@ provider "stackit" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.11.0 | -| [meshstack](#requirement\_meshstack) | >= 0.21.0 | +| [meshstack](#requirement\_meshstack) | >= 0.23.0 | | [stackit](#requirement\_stackit) | >= 0.98.0 | ## Modules diff --git a/modules/stackit/project/buildingblock/versions.tf b/modules/stackit/project/buildingblock/versions.tf index 364654db..fc4c659f 100644 --- a/modules/stackit/project/buildingblock/versions.tf +++ b/modules/stackit/project/buildingblock/versions.tf @@ -7,7 +7,7 @@ terraform { } meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/modules/stackit/storage-bucket/e2e/terraform.tf b/modules/stackit/storage-bucket/e2e/terraform.tf index b86edddb..749570ff 100644 --- a/modules/stackit/storage-bucket/e2e/terraform.tf +++ b/modules/stackit/storage-bucket/e2e/terraform.tf @@ -3,7 +3,8 @@ terraform { required_providers { meshstack = { - source = "meshcloud/meshstack" + source = "meshcloud/meshstack" + version = ">= 0.23.0" } stackit = { source = "stackitcloud/stackit" diff --git a/modules/stackit/storage-bucket/meshstack_integration.tf b/modules/stackit/storage-bucket/meshstack_integration.tf index bf04b12c..11105604 100644 --- a/modules/stackit/storage-bucket/meshstack_integration.tf +++ b/modules/stackit/storage-bucket/meshstack_integration.tf @@ -240,7 +240,7 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - version = ">= 0.21.0" + version = ">= 0.23.0" } } } diff --git a/tools/scorecard/pr-scorecard.sh b/tools/scorecard/pr-scorecard.sh index 04747b2b..15de3037 100755 --- a/tools/scorecard/pr-scorecard.sh +++ b/tools/scorecard/pr-scorecard.sh @@ -25,6 +25,11 @@ MODULES=$(git diff --name-only "${BASE_REF}...HEAD" \ | sed 's|^modules/\([^/]*/[^/]*\)/.*|\1|' \ | sort -u || true) +# A change to the scorecard tool itself is hub-wide in effect: a new detector, or a cranked +# PROVIDER_FLOOR, can turn modules red that this PR never touched. Scoping the report to +# changed modules would show nothing at all for a PR that only turns the floor knob. +TOOL_CHANGED=$(git diff --name-only "${BASE_REF}...HEAD" | grep -E '^tools/scorecard/' || true) + emit() { if [ -n "$OUTPUT_FILE" ]; then printf '%s\n' "$@" >> "$OUTPUT_FILE" @@ -35,6 +40,18 @@ emit() { [ -n "$OUTPUT_FILE" ] && : > "$OUTPUT_FILE" +if [ -n "$TOOL_CHANGED" ]; then + emit "> Scorecard run on commit \`$(git rev-parse HEAD)\` relative to \`${BASE_REF}\`" \ + "" \ + "> ⚙️ Scorecard tooling changed — reporting on **all** modules, not just the ones this PR touches." \ + "" + echo "Scorecard tooling changed — running the full hub" >&2 + node "$SCRIPT_DIR/scorecard.mjs" 2>&1 | { + if [ -n "$OUTPUT_FILE" ]; then cat >> "$OUTPUT_FILE"; else cat; fi + } || true + exit 0 +fi + if [ -z "$MODULES" ]; then emit "_No module changes detected relative to \`${BASE_REF}\`._" exit 0 diff --git a/tools/scorecard/scorecard.mjs b/tools/scorecard/scorecard.mjs index 83b04265..15260f87 100755 --- a/tools/scorecard/scorecard.mjs +++ b/tools/scorecard/scorecard.mjs @@ -19,6 +19,40 @@ import { join, relative } from "path"; const ROOT = new URL("../../", import.meta.url).pathname.replace(/\/$/, ""); const MODULES_DIR = join(ROOT, "modules"); +// ─── THE PROVIDER FLOOR KNOB ──────────────────────────────────────────────── +// +// One central place defining the minimum provider version the *entire hub* is +// meant to be tested and used with. Crank a value here and the `provider_floor` +// check turns red on every module still declaring less — that list is the work +// item for the bump. Nothing else in the repo hardcodes the floor. +// +// Keyed by registry source address (`namespace/type`), because the local name in +// `required_providers` is per-module and cannot be relied on. +// +// Why a floor is needed at all: nothing else establishes one. Module +// `required_providers` blocks that omit `version` let a local `tofu init` resolve +// an arbitrarily old provider, and the hub commits no `.terraform.lock.hcl` +// (root configurations own locking). CI does not catch this either — the +// smoke-test workflow builds the provider from `main` and injects it through +// Terraform `dev_overrides`, which bypasses both `required_providers` and any +// lockfile. So this floor is what protects *local* runs. +// +// Why meshstack sits at 0.23.0: that is the first release carrying the current +// `meshstack_building_block` resource. Older providers (on-disk local lockfiles +// were found reaching back to 0.20.5) predate the lifecycle-aware building block +// delete poll, where `DeletionSuccessful` accepted only a 404 and could not see a +// soft delete. A local e2e destroy then returns before the block is gone and the +// following building-block-*definition* delete fails with +// 409 "…because there are existing BuildingBlocks referencing it", stranding +// objects in the smoke-test workspace. +const PROVIDER_FLOOR = { + "meshcloud/meshstack": "0.23.0", +}; + +// Tiers a module owns whose provider constraints are subject to PROVIDER_FLOOR. +// `.` is the module root, i.e. `meshstack_integration.tf`. +const FLOOR_TIERS = [".", "backplane", "buildingblock", "e2e"]; + // ─── Category definitions ─────────────────────────────────────────────────── const CATEGORIES = { @@ -158,6 +192,53 @@ const detectors = [ }; }, }, + { + id: "provider_floor", + category: "core", + name: "Provider constraints meet the hub-wide version floor", + emoji: "⬆️", + fn: (mod) => { + // Every tier is in scope: the floor is what the hub as a whole is tested and + // used with, and each tier gets initialised somewhere — the integration by + // consumers, backplane + buildingblock by meshStack, and all three plus the + // e2e root by `task e2e:run` locally. + const entries = collectProviderEntries(mod, FLOOR_TIERS).filter( + (e) => e.source && PROVIDER_FLOOR[e.source] + ); + + if (entries.length === 0) { + return { pass: null, detail: "declares no floor-managed provider" }; + } + + const violations = []; + for (const e of entries) { + const floor = PROVIDER_FLOOR[e.source]; + if (e.constraint === null) { + violations.push(`${e.file}: ${e.name} has no version (need ">= ${floor}")`); + continue; + } + const lower = constraintLowerBound(e.constraint); + if (lower === null) { + violations.push( + `${e.file}: ${e.name} = "${e.constraint}" sets no lower bound (need ">= ${floor}")` + ); + continue; + } + if (compareVersions(lower, floor) < 0) { + violations.push(`${e.file}: ${e.name} = "${e.constraint}" is below ${floor}`); + } + } + + if (violations.length === 0) return { pass: true }; + + const shown = violations.slice(0, 4).join(", "); + const more = violations.length > 4 ? `, +${violations.length - 4} more` : ""; + return { + pass: false, + detail: `raise to the floor in tools/scorecard/scorecard.mjs (PROVIDER_FLOOR): ${shown}${more}`, + }; + }, + }, // ─── Integration ──────────────────────────────────────────────────────── { @@ -871,21 +952,28 @@ function stripHeredocs(content) { return content.replace(/<<-?\s*([A-Za-z_]\w*)\r?\n[\s\S]*?^\s*\1\s*$/gm, ""); } -// Every `version` attribute inside a `required_providers` block across BOTH tiers. -// -// Both tiers matter because they are consumed together: a hub e2e test module loads the -// backplane and the buildingblock into one configuration, so their constraints have to -// intersect on a version that exists. A `~>` or exact pin in either tier caps the whole -// configuration — that is how modules/meshstack/noop pinned the e2e suite to meshstack -// v0.21.0 from its backplane while its buildingblock declared only `>=`. +// The .tf files belonging to one tier. `.` is the module root — only its top-level +// files, so `meshstack_integration.tf` is covered without re-walking the other tiers. +function tfFilesForTier(mod, tier) { + if (tier !== ".") return collectTfFilesRecursive(join(mod.path, tier)); + return readdirSync(mod.path) + .filter((e) => e.endsWith(".tf")) + .map((e) => join(mod.path, e)) + .filter((p) => statSync(p).isFile()); +} + +// Every provider entry inside a `required_providers` block across the given tiers, +// as { file, tier, name, source, constraint } — `source`/`constraint` are null when the +// entry omits them. Entries are returned even without a version so callers can tell +// "declared with no constraint" (unbounded) apart from "provider not used at all". // -// Constraints are read from any .tf file, not just versions.tf: `provider.tf` is part of the +// Entries are read from any .tf file, not just versions.tf: `provider.tf` is part of the // documented module layout and legitimately carries required_providers. -function collectProviderConstraints(mod) { - const constraints = []; +function collectProviderEntries(mod, tiers) { + const entries = []; - for (const tier of ["backplane", "buildingblock"]) { - for (const file of collectTfFilesRecursive(join(mod.path, tier))) { + for (const tier of tiers) { + for (const file of tfFilesForTier(mod, tier)) { const content = stripHeredocs(readFileSync(file, "utf-8")); for (const m of content.matchAll(/required_providers\s*\{/g)) { @@ -897,18 +985,70 @@ function collectProviderConstraints(mod) { // Provider entries hold no nested braces, so a flat `name = { ... }` match is enough. for (const entry of body.matchAll(/([\w-]+)\s*=\s*\{([^{}]*)\}/g)) { const version = entry[2].match(/version\s*=\s*"([^"]+)"/); - if (!version) continue; - constraints.push({ + const source = entry[2].match(/source\s*=\s*"([^"]+)"/); + entries.push({ file: relative(mod.path, file), - provider: entry[1], - constraint: version[1], + tier, + name: entry[1], + source: source ? source[1] : null, + constraint: version ? version[1] : null, }); } } } } - return constraints; + return entries; +} + +// Every `version` attribute inside a `required_providers` block across BOTH tiers. +// +// Both tiers matter because they are consumed together: a hub e2e test module loads the +// backplane and the buildingblock into one configuration, so their constraints have to +// intersect on a version that exists. A `~>` or exact pin in either tier caps the whole +// configuration — that is how modules/meshstack/noop pinned the e2e suite to meshstack +// v0.21.0 from its backplane while its buildingblock declared only `>=`. +function collectProviderConstraints(mod) { + return collectProviderEntries(mod, ["backplane", "buildingblock"]).filter( + (e) => e.constraint !== null + ).map((e) => ({ file: e.file, provider: e.name, constraint: e.constraint })); +} + +// ─── Version constraint arithmetic ────────────────────────────────────────── + +function parseVersion(v) { + const parts = v.split(".").map((p) => parseInt(p, 10)); + return [parts[0] || 0, parts[1] || 0, parts[2] || 0]; +} + +function compareVersions(a, b) { + const pa = parseVersion(a); + const pb = parseVersion(b); + for (let i = 0; i < 3; i++) { + if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1; + } + return 0; +} + +// The lowest version a Terraform constraint string still admits, or null when it +// admits arbitrarily old versions (e.g. "< 1.0.0", or an empty/unparsable string). +// `>` is treated as its own version — deliberately conservative: `> 0.22.0` admits +// 0.22.1, which is still below a 0.23.0 floor, so it must not pass. +function constraintLowerBound(constraint) { + let lower = null; + + for (const raw of constraint.split(",")) { + const term = raw.trim(); + if (term === "") continue; + const m = term.match(/^(>=|>|~>|==|=|!=|<=|<)?\s*v?(\d+(?:\.\d+){0,2})/); + if (!m) continue; + const op = m[1] ?? "="; + if (op === "<" || op === "<=" || op === "!=") continue; + // >=, >, ~>, ==, = all put a floor at the stated version. + if (lower === null || compareVersions(m[2], lower) > 0) lower = m[2]; + } + + return lower; } function readAllBackplaneTf(mod) {