Skip to content

Take a pinned version without resolving it - #446

Open
benlangfeld wants to merge 2 commits into
dflook:mainfrom
benlangfeld:exact-version-fast-path
Open

benlangfeld wants to merge 2 commits into
dflook:mainfrom
benlangfeld:exact-version-fast-path

Conversation

@benlangfeld

@benlangfeld benlangfeld commented Sep 23, 2026 •

Copy link
Copy Markdown

Implements the suggestion in #443 (comment), and then some — it now reads the pin from the module rather than needing anything set in the workflow.

Stacked on #444. This branch includes that commit, so the diff below shows both until it merges. Review #444 first; afterwards this reduces to terraform_version/ only.

The problem

determine_version does two expensive things unconditionally, before anything has looked at what the version actually needs:

def determine_version(inputs, cli_config_path, actions_env, github_env) -> Version:
    versions = list(get_terraform_versions())   # release index, first statement
    ...
    module = load_module(Path(inputs.get('INPUT_PATH', '.')))   # parses every file

When the version is pinned exactly there is nothing to resolve, and load_module means parsing every .tf file in the root module.

The change

Work out whether the version is pinned using only what can be read cheaply. Just three block names can settle it, so only files mentioning one of them are parsed:

module = load_module(Path(inputs.get('INPUT_PATH', '.')), declaring=['required_version', 'backend', 'cloud'])

An exact required_version — one = constraint naming major, minor and patch — needs no resolving, so it is used directly and the release index is never fetched. TERRAFORM_VERSION/OPENTOFU_VERSION naming a version exactly serves as a fallback when the module requires nothing.

Precedence is unchanged

This was the open question on the previous revision of this PR, which read the environment variable only and so jumped it above five sources that outrank it. Reading the pin from the module removes the problem, because the gating can now be decided from the same cheap parse:

Source Rank Handled by
remote workspace version 1 shortcut skipped entirely when the backend is remote or cloud
required_version 2 used, when it is an exact pin
.tfswitchrc 3 checked for; present means the shortcut is skipped
.terraform-version / .opentofu-version 4 as above
.tool-versions 5 as above, searching parents as try_read_asdf does
TERRAFORM_VERSION 6 the fallback, only once 1–5 have nothing to say

So the shortcut is only taken where it agrees with what the usual resolution would have chosen. No workflow that works today changes behaviour, and the TERRAFORM_VERSION_EXACT variable I offered as an alternative is no longer needed.

Effect

End to end on the real 245 file root module from #443, with required_version = "1.16.3" and nothing set in the workflow:

Using Terraform 1.16.3, which is pinned exactly
  switched to '1.16.3' in 1.72s, release index fetches: 0

Against 32.07s for the full parse it replaces, plus the index fetch. Constraints are untouched: a range still needs the available versions and still fetches them.

Correctness notes

  • Only the environment variable for the product being run is read, so it can never select the other product.
  • The environment value needs a full regex match. Version() reads a prefix, so Version('1.16.3, <2.0.0') would silently yield 1.16.3; there is a test for exactly that.
  • 1.16 is not an exact pin — Constraint.patch is None — so it still resolves normally.
  • The log says the version was pinned, so a version chosen this way is visible in the run.

Tests

Eighteen in tests/terraform_version/test_exact.py: exact pins including pre-releases, six constraint shapes left alone, the product following the action, and one per precedence rule in the table above — remote backend, cloud block, each of the three version files, pin beating the environment, environment as fallback, and nothing named at all.

pytest tests --ignore=tests/github_pr_comment gives 171 passed against 147 on main, with the same 2 failures and 7 errors before and after — those need S3 and environment I do not have. tests/github_pr_comment fails to collect identically before and after, on a missing TERRAFORM_ACTIONS_* variable.

🤖 Generated with Claude Code

terraform-backend loads the whole root module in order to read one backend
block. The cost scales with the size of the module rather than with the thing
being looked up, and on a large root module that is most of a minute before
terraform has been asked to do anything.

A block cannot appear in a file that does not mention its name, so the files
worth parsing can be picked out by a substring check first. Files that mention
a word only in a comment or a string are still parsed, so the result is
unchanged; the scan only decides what to skip, never what the answer is. A file
that cannot be read is treated as a candidate so that the usual parsing and
error handling still applies to it.

On a generated 261 file module with one backend block, reading the backend type
goes from 34.07s to 0.20s for the same answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
determine_version fetches the release index as its first statement and loads the
module a few lines later, both before anything has looked at what the version
actually needs. When the version is pinned exactly there is nothing to resolve,
and loading the module means parsing every file in it.

Decide it from what can be read cheaply instead. Only files mentioning
required_version, backend or cloud can say whether the version is pinned, so
parsing those answers it - on a real 245 file module that is 1.7s rather than
32s, and the release index is not fetched at all.

The order sources are considered in is unchanged, which is what the gating is
for. A remote or cloud backend means the workspace version outranks the pin, so
the shortcut is skipped and the usual resolution runs. An exact required_version
otherwise wins, as it already would. Only when the module requires nothing does
the environment get a say, and then only if no version file is present, since
those outrank it too.

Constraints are untouched - a range still needs the available versions, so it
still fetches them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@benlangfeld
benlangfeld force-pushed the exact-version-fast-path branch from e7ffcf9 to ff0c07a Compare September 23, 2026 15:03
@benlangfeld benlangfeld changed the title Take an exactly pinned version without resolving it Take a pinned version without resolving it Sep 23, 2026
@benlangfeld
benlangfeld marked this pull request as ready for review September 23, 2026 15:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant