Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,15 @@
- --args=terraform
files: \.(tf|tofu)$
require_serial: true

- id: terraform_provider_version_consistency
name: Terraform provider version consistency
Copy link
Collaborator

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 ?

Copy link
Collaborator

@yermulnik yermulnik Feb 9, 2026

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 tfupdate hook 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.

description: >-
Checks that provider version constraints are consistent across all
versions.tf files in the repository.
entry: hooks/terraform_provider_version_consistency.sh
language: script
pass_filenames: false
files: versions\.tf$
exclude: \.terraform/.*$
require_serial: true
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`) | |
Expand Down Expand Up @@ -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
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example configuration shows setting the hook config parameters to their default values (versions.tf and .terraform/), which is redundant and could be confusing for users. This example doesn't demonstrate the utility of the configuration options. Consider either removing this example entirely (since the basic example above already shows the default behavior), or showing a more meaningful example with non-default values to illustrate when customization would be useful.

Suggested change
- --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/

Copilot uses AI. Check for mistakes.
Copy link
Author

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

```

### terraform_wrapper_module_for_each

`terraform_wrapper_module_for_each` generates module wrappers for Terraform modules (useful for Terragrunt where `for_each` is not supported). When using this hook without arguments it will create wrappers for the root module and all modules available in "modules" directory.
Expand Down
111 changes: 111 additions & 0 deletions hooks/terraform_provider_version_consistency.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/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
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing call to common::parse_and_export_env_vars that is present in all other hooks in the codebase. This function is responsible for expanding environment variables in the ARGS array. Without this call, users cannot use environment variable substitution in hook arguments, which is a feature documented in the README and consistently implemented across all other hooks (see terraform_docs.sh:22, terraform_validate.sh:17, terraform_wrapper_module_for_each.sh:14, etc.). Add this call after common::export_provided_env_vars for consistency.

Copilot uses AI. Check for mistakes.
Copy link
Author

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


# 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]## }"
local value=${config[1]}

case $key in
--version-file-pattern)
if [[ -n "$value" ]]; then
version_file_pattern="$value"
fi
;;
--exclude-pattern)
if [[ -n "$value" ]]; then
exclude_pattern="$value"
fi
;;
esac
done

check_provider_version_consistency "$version_file_pattern" "$exclude_pattern"
}

#######################################################################
# Check that provider version constraints are consistent across all
# versions.tf files in the repository
# Arguments:
# version_file_pattern (string) Pattern for version files (default: versions.tf)
# exclude_pattern (string) Pattern to exclude (default: .terraform/)
# Outputs:
# If inconsistent - print out which files have different versions
#######################################################################
function check_provider_version_consistency {
local -r version_file_pattern="$1"
local -r exclude_pattern="$2"

# 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 | \
Comment on lines +75 to +76
Copy link

Copilot AI Feb 5, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

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

sed 's/^[[:space:]]*//' | sort -u)
Comment on lines +76 to +77
Copy link

Copilot AI Feb 5, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

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


# 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
Comment on lines +98 to +99
Copy link

Copilot AI Feb 5, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

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

echo "--- $file"
grep -E '^\s*version\s*=' "$file" 2>/dev/null | sed 's/^/ /'
Comment on lines +56 to +101
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix grep matching + errexit handling + filename splitting (current logic can miss constraints or exit early).

  • Line 72 uses \s with grep -E, which won’t match indented version = lines.
  • With set -e + pipefail, a no‑match grep exits the script before the “no constraints” branch.
  • Word-splitting $version_files breaks 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.

Suggested change
# 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).

done

echo ""
common::colorify "yellow" "Found $unique_count different version constraints:"
echo "$all_versions" | sed 's/^/ /'

return 1
}

[ "${BASH_SOURCE[0]}" != "$0" ] || main "$@"