Contributing¶
Thank you for your interest in contributing to TerraTidy!
For the quick-start contributing guide, see CONTRIBUTING.md at the repository root.
This page provides the comprehensive development reference.
Getting Started¶
Prerequisites¶
- Go 1.26 or later (development uses 1.27)
- mise task runner
- Git
Clone the Repository¶
Install Dependencies¶
Build¶
Run Tests¶
Development Workflow¶
Create a Branch¶
Make Changes¶
- Write your code
- Add tests
- Update documentation
Run Checks¶
# Format code
mise run fmt
# Run linter
mise run lint
# Run all tests
mise run test
# Build
mise run build
Commit¶
Follow Conventional Commits:
git commit -m "feat: add new style rule for attribute ordering"
git commit -m "fix: correct HCL parsing edge case"
git commit -m "docs: update installation instructions"
Create Pull Request¶
- Push your branch
- Open a PR against
main - Fill out the PR template
- Wait for review
Code Style¶
Go Code¶
- Follow Effective Go
- Use
gofmtfor formatting - Run
golangci-lintfor linting
Documentation¶
- Use clear, concise language
- Include code examples
- Keep lines under 100 characters
Testing¶
Unit Tests¶
func TestMyFunction(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"basic case", "input", "expected"},
{"edge case", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MyFunction(tt.input)
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}
Integration Tests¶
Coverage¶
Adding New Features¶
New Engine¶
- Create package in
internal/engines/ - Implement
Engineinterface - Add command in
cmd/terratidy/ - Add configuration support
- Write tests
- Update documentation
New Rule¶
- Add rule to appropriate engine
- Implement
Ruleinterface - Add configuration options
- Write tests
- Document in rules reference
New Output Format¶
- Implement
Formatterinterface - Register in output factory
- Add CLI flag support
- Write tests
- Document usage
Project Structure¶
terratidy/
├── cmd/terratidy/ # CLI commands
├── internal/
│ ├── runner/ # Engine runner, parallel execution
│ ├── config/ # Configuration loading
│ ├── output/ # Output formatting
│ ├── engines/ # Engine implementations
│ ├── lsp/ # Language server
│ └── plugins/ # Plugin system
├── pkg/sdk/ # Public SDK
├── docs/ # Documentation
└── testdata/ # Test fixtures
Pre-commit Setup¶
Install pre-commit hooks for automatic checks on commit:
# pre-commit is installed by mise (see mise.toml)
mise install
pre-commit install
pre-commit install --hook-type commit-msg # Conventional commit validation
The hooks run formatting, linting, and commit message validation automatically.
CI Pipeline¶
The CI pipeline runs on every PR:
Test workflow (.github/workflows/test.yml):
- Build: Compiles on ubuntu, macOS, and Windows
- Tests:
go test -v -race -cover ./...with coverage collection - Linting: golangci-lint and revive
- Coverage: Uploaded to Codecov (ubuntu-only)
- Reproducible build: Builds the release artifact through GoReleaser twice, from different paths and seconds apart, and fails if the binaries differ
Security workflow (.github/workflows/security.yml):
- govulncheck: Scans Go dependencies for known vulnerabilities
- dependency-review: Reviews dependency changes in PRs for advisories
- gitleaks: Scans for accidentally committed secrets
- go-licenses: Verifies all dependencies use approved licenses
- zizmor: Scans GitHub Actions workflows for security issues
- gorelease: Checks
pkg/sdkAPI compatibility on PRs (only when SDK files change)
Fuzz workflow (.github/workflows/fuzz.yml):
- Runs every fuzz target in the repository, one parallel job per package
- 30s per target on PRs labeled
fuzz, 5m weekly for deeper exploration
See Security for details on the scanning pipeline.
Required checks¶
A pull request into main can't merge until two groups of checks pass:
- Checks that run on every pull request:
pr-title,pre-commit,govulncheck,dependency-review,gitleaks,licenses,osv-scanner,zizmor,api-compat,Analyze goandAnalyze actions. - One result check per workflow that only tests some files:
Test result,Container result,GitHub Action result,VS Code result,Example rules resultandPre-commit hooks result.
The workflows behind the result checks run on every pull request, but their first job, Detect relevant changes, compares the changed files with the paths that workflow covers. If none match, the other jobs skip, and the result check passes within seconds. Otherwise the jobs run, and the result check passes only if each one passed. A docs-only pull request therefore doesn't wait for the full test matrix, while a Go change can't merge without it.
If you add a job to one of those workflows, list it in that workflow's result job needs. A job the result check doesn't wait on can fail without blocking a merge; the workflow-result-jobs pre-commit hook rejects the change if you forget. To change which files a workflow covers, edit the relevant filter of its changes job; it takes the same glob syntax as a workflow's paths:.
The branch ruleset for main is the authoritative list of required checks.
Benchmark workflow (.github/workflows/benchmark.yml):
- Runs on main pushes (when baseline changes) or PRs with
benchmarklabel - Compares against
benchmarks/baseline.txt - 15% regression threshold triggers a warning and PR failure
- Posts comparison results as PR comment
See Performance for the full benchmarking guide.
Pre-commit hook test workflow (.github/workflows/precommit-test.yml):
- Validates
.pre-commit-hooks.yamlYAML syntax and structure - Tests all hooks (
terratidy-fmt,terratidy-check,terratidy-fix, etc.) against real Terraform fixtures - Verifies hooks work with the actual hook definitions from the repository
- Runs on Ubuntu and macOS (Windows excluded due to PATH/extension handling complexity)
API Stability¶
The pkg/sdk package is the public API for rule authors. Changes to exported types and functions are checked by gorelease on every PR that modifies pkg/sdk/**. Breaking changes require a major version bump.
Commit Format¶
Follow Conventional Commits:
| Prefix | Use For |
|---|---|
feat: | New features |
fix: | Bug fixes |
docs: | Documentation changes |
test: | Test additions/changes |
chore: | Maintenance, dependencies |
refactor: | Code restructuring |
perf: | Performance improvements |
PR Requirements¶
- Clear description of what changed and why
- Tests for new behavior
- Updated documentation for user-facing changes
- All CI checks passing
- Conventional commit messages
Release Process¶
Releases are driven by a signed version tag. Pushing it triggers the pipeline:
- Bump the version:
mise run bump:patch,bump:minor, orbump:major. Each opens a pre-release cycle (0.3.0→0.4.0-alpha), so for a stable release follow it withmise run bump:stable. The bump commits the change but does not tag it. - Create and push a signed tag:
git tag -s v1.2.3 -m "v1.2.3" && git push origin v1.2.3. Pushing the tag is the only way to get correctly signed release artifacts. - The release workflow handles everything else:
- GoReleaser cross-compiles binaries, builds Docker images, and publishes the Homebrew cask to
santosr2/homebrew-tap - git-cliff generates release notes from conventional commits
- CHANGELOG.md is automatically updated and committed to main
- Version alias tags (e.g.,
v1,v1.2) are created for stable releases - Docker alias tags are updated (
:latestalways points to the latest stable release) - Checksums are signed with cosign and build provenance is attested
- SBOMs are generated for each release archive
- Post-release smoke tests verify the binary on ubuntu and macOS
- The Homebrew cask is installed and checked with
brew auditon macOS (stable releases only)
- GoReleaser cross-compiles binaries, builds Docker images, and publishes the Homebrew cask to
Getting Help¶
- Open an issue for bugs
- Use discussions for questions
Code of Conduct¶
Be respectful and inclusive. We follow the Contributor Covenant.
License¶
By contributing, you agree that your contributions will be licensed under the MIT License.