From aead20fae426779f5ecdf6bddf9f0f6589988ea0 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 23 Sep 2026 18:10:37 +0000 Subject: [PATCH] Simplify local release validation and publication --- docs/development.md | 6 +- docs/policy/release.md | 81 ++++++------ docs/release.md | 173 ++++++++++++------------- docs/roadmap/release-simplification.md | 149 +++------------------ release_check_test.go | 122 ++++++++++++++++- scripts/release-check.sh | 18 +-- 6 files changed, 273 insertions(+), 276 deletions(-) diff --git a/docs/development.md b/docs/development.md index c552b2d..fe13d4c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/docs/policy/release.md b/docs/policy/release.md index d8c35da..9079966 100644 --- a/docs/policy/release.md +++ b/docs/policy/release.md @@ -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). diff --git a/docs/release.md b/docs/release.md index a7ebc86..cafe63e 100644 --- a/docs/release.md +++ b/docs/release.md @@ -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 diff --git a/docs/roadmap/release-simplification.md b/docs/roadmap/release-simplification.md index 0c69b0d..44518d8 100644 --- a/docs/roadmap/release-simplification.md +++ b/docs/roadmap/release-simplification.md @@ -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. diff --git a/release_check_test.go b/release_check_test.go index fc24ca9..a08122b 100644 --- a/release_check_test.go +++ b/release_check_test.go @@ -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 ./...") { diff --git a/scripts/release-check.sh b/scripts/release-check.sh index 6c2c2ae..2722993 100755 --- a/scripts/release-check.sh +++ b/scripts/release-check.sh @@ -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