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) {