Add guarded single-command release publication
This commit is contained in:
299
docs/release.md
299
docs/release.md
@@ -1,39 +1,34 @@
|
||||
# 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.
|
||||
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`, expected to fetch from
|
||||
- Authoritative remote: `origin`, with fetch and push destination
|
||||
`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
|
||||
- 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](policy/release.md#platform-support)
|
||||
|
||||
The local Linux checker includes one offline race suite, one full vet pass, and
|
||||
CGO-disabled amd64/arm64 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.
|
||||
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
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
```sh
|
||||
@@ -41,9 +36,9 @@ 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:
|
||||
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:
|
||||
|
||||
```sh
|
||||
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.14
|
||||
@@ -51,205 +46,127 @@ 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.
|
||||
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 and compare the candidate with the previous tag.
|
||||
For the first release, review the whole public surface and current contracts.
|
||||
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.
|
||||
|
||||
```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
|
||||
```
|
||||
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.
|
||||
|
||||
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.
|
||||
## 3. Write and commit the release note
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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):
|
||||
End every new note with these links, replacing `VERSION` with the complete 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.
|
||||
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. Validate the source candidate
|
||||
## 4. Publish the committed 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
```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)"
|
||||
./scripts/release.sh v0.5.0
|
||||
```
|
||||
|
||||
If the final candidate is already committed, omit the staging and commit commands;
|
||||
do not create an empty commit. The checker runs one full offline race suite with
|
||||
verbose skip reporting and test-time vet disabled, one full vet pass, and
|
||||
CGO-disabled Linux amd64/arm64 builds. Go commands use readonly module mode.
|
||||
The suite includes examples, consumer fixtures, documentation links, and package
|
||||
boundaries; no separate reruns are required. Formatting and whitespace remain
|
||||
ordinary development checks in `scripts/check.sh`; module tidiness is reviewed
|
||||
when dependencies change. No additional linter is claimed. Sibling-consumer links
|
||||
are checked when those checkouts exist and are otherwise inapplicable in an
|
||||
isolated checkout.
|
||||
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`](../scripts/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 changed candidate requires validation again. Do not reuse a recorded candidate
|
||||
from an older revision. The record is written only after successful local checks.
|
||||
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. Prepare publication
|
||||
## 5. Recover from a failed or uncertain push
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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:
|
||||
Inspect exact refs before any retry. Set `release_version` to the attempted
|
||||
version and compare main with the candidate printed or recorded from the run:
|
||||
|
||||
```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
|
||||
release_version=v0.5.0
|
||||
authoritative=https://gitea.maximumdirect.net/eric/ConfigKit.git
|
||||
git ls-remote --refs --exit-code "$authoritative" refs/heads/main
|
||||
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
|
||||
test $? -eq 2
|
||||
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
|
||||
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)"
|
||||
git rev-parse --verify "refs/tags/$release_version"
|
||||
```
|
||||
|
||||
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`.
|
||||
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:
|
||||
|
||||
```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"
|
||||
git tag -d "$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.
|
||||
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.
|
||||
|
||||
## 8. Optional post-publication checks
|
||||
## 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; 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.
|
||||
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, use a temporary consumer module and check the exact module path
|
||||
and version reported by `go list -m`. Report optional verification separately
|
||||
from publication success.
|
||||
|
||||
Reference in New Issue
Block a user