8.8 KiB
ConfigKit Release Procedure
This is the maintainer runbook for ConfigKit's release policy. ConfigKit publishes one source-only Go module. The immutable annotated tag and matching checked-in release note are the deliverables; there is no binary, container image, generated package, or separate registry publication.
Release identity and support
- Module:
gitea.maximumdirect.net/eric/ConfigKit - Primary branch:
main - Authoritative remote:
origin, with fetch and push destinationhttps://gitea.maximumdirect.net/eric/ConfigKit.git - Tags: stable Semantic Versions such as
v0.5.0; no prereleases - Minimum Go line: Go 1.25, validated with one maintained patch
- Release-gated targets: Linux amd64 and arm64
- Best-effort targets: macOS amd64 and arm64, under the platform support policy
The checker runs one offline race suite, one full vet pass, and CGO-disabled builds for both Linux targets. It reports skipped filesystem cases. macOS checks and newer-toolchain tests are optional. CI is never a readiness or completion gate; do not wait for or poll it.
1. Prepare access and tools
Use a clean checkout with Git, Bash 3.2 or newer, a maintained Go 1.25 patch, and a native compiler for race tests. Configure private-host access through the normal credential manager. Keep credentials and environment dumps outside the repository and command transcript.
Verify access without displaying credentials:
git remote get-url origin
git ls-remote origin HEAD
The fetch URL and all configured push destinations must resolve to the authoritative URL above. The publication command checks this again. Acquire the selected toolchain and dependencies explicitly before offline validation:
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.14
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download
A maintained patch may replace the example pin. No per-release latest-version
survey is required. Dependencies must be cached; the checker sets GOPROXY=off.
Publication requires user authorization. Standing authorization for local
commits does not authorize a push or release.
2. Select the version and review compatibility
Choose an unused stable version. Compare the candidate with the previous tag.
Review exported Go signatures and ownership, YAML and scalar semantics,
precedence, diagnostic codes, minimum Go version, and platform support. A
signature-preserving change may still affect consumers. Before v1.0.0,
intentional incompatible minor-release changes need a note explaining impact
and consumer action; patch releases remain compatible within their minor line.
Review the maintained consumer examples and migration guide.
When dependencies change, review parser behavior and migration impact as part of that change. Module tidiness belongs to dependency-change review, not the release checker. Do not repeat a dependency-version survey for every release.
3. Write and commit the release note
Create docs/releases/<version>.md for the selected version. Its first line
must be # ConfigKit <version>. Give a meaningful summary and include
compatibility impact or upgrade instructions when applicable. Link to current
contracts for exact behavior. Review the note and affected canonical documents
as ordinary preparation, then commit the complete candidate.
End every new note with these links, replacing VERSION with the complete tag:
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/docs/oss/attribution.md)
Confirm the three target files exist in the candidate and review the version in the URLs. The future tag need not resolve remotely yet; this review adds no network gate. ConfigKit uses checked-in notes without hosted release objects. If a hosted page is used later, reuse the note and preserve its footer. Do not rewrite historical notes or add downloadable license assets.
4. Publish the committed candidate
Before publication, review compatibility, release-note accuracy, and the file list for unintended content or credentials. Retire completed roadmaps as part of release preparation, preserving unfinished and deferred work and repairing links. These are ordinary preparation tasks, not separate human sign-offs.
With the release explicitly authorized, run the one publication command. The script resolves the repository root from its own path, so it may be invoked from another directory by using its path there. Replace the sample version with the selected version:
./scripts/release.sh v0.5.0
The command requires main, a clean index/worktree, an unused version locally
and on the authoritative remote, and a matching note in the captured commit.
Ignored local files do not block it. It checks remote ancestry, runs
release-check.sh once against that commit,
rechecks candidate and version state, pushes only that commit to main, then
creates and pushes one annotated tag. The tag message is the checked-in note.
It never commits or repairs the checkout.
A successful intended tag push completes publication. The command reports the version and commit immediately. No CI, module retrieval, remote read-back, or hosted release object is required. The checker may be run separately for diagnosis, but it is not a prerequisite: a fresh publication invocation validates its candidate once.
5. Recover from a failed or uncertain push
The command performs one bounded authoritative-state check after a failed push. If the main push failed but authoritative main equals the candidate, it proceeds. If the tag push failed but its exact tag object reached the remote, it reports success. Otherwise it reports branch-only, conflicting, or unknown state and retains any local tag.
Inspect exact refs before any retry. Set release_version to the attempted
version and compare main with the candidate from the command's report:
release_version=v0.5.0
authoritative=https://gitea.maximumdirect.net/eric/ConfigKit.git
if remote_main=$(git ls-remote --refs --exit-code "$authoritative" refs/heads/main); then
printf 'remote main: %s\n' "$remote_main"
else
result=$?
if [ "$result" -eq 2 ]; then
printf 'authoritative main confirmed absent\n' >&2
else
printf 'remote main state unknown; inspect before retrying\n' >&2
fi
fi
ref="refs/tags/$release_version"
if remote_tag=$(git ls-remote --refs --exit-code "$authoritative" "$ref"); then
printf 'remote tag: %s\n' "$remote_tag"
else
result=$?
if [ "$result" -eq 2 ]; then
printf 'remote tag confirmed absent\n'
else
printf 'remote tag state unknown; do not reuse the version\n' >&2
fi
fi
if local_tag=$(git rev-parse --verify "refs/tags/$release_version" 2>/dev/null); then
printf 'local tag object: %s\n' "$local_tag"
else
printf 'no local tag object\n'
fi
A query failure is not proof that the tag is absent. Compare the remote tag object ID with the local annotated tag object ID; its peeled commit alone is insufficient to establish identical publication. A conflicting published tag must not be moved or replaced. If remote main advanced but the tag was not published, report that partial progress. Only after confirming the local tag was never published or used by another channel may a maintainer remove it:
git tag -d "$release_version"
A subsequent invocation revalidates the candidate; a correctly published tag needs no rerun. Do not roll back main or automatically delete a tag. Transport or authentication failure alone does not require a new version. A defect in published source requires a corrected commit, a new semantically appropriate version and note, and the release procedure again.
6. Optional consumer verification
Only when useful or requested, read back the remote tag or retrieve the exact
version into a new module without a local replace directive. These checks
are optional and non-gating. Do not poll CI or wait for module propagation.
Private-host credentials must already be configured. For direct-origin
verification, set the version to the release just published (replace the sample
below), then use a temporary consumer module and check the exact module path
and version reported by go list -m:
release_version=v0.5.0
consumer_dir=$(mktemp -d)
(
cd "$consumer_dir"
go mod init example.invalid/configkit-release-check
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
)
The output should show the exact module path and selected version. Report this optional check separately from publication success.