Files
ConfigKit/docs/release.md

253 lines
12 KiB
Markdown

# ConfigKit Release Procedure
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
ConfigKit publishes one source-only Go module: the immutable source tag and the
matching checked-in release note are the deliverables. There is no application
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`, expected to fetch from
`https://gitea.maximumdirect.net/eric/ConfigKit.git`
- Tags: stable Semantic Versions such as `v0.1.0`; prerelease tags are not supported
- Minimum Go language/toolchain line: Go 1.25, release-validated with one maintained
patch of that line; newer-toolchain testing is optional and non-gating
- Release-gated build targets: Linux on amd64 and arm64
- Best-effort targets: macOS on amd64 and arm64, under the
[platform support policy](policy/release.md#platform-support)
The local Linux checker includes native race tests and CGO-disabled cross-builds.
Its output reports skipped filesystem cases; no separate evidence record or
human sign-off is required. Native macOS checks and Darwin cross-builds are
optional and do not gate publication.
CI is never a release gate. Do not wait for it, poll its status, or delay reporting
completion because it is pending, unavailable, or failed.
## 1. Prepare access and tools
The release maintainer owns this procedure. Use a clean checkout (a dedicated clone is optional) with Git, Bash 3.2 or newer,
a maintained Go 1.25 patch, and the compiler needed for native race tests. The checker supports the stock macOS
Bash; its filename enumeration is null-delimited, including names with whitespace. Configure private-host Git and Go module
access through the normal credential manager before starting. Keep tokens, passwords,
SSH material, and environment dumps outside the repository and command transcript.
Verify access without displaying credentials:
```sh
git remote get-url origin
git ls-remote origin HEAD
```
The remote URL must equal the authoritative URL above. Acquire the selected
toolchain and dependencies explicitly. For example, with the maintained patch
used when this procedure was simplified:
```sh
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.14
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download
```
Keep toolchains maintained, but do not require latest-version discovery for each
release. The checker sets `GOPROXY=off` and fails if the cache is incomplete.
Toolchain and YAML-dependency update research belongs to periodic maintenance or
relevant dependency changes. No renewed approval of an unchanged pin is required.
Use Bash with `set -euo pipefail` for the command blocks below so any failed gate
stops the sequence. Publication requires user authorization; the agent policy's
local-commit authorization is not permission to push or release.
## 2. Select the version and review compatibility
Choose an unused stable version and compare the candidate with the previous tag.
For the first release, review the whole public surface and current contracts.
```sh
release_version=v0.1.0
printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
if git show-ref --verify --quiet "refs/tags/$release_version"; then exit 1; fi
if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then
printf 'remote tag already exists: %s\n' "$release_version" >&2
exit 1
else
test $? -eq 2
fi
git log --oneline --decorate --no-merges
```
Review exported Go signatures and ownership, YAML and scalar behavior, precedence,
diagnostic codes, minimum Go version, and platform support. Before `v1.0.0`, any
intentional incompatible minor-release change must be explained with consumer action;
patch releases must remain compatible within their minor line. Review the maintained
consumer examples and the migration guide even when signatures did not change.
When dependencies change, review parser behavior and migration impact as part of
that change. Keep Compatibility accurate about the dependency pin, but do not
perform an external version survey for every patch release.
## 3. Write the release note
Create `docs/releases/$release_version.md` before selecting the candidate. It must
begin with `# ConfigKit $release_version` and contain `Summary`, `Compatibility`,
`Upgrade`, and `Changes` sections. Use “No action required” where appropriate. Link
to current canonical contracts instead of copying their definitions. Do not create a
release note until there is a real candidate and reviewed content.
End each new release note with the footer required by the
[OSS policy](policy/oss.md#discoverability). For the selected version, use these
absolute Gitea URLs (replace every `VERSION` with the complete intended tag):
```markdown
[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 the version is correct
during ordinary note review. The future tag need not resolve remotely yet; do
not add publication-time network checks. ConfigKit publishes source tags with
checked-in notes, not separate hosted download pages. If a hosted release page
is used later, reuse the checked-in note as its body and retain this footer.
Do not retrofit published notes or add separate downloadable license assets.
## 4. Validate the source candidate
Before release validation, finish compatibility review, inspect the diff and file
list for unintended content or credentials, retire completed roadmaps, repair
links, and review the release note. These are ordinary preparation tasks the
executing agent may perform, not separate human approvals or evidence records.
Ask the maintainer only for an unresolved product decision or missing authority.
Commit the candidate, staging only its intended changes, then run the checker
once against that clean commit. Do not run both everyday and release checkers to
repeat the same gates.
```sh
git status --short
# Stage the specific files belonging to the candidate, then review:
git diff --staged
git commit --message "Prepare ConfigKit $release_version"
test -z "$(git status --porcelain=v1 --untracked-files=all)"
candidate_commit=$(git rev-parse HEAD)
./scripts/release-check.sh
test "$(git rev-parse HEAD)" = "$candidate_commit"
test -z "$(git status --porcelain=v1 --untracked-files=all)"
printf '%s\n' "$candidate_commit" > "$(git rev-parse --git-path configkit-release-candidate)"
```
If the final candidate is already committed, omit the staging and commit commands;
do not create an empty commit. The checker validates module manifests in a
disposable copy, formatting, whitespace, the full offline suite once with the race
detector and verbose skip reporting, vet once, and CGO-disabled Linux amd64/arm64
builds. The suite includes examples, consumer fixtures, documentation links, and
package boundaries; no separate reruns are required. No additional linter is
claimed. Sibling-consumer links are checked when those checkouts exist and are
otherwise inapplicable in an isolated checkout.
A changed candidate requires validation again. Do not reuse a recorded candidate
from an older revision. The record is written only after successful local checks.
## 5. Prepare publication
The following guarded sequence pushes only `main` and the selected tag. Local
validation failures block it; CI does not. Keep the selected `release_version`
from step 2 in the same shell. If starting a new shell, set it explicitly again.
## 6. Run publication guards
Immediately before tagging, fetch the authoritative branch and tags, restore the
recorded variables if this is a new shell, and execute every guard:
```sh
candidate_commit=$(cat "$(git rev-parse --git-path configkit-release-candidate)")
git fetch --prune origin main
git fetch --tags origin
test "$(git branch --show-current)" = main
test -z "$(git status --porcelain=v1 --untracked-files=all)"
test "$(git rev-parse HEAD)" = "$candidate_commit"
test "$(git remote get-url origin)" = 'https://gitea.maximumdirect.net/eric/ConfigKit.git'
printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
test -f "docs/releases/$release_version.md"
test "$(sed -n '1p' "docs/releases/$release_version.md")" = "# ConfigKit $release_version"
for heading in Summary Compatibility Upgrade Changes; do
grep -Fxq "## $heading" "docs/releases/$release_version.md"
done
if git show-ref --verify --quiet "refs/tags/$release_version"; then exit 1; fi
if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then
printf 'remote tag already exists: %s\n' "$release_version" >&2
exit 1
else
test $? -eq 2
fi
git push origin HEAD:refs/heads/main
git fetch origin main
test "$(git rev-parse refs/remotes/origin/main)" = "$candidate_commit"
test "$(git rev-parse HEAD)" = "$candidate_commit"
test -z "$(git status --porcelain=v1 --untracked-files=all)"
```
The recorded local validation must apply to exactly `$candidate_commit`. No
separate human sign-off, CI status, or manual evidence document is required.
## 7. Tag and publish the exact ref
Create one annotated tag at the guarded commit, verify its peeled target, and push
only that ref. Do not use `git push --tags`.
```sh
git tag --annotate "$release_version" --message "ConfigKit $release_version" "$candidate_commit"
test "$(git rev-list -n 1 "$release_version")" = "$candidate_commit"
git push origin "refs/tags/$release_version:refs/tags/$release_version"
```
A successful push of the intended tag completes the release. Report success now;
do not wait for CI, module propagation, or any post-publication check. No Gitea
release object or binary upload is required.
## 8. Optional post-publication checks
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; their failure or unavailability does not delay release
completion. Do not poll CI or wait for module propagation. Private-host credentials
must already be configured. Example direct-origin verification:
```sh
remote_tag_commit=$(git ls-remote origin "refs/tags/$release_version^{}" | awk '{print $1}')
test "$remote_tag_commit" = "$candidate_commit"
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"
cat > main.go <<'EOF'
package main
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
func main() {
_ = configkit.NoSource()
}
EOF
gofmt -w main.go
go test ./...
go build ./...
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
)
rm -rf -- "$consumer_dir"
```
If performed, the final `go list` output should show the exact module path and
`$release_version`. Report optional verification separately from publication success.
## 9. Handle failures without replacement
Before publication, fix and commit the candidate, validate it, record the new
exact candidate, and repeat the guarded publication sequence. An unpublished local tag may be deleted only after verifying it
was never pushed or used by another release channel.
After publication, never move, delete, or recreate the tag and never replace its
release note. Correct the defect on `main`, choose a new semantically appropriate
version, and repeat this procedure. A supported advisory or deprecation mechanism
may warn consumers about the old version but cannot change its contents.