Skip to content

Releasing Go modules at one version

Several PRs can merge before a version is published. Source merges do not publish modules. All maintained public modules release together at one version, even when only one component changes. Go module boundaries and import paths stay independent; nested modules still require their own prefixed tags. Each version has one project GitHub Release and one consolidated notes document. The repository publishes Go source modules and the project Release with GoReleaser OSS; Lambda ZIPs remain CI example artifacts. No cloud AWS account is needed.

Before preparing v1, complete the readiness gates. Documentation and test identity follow VERSION_POLICY.md.

Contribution notes accumulate until publication

Every PR includes a ## Release notes section. Write one English, user-facing description per line, with the module directory and change type:

## Release notes

- logger | fix | Preserve temporary attribute lifetimes in child loggers.
- logger | fix | Include overflow error details in buffered output.
- metrics | feature | Add an optional metric configuration setting.

Use module directories from tools/modules.json, including . for root Commons. Use repository for repository-only tooling and contribution work; these entries appear in the root version's unified summary. Types are breaking, feature, fix, documentation, and maintenance. A PR may describe several changes across several modules. For a change with no release impact, write None: <specific reason>. The contribution policy requires the section and syntax; release preparation checks actual module names. Maintainers review whether the declared modules and summaries accurately describe the changes.

For Logger v0.1.1, the generator collects all merged PRs between logger/v0.1.0 and the selected source commit, then includes only Logger entries. A Metrics component note uses its previous tag. The root v0.1.1 Release groups all actual changes by component and includes repository notes and a full module version table. New plans freeze each module's actual changed-file list against its previous tag, before preparation writes version/dependency metadata. Nested modules own their files; repository documentation and tooling do not count as Commons changes. Deleted and renamed files are included, while a change that was fully reverted has no net file change.

  • Updated modules: show categorized PR/direct-commit summaries and an expandable changed-file list. File changes remain visible even if a PR has no user-facing summary.
  • Version bumps only: list MODULE: bumped to VERSION without empty feature sections or generated dependency-update noise. Broad PR notes alone cannot make an unchanged module appear updated.
  • Initial release: explicitly describe first publication when no previous module Release exists; it is never described as an unchanged version bump.

Updated-module headings, the bump-only list, and the module version table use project-qualified display names: powertools-lambda-go for the root, powertools-lambda-go/logger for Logger, and powertools-lambda-go/commons/awssdk for a nested module. PR note keys remain . and repository-relative module directories; tag names and Go import paths keep their existing forms.

The module version table uses the same classification. New plans use notes_format: 2; publication verifies their frozen file lists against Git. Existing schema 1/2 plans without this field retain their original rendering so historical frozen plans remain readable and verifiable. One PR fixing three Logger behaviors can produce three notes with the same PR link. Notes group breaking changes, features, fixes, documentation, and maintenance in that order.

History uses Git ancestry and associated merged PRs, not date windows. Squash, merge, and rebase histories are deduplicated. Stable releases compare against previous stable releases; prereleases may compare against previous prereleases. Only published GitHub Releases on the source ancestry are release boundaries, not draft Releases or unrelated module tags.

Preview the next release notes

Generate notes against a published project version without editing go.mod, go.work, manifest versions, or a release preparation plan:

uv run --no-project python tools/release.py preview --base v0.1.0 --target origin/main

The target is resolved to an immutable source SHA. Notes and their input snapshot are saved under dist/releases/previews/VERSION/SOURCE/. --bump patch|minor|major can override the normal automatic candidate-version choice. Preview links point to the source comparison, not to component Releases that do not exist yet.

Add --draft to create a reviewable GitHub Release draft:

uv run --no-project python tools/release.py preview --base v0.1.0 --target origin/main --draft

It uses the separate notes-preview/VERSION name, which does not reserve a Go module version. Repeating the command updates only a matching, tool-owned draft and saves its previous contents locally. Existing official Releases are retained. This draft contains documentation only: use Prepare release after reviewing the notes. Do not publish the notes-preview draft as a module release; its source has not gone through version preparation. Publication rejects preview snapshots.

Prepare the unified release

After the workflows are merged, open Actions: Prepare release and choose Run workflow on main:

Input Value
bump Leave auto for note-based versioning, or select patch, minor, or major
version Optional explicit target such as v1.0.0-rc.1; keep bump=auto when specifying it
auto_publish Leave checked to publish after the preparation PR merges and main checks pass

The workflow creates a Release tracking issue and a preparation PR. It computes one shared version, includes every maintained public module, synchronizes manifest versions and internal go.mod requirements, rebuilds go.work version mappings, tidies dependency sums, and freezes each module's accumulated notes. No manual version-file edits, module-list script, issue number, SHA, or plan name are required. Every release includes all maintained public modules and Commons; development modules and the frozen tracer/xray adapter are excluded.

Review the generated PR's shared version, full module table, notes, initial compatibility statements, and automatic-publication setting. Required checks use the native PR workflows. Preparation waits for those runs to appear and attempts to authorize pending runs with its Actions write permission. If GitHub requires maintainer authorization, select Approve workflows to run in the PR. A manually dispatched job check does not satisfy PR rulesets, even when it passes on the same commit; see required-check troubleshooting. Merge the preparation PR with squash or merge after the checks pass. The recorded source must still be its first parent; if main advances first, rerun preparation against current main and use the newly generated PR.

With automatic publication enabled, merging this preparation PR authorizes its frozen release batch. After the CI gate, Modules and Lambda artifacts, Runtime simulation, Documentation / Build documentation, and Automation / Release tooling checks pass on main, GoReleaser publishes one project Release after every module tag and public consumer check succeeds. Tags are created in dependency order; the tracking issue then closes. Ordinary feature/bug PR merges do not publish modules.

The repository's Settings → Actions → General → Workflow permissions must allow GitHub Actions to create and approve pull requests for automated PR creation. The preparation workflow does not approve or merge PRs. Its explicit job permissions grant the built-in token only the operations needed for issue, branch, PR, and native workflow activation; no additional secret is required.

An organization or enterprise policy can prohibit that repository setting. If GitHub reports that the organization does not allow Actions to create or approve PRs, an organization owner must first permit it under Organization Settings → Actions → General → Workflow permissions. Then enable the repository setting above. See GitHub's organization policy documentation. The CLI preparation commands below can use a maintainer's existing gh authentication while the organization policy stays restricted.

Automatic versions and dependency metadata

For a reviewed candidate, the CLI supports an explicit target:

export CGO_ENABLED=0
uv run --no-project python tools/release.py prepare --all --version v1.0.0-rc.1

Omitting --auto-publish leaves automatic publication disabled. The Actions input still defaults to enabled; uncheck it for a preparation-only review. The explicit version must advance beyond current/published versions and every reserved tag in the maintained cohort. Invalid SemVer, downgrades and v2+ paths are rejected. Successive candidates compare notes against the preceding published candidate; stable promotion compares against the previous stable release. Every candidate uses the ordinary reviewed-plan, full-CI and fresh public-consumer gates.

Situation Default target
First public release The configured initial version, normally v0.1.0
Fix, maintenance, documentation, or an explicit batch with no module notes Next patch version
Feature Next minor version
Breaking change while on v0 Next minor version
Breaking change while on v1 Stop for a separately reviewed v2 module-path migration

For example, Logger fixes produce v0.1.0 → v0.1.1; a Logger feature produces v0.1.0 → v0.2.0 for every maintained component. The strongest accumulated change across components and repository notes determines one automatic increment. Reserved tags from any maintained module, including drafts or tags without Releases, are never reused; the entire cohort advances beyond reserved versions. An explicit major increment can move v0 to v1; v2+ paths are not automated.

The project version lives in tools/modules.json as release_version; all maintained module entries must match it. The automation updates internal dependency requirements and workspace mappings while preserving module paths and Go language-version directives. Workspace consumer go.mod files are synchronized too. Publication always covers the full maintained cohort. Dependency changes appear in the frozen plan. New plans and their prepared manifests record publication_mode: project; module records preserve the reviewed change and dependency evidence for the single notes document. Go commands keep CGO_ENABLED=0; no module-file replacements are added. Tidy uses the existing local module fixtures before published external modules.

Maintainers who prefer the CLI can start from a clean checkout of current origin/main, authenticate gh, and run:

uv run --no-project python tools/release.py prepare --all --auto-publish

This command creates the issue, branch, and PR automatically. Component selection is no longer supported. Use --bump patch to override the shared version policy. CLI preparation enables automatic publication only with --auto-publish. For local metadata/plan generation with no GitHub writes, pass --local --issue 123; --issue otherwise reuses an existing open tracking issue. --plan and --overrides are optional advanced controls. All tracked metadata is restored if preparation/tidy fails before the branch is committed.

Historical PRs and first releases

Current PRs require structured release notes. Detailed entries are preserved and grouped by module and category. Historical PRs without that section use their actual titles and changed module paths; the full file-count coverage is checked, and the plan identifies inferred summaries for review. Malformed structured notes still fail rather than being silently replaced. Direct commits are acknowledged using their subjects and affected modules. Initial releases receive a conservative scope statement referring to module documentation.

The first release therefore needs no hand-authored overrides file. Maintainers can improve generated historical summaries and initial capability statements in the preparation PR, or supply optional JSON overrides for preparation. Each pull_requests value contains the release-note section without its heading. Repository-only entries are included in the root unified summary.

{
  "pull_requests": {
    "2": "- repository | maintenance | Establish an issue-to-PR contribution workflow.",
    "6": "- logger | fix | Preserve temporary attribute lifetimes in child loggers.\n- logger | fix | Omit ordinary empty and null top-level attributes.\n- logger | fix | Correct buffer trace ownership and overflow error details."
  },
  "initial_summaries": {
    "logger": "Initial structured logging module. Describe approved capabilities and compatibility limitations here."
  },
  "untracked_commits": {
    "0123456789012345678901234567890123456789": {
      "modules": ["logger"],
      "description": "Initial source import; supported capabilities are described in the initial scope."
    }
  }
}

Use actual source hashes, PR numbers, and approved descriptions. Save input in an ignored file such as .tmp/release-overrides.json, then pass --overrides .tmp/release-overrides.json. Relevant overrides are copied into the reviewed plan. Historical overrides do not rewrite merged PR descriptions. First release notes include both a capability/compatibility summary and PR changes.

Automatic publication and manual recovery

Automatic publication listens for the completed CI workflow on main push commits. Release plans and module manifests select every CI suite, and publication requires the aggregate CI gate plus actual module, runtime, documentation and release-tooling success; skipped jobs cannot authorize publication. It resolves the merged preparation PR and its exact plan, requires the plan's auto_publish: true, and rechecks the latest required main checks. If another check is still pending or failed, it makes no publication writes; the next completion event re-evaluates the batch. Fork and PR-check events are excluded. A closed tracking issue prevents duplicate automatic completion from repeating an already finished batch.

For a plan prepared with automatic publication disabled, or to resume after a failure, open Actions: Publish Go modules and choose Run workflow on main. Enter only the merged preparation PR number. Leave publish unchecked for preflight or check it to publish/resume. Issue, SHA, and plan are resolved from the merged PR; they need no manual copying.

The equivalent CLI commands are:

uv run --no-project python tools/release.py publish --pr 456
uv run --no-project python tools/release.py publish --pr 456 --publish

Publication checks caller write permission, Issue/PR association, merged SHA, clean checkout, reviewed plan, required PR/main checks, module paths/versions, dependency metadata, previous release boundaries, dependency order, and tag/Release conflicts. Preflight writes local artifacts only, including a nonpublishing GoReleaser run for the root project Release. Publication is serialized and an active run is not canceled by a newer request.

Release preparation changes select complete CI automatically. Publication requires Full regression, DynamoDB Local, module/architecture checks, runtime simulation, documentation and release-tooling success on both the preparation PR and exact main commit. Ordinary PRs use affected-module checks; a green scoped gate or a skipped full-regression marker cannot publish a release.

For each module, publication creates a tag at the selected SHA and verifies a real public consumer before proceeding to dependent modules. GoReleaser creates one draft at the root tag with all reviewed notes. This project Release stays draft until every module consumer succeeds, then is finalized once. It marks the stable project version as GitHub's Latest Release. Root Commons uses vX.Y.Z; Logger uses logger/vX.Y.Z. Consumers use fresh caches, GOWORK=off, CGO_ENABLED=0, the public Go proxy and checksum database, no local proxies/replacements, and a consumer build. This differs from synthetic local module verification. After all selected modules pass, the workflow posts the project Release link and module tag links, then closes the tracking issue.

The built-in GitHub token performs writes. Publication does not depend on a tag-triggered follow-up workflow. Nested tags do not create component Releases. Prerelease versions create one prerelease project Release and do not change GitHub's Latest label. v2+ module-path migrations need a separate feature and are rejected by this initial tool.

GoReleaser configuration and independent tags

New preparations use schema 2: the complete maintained module set, one version, aligned internal requirements, explicit project publication mode, and frozen unified summary are required before publication writes. Immutable schema-1 plans from before this policy can still be recovered at their original commits, whose manifests lack release_version. They cannot be used to bypass the policy at a new preparation commit. Published v0.1.0 and v0.2.0 tags, Releases, notes, and acceptance records are not rewritten. Previously frozen schema-2 plans without a publication mode retain their component-Release recovery behavior at their original commits. New preparation always selects project mode; a prepared project manifest rejects unmarked plans.

For later releases, each module's history boundary is its previous published component Release or a published project Release with that module in the version cohort recorded at the tagged commit. Project Releases require the matching prefixed module tag at that same commit. Missing or conflicting tags stop preparation; drafts and unrelated commits are excluded. Modules added after the published cohort receive an initial summary. Stable releases exclude prerelease boundaries; successive candidates may use published candidates.

