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 VERSIONwithout 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:
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:
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:
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.