-
-
Notifications
You must be signed in to change notification settings - Fork 583
feat: Add terraform_provider_version_consistency hook #960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -67,6 +67,7 @@ If you want to support the development of `pre-commit-terraform` and [many other | |||||||||||||
| * [terraform\_tfsec (deprecated)](#terraform_tfsec-deprecated) | ||||||||||||||
| * [terraform\_trivy](#terraform_trivy) | ||||||||||||||
| * [terraform\_validate](#terraform_validate) | ||||||||||||||
| * [terraform\_provider\_version\_consistency](#terraform_provider_version_consistency) | ||||||||||||||
| * [terraform\_wrapper\_module\_for\_each](#terraform_wrapper_module_for_each) | ||||||||||||||
| * [terrascan](#terrascan) | ||||||||||||||
| * [tfupdate](#tfupdate) | ||||||||||||||
|
|
@@ -337,6 +338,7 @@ There are several [pre-commit](https://pre-commit.com/) hooks to keep Terraform | |||||||||||||
| | `terraform_tfsec` | [TFSec][tfsec repo] static analysis of terraform templates to spot potential security issues. **DEPRECATED**, use `terraform_trivy`. [Hook notes](#terraform_tfsec-deprecated) | `tfsec` | | ||||||||||||||
| | `terraform_trivy` | [Trivy][trivy repo] static analysis of terraform templates to spot potential security issues. [Hook notes](#terraform_trivy) | `trivy` | | ||||||||||||||
| | `terraform_validate` | Validates all Terraform configuration files. [Hook notes](#terraform_validate) | `jq`, only for `--retry-once-with-cleanup` flag | | ||||||||||||||
| | `terraform_provider_version_consistency` | Checks that provider version constraints are consistent across all `versions.tf` files. [Hook notes](#terraform_provider_version_consistency) | - | | ||||||||||||||
| | `terragrunt_fmt` | Reformat all [Terragrunt][terragrunt repo] configuration files (`*.hcl`) to a canonical format. | `terragrunt` | | ||||||||||||||
| | `terragrunt_validate` | Validates all [Terragrunt][terragrunt repo] configuration files (`*.hcl`) | `terragrunt` | | ||||||||||||||
| | `terragrunt_validate_inputs` | Validates [Terragrunt][terragrunt repo] unused and undefined inputs (`*.hcl`) | | | ||||||||||||||
|
|
@@ -1085,9 +1087,53 @@ To replicate functionality in `terraform_docs` hook: | |||||||||||||
| - repo: https://github.com/pre-commit/pre-commit-hooks | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| > **Tip** | ||||||||||||||
| > **Tip** | ||||||||||||||
| > The latter method will leave an "aliased-providers.tf.json" file in your repo. You will either want to automate a way to clean this up or add it to your `.gitignore` or both. | ||||||||||||||
|
|
||||||||||||||
| ### terraform_provider_version_consistency | ||||||||||||||
|
|
||||||||||||||
| `terraform_provider_version_consistency` checks that provider version constraints are consistent across all `versions.tf` files in the repository. This is useful for multi-module repositories where each module has its own `versions.tf` file and you want to ensure they all use the same provider versions. | ||||||||||||||
|
|
||||||||||||||
| The hook: | ||||||||||||||
|
|
||||||||||||||
| - Finds all `versions.tf` files (excluding `.terraform/` directories) | ||||||||||||||
| - Extracts provider version constraints (`version = "..."` lines) | ||||||||||||||
| - Fails if different version constraints are found across files | ||||||||||||||
| - Reports which files have inconsistent versions | ||||||||||||||
|
|
||||||||||||||
| Example configuration: | ||||||||||||||
|
|
||||||||||||||
| ```yaml | ||||||||||||||
| - id: terraform_provider_version_consistency | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| Example output when versions are inconsistent: | ||||||||||||||
|
|
||||||||||||||
| ```text | ||||||||||||||
| Inconsistent provider versions found across 6 files: | ||||||||||||||
|
|
||||||||||||||
| --- ./examples/complete/versions.tf | ||||||||||||||
| version = ">= 6.31" | ||||||||||||||
| --- ./versions.tf | ||||||||||||||
| version = "= 6.30" | ||||||||||||||
|
|
||||||||||||||
| Found 2 different version constraints: | ||||||||||||||
| version = "= 6.30" | ||||||||||||||
| version = ">= 6.31" | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| You can customize the hook behavior using `--hook-config`: | ||||||||||||||
|
|
||||||||||||||
| 1. `--version-file-pattern=...` - Pattern for version files (default: `versions.tf`) | ||||||||||||||
| 2. `--exclude-pattern=...` - Pattern to exclude from search (default: `.terraform/`) | ||||||||||||||
|
|
||||||||||||||
| ```yaml | ||||||||||||||
| - id: terraform_provider_version_consistency | ||||||||||||||
| args: | ||||||||||||||
| - --hook-config=--version-file-pattern=versions.tf | ||||||||||||||
| - --hook-config=--exclude-pattern=.terraform/ | ||||||||||||||
|
Comment on lines
+1133
to
+1134
|
||||||||||||||
| - --hook-config=--version-file-pattern=versions.tf | |
| - --hook-config=--exclude-pattern=.terraform/ | |
| # Look for provider versions in providers.tf instead of the default versions.tf | |
| - --hook-config=--version-file-pattern=providers.tf | |
| # Exclude both the .terraform directory and a custom generated/ directory | |
| - --hook-config=--exclude-pattern=.terraform/,generated/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll rewrite this part of the readme to be better tailored for readers
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,107 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -eo pipefail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # globals variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly SCRIPT_DIR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # shellcheck source=_common.sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| . "$SCRIPT_DIR/_common.sh" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function main { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| common::initialize "$SCRIPT_DIR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| common::parse_cmdline "$@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| common::export_provided_env_vars "${ENV_VARS[@]}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+13
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Default patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| local version_file_pattern="versions.tf" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| local exclude_pattern='.terraform/' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Parse hook-specific config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IFS=";" read -r -a configs <<< "${HOOK_CONFIG[*]}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for c in "${configs[@]}"; do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IFS="=" read -r -a config <<< "$c" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key="${config[0]## }" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value=${config[1]} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key="${config[0]## }" | |
| value=${config[1]} | |
| local key="${config[0]## }" | |
| local value=${config[1]} |
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the issue in the loop below, the unquoted variable expansion in grep can break if file paths contain spaces or special characters. While the shellcheck disable indicates this is intentional, it creates a fragile implementation. A safer approach would be to use an array and proper quoting, or use a while loop with null-delimited input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern '^\s*version\s*=' matches any line that starts with whitespace followed by "version =", which could match version constraints in different contexts within a versions.tf file (e.g., terraform version requirements, provider version constraints, and potentially module version constraints). This lack of specificity could lead to false positives when comparing version constraints that are actually in different contexts (terraform vs. provider versions). Consider making the pattern more specific to only match provider version constraints within provider blocks, or document this limitation clearly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the shellcheck disable comment indicates word splitting is intentional, this approach can break if any file paths contain spaces or special characters. The current implementation relies on newline-separated output from find, but doesn't properly handle edge cases where filenames might contain spaces. Consider using a safer approach with arrays or null-delimited output (find with -print0 and read with -d ''). This is particularly important since the hook operates on user-provided file patterns and exclusion patterns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix grep matching + errexit handling + filename splitting (current logic can miss constraints or exit early).
- Line 72 uses
\swithgrep -E, which won’t match indentedversion =lines. - With
set -e+pipefail, a no‑matchgrepexits the script before the “no constraints” branch. - Word-splitting
$version_filesbreaks paths with spaces.
✅ Suggested fix (Bash 3.2‑compatible)
- local version_files
- version_files=$(find . -name "$version_file_pattern" -type f ! -path "*${exclude_pattern}*" 2>/dev/null | sort)
+ local -a version_files=()
+ while IFS= read -r file; do
+ version_files+=("$file")
+ done < <(find . -name "$version_file_pattern" -type f ! -path "*${exclude_pattern}*" 2>/dev/null | sort)
- if [[ -z "$version_files" ]]; then
+ if ((${`#version_files`[@]} == 0)); then
common::colorify "yellow" "No $version_file_pattern files found"
return 0
fi
local file_count
- file_count=$(echo "$version_files" | wc -l | tr -d ' ')
+ file_count=${`#version_files`[@]}
# Extract all unique provider version constraint lines
local all_versions
- # shellcheck disable=SC2086 # Word splitting is intentional
- all_versions=$(grep -hE '^\s*version\s*=' $version_files 2>/dev/null | \
- sed 's/^[[:space:]]*//' | sort -u)
+ all_versions=$(
+ { grep -hE '^[[:space:]]*version[[:space:]]*=' "${version_files[@]}" 2>/dev/null || true; } \
+ | sed 's/^[[:space:]]*//' | sort -u
+ )
# Handle case where no provider version constraints found
if [[ -z "$all_versions" ]]; then
common::colorify "yellow" "No provider version constraints found in $version_file_pattern files"
return 0
fi
local file
- # shellcheck disable=SC2086 # Word splitting is intentional
- for file in $version_files; do
+ for file in "${version_files[@]}"; do
echo "--- $file"
- grep -E '^\s*version\s*=' "$file" 2>/dev/null | sed 's/^/ /'
+ { grep -E '^[[:space:]]*version[[:space:]]*=' "$file" 2>/dev/null || true; } | sed 's/^/ /'
done📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Find all version files, excluding specified pattern | |
| local version_files | |
| version_files=$(find . -name "$version_file_pattern" -type f ! -path "*${exclude_pattern}*" 2>/dev/null | sort) | |
| if [[ -z "$version_files" ]]; then | |
| common::colorify "yellow" "No $version_file_pattern files found" | |
| return 0 | |
| fi | |
| local file_count | |
| file_count=$(echo "$version_files" | wc -l | tr -d ' ') | |
| if [[ "$file_count" -eq 1 ]]; then | |
| common::colorify "green" "Only one $version_file_pattern file found, skipping consistency check" | |
| return 0 | |
| fi | |
| # Extract all unique provider version constraint lines | |
| local all_versions | |
| # shellcheck disable=SC2086 # Word splitting is intentional | |
| all_versions=$(grep -hE '^\s*version\s*=' $version_files 2>/dev/null | \ | |
| sed 's/^[[:space:]]*//' | sort -u) | |
| # Handle case where no provider version constraints found | |
| if [[ -z "$all_versions" ]]; then | |
| common::colorify "yellow" "No provider version constraints found in $version_file_pattern files" | |
| return 0 | |
| fi | |
| local unique_count | |
| unique_count=$(echo "$all_versions" | wc -l | tr -d ' ') | |
| if [[ "$unique_count" -eq 1 ]]; then | |
| common::colorify "green" "All provider versions are consistent across $file_count files" | |
| return 0 | |
| fi | |
| # Versions are inconsistent - report details | |
| common::colorify "red" "Inconsistent provider versions found across $file_count files:" | |
| echo "" | |
| local file | |
| # shellcheck disable=SC2086 # Word splitting is intentional | |
| for file in $version_files; do | |
| echo "--- $file" | |
| grep -E '^\s*version\s*=' "$file" 2>/dev/null | sed 's/^/ /' | |
| # Find all version files, excluding specified pattern | |
| local -a version_files=() | |
| while IFS= read -r file; do | |
| version_files+=("$file") | |
| done < <(find . -name "$version_file_pattern" -type f ! -path "*${exclude_pattern}*" 2>/dev/null | sort) | |
| if ((${`#version_files`[@]} == 0)); then | |
| common::colorify "yellow" "No $version_file_pattern files found" | |
| return 0 | |
| fi | |
| local file_count | |
| file_count=${`#version_files`[@]} | |
| if [[ "$file_count" -eq 1 ]]; then | |
| common::colorify "green" "Only one $version_file_pattern file found, skipping consistency check" | |
| return 0 | |
| fi | |
| # Extract all unique provider version constraint lines | |
| local all_versions | |
| all_versions=$( | |
| { grep -hE '^[[:space:]]*version[[:space:]]*=' "${version_files[@]}" 2>/dev/null || true; } \ | |
| | sed 's/^[[:space:]]*//' | sort -u | |
| ) | |
| # Handle case where no provider version constraints found | |
| if [[ -z "$all_versions" ]]; then | |
| common::colorify "yellow" "No provider version constraints found in $version_file_pattern files" | |
| return 0 | |
| fi | |
| local unique_count | |
| unique_count=$(echo "$all_versions" | wc -l | tr -d ' ') | |
| if [[ "$unique_count" -eq 1 ]]; then | |
| common::colorify "green" "All provider versions are consistent across $file_count files" | |
| return 0 | |
| fi | |
| # Versions are inconsistent - report details | |
| common::colorify "red" "Inconsistent provider versions found across $file_count files:" | |
| echo "" | |
| local file | |
| for file in "${version_files[@]}"; do | |
| echo "--- $file" | |
| { grep -E '^[[:space:]]*version[[:space:]]*=' "$file" 2>/dev/null || true; } | sed 's/^/ /' | |
| done |
🤖 Prompt for AI Agents
In `@hooks/terraform_provider_version_consistency.sh` around lines 52 - 97, The
grep patterns and looping can fail: change grep -E '^\s*version\s*=' to grep -hE
'^[[:space:]]*version\s*=' so indentation is matched, ensure grep failures don't
trigger set -e by appending "|| true" to the grep pipeline when building
all_versions (e.g. all_versions=$(grep -hE '^[[:space:]]*version\s*='
$version_files 2>/dev/null || true | sed ...)), and stop word-splitting of
version_files by using find -print0 and reading with while IFS= read -r -d ''
file (or use an array) when iterating files (replace the for file in
$version_files loop with a null-delimited read loop) so paths with spaces are
handled correctly; keep the existing sed/sort -u logic and shellcheck disables
as appropriate (referenced symbols: version_files, all_versions, unique_count,
the grep invocations and the for file loop).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why you cant use https://github.com/antonbabenko/pre-commit-terraform#tfupdate ?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@peternied Any update on this please?
The quality level of this AI slop is a bit disputable and requires some work and effort to make it decent and acceptable. Whereas if you threw this at us to take care further, it doesn't look good, you know.
If you think
tfupdatehook doesn't meet your needs (and you can reason that) and you feel you'd need a hand getting the code in this PR improved, we're here to help. Thanks.