Simplify local release validation and publication

This commit is contained in:
2026-09-23 18:10:37 +00:00
parent c5a2ddee7a
commit aead20fae4
6 changed files with 273 additions and 276 deletions

View File

@@ -90,8 +90,10 @@ Use focused checks while iterating, with validation proportionate to the change
under the testing policy. Documentation-only work normally needs the local-link
check below and whitespace review; behavior-bearing examples and commands also
need execution. Use `go test -race ./...` for ownership or concurrency changes.
The separate [release checker](release.md#4-validate-the-source-candidate) adds
two-toolchain, module-hygiene, race, and cross-build gates.
The [release checker](release.md#4-validate-the-source-candidate) performs one
minimum-toolchain race-suite run, vet, module hygiene, and Linux cross-builds.
It already covers examples, links, and package boundaries; do not additionally
run the everyday checker just to repeat those checks. CI never gates a release.
## Task-to-test map

View File

@@ -21,7 +21,8 @@ ConfigKit adopts a source-only Go module release model. The source tag and
version-matched release note are the declared deliverables. Standalone binaries,
container images, and separate binary/package-registry publication are not
required. This does not waive source compilation, module hygiene, testing, or
consumer-facing module retrieval and build verification.
local target-build validation. Post-publication consumer retrieval is optional,
as documented under Policy Exceptions.
The release procedure must define the module path, authoritative remote,
supported Go versions and operating-system/architecture targets, prerequisite
@@ -74,6 +75,10 @@ responsibility is documented. Automation must not silently change the source
candidate, move a published tag, replace a published deliverable, or repair a
release in place.
A successful push of the intended annotated tag completes the source-only release.
Never wait for CI or use its status as a readiness or completion gate. Optional
post-publication checks must not delay reporting success.
## Versioning And Compatibility
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with
@@ -132,38 +137,28 @@ current documentation or defer required documentation changes.
## Candidate Preparation And Validation
Each repository must provide one documented release-check command or script
that exercises the complete set of automated source-candidate gates. The
repository-specific procedure must state all additional manual gates.
ConfigKit provides one local release checker. Run it once against the final
committed candidate using one maintained patch of the minimum supported Go line
on Linux. It checks module hygiene, formatting and whitespace, the full offline
suite with the race detector, static analysis, and CGO-disabled Linux amd64/arm64
builds. Examples, documentation links, consumer fixtures, and package boundaries
are covered by that suite, not separate repeated runs. Native package compilation
is covered by the test run. A second toolchain is optional and non-gating.
The required gates are:
Commit the release note and associated changes before release validation. Record
the exact validated commit; revalidate if the candidate changes. Do not require
both pre-commit and post-commit full checker runs for unchanged content.
Do not publish uncommitted or unvalidated content. Local check failures still
block publication. Skipped test cases are reported, not separate human sign-off
requirements.
- dependency or module hygiene appropriate to the language and ecosystem;
- the complete deterministic, offline default test suite;
- race, concurrency, or equivalent safety checks where the implementation can
be meaningfully checked for those risks;
- static analysis and linting used by the repository;
- production builds for every supported target that can be built in the
release environment;
- formatting and whitespace checks;
- validation of maintained examples, configuration, schemas, generated
sources, and documentation commands where applicable;
- review of changed local documentation links;
- removal of obsolete roadmap documents under `docs/roadmap/` that describe
completed work included in the release;
- review for unintended files, generated output, credentials, private data,
and unrelated changes; and
- any additional security, compatibility, migration, packaging, or platform
checks required by the product.
Unavailable tooling or an inapplicable check must be addressed explicitly in
the adopted policy or release procedure; it must not disappear silently from
the candidate gates.
Run the complete checker while preparing the candidate. Commit the release
note and all associated code, tests, documentation, and configuration, then
run the complete checker again against the exact commit intended for release.
Do not publish a release from uncommitted or unvalidated content.
Compatibility review, release-note accuracy, removal of completed roadmaps, and
review for unintended files, credentials, private data, and unrelated changes
are ordinary preparation. The executing agent may perform them without routine
human approval or separate manual evidence records. Ask the maintainer only for
genuine unresolved decisions or missing authority. A clean existing checkout is
sufficient; a dedicated clone is optional. Dependency and toolchain update
research belongs to maintenance or relevant changes, not every release.
## Publication Guards
@@ -178,7 +173,8 @@ verify all of the following:
- the authoritative remote branch points to that same commit;
- the matching release note exists in that commit and has the required title
and sections;
- all required approvals and automated gates succeeded for that commit; and
- release publication is authorized and the local automated gates succeeded for
that commit; and
- no local or remote tag already uses the selected version.
Create the version tag against the guarded commit and verify its target before
@@ -219,17 +215,17 @@ order:
1. maintainer prerequisites and required access;
2. version selection and compatibility review;
3. release-note creation;
4. candidate preparation and the complete validation command;
5. candidate commit, push, and exact commit recording;
4. candidate preparation and commit;
5. one local validation pass, exact candidate recording, and push;
6. publication guards;
7. exact tag and deliverable publication commands;
8. authoritative remote and consumer-facing verification; and
8. completion on successful tag push and optional post-publication checks; and
9. failure handling and correction.
The procedure should use copyable, fail-fast commands where practical. It must
name the primary branch, authoritative remote, tag form, published
deliverables, supported build targets, release automation responsibilities,
and consumer verification path. It must link to this policy instead of
and optional consumer verification path. It must link to this policy instead of
restating or redefining its requirements.
## Policy Exceptions
@@ -240,6 +236,15 @@ compensating controls. Customizing release mechanics must not silently weaken
the policy's integrity, validation, documentation, verification, or immutability
requirements.
For ConfigKit's source-only releases, the maintainer accepts successful publication
of the intended tag as sufficient completion evidence. This is an explicit
exception to the shared standard's mandatory post-publication verification:
authoritative tag read-back and fresh consumer retrieval are optional and
non-gating. CI status and downstream retrieval availability must not delay release
completion. Compensating controls are local validation of the exact candidate,
automated pre-publication guards, matching remote main, verification of the local
annotated tag target, explicit ref pushes, and immutable published versions.
For v0.1.1 only, the maintainer approved renaming the reserved `_configkit` key to
`configkit` as an error-correcting point release because there are no current
consumers. This intentionally changes the v0.1.0 configuration contract without a
@@ -253,5 +258,5 @@ only. The final candidate still requires the complete Linux release checker.
This exception waives only repeating the macOS check after those documentation
changes; it does not waive other publication or verification gates.
Candidate selection, publication, and post-publication verification remain
mandatory steps in the [release procedure](../release.md).
Candidate selection and guarded publication remain mandatory; post-publication
verification is optional in the [release procedure](../release.md).

View File

@@ -12,21 +12,24 @@ binary, container image, generated package, or separate registry publication.
- 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, tested with a supported patched
Go 1.25 toolchain and one newer supported toolchain
- 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 automated checker cross-builds the Linux targets. Before release, the maintainer
must also obtain native test, race, symlink, and permission-fixture evidence on Linux;
cross-compilation is not native execution evidence. Native macOS checks and Darwin
cross-builds are optional and do not gate publication.
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. Work in a dedicated clone with Git, Bash 3.2 or newer,
and two supported Go toolchains available. The checker supports the stock macOS
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.
@@ -37,23 +40,24 @@ git remote get-url origin
git ls-remote origin HEAD
```
The remote URL must equal the authoritative URL above. Select the latest supported
patch in the Go 1.25 line and a newer supported toolchain. The values below are the
toolchains used while this runbook was introduced; update them to supported patch
releases during candidate preparation and record the actual versions in the release
note or validation record.
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.5
export CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.14
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download
GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go mod download
```
Those commands explicitly acquire toolchains and dependencies. The checker itself
sets `GOPROXY=off` and fails if its required cache is incomplete.
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
@@ -63,7 +67,7 @@ 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]*)$'
git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || :
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
@@ -79,16 +83,9 @@ intentional incompatible minor-release change must be explained with consumer ac
patch releases must remain compatible within their minor line. Review the maintained
consumer examples and the migration guide even when signatures did not change.
Reassess `go.yaml.in/yaml/v4` rather than using `@latest` implicitly:
```sh
go list -m -versions go.yaml.in/yaml/v4
go list -m -json go.yaml.in/yaml/v4@latest
```
If changing the explicit pin, make that a reviewed dependency change and rerun the
complete checker, especially parser policy, limits, diagnostics, and rendering tests.
If retaining a prerelease pin, record that decision in Compatibility.
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
@@ -100,55 +97,46 @@ release note until there is a real candidate and reviewed content.
## 4. Validate the source candidate
Run the complete checker on Linux from the repository root after dependencies have been
acquired. It validates module manifests in a disposable copy, both supported Go
toolchains, offline default tests, the native race detector, vet, native builds,
CGO-disabled Linux cross-builds, formatting and whitespace (including untracked files),
examples, package isolation, and local documentation links.
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
./scripts/release-check.sh
```
`go vet` is the repository's static-analysis baseline. No additional linter is
claimed. Links to current ConfigKit files and headings are strict. Recorded links to
sibling consumer source are checked when those checkouts exist and are otherwise
inapplicable in an isolated clone; their immutable revisions remain recorded in the
extraction inventory.
Complete these manual gates and record their evidence with the candidate:
- The maintainer reviews compatibility and downstream migration impact against
`docs/config.md`, Go API comments, and the consumer guide.
- The maintainer inspects `git status --short`, `git diff`, and the candidate file
list for credentials, private data, generated output, unintended files, and
unrelated changes.
- The maintainer records a successful native checker run on Linux, including
filesystem capability tests. Missing required Linux evidence blocks release;
no macOS run, sign-off, or cross-build result is required.
- The maintainer removes completed, obsolete documents under `docs/roadmap/`, repairs
inbound links, and reruns the checker. ADR rationale remains historical.
- The maintainer reviews the exact version-matched release note for accuracy and
confirms it describes only deliverables that will actually be published.
## 5. Commit, push, and record the candidate
Commit all candidate source, contract, example, roadmap-retirement, and release-note
changes, rerun the checker against that clean commit, then push only `main` and record
the exact commit in Git metadata local to the clone:
```sh
git add .
git status --short
# Stage the specific files belonging to the candidate, then review:
git diff --staged
git commit --message "Prepare ConfigKit $release_version"
./scripts/release-check.sh
test -z "$(git status --porcelain=v1 --untracked-files=all)"
git push origin HEAD:refs/heads/main
git rev-parse HEAD | tee .git/configkit-release-candidate
candidate_commit=$(cat .git/configkit-release-candidate)
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 review changes the source after this point, create and validate a new candidate;
do not reuse the recorded commit.
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
@@ -156,14 +144,12 @@ Immediately before tagging, fetch the authoritative branch and tags, restore the
recorded variables if this is a new shell, and execute every guard:
```sh
release_version=v0.1.0
candidate_commit=$(cat .git/configkit-release-candidate)
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 rev-parse refs/remotes/origin/main)" = "$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"
@@ -171,17 +157,22 @@ test "$(sed -n '1p' "docs/releases/$release_version.md")" = "# ConfigKit $releas
for heading in Summary Compatibility Upgrade Changes; do
grep -Fxq "## $heading" "docs/releases/$release_version.md"
done
git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || :
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)"
```
Confirm the recorded automated and manual approvals apply to exactly
`$candidate_commit`. A successful older run is not evidence for a changed commit.
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
@@ -194,15 +185,17 @@ test "$(git rev-list -n 1 "$release_version")" = "$candidate_commit"
git push origin "refs/tags/$release_version:refs/tags/$release_version"
```
The pushed tag completes the declared source-only publication. No Gitea release
object or binary upload is required.
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. Verify publication and clean consumer retrieval
## 8. Optional post-publication checks
Verify the authoritative tag and then retrieve that exact version into a new module
without a local `replace` directive. Private-host credentials must already be
configured. Use direct origin retrieval unless an explicitly approved internal Go
proxy is the publication channel.
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}')
@@ -229,13 +222,13 @@ EOF
rm -rf -- "$consumer_dir"
```
The final `go list` output must show the exact module path and `$release_version`.
Record the result with the release evidence.
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 the candidate, rerun every gate, commit, push, and record a
new exact candidate. An unpublished local tag may be deleted only after verifying it
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

