Skip to content

Releasing independent Go modules

Several PRs can merge before a version is published. Source merges do not publish modules. Each module has its own version and release-note boundary. The repository publishes Go source modules and GitHub Releases with GoReleaser OSS; Lambda ZIPs remain CI example artifacts. No cloud AWS account is needed.

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 do not appear in individual module notes. 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 release uses its own previous tag; it does not lose changes just because Logger was released first. 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.

Prepare one component or all components

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

Input Value
scope A module directory such as logger, several comma-separated directories such as logger,metrics, or all
bump Leave auto for note-based versioning, or select patch, minor, or major
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 versions, includes unpublished internal dependencies transitively, 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. all selects all maintained public modules, including Commons; development modules and the frozen tracer/xray adapter are excluded.

Review the generated PR's module/version table, included dependencies, notes, initial compatibility statements, and automatic-publication setting. Required checks are dispatched explicitly on its branch using the built-in GitHub token. 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 both required main checks pass, GoReleaser publishes in dependency order and the tracking issue closes after public consumer checks. 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 check creation; 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

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. Each selected module uses its own previous release and can have a different version. Reserved tags, including drafts or tags without Releases, are never reused; preparation advances beyond reserved versions. An explicit major increment can move v0 to v1; v2+ paths are not automated.

Versions live in the manifest and Git tags. 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 scope is the requested components plus required unpublished dependencies. Dependency changes appear in the plan and selected modules' notes, and other consumers receive preparation-PR notes for their next release. 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 either command:

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

These commands create the issue, branch, and PR automatically. Repeat --module to select several components and use --bump patch to override the 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 stay out of module notes.

{
  "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 completed Go CI and Documentation workflows on main push commits. 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 every selected module. Publication is serialized and an active run is not canceled by a newer request.

For each module, publication creates a tag at the selected SHA and uses GoReleaser to create its draft GitHub Release with the reviewed notes. After a real public consumer passes, a second GoReleaser invocation publishes that same draft before proceeding to the next module. 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 Release links and closes the tracking issue.

The built-in GitHub token performs writes. Publication does not depend on a tag-triggered follow-up workflow. Independently versioned modules do not set a repository-wide latest Release label. Prerelease versions create prerelease Releases. v2+ module-path migrations need a separate feature and are rejected by this initial tool.

GoReleaser configuration and independent tags

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 per-module configurations under dist/releases/NAME/MODULE/, 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. It passes each frozen 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. This source-library integration 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; only missing or failed checks are redispatched. 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 per-module GoReleaser phase configurations, metadata, and logs. 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