Upgrade Guide¶
How to upgrade TerraTidy between versions.
Checking Your Version¶
terratidy version
terratidy version --short # Just the version number
terratidy version --format json # Machine-readable
Upgrading¶
Go Install¶
Homebrew¶
Docker¶
Pre-commit¶
Update the rev in .pre-commit-config.yaml:
repos:
- repo: https://github.com/santosr2/TerraTidy
rev: v0.2.0-alpha.4 # Update this
hooks:
- id: terratidy-check
Or auto-update:
Version Compatibility¶
Config Version¶
TerraTidy currently supports version: 1 in configuration files. Future major versions may introduce a new config format.
Go Version¶
TerraTidy requires Go 1.26.3 or later for building from source and compiling Go plugins. Go plugins (.so files) must be compiled with the same Go version as TerraTidy.
OPA Version¶
The policy engine uses OPA v1.15.0 with Rego v1 syntax. Policies must use import rego.v1 and the contains/if keywords.
Breaking Changes¶
v0.2.0-alpha.5: Distinct Exit Codes¶
Exit codes now distinguish between different error types:
| Code | Before | After |
|---|---|---|
0 | Success | Success |
1 | All errors | Findings found only |
2 | N/A | Configuration errors |
3 | N/A | Internal errors |
Migration: If your CI/CD scripts check for non-zero exit codes, update them to handle the new codes appropriately:
# Before: "if exit != 0 then fail"
# After: distinguish error types
terratidy check
case $? in
0) echo "Pass" ;;
1) echo "Findings found - fail the build" ;;
2) echo "Config error - fail the build" ;;
3) echo "Internal error - fail the build" ;;
esac
Most scripts that just check for non-zero will still work correctly.
v0.2.0-alpha.5: CLI Flag Shorthand Reassignments¶
Short flags have been reassigned to more commonly used global flags:
| Short | Before | After |
|---|---|---|
-p | check --parallel | --profile (global) |
-f | init --force | --format (global) |
-c | N/A | --config (global) |
Migration: Update any scripts using the old shorthands:
# Before
terratidy check -p # Meant --parallel
terratidy init -f # Meant --force
# After
terratidy check --parallel # Use long form
terratidy init --force # Use long form
v0.2.0-alpha.5: Version Command JSON Output¶
The version --json flag has been replaced with version --format json:
JSON field names changed to snake_case for consistency:
| Before | After |
|---|---|
goVersion | go_version |
v0.2.0-alpha.5: SDK Finding.Fix Replaced with Fixable Flag¶
Applies to authors of Go SDK plugins (pkg/sdk). The Finding.Fix *FixResult field and the FixResult struct have been removed. Check() no longer precomputes fix content; the style engine calls Fixer.Fix() lazily, only when running in fix or diff mode.
| Before | After |
|---|---|
Finding.Fix *FixResult | Finding.Fixable bool |
FixResult.Content []byte | Removed — call Fixer.Fix() to obtain content |
FixResult.Diff string | Use Finding.Message + Finding.IsDiff bool |
Migration for SDK plugin authors:
Note: This signature is accurate as of v0.2.0-alpha.5. It was superseded by the byte-range edits change documented below — see the next upgrade section. The migration story shown here remains historically faithful; do not copy this signature for new code.
// Before: Check() returned a Finding with precomputed fix content.
func (r *MyRule) Check(ctx *sdk.Context, file *hcl.File) ([]sdk.Finding, error) {
fixed := applyFix(file)
return []sdk.Finding{{
Rule: "my-rule",
Message: "issue found",
Fix: &sdk.FixResult{Content: fixed},
}}, nil
}
// After: Check() reports findings only; Fix() produces the corrected bytes.
func (r *MyRule) Check(ctx *sdk.Context, file *hcl.File) ([]sdk.Finding, error) {
return []sdk.Finding{{
Rule: "my-rule",
Message: "issue found",
}}, nil
}
func (r *MyRule) Fix(ctx *sdk.Context, file *hcl.File) ([]byte, error) {
return applyFix(file), nil // your existing fix logic, returning rewritten bytes
}
The engine sets Finding.Fixable = true automatically for any rule that implements sdk.Fixer. The field is read-only from a plugin perspective; the engine populates it.
Migration for finding consumers (formatters, IDE integrations):
// Before: nil-check on the pointer field.
for _, f := range findings {
if f.Fix != nil {
renderFixable(f, f.Fix.Diff)
} else {
renderPlain(f)
}
}
// After: read the boolean flag; if the message carries a diff, route it
// through a diff renderer.
for _, f := range findings {
switch {
case f.IsDiff:
renderDiff(f, f.Message)
case f.Fixable:
renderFixable(f)
default:
renderPlain(f)
}
}
v0.2.0-alpha.5: SDK Fixer.Fix Returns *FixResult Instead of []byte¶
Applies to authors of Go SDK plugins (pkg/sdk). The Fixer.Fix method now returns a *FixResult carrying one or more byte-range TextEdits, replacing the previous "rewrite the whole file as []byte" contract. This lets the engine collect edits from every fixable finding in a file and apply them in a single write, instead of looping one rule at a time.
YAML and Bash rule authors are not affected — those rule types don't write Go and their plugin stubs remain non-fixing.
| Before | After |
|---|---|
Fix(ctx, file) ([]byte, error) | Fix(ctx, file) (*FixResult, error) |
Return nil for no-op | Return nil or &FixResult{} with no edits |
| Engine rewrites the whole file each pass | Engine splices byte ranges in descending Start order |
FixResult name reuse
The FixResult type name was previously used in the SDK (pre-v0.2.0-alpha.5) for an unrelated type ({Content []byte; Diff string}) that has since been removed — see v0.2.0-alpha.5: SDK Finding.Fix Replaced with Fixable Flag. The new FixResult introduced here has a different shape ({Edits []TextEdit}) and is unrelated to the old type. A reader walking the upgrade history chronologically sees the removal first, then the reintroduction.
Migration for SDK plugin authors:
The fastest migration is to wrap your existing whole-file output as a single TextEdit spanning the full input range. This preserves your rule's existing algorithm; the engine handles the byte-range splice for you.
import (
"bytes"
"github.com/hashicorp/hcl/v2"
"github.com/santosr2/TerraTidy/pkg/sdk"
)
// Before: return the rewritten file as []byte.
func (r *MyRule) Fix(ctx *sdk.Context, file *hcl.File) ([]byte, error) {
return applyFix(file), nil // your existing fix logic, returning rewritten bytes
}
// After: wrap the rewritten content as a whole-file TextEdit; return nil for
// no-op (engine treats nil result or empty Edits the same as no fix).
func (r *MyRule) Fix(ctx *sdk.Context, file *hcl.File) (*sdk.FixResult, error) {
original := file.Bytes
fixed := applyFix(file) // your existing fix logic, returning rewritten bytes
if bytes.Equal(original, fixed) {
return nil, nil
}
return &sdk.FixResult{
Edits: []sdk.TextEdit{{
Start: 0,
End: len(original),
Replacement: fixed,
}},
}, nil
}
The bytes.Equal guard matches the prior contract where rules returned nil bytes to signal "no change". TerraTidy's in-repo style rules use a shared WholeFileEdit helper at internal/engines/style/rules/helpers.go that collapses these lines — feel free to copy it into your own plugin module if you have many rules following this pattern.
Producing narrow edits (recommended for new rules):
func (r *MyRule) Fix(ctx *sdk.Context, file *hcl.File) (*sdk.FixResult, error) {
return &sdk.FixResult{
Edits: []sdk.TextEdit{
// Delete bytes [42, 50).
{Start: 42, End: 50, Replacement: nil},
// Insert " # tidied" at offset 100.
{Start: 100, End: 100, Replacement: []byte(" # tidied")},
},
}, nil
}
Offsets are half-open [Start, End). The engine sorts edits by Start descending before applying, so earlier splices don't invalidate later offsets. Out-of-bounds edits (End > len(content)) are rejected with an error. Return nil, nil (or &sdk.FixResult{} with no edits) when no fix is applicable; the engine treats both forms identically as a no-op.
Whole-file exclusivity: if any edit in a pass spans the entire file (Start == 0 && End == len(content)), it is applied alone — all other edits in the same pass are discarded and re-emit against the rewritten content on the next pass. This avoids ambiguous interactions between whole-file rewriters and narrow edits.
See the SDK reference for the full contract: Fixer interface, TextEdit, and FixResult — including the apply ordering, overlap rules, and field semantics.
Pre-release to Stable¶
When TerraTidy reaches v1.0.0, expect:
- Config
version: 1will remain supported - New
version: 2config format may be introduced - Deprecated features will be removed with migration guidance
terratidy config validatewill warn about deprecated options
Validating After Upgrade¶
After upgrading, verify your setup: