Skip to content

Add Terraform dependency updates to the update-deps command - #97

Open
mattmenefee wants to merge 1 commit into
mainfrom
update-deps-terraform-init-upgrade
Open

Add Terraform dependency updates to the update-deps command#97
mattmenefee wants to merge 1 commit into
mainfrom
update-deps-terraform-init-upgrade

Conversation

@mattmenefee

@mattmenefee mattmenefee commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Terraform to the /update-deps workflow as two steps:

  • Step 5 — Decide Terraform version bumps. Locate the providers and modules pinned in the .tf files, query the registry for each one's latest version, and decide what to edit based on the constraint's form: an exact pin always, a range only when the new release falls outside it, a child-module bare source never, and a root-module bare source never but watch it — nothing governs it, so init -upgrade takes the newest release published.
  • Step 6 — Re-resolve and verify the lock files. Run terraform init -upgrade -input=false in every root module and verify the outcome. Opens with the stop condition, because a failed init leaves the lock file untouched while the .tf edits stand, producing a commit that can neither init nor plan.
  • Terraform version changes flow through to the commit message, the PR labels, and the PR summary alongside the gems and npm packages
  • Background on why each flag is needed lives in a Terraform Update Notes reference section, matching the existing Version-Locked Packages pattern

Correctness of the prescribed commands

This is a command file an agent executes unsupervised, so every command is written so that a failure is loud and an absence is never mistaken for a negative answer. Each behavior below was verified by running it, including against Terraform 1.15.8 and the live registry:

  • A failed init halts the workflow. Across five distinct failure modes (unreachable private registry, nonexistent version, malformed HCL, no build for the platform, missing module repo) init exited non-zero and wrote nothing to the lock file — even where the output said Installed … first. The step distinguishes self-inflicted failures worth fixing in place from ones worth stopping over.
  • The registry lookup ignores .version, which reports the highest semver version including prereleases5.5.0-pre.1 for heroku/heroku while stable is 5.4.0.
  • The Git tag lookup strips the prefix and sorts numerically. --sort=-v:refname ranks every v-prefixed tag above every bare-numeric one, so a repo that dropped the prefix at 2.0.0 reports the older v1.10.0, and it ranks prereleases and floating tags like stable above real releases.
  • ls-remote output is captured, not piped into head. A pipeline's status is the last command's, so an unreachable repository would exit 0 and read as "no newer tag" — the same trap -fsS/jq -e guard against for curl.
  • Lock-file writes are confirmed with git status, not git diff alone. A brand-new root module's lock file is untracked and invisible to diff, so diff alone would report a successful write as a failure and trigger a needless revert.
  • The refresh iterates every root module, not only those with changed .tf files — a range constraint updates with no .tf edit at all. Root modules are identified as the directories owning a lock file, which excludes child modules automatically.
  • The provider baseline keeps versions and filenames. Providers arriving transitively through child modules never appear in a required_providers block and can only be observed moving in the lock file.
  • The ~> bounds are exact. Verified against Terraform's own resolver: ~> 2.99 selects 2.100.0, ~> 2.99.0 selects 2.99.1, >= 2.5 drifts to 3.1.0.
  • Staging splits the globs across two git add calls, since git validates every pathspec before staging anything and one matching nothing aborts the whole invocation. Both exclude the modules terraform init vendors into .terraform/ — verified against a run that vendored 110 .tf files.

Test plan

  • Run /update-deps in a repo with Terraform configuration and confirm the registry lookup reports the latest stable version, not a prerelease
  • Confirm the pin is bumped and terraform init -upgrade -input=false rewrites .terraform.lock.hcl
  • Confirm terraform plan succeeds afterward instead of reporting an inconsistent dependency lock file
  • Confirm a repo with a ranged constraint still gets init -upgrade run, despite no .tf file changing
  • Confirm a repo with multiple root modules gets each one refreshed, and that child modules are skipped
  • Confirm a failing init halts the workflow rather than staging a .tf/lock-file pair that cannot be planned
  • Confirm a brand-new root module's lock file is recognized as written rather than reverted
  • Confirm the provider bump appears in the commit message with a release link, and the PR picks up the terraform label
  • Run /update-deps in a repo with no .tf files and confirm both steps are skipped without breaking staging
  • Spot-check that every renumbered step heading and anchor link still resolves

@mattmenefee mattmenefee self-assigned this Aug 10, 2026
@mattmenefee
mattmenefee force-pushed the update-deps-terraform-init-upgrade branch 2 times, most recently from 8c08241 to df0d40b Compare August 10, 2026 17:53
@mattmenefee mattmenefee changed the title Require terraform init -upgrade after Terraform dependency bumps Add Terraform dependency updates to the update-deps command Aug 10, 2026
@mattmenefee
mattmenefee force-pushed the update-deps-terraform-init-upgrade branch 3 times, most recently from d76573f to fd67eb4 Compare August 10, 2026 20:18
The /update-deps workflow covered Ruby and JavaScript dependencies but
skipped Terraform entirely, so provider pins drifted until a plan
failed. Nothing surfaces an available release on its own: Terraform has
no bundle outdated equivalent, and terraform init -upgrade only
re-resolves versions within the existing constraints, so against an
exact pin it never finds anything newer.

Add two steps. The first locates the providers and modules pinned in the
.tf files, queries the registry for each one's latest version, and
decides what to edit from the constraint's form — an exact pin always,
a range only when the new release falls outside it, a root-module bare
source never, since nothing governs it and init -upgrade will take the
newest release published. The second re-resolves every root module's
lock file and verifies the result. Splitting them keeps the stop
condition at the top of its own step rather than buried at the end of a
long one, and the failure it guards against is the reason it exists: a
failed init leaves the lock file untouched while the .tf edits stand,
and staging that pair produces a commit that can neither init nor plan.

The commands are written so that every failure is loud and every result
is trustworthy, because an absence is otherwise indistinguishable from a
negative answer. The registry lookup ignores the response's .version
field, which reports the highest semver version including prereleases
and would pin infrastructure to a beta. The Git tag lookup strips the
tag prefix and sorts numerically rather than trusting
--sort=-v:refname, which ranks every v-prefixed tag above every bare
one and every prerelease above its own release, and it captures
ls-remote's status instead of piping into head, where an unreachable
repository would exit 0 and read as "no newer tag". curl runs with -fsS
and jq with -e for the same reason. init and plan both take
-input=false so they error rather than blocking on a prompt with no TTY.
Writing of the lock file is confirmed with git status rather than git
diff alone, since a brand-new root module's lock file is untracked and
invisible to diff.

The lock-file refresh iterates every root module rather than only those
whose .tf files changed, since a range constraint updates with no .tf
edit at all; root modules are identified as the directories owning a
lock file, which excludes child modules on its own. The provider
baseline captured beforehand keeps versions and filenames, because
providers that arrive transitively through child modules never appear in
a required_providers block and can only be seen moving in the lock file.

Terraform version changes flow through to the commit message and to the
PR labels and summary alongside the gems and npm packages. Background on
why the commands take the flags they do lives in a Terraform Update
Notes reference section. Most repos this command runs against have no
Terraform at all, so the first step opens with how to recognize that
case and skip both. Renumbering the later steps and their
cross-references follows from inserting the new ones.
@mattmenefee
mattmenefee force-pushed the update-deps-terraform-init-upgrade branch from fd67eb4 to 43ebb29 Compare August 10, 2026 20:47
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