The workflow installs GoReleaser OSS v2.18.2 through a commit-pinned official action. .goreleaser.json is the shared configuration; GoReleaser accepts JSON through its YAML parser. The publisher derives one project configuration under dist/releases/NAME/commons/, changing the project name, output directory, and explicit prerelease status from the reviewed plan. Library releases skip binary builds, checksums, and artifact uploads. Each invocation saves a phase configuration with the required draft state; the shared configuration defaults to draft releases.

GoReleaser owns GitHub Release creation and finalization. The Python tooling prepares accumulated PR notes, enforces the reviewed plan, orders dependencies, creates exact tags, verifies public consumers, and completes the tracking issue. GitHub's by-tag API returns published Releases. The adapter finds matching drafts through authenticated release listing, rejects duplicate matches, and validates existing notes/tag targets before resuming. After a successful GoReleaser write, the publisher waits up to 30 seconds for the requested draft or published state to become visible. It checks reviewed content on every observed Release and stops immediately on conflicts or API errors. This bounded read-after-write wait never repeats a publication write. If visibility does not converge, existing objects are preserved for recovery. It passes the frozen consolidated Markdown file using --release-notes; GoReleaser does not replace it with a repository-wide commit changelog. Existing notes are kept; conflict detection tolerates only terminal newline formatting differences.

Native monorepo tag-prefix support requires GoReleaser Pro. New project publication invokes OSS only for the root tag; the publisher creates the independent prefixed tags required by Go. Historical component-plan recovery uses the OSS release command with GORELEASER_CURRENT_TAG set to the complete reviewed tag and --skip=validate. It is a compatibility adapter, rather than native OSS monorepo support. The publisher replaces those skipped checks: the checkout must be clean at the exact merged SHA, the module version must be valid and agree with the manifest, and local/remote tags must identify that SHA. It also checks required CI and the preparation PR before writes. Prerelease status is set explicitly from the reviewed version; no artifact templates use GoReleaser's semantic-version fields for prefixed tags.

Preflight additionally skips publish and announce, removes inherited SCM tokens from the GoReleaser environment, and creates no public tags or Releases. For first releases, the selected SHA fills the previous-tag environment field; PR history boundaries still come exclusively from the reviewed plan. Use the documented workflow or tools/release.py publish entry point rather than publishing directly with GoReleaser: the entry point supplies these gates, notes, tag context, and dependency ordering.

For local offline checks, install GoReleaser OSS v2.18.2 on PATH, then run:

goreleaser check --config .goreleaser.json
uv run --project website --frozen python tools/test_release.py
uv run --project website --frozen python tools/test_release_automation.py

The real CLI regression creates an isolated temporary Git repository and checks root, nested-module, and prerelease tags with publication disabled. CI installs the pinned CLI and runs this regression. A second real CLI regression uses a local GitHub API fixture to verify draft creation, draft reuse/finalization, notes, tag targets, stable/prerelease flags, and the latest-release setting. These CLI regressions skip locally only when the CLI is absent. Automation regressions use isolated Git repositories, real Go metadata/tidy commands, and a fake GitHub API; they do not create real issues, tags, or Releases. Updating the pinned version requires updating the workflow, tool version gate, and CLI compatibility coverage together.

Failure and recovery

For preparation failures, rerun Prepare release with the same inputs on the same main source. An existing preparation PR is reused; successful and pending native checks are retained, runs needing authorization are activated, and failed native runs are rerun while preserving their PR event association. If automatic authorization is forbidden, approve workflows in the PR and use the existing runs. A pushed preparation branch whose PR creation failed is reused after its source/request identity is verified. It is never force-pushed. Local metadata is restored on generation/tidy failures. If main advances, preparation creates a fresh plan and PR for the new source.

For publication failures, inspect workflow logs and the release-progress-RUN_ID artifact, including project GoReleaser phase configurations, metadata, and logs. Public-consumer dependency/build caches stay on the runner. Failed consumer checks preserve tags/drafts and leave the tracking issue open. A tag already makes a Go version publicly addressable; a draft Release is not rollback. Never delete, move, or rewrite a conflicting version.

Consumer checks make one attempt. If the proxy is not ready, resolve the condition and rerun Publish Go modules with the same preparation PR and publish checked. Matching tags/Releases are reused and incomplete steps resume; notes are never overwritten. Changed source or scope needs a new preparation PR and version. Workflow installation does not publish the first real version; hosted acceptance requires an approved preparation PR and actual public results.

Sources