Terratest v2 Release Runbook

How to cut a coordinated release of the v2 submodules. Read this before tagging.

The whole release is one manual workflow: Create v2 Release (.github/workflows/create-release.yml, run via workflow_dispatch). Give it a version (e.g. v2.0.0-beta.1) and a ref (default main); it runs the pre-flight checks, pins the cross-module requires, commits that in CI, pushes the 16 tags in dependency order, and verifies the proxy. Run it once with dry_run=true to preview the exact tags, then again with dry_run=false to push.

The rest of this runbook explains what that workflow does under the hood and how to do each step by hand if you ever need to.

Layout

v2 is split into per-domain modules under modules/<name>/, each declaring module github.com/gruntwork-io/terratest/modules/<name>/v2. Local development uses the root go.work, which resolves every submodule to its local tree, so the submodule go.mod files do not need internal require lines or replace directives during normal development.

Why pinning is a release-time step (do not commit it early)

On main, a submodule’s cross-module require sits at the local zero placeholder (e.g. core/v2 v2.0.0-00010101000000-000000000000) that go.work resolves; it is never pinned to a real, to-be-published version. Pinning a sibling require to the to-be-published version (e.g. core/v2 v2.0.0-beta.1) BREAKS the workspace build until that tag actually exists: go.work does not shadow an unpublished required version, so go build and go work sync try to fetch the missing revision and fail. The pin must therefore happen immediately before the tags are pushed, never on main or a modularization PR. The Create v2 Release workflow does this pin in CI (via scripts/release-prep-pin.sh) on an ephemeral commit that only the tags point at, so the broken-build state never lands on a branch.

CI validates the release-mode build continuously without committing the pin: the GOWORK=off check generates the pinned state with throwaway replace directives, builds a consumer, and discards it (see scripts/).

Pre-flight (what the workflow does before tagging)

  1. Choose the version, e.g. v2.0.0-beta.1.
  2. Pin each module, in dependency order (core first, then helpers, tooling, platforms, k8s/helm, IaC). For each module:

scripts/check-release-mode.sh performs this exact pin (transitive replaces + root-version seeding + an all-module external consumer build under GOWORK=off) in a throwaway and reverts it, and runs in CI on every PR so the release-mode build is validated continuously without committing the pin or needing tags.

  1. Set every internal require to the exact version being tagged, then DROP all internal replace directives. Do not run go work sync against the unpinned tree.
  2. Before tagging, confirm no internal replace directives remain in any modules/*/go.mod. A committed replace would publish a module pointing at a local path and break consumers.
  3. Move test/ to its own module here too if not already done, and pin it the same way (it is test-only, so committed replaces are acceptable for it).

Tag push order

All tags point at the same release commit. The tag name is the module’s on-disk subdirectory (which does NOT include the /v2, since /v2 is only the SIV in the module path, not a directory) followed by the version: modules/<name>/<version>, e.g. modules/aws/v2.0.0-beta.1. Do NOT put the /v2 in the tag (modules/<name>/v2/<version>), Go looks for the tag at <subdir>/<version> and will not find it. Push in dependency order:

  1. modules/core/v2.0.0-beta.1
  2. helpers: ssh, httphelper, dnshelper
  3. tooling: docker, packer, database, opa
  4. platforms: aws, azure, gcp, then k8s, helm
  5. IaC: terraform, terragrunt, teststructure

After each tier, probe the proxy before continuing: curl -o /dev/null -w '%{http_code}' https://proxy.golang.org/github.com/gruntwork-io/terratest/modules/<name>/v2/@v/<version>.info should return 200.

Verify

Build a throwaway external consumer that imports every published module with GOWORK=off. This is what scripts/check-release-mode.sh generates in a temp directory, except now it resolves against the real published tags rather than local replaces. A clean consumer should resolve, build, and test green with zero local references.

If a tag is wrong

Proxy tags are immutable. Recover by cutting the next patch (v2.0.0-beta.2), never by editing in place. The pre-flight checks exist to keep this rare.

Beta to GA

After the beta soaks (suggested two weeks minimum), repeat the same procedure at v2.0.0 with no suffix: same release commit shape, same tag sequence, same proxy verification, then announce.