View File

@@ -1,139 +1,26 @@
# Release Workflow Simplification
Status: approved target, not yet implemented.
Status: implemented; retain this completion record until release preparation.
## Intent and scope
## Outcome
ConfigKit should have the minimum release gates reasonably needed for a small
source-only Go library. Favor inexpensive local automation over repeated checks
and human intervention. A successful push of the intended release tag completes
the release; CI reliability and timing must never determine release readiness
or completion.
The maintainer-approved simplification is implemented in the
[release policy](../policy/release.md), [procedure](../release.md), and
[checker](../../scripts/release-check.sh). Those documents own current requirements.
This roadmap records the maintainer-approved outcome of the release-gate audit.
It does not itself update the [release policy](../policy/release.md),
[procedure](../release.md), or [checker](../../scripts/release-check.sh).
Those owners must be brought into agreement when this work is implemented.
There are no library API, configuration, or runtime behavior changes in scope.
The workflow uses one minimum-toolchain local race-suite run, vet, module hygiene,
formatting/whitespace, and Linux cross-builds. Redundant suite/subset runs and
routine human sign-offs have been removed. CI never gates publication or
completion; a successful push of the intended tag completes the release.
Post-publication verification is optional under the explicit policy exception.
## Target release workflow
The [check-script and runbook tests](../../release_check_test.go) cover the
reduced command set, failure handling, and publication guards using disposable
local repositories. Implementation does not authorize an actual release.
1. Prepare the version-matched release note, review the changes, retire completed
roadmap material where applicable, and commit the final candidate.
2. Validate that committed candidate with one local release-check invocation.
3. Run automated publication guards for the clean checkout, candidate identity,
primary branch, authoritative remote, release-note structure, and unused version.
4. Push the candidate to `main`, verify the remote branch matches the candidate,
create the annotated tag at that exact commit, and push only that tag ref.
5. Report the release complete as soon as the intended tag push succeeds.
## Scope
The candidate must remain the exact validated commit through publication. A
changed candidate requires validation again, but there is no mandatory pair of
full runs before and after committing unchanged content. A clean existing
checkout is sufficient; a dedicated clone is optional.
Retain immutable published tags, version selection and compatibility rules,
version-matched release notes, and explicit push targets. Do not add a separate
Gitea release object, binary upload, or other deliverable to this source-only model.
## Local automated gates
Use one maintained patch of the minimum supported Go toolchain line (currently
Go 1.25) in a local Linux environment. Toolchains and dependencies are acquired
explicitly; the checker remains offline. Toolchain maintenance is not a mandatory
latest-version lookup on every release.
| Check | Target treatment and reason |
| --- | --- |
| Full default test suite | Run once with `go test -race ./...`; preserve race coverage for shared-state isolation without a separate ordinary full-suite run. |
| Static analysis | Run `go vet ./...` once; it covers more than the default vet subset run by `go test`. |
| Module hygiene | Retain the disposable-copy tidy comparison without changing candidate manifests. |
| Formatting and whitespace | Retain inexpensive local checks. |
| Linux amd64/arm64 builds | Retain CGO-disabled cross-builds; these cover advertised targets and configurations distinct from the native race-test build. |
| Examples, consumer fixtures, documentation links, and package boundaries | Keep their tests in the full suite; remove separate release-time reruns already covered by that suite. |
| Separate native production builds | Remove the extra native `go build` gates; this library's packages are compiled by the test run. |
| Second Go toolchain | Make testing on a newer toolchain optional and non-gating. |
| Native filesystem evidence | Use the local suite results rather than separate manual certification. Report skipped cases without automatically requiring human approval. |
The local release suite must still pass. Removing redundant gates does not mean
ignoring actual local test failures. macOS remains best-effort, with no required
native validation or Darwin cross-builds.
Avoid requiring the everyday checker in addition to the release checker when it
would merely repeat the same checks. Do not introduce an approval service,
attestation framework, or new release dependency to implement this simplification.
## Preparation without human sign-off gates
Compatibility and version selection, release-note accuracy, reviewing the diff
for credentials or unintended content, and retiring completed roadmaps remain
normal release preparation. The executing agent may perform these tasks without
seeking routine maintainer approval or producing separate manual evidence records.
Only genuine unresolved product decisions or missing authority require a question
to the maintainer. Preserve authorization boundaries for pushing and releasing;
the standing local-commit policy alone is not publication authorization.
Move toolchain update research and reassessment of available YAML dependency
versions to periodic maintenance or relevant dependency changes. Do not require
external version research or renewed approval of an unchanged dependency pin for
every patch release. Dependency changes remain reviewed changes with applicable
validation and compatibility documentation.
## Publication is the completion condition
Never wait for CI, poll CI until it finishes, require green CI status, or treat a
missing, slow, unreliable, or failed CI run as a release blocker. This applies
both before publication and after a successful tag push. Optional CI may provide
maintenance feedback independently; it must not be a hidden release gate.
Fresh consumer-module retrieval, remote tag read-back after a successful push,
and other post-publication smoke checks are optional and non-gating. Do not wait
for module proxy propagation or credential-dependent consumer downloads before
reporting success. Publication push failures still mean publication has not been
confirmed; the completion rule requires a successful push of the intended tag.
An optional later check finding a defect does not authorize replacing the tag.
Use the existing immutable-release correction rules and an appropriate new
version for any correction.
## Explicit policy exception
The adopted release policy must record the deliberate exception to the shared
standard's mandatory post-publication verification requirement:
- Scope: ConfigKit's source-only tag publication; subsequent authoritative
read-back and fresh consumer retrieval are optional, not completion gates.
- Rationale: the maintainer accepts a successful intended tag push as sufficient
publication evidence and rejects dependence on CI or downstream retrieval
availability for release completion.
- Compensating controls: local validation of the exact candidate, automated
pre-publication guards, matching remote `main`, an annotated tag with verified
local target, explicit ref pushes, and immutable published versions.
Record this exception openly rather than retaining contradictory mandatory
verification wording elsewhere. Preserve historical release notes and scoped
historical exceptions unchanged.
## Affected owners and completion criteria
Implementation must reconcile the release policy, release procedure, checker,
relevant [check-script tests](../../release_check_test.go), and any affected
[development guidance](../development.md). Current documentation must describe
only the implemented workflow once the change lands.
Completion requires:
- One local validation pass for the final candidate, with the retained checks
and no duplicate full-suite or focused subset runs.
- No required human sign-off, dedicated clone, second toolchain, latest-version
research, CI waiting, or post-publication consumer verification.
- Automated safeguards still prevent publishing an unvalidated or unintended
candidate or reusing an existing version.
- Tests and documentation agree on the reduced workflow, including failure
handling and completion immediately after successful tag publication.
- Validation of the changes uses local tests and disposable fixtures; implementing
this roadmap does not itself authorize a real release or remote publication.
There are no outstanding product decisions for this target. A later implementation
plan may sequence the work without expanding these gates or guarantees.
No library API, configuration, or runtime behavior changes were made. Published
tag immutability, release notes, compatibility rules, and exact-candidate
publication safeguards remain in place. There is no remaining implementation
work or unresolved product decision for this roadmap.

View File

@@ -9,6 +9,120 @@ import (
"testing"
)
// Exercise the copyable runbook against disposable Git repositories, not a
// publication service. The checker itself is covered separately.
func TestReleaseRunbookGuards(t *testing.T) {
document, err := os.ReadFile("docs/release.md")
if err != nil {
t.Fatal(err)
}
block := func(heading string) string {
t.Helper()
_, section, ok := strings.Cut(string(document), heading)
if !ok {
t.Fatalf("missing section %q", heading)
}
_, section, ok = strings.Cut(section, "```sh\n")
if !ok {
t.Fatalf("missing commands in %q", heading)
}
commands, _, ok := strings.Cut(section, "```")
if !ok {
t.Fatalf("unterminated commands in %q", heading)
}
return commands
}
prepare := block("## 4. Validate the source candidate")
publish := block("## 6. Run publication guards") + block("## 7. Tag and publish the exact ref")
for _, scenario := range []string{"success", "dirty", "changed-candidate", "remote-tag-exists", "failed-validation"} {
t.Run(scenario, func(t *testing.T) {
root := t.TempDir()
remote := filepath.Join(t.TempDir(), "remote.git")
run := func(program string, args ...string) string {
t.Helper()
cmd := exec.Command(program, args...)
cmd.Dir = root
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %v: %v\n%s", program, args, err, out)
}
return strings.TrimSpace(string(out))
}
run("git", "init", "-q", "-b", "main")
run("git", "init", "-q", "--bare", remote)
run("git", "config", "user.name", "Test")
run("git", "config", "user.email", "test@example.invalid")
run("git", "config", "commit.gpgsign", "false")
run("git", "config", "tag.gpgsign", "false")
const origin = "https://gitea.maximumdirect.net/eric/ConfigKit.git"
run("git", "remote", "add", "origin", remote)
// Substitute only the fixture's authoritative URL; execute the guards
// themselves unchanged. Disallow network transports as a backstop.
localPublish := strings.ReplaceAll(publish, origin, remote)
for _, dir := range []string{"docs/releases", "scripts"} {
if err := os.MkdirAll(filepath.Join(root, dir), 0700); err != nil {
t.Fatal(err)
}
}
write := func(path, content string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, path), []byte(content), 0700); err != nil {
t.Fatal(err)
}
}
checker := "#!/bin/sh\nexit 0\n"
if scenario == "failed-validation" {
checker = "#!/bin/sh\nexit 37\n"
}
write("scripts/release-check.sh", checker)
write("docs/releases/v0.9.0.md", "# ConfigKit v0.9.0\n\n## Summary\n\n## Compatibility\n\n## Upgrade\n\n## Changes\n")
run("git", "add", ".")
bash := func(commands string) ([]byte, error) {
cmd := exec.Command("bash", "-c", "set -euo pipefail\nrelease_version=v0.9.0\n"+commands)
cmd.Dir = root
cmd.Env = append(os.Environ(), "GIT_ALLOW_PROTOCOL=file")
return cmd.CombinedOutput()
}
out, err := bash(prepare)
if scenario == "failed-validation" {
if err == nil {
t.Fatal("failed validation accepted")
}
if _, err := os.Stat(filepath.Join(root, ".git/configkit-release-candidate")); !os.IsNotExist(err) {
t.Fatal("failed validation recorded a candidate")
}
} else if err != nil {
t.Fatalf("prepare: %v\n%s", err, out)
}
switch scenario {
case "dirty":
write("unexpected.txt", "uncommitted\n")
case "changed-candidate":
run("git", "commit", "--allow-empty", "-qm", "Changed candidate")
case "remote-tag-exists":
run("git", "tag", "v0.9.0")
run("git", "push", remote, "refs/tags/v0.9.0")
run("git", "tag", "-d", "v0.9.0")
}
// Seed the remote primary branch so fetch works before first publication.
run("git", "push", remote, "HEAD:refs/heads/main")
out, err = bash(localPublish)
if scenario == "success" {
if err != nil {
t.Fatalf("publish: %v\n%s", err, out)
}
if got := run("git", "--git-dir="+remote, "rev-parse", "v0.9.0^{}"); got != run("git", "rev-parse", "HEAD") {
t.Fatal("published wrong commit")
}
} else if err == nil {
t.Fatalf("publication accepted %s", scenario)
} else if scenario != "remote-tag-exists" && run("git", "--git-dir="+remote, "tag", "--list") != "" {
t.Fatal("failed guard published a tag")
}
})
}
}
func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
for _, scriptName := range []string{"release-check.sh", "check.sh"} {
t.Run(scriptName, func(t *testing.T) {
@@ -66,7 +180,7 @@ func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
t.Fatal(err)
}
cmd := exec.Command("/bin/bash", filepath.Join(root, "scripts", scriptName))
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), "CONFIGKIT_BASELINE_TOOLCHAIN=test-baseline", "CONFIGKIT_NEWER_TOOLCHAIN=test-newer", "CONFIGKIT_TEST_FMT_LOG="+log, "CONFIGKIT_TEST_GO_LOG="+calls)
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), "CONFIGKIT_BASELINE_TOOLCHAIN=test-baseline", "CONFIGKIT_NEWER_TOOLCHAIN=", "CONFIGKIT_TEST_FMT_LOG="+log, "CONFIGKIT_TEST_GO_LOG="+calls)
// Log only targets selected by the script, not inherited host settings.
cmd.Env = append(cmd.Env, "GOOS=", "GOARCH=")
if name == "failed-go-check" {
@@ -104,6 +218,12 @@ func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
t.Fatal("checker continued after failed test")
}
if scriptName == "release-check.sh" && !fail {
if strings.Count(string(invoked), " test ") != 1 || !strings.Contains(string(invoked), " test -race -v ./...\n") {
t.Fatalf("want one complete race-test run, got:\n%s", invoked)
}
if strings.Count(string(invoked), " vet ./...") != 1 || strings.Count(string(invoked), " build ./...") != 2 {
t.Fatalf("want one vet run and only two cross-builds, got:\n%s", invoked)
}
// These are selected release gates, not incidental loop details.
for _, target := range []string{"linux/amd64", "linux/arm64"} {
if !strings.Contains(string(invoked), target+" build ./...") {

View File

@@ -5,7 +5,6 @@ repository_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repository_dir"
baseline_toolchain=${CONFIGKIT_BASELINE_TOOLCHAIN:?set CONFIGKIT_BASELINE_TOOLCHAIN to a cached supported Go 1.25 patch release}
newer_toolchain=${CONFIGKIT_NEWER_TOOLCHAIN:?set CONFIGKIT_NEWER_TOOLCHAIN to a cached newer supported Go toolchain}
run_go() {
local toolchain=$1
@@ -20,7 +19,6 @@ check_toolchain() {
}
check_toolchain "$baseline_toolchain"
check_toolchain "$newer_toolchain"
bash -n scripts/release-check.sh
temporary_dir=$(mktemp -d)
@@ -69,21 +67,13 @@ while IFS= read -r -d '' candidate_file; do
esac
done < <(git ls-files --cached --others --exclude-standard -z)
for toolchain in "$baseline_toolchain" "$newer_toolchain"; do
run_go "$toolchain" test ./...
run_go "$toolchain" vet ./...
run_go "$toolchain" build ./...
done
run_go "$newer_toolchain" test -race ./...
run_go "$newer_toolchain" test . -run '^Example'
run_go "$newer_toolchain" test ./examples/consumers
run_go "$newer_toolchain" test . -run '^TestLocalDocumentationLinks$'
run_go "$newer_toolchain" test . -run '^TestDependencyImportBoundary$'
# Verbose output reports skipped cases without a separate evidence run.
run_go "$baseline_toolchain" test -race -v ./...
run_go "$baseline_toolchain" vet ./...
# Only release-validated targets gate publication; macOS is best-effort.
for target in linux/amd64 linux/arm64; do
target_os=${target%/*}
target_arch=${target#*/}
GOTOOLCHAIN="$newer_toolchain" GOPROXY=off CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build ./...
GOTOOLCHAIN="$baseline_toolchain" GOPROXY=off CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build ./...
done