Creating a Policy to Warn When a Stack Is Not Using the Latest Version of a Spacelift-Hosted Module
Last updated: March 6, 2026
You can create an automated policy in Spacelift that warns whenever an OpenTofu/Terraform stack is not using the latest available version of a module.
This solution works by combining:
An
after_planhook that queries the Spacelift module registry to determine the latest version of each module used in the configuration.A Plan Policy that compares the versions defined in your OpenTofu/Terraform configuration against the latest available versions.
If a module is outdated, the policy generates a warning during the plan phase.
Step 1: Create a Script to Query the Latest Module Versions
Create a script that reads the OpenTofu/Terraform configuration metadata produced by Spacelift and queries the module registry to determine the latest available version of each module.
It is recommended to place this script in a Spacelift context as a mounted file so it can be reused across multiple stacks.
Save the following script as:
query-module-versions.sh#!/bin/sh
set -eu
CONFIG_JSON="configuration.custom.spacelift.json"
OUT_JSON="module_versions.custom.spacelift.json"
PLAN_FILE="spacelift.plan"
# Spacelift module registry API (Terraform registry protocol)
MODULE_API_BASE="https://app.spacelift.io/registry/modules/v1"
# Optional auth header
AUTH_HEADER=""
if [ -n "${SPACELIFT_API_TOKEN:-}" ]; then
AUTH_HEADER="Authorization: Bearer ${SPACELIFT_API_TOKEN}"
fi
# Export configuration from the generated plan
if command -v tofu >/dev/null 2>&1; then
tofu show -json "$PLAN_FILE" | jq -c '.configuration' > "$CONFIG_JSON"
elif command -v terraform >/dev/null 2>&1; then
terraform show -json "$PLAN_FILE" | jq -c '.configuration' > "$CONFIG_JSON"
else
echo "ERROR: neither tofu nor terraform is installed" >&2
exit 1
fi
echo '{}' > "$OUT_JSON"
# Collect unique module sources from the root module
jq -r '.root_module.module_calls[]?.source // empty' "$CONFIG_JSON" | sort -u | while read -r SRC; do
# Expect registry-style sources like:
# spacelift.io/org/name/provider
NS=$(echo "$SRC" | cut -d'/' -f2)
NAME=$(echo "$SRC" | cut -d'/' -f3)
PROVIDER=$(echo "$SRC" | cut -d'/' -f4)
# Skip anything that doesn't look like a registry source
if [ -z "$NS" ] || [ -z "$NAME" ] || [ -z "$PROVIDER" ]; then
echo "Skipping non-registry module source: $SRC" >&2
continue
fi
URL="${MODULE_API_BASE}/${NS}/${NAME}/${PROVIDER}/versions"
echo "Querying latest version for ${SRC} -> ${URL}" >&2
if [ -n "$AUTH_HEADER" ]; then
RESP=$(curl -sS -H "$AUTH_HEADER" "$URL" || true)
else
RESP=$(curl -sS "$URL" || true)
fi
# Pick the highest semantic version as "latest"
LATEST=$(echo "$RESP" \
| jq -r '.modules[0].versions[].version' 2>/dev/null \
| sort -Vr \
| head -n1)
if [ -z "$LATEST" ] || [ "$LATEST" = "null" ]; then
echo "WARN: could not determine latest version for ${SRC}; response: $RESP" >&2
continue
fi
echo "Latest version for ${SRC} is ${LATEST}" >&2
TMP="$(mktemp)"
jq --arg src "$SRC" --arg ver "$LATEST" '. + {($src): $ver}' "$OUT_JSON" > "$TMP"
mv "$TMP" "$OUT_JSON"
doneThis script:
Extracts module sources from the OpenTofu/Terraform configuration
Queries the Spacelift module registry
Determines the latest version available for each module
Writes the results to a JSON file that becomes available to policies
Step 2: Configure API Authentication
The script needs access to the Spacelift Module Registry API.
Create a Spacelift API key, then add it as an environment variable in the context.
Environment variable:
Name: SPACELIFT_API_TOKEN
Value: <your API token>Step 3: Add the after_plan Hook
Add the script as an after_plan hook so it runs after OpenTofu/Terraform generates the plan.
Hook command:
/mnt/workspace/query-module-versions.shRunning this hook produces a metadata file named:
module_versions.custom.spacelift.jsonThis file is automatically exposed to policies under:
input.third_party_metadata.custom.module_versionsStep 4: Create the Plan Policy
Create a new Plan Policy in Spacelift using the following Rego code.
This policy compares the module versions used in your configuration against the latest versions retrieved by the script.
package spacelift
latest_module_versions := input.third_party_metadata.custom.module_versions
tracked_module_calls contains module if {
some name
module := input.third_party_metadata.custom.configuration.root_module.module_calls[name]
required_version := latest_module_versions[module.source]
required_version != ""
}
# Warn if no version is specified
warn contains reason if {
module := tracked_module_calls[_]
not module.version_constraint
reason := sprintf(
"Module source '%s' must pin a version, but no version was found in the module block.",
[module.source],
)
}
warn contains reason if {
module := tracked_module_calls[_]
module.version_constraint == ""
reason := sprintf(
"Module source '%s' must pin a version, but version_constraint is empty.",
[module.source],
)
}
# Warn if the version constraint is not valid SemVer
warn contains reason if {
module := tracked_module_calls[_]
required_version := latest_module_versions[module.source]
current_version := module.version_constraint
not semver.is_valid(current_version)
reason := sprintf(
"Module source '%s' has non-SemVer version constraint '%s'. Latest available version is %s.",
[module.source, current_version, required_version],
)
}
# Warn if the module version is older than the latest available version
warn contains reason if {
module := tracked_module_calls[_]
required_version := latest_module_versions[module.source]
current_version := module.version_constraint
semver.is_valid(required_version)
semver.is_valid(current_version)
semver.compare(current_version, required_version) < 0
reason := sprintf(
"Module source '%s' should use version '%s' or newer, but version '%s' was found.",
[module.source, required_version, current_version],
)
}Step 5: Attach the Context and Policy to Your Stack
Attach both the context and the plan policy to your stacks.
A common approach is to use an autoattach label so that:
Any stack with the matching label automatically receives the context
The policy is automatically applied
This allows you to enforce module version checks consistently across many stacks.
The Result

How the Solution Works
When a stack run executes:
OpenTofu/Terraform generates the plan.
The after_plan hook runs the script.
The script queries the Spacelift module registry and determines the latest versions of all modules used in the configuration.
The script outputs the results as metadata.
The Plan Policy reads this metadata and compares it with the versions used in the OpenTofu/Terraform configuration.
If a module version is outdated, a warning appears in the run output, for example:
Module source 'spacelift.io/org/module/provider' should use version '1.2.3' or newer, but version '1.2.0' was found.Optional: Enforcing Instead of Warning
This policy currently generates warnings only, which means runs will still proceed.
If you want to block deployments using outdated modules, replace:
warn[reason]with
deny[reason]in the policy rules.
This will cause the run to fail whenever an outdated module version is detected.