This is a quick reference guide for AI agents working with tf-version-bump. For comprehensive development documentation, see CLAUDE.md.
A CLI tool written in Go that updates Terraform module versions, Terraform required_version, and provider versions across multiple files using glob patterns.
Key tech: Go 1.25+, HashiCorp HCL library, YAML configs
# 1. Install dependencies
go mod download && go mod verify
# 2. Build
go build -o tf-version-bump
# 3. Test
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
# 4. Lint
golangci-lint run --timeout=5mtf-version-bump/
├── main.go # Core logic
├── config.go # YAML config handling
├── audit.go # -audit-file comparison
├── *_test.go # Comprehensive tests (Go tests)
├── schema/config-schema.json # YAML validation schema
├── examples/ # Sample configs, .tf files, scenarios, and branch and Actions automation
├── scripts/ # Lint launchers and the Actions example's release-pin updater
├── .github/workflows/ # CI/CD pipelines
└── docs/ # Usage, configuration, automation, and release guides
| File | Purpose |
|---|---|
main.go |
CLI parsing, HCL processing, version updates |
config.go |
YAML config loading and validation |
audit.go |
Read-only -audit-file comparison of files with a config |
Focused *_test.go files |
Contract owners by responsibility; see the detailed test layout in CLAUDE.md |
config_test.go |
Config validation tests |
schema/config-schema.json |
JSON Schema for YAML |
go build -o tf-version-bump # Local build
GOOS=linux GOARCH=amd64 go build -o tf-version-bump # Cross-compilego test -v ./... # Quick test
go test -v -race -coverprofile=coverage.out -covermode=atomic ./... # Full CI test
make test-coverage # With coverage reportgolangci-lint run --timeout=5m # Run linter
golangci-lint run --fix # Auto-fix issues
make actionlint # Workflows, including the Actions example
make shellcheck # Every tracked shell script# Single module update
./tf-version-bump -pattern "*.tf" -module "terraform-aws-modules/vpc/aws" -to "5.0.0"
# Batch update with config ('**' recurses to any depth; .terraform and .git are skipped)
./tf-version-bump -pattern "**/*.tf" -config examples/config-basic.yml
# Dry run
./tf-version-bump -pattern "*.tf" -module "..." -to "..." -dry-run
# CI check: 0 with no eligible update, 2 when updates are required, 1 on error
./tf-version-bump -pattern "**/*.tf" -config examples/config-basic.yml -check
# Update Terraform version
./tf-version-bump -pattern "*.tf" -terraform-version ">= 1.5"
# Update provider version
./tf-version-bump -pattern "*.tf" -provider aws -to "~> 5.0"main.go reads and writes every Terraform file through readTerraformFile and terraformFile.write; reuse them for any new update path.
// 1. Stat, read and parse; a file a failed write left untrusted is refused
file, err := readTerraformFile(filename)
// 2. Navigate and modify
for _, block := range file.hcl.Body().Blocks() {
if block.Type() == "module" {
block.Body().SetAttributeValue("version", cty.StringVal(targetVersion))
}
}
// 3. Format and rewrite in place; a failed rewrite writes the original bytes back
err = file.write()// Strict YAML parsing with validation
config, err := loadConfig(filename)
// Custom FromVersions type accepts string or []string
// Whitespace trimming and validation- Package: Single
mainpackage (flat structure) - Style: Standard Go formatting (
go fmt) - Errors: Wrapped with
fmt.Errorf("context: %w", err) - Tests: Table-driven tests with
t.Run()subtests - Temp files: Always use
t.TempDir()in tests
// Standard test structure
func TestExample(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.tf")
// Setup
initial := `module "vpc" { source = "aws/vpc" version = "1.0.0" }`
os.WriteFile(testFile, []byte(initial), 0644)
// Execute (fromVersions, ignoreVersions, ignorePatterns, forceAdd, dryRun, verbose, outputFormat)
updated, err := updateModuleVersion(testFile, "aws/vpc", "2.0.0", nil, nil, nil, false, false, false, "text")
// Assert
if err != nil {
t.Fatalf("updateModuleVersion failed: %v", err)
}
if !updated {
t.Error("Expected module to be updated")
}
// Verify
result, _ := os.ReadFile(testFile)
if !strings.Contains(string(result), `version = "2.0.0"`) {
t.Errorf("Version not updated correctly, got: %s", result)
}
}- Update
main.goorconfig.go - Add CLI flag in
parseFlags()if needed - Add config field to
Configstruct if needed - Update
schema/config-schema.jsonfor YAML validation - Add tests in
*_test.go - Update the appropriate user guide under
docs/and keep the README quick start concise - Run full validation:
make test-coverage && golangci-lint run && make actionlint && make shellcheck
go test -v -run TestSpecific # Run specific test
go test -coverprofile=coverage.out # Generate coverage
go tool cover -html=coverage.out # View coverage HTML
make test-coverage # Coverage with reportGitHub Actions runs on every push/PR:
- Test: matrix of Go 1.25.14 (go.mod floor) and 1.26.8, race detection, coverage upload
- Build: 6 platforms (Linux/macOS/Windows × amd64/arm64)
- Lint: golangci-lint, then
make actionlintandmake shellcheckwith pinned shellcheck - Documentation:
make docs-checkfor Markdown, schema, maintained example, and documentation-test changes
Priority order across updateModuleBlockResult and shouldSkipModuleVersion in main.go:
- Source and locality: Require an exact source match; skip local sources
- Ignore patterns: If module name matches
ignore_modules, skip; an entry may be branch-scoped as<branch>/<name>and resolved against-branchbeforehand - Missing version: Skip unless
force-addis enabled and the source is a registry module - Ignore versions: If current version is in
ignore_versions, skip (takes precedence) - From filter: If
fromis set and current version doesn't match, skip - Update: If all checks pass, update the version
-
✓ Always read files before modifying - Use Read tool first
-
✓ Run tests with race detector -
go test -race -
✓ Use dry-run - Preview changes before applying
-
✓ Check coverage -
make test-coverage -
✓ Follow existing patterns - Match the codebase style
-
✓ Update docs - Keep the README, detailed guides, and agent documentation in sync
-
✓ Keep Markdown unwrapped - One line per paragraph;
make docs-checkenforces it -
✗ Never break HCL format - Use
hclwriteAPI only -
✗ Don't skip linting - CI will fail
-
✗ Don't change public API - CLI flags are user-facing
-
✗ Don't commit binaries - They're gitignored
Direct dependencies — see go.mod for versions:
github.com/hashicorp/hcl/v2 # Official HCL parser
github.com/hashicorp/terraform-registry-address # Registry module source parser
github.com/zclconf/go-cty # HCL type system
go.yaml.in/yaml/v3 # YAML parsing
github.com/bmatcuk/doublestar/v4 # Recursive '**' globbing
- Comprehensive guide: CLAUDE.md
- User documentation: README.md
- Usage reference: docs/USAGE.md
- Configuration reference: docs/CONFIGURATION.md
- Branch automation: docs/ADVANCED-USAGE.md
- Release process: docs/RELEASING.md
- Examples:
examples/directory - HCL docs: https://github.com/hashicorp/hcl
- Version constraints: https://developer.hashicorp.com/terraform/language/expressions/version-constraints
Need more details? See CLAUDE.md for comprehensive development documentation.