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.
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.
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/).
v2.0.0-beta.1.replace for EVERY sibling it transitively needs, not just
its direct imports. Tidy follows transitive edges, so a partial replace set
fails with unknown revision on a deeper sibling.go.mod’s exact dependency versions before
tidying (copy its require lines in). Otherwise tidy floats deps to the
latest compatible release and you get silent version drift, e.g. the Azure
SDK advancing to a release that drops a symbol the code uses
(undefined: armmonitor.DiagnosticSettingsClient). The code is tested
against the root’s pinned versions; the submodules must inherit them.GOWORK=off go mod tidy to populate external requires and go.sum.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.
require to the exact version being tagged, then DROP all
internal replace directives. Do not run go work sync against the unpinned
tree.replace directives remain in any
modules/*/go.mod. A committed replace would publish a module pointing at a
local path and break consumers.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).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:
modules/core/v2.0.0-beta.1ssh, httphelper, dnshelperdocker, packer, database, opaaws, azure, gcp, then k8s, helmterraform, terragrunt, teststructureAfter 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.
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.
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.
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.