Skip to content

Go Edition: Version resolution engine (latest, latest-allowed, min-required) #495

@Zordrak

Description

@Zordrak

Summary

Implement the version resolution engine — resolving version keywords (latest, latest:<regex>, latest-allowed, min-required) and semantic version constraints from HCL files into concrete Terraform version numbers.

Parent Epic

Part of #488 — Go Edition: Full Feature Parity Implementation

Motivation

This is the brain of tfenv — the logic that takes a version specifier from .terraform-version or TFENV_TERRAFORM_VERSION and resolves it to a concrete, installable version number. It handles the full keyword vocabulary, regex matching, HCL required_version constraint parsing, and semver comparison. Getting this wrong means installing the wrong Terraform version.

Clean-Room Constraint

This is a clean-room implementation. Contributors MUST NOT read, reference, copy, or adapt source code from tofuutils/tenv, hashicorp/hc-install, or any other third-party tfenv-like tool. The sole reference is tfenv's own Bash source code, documentation, and test suite.

Proposed Design

Package Location

go/internal/resolve/

Version Specifiers

Specifier Behaviour Bash Reference
1.5.0 Exact version Direct use
latest Latest stable release (excludes pre-release) libexec/tfenv-resolve-version
latest:<regex> Latest version matching Go RE2 regex (Bash uses ERE) libexec/tfenv-resolve-version
latest-allowed Latest version satisfying required_version constraints in .tf files libexec/tfenv-resolve-version
min-required Minimum version satisfying required_version constraints libexec/tfenv-min-required

Regex Dialect

The Bash edition uses POSIX Extended Regular Expressions (ERE). The Go edition uses RE2 (Go's native regex). This is a known divergence — document it clearly. Most user patterns (^1\.5, ^1\.6\.) work identically in both dialects.

HCL Constraint Parsing

For latest-allowed and min-required:

  1. Find .tf and .tf.json files in the current directory (or TFENV_DIR)
  2. Parse terraform { required_version = "..." } blocks
  3. Extract version constraint strings
  4. Parse constraints using hashicorp/go-version
  5. Filter remote version list against constraints
  6. Return latest match (for latest-allowed) or earliest match (for min-required)

latest-allowed resolution (Bash reference: libexec/tfenv-resolve-version lines 128-162):

The Bash edition parses the first required_version it finds (using grep, not full HCL parsing) and handles these constraint operators:

Constraint Bash Behaviour Example
> X.Y.Z or >= X.Y.Z latest (any version newer) >= 1.0.0latest
<= X.Y.Z → exact version X.Y.Z <= 1.5.71.5.7
~> X.Y.Z latest:^X.Y\. (pessimistic, pin major.minor) ~> 1.5.0latest:^1.5\.
~> X.Y latest:^X\. (pessimistic, pin major) ~> 1.5latest:^1\.
= X.Y.Z or bare X.Y.Z → exact version X.Y.Z = 1.5.01.5.0
!= X.Y.Z Ignored (Bash skips negations)
Comma-separated Only the FIRST constraint is used (%%,* strips after first comma) >= 1.0, < 2.0 → uses >= 1.0 only

Important: The Bash implementation only reads the FIRST constraint from the FIRST matching required_version line. It does NOT intersect multiple constraints. The Go edition should improve on this by using hashicorp/go-version.Constraints for proper multi-constraint evaluation, but the acceptance tests must verify the Bash-compatible happy-path behaviour first.

min-required resolution (Bash reference: lib/tfenv-min-required.sh):

  1. cat ${path}/{*.tf,*.tf.json} — read all HCL files
  2. grep -Eh '^\s*[^#]*\s*required_version' — find required_version lines (excluding comments)
  3. Extract version numbers with regex: [~=!<>]{0,2}\s*([0-9]+\.?){2,3}(-[a-z]+[0-9]+)?
  4. Skip negation operators (!=)
  5. Take the first qualifying version
  6. Pad with .0 if missing minor/patch (e.g., 12.012.0.0)
  7. Return as the min-required version

v Prefix Handling

Both latest-allowed and exact version specifiers may arrive with a v prefix (e.g., v1.5.0). The resolution engine must strip this prefix early in the pipeline. Reference: lib/tfenv-version-name.sh lines 109-112 and libexec/tfenv-resolve-version lines 118-121.

Resolution Flow

Input: version specifier string
Output: "version:regex" pair (e.g., "latest:^1\.5\." or "1.5.0:^1\.5\.0$")

1. Strip `v` prefix if present
2. If "min-required" → parse .tf files, extract min version, return "min_version:^min_version$"
3. If "latest-allowed" → parse .tf files, interpret constraint, convert to latest:<regex>
4. If "latest:<regex>" → return "latest:<regex>"
5. If "latest" (bare) → return "latest:^[0-9]+\.[0-9]+\.[0-9]+$"
6. If exact version → return "version:^version$"

The resolve output is a version:regex pair. The caller (install, use) then uses the version part to determine if it's latest (needs remote lookup) or exact (can check local first), and the regex part to filter the version list.

Acceptance Criteria

  • Exact version specifiers (1.5.0) resolve to themselves
  • latest resolves to the newest stable (non-pre-release) version
  • latest:<regex> filters versions by RE2 regex and returns newest match
  • latest:^1\.5 correctly matches only 1.5.x versions (not 1.50.x)
  • latest-allowed reads required_version from .tf files and returns the latest satisfying version
  • min-required reads required_version from .tf files and returns the minimum satisfying version
  • Multiple required_version constraints across multiple .tf files are intersected
  • Missing .tf files or missing required_version blocks produce clear errors for latest-allowed/min-required
  • Pre-release versions are excluded by default for latest
  • Invalid regex patterns produce clear error messages
  • Invalid version constraints produce clear error messages
  • Version comparison uses hashicorp/go-version (not string comparison)
  • Comprehensive unit tests with table-driven patterns covering edge cases
  • Unit tests cover: single constraint, multiple constraints, no matching version, pre-release handling, regex edge cases, exact constraint (= 1.5.0)

Dependencies

Implementation Notes

Labels

type:feature, priority:high, complexity:large, category:version-resolution

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions