Simplify release validation gates
This commit is contained in:
@@ -117,7 +117,8 @@ under the testing policy. Documentation-only work normally needs the local-link
|
|||||||
check below and whitespace review; behavior-bearing examples and commands also
|
check below and whitespace review; behavior-bearing examples and commands also
|
||||||
need execution. Use `go test -race ./...` for ownership or concurrency changes.
|
need execution. Use `go test -race ./...` for ownership or concurrency changes.
|
||||||
The [release checker](release.md#4-validate-the-source-candidate) performs one
|
The [release checker](release.md#4-validate-the-source-candidate) performs one
|
||||||
minimum-toolchain race-suite run, vet, module hygiene, and Linux cross-builds.
|
minimum-toolchain offline race-suite run, one full vet pass, and Linux
|
||||||
|
cross-builds using readonly module mode.
|
||||||
It already covers examples, links, and package boundaries; do not additionally
|
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.
|
run the everyday checker just to repeat those checks. CI never gates a release.
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ policy and link to it rather than introduce additional release requirements.
|
|||||||
ConfigKit adopts a source-only Go module release model. The source tag and
|
ConfigKit adopts a source-only Go module release model. The source tag and
|
||||||
version-matched release note are the declared deliverables. Standalone binaries,
|
version-matched release note are the declared deliverables. Standalone binaries,
|
||||||
container images, and separate binary/package-registry publication are not
|
container images, and separate binary/package-registry publication are not
|
||||||
required. This does not waive source compilation, module hygiene, testing, or
|
required. This does not waive source compilation, dependency maintenance, testing, or
|
||||||
local target-build validation. Post-publication consumer retrieval is optional,
|
local target-build validation. Post-publication consumer retrieval is optional,
|
||||||
as described under Release Model.
|
as described under Release Model.
|
||||||
|
|
||||||
@@ -148,11 +148,17 @@ gate; historical release notes are unchanged.
|
|||||||
|
|
||||||
ConfigKit provides one local release checker. Run it once against the final
|
ConfigKit provides one local release checker. Run it once against the final
|
||||||
committed candidate using one maintained patch of the minimum supported Go line
|
committed candidate using one maintained patch of the minimum supported Go line
|
||||||
on Linux. It checks module hygiene, formatting and whitespace, the full offline
|
on Linux. It runs one full offline race suite, one full vet pass, and CGO-disabled
|
||||||
suite with the race detector, static analysis, and CGO-disabled Linux amd64/arm64
|
Linux amd64/arm64 builds. The race suite protects library behavior, ownership,
|
||||||
builds. Examples, documentation links, consumer fixtures, and package boundaries
|
and concurrent use; it includes examples, documentation links, consumer fixtures,
|
||||||
are covered by that suite, not separate repeated runs. Native package compilation
|
and package boundaries without separate runs. Full vet checks more than the
|
||||||
is covered by the test run. A second toolchain is optional and non-gating.
|
subset normally run by tests, so test-time vet is disabled. The two cross-builds
|
||||||
|
cover supported configurations that native race compilation does not. A second
|
||||||
|
toolchain is optional and non-gating.
|
||||||
|
|
||||||
|
All Go checks use readonly module mode and an offline dependency cache.
|
||||||
|
Formatting and whitespace remain ordinary development checks. Review module
|
||||||
|
tidiness when dependencies change, rather than auditing it on every release.
|
||||||
|
|
||||||
Commit the release note and associated changes before release validation. Record
|
Commit the release note and associated changes before release validation. Record
|
||||||
the exact validated commit; revalidate if the candidate changes. Do not require
|
the exact validated commit; revalidate if the candidate changes. Do not require
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ binary, container image, generated package, or separate registry publication.
|
|||||||
- Best-effort targets: macOS on amd64 and arm64, under the
|
- Best-effort targets: macOS on amd64 and arm64, under the
|
||||||
[platform support policy](policy/release.md#platform-support)
|
[platform support policy](policy/release.md#platform-support)
|
||||||
|
|
||||||
The local Linux checker includes native race tests and CGO-disabled cross-builds.
|
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
|
Its output reports skipped filesystem cases; no separate evidence record or
|
||||||
human sign-off is required. Native macOS checks and Darwin cross-builds are
|
human sign-off is required. Native macOS checks and Darwin cross-builds are
|
||||||
optional and do not gate publication.
|
optional and do not gate publication.
|
||||||
@@ -136,13 +137,15 @@ printf '%s\n' "$candidate_commit" > "$(git rev-parse --git-path configkit-releas
|
|||||||
```
|
```
|
||||||
|
|
||||||
If the final candidate is already committed, omit the staging and commit commands;
|
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
|
do not create an empty commit. The checker runs one full offline race suite with
|
||||||
disposable copy, formatting, whitespace, the full offline suite once with the race
|
verbose skip reporting and test-time vet disabled, one full vet pass, and
|
||||||
detector and verbose skip reporting, vet once, and CGO-disabled Linux amd64/arm64
|
CGO-disabled Linux amd64/arm64 builds. Go commands use readonly module mode.
|
||||||
builds. The suite includes examples, consumer fixtures, documentation links, and
|
The suite includes examples, consumer fixtures, documentation links, and package
|
||||||
package boundaries; no separate reruns are required. No additional linter is
|
boundaries; no separate reruns are required. Formatting and whitespace remain
|
||||||
claimed. Sibling-consumer links are checked when those checkouts exist and are
|
ordinary development checks in `scripts/check.sh`; module tidiness is reviewed
|
||||||
otherwise inapplicable in an isolated checkout.
|
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.
|
||||||
|
|
||||||
A changed candidate requires validation again. Do not reuse a recorded candidate
|
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.
|
from an older revision. The record is written only after successful local checks.
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ Bash 3.2 and existing Git/Go prerequisites; no new release framework is needed.
|
|||||||
|
|
||||||
## Stage 1 — Simplify the release checker and select its gates
|
## Stage 1 — Simplify the release checker and select its gates
|
||||||
|
|
||||||
|
**Completed 2026-09-28.** The checker now runs one readonly offline race suite
|
||||||
|
with test-time vet disabled, one full vet pass, and CGO-disabled Linux amd64/arm64
|
||||||
|
builds. The disposable module audit and release formatting/whitespace scans were
|
||||||
|
removed. Focused checker tests, Bash syntax checks, and the real Go 1.25.14
|
||||||
|
release checker passed on Linux.
|
||||||
|
|
||||||
Change `scripts/release-check.sh`, checker tests in `release_check_test.go`, and
|
Change `scripts/release-check.sh`, checker tests in `release_check_test.go`, and
|
||||||
the affected descriptions in `docs/policy/release.md`, `docs/release.md`, and
|
the affected descriptions in `docs/policy/release.md`, `docs/release.md`, and
|
||||||
`docs/development.md` together. The existing guarded publication blocks remain
|
`docs/development.md` together. The existing guarded publication blocks remain
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ func TestReleaseRunbookGuards(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
|
func TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure(t *testing.T) {
|
||||||
for _, scriptName := range []string{"release-check.sh", "check.sh"} {
|
for _, scriptName := range []string{"check.sh"} {
|
||||||
t.Run(scriptName, func(t *testing.T) {
|
t.Run(scriptName, func(t *testing.T) {
|
||||||
script, err := os.ReadFile("scripts/" + scriptName)
|
script, err := os.ReadFile("scripts/" + scriptName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,25 +217,95 @@ func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
|
|||||||
if fail && strings.Contains(string(invoked), "vet ./...") {
|
if fail && strings.Contains(string(invoked), "vet ./...") {
|
||||||
t.Fatal("checker continued after failed test")
|
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 ./...") {
|
|
||||||
t.Errorf("checker omitted required target %s", target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(string(invoked), "darwin/") {
|
|
||||||
t.Fatal("checker requires best-effort macOS validation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckScriptsReleaseGates(t *testing.T) {
|
||||||
|
script, err := os.ReadFile("scripts/release-check.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, scenario := range []string{"success", "failed-test", "missing-toolchain"} {
|
||||||
|
t.Run(scenario, func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
bin := t.TempDir()
|
||||||
|
for _, directory := range []string{"scripts", "nested"} {
|
||||||
|
if err := os.Mkdir(filepath.Join(root, directory), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checker := filepath.Join(root, "scripts", "release-check.sh")
|
||||||
|
if err := os.WriteFile(checker, script, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
calls := filepath.Join(bin, "go.calls")
|
||||||
|
goStub := `#!/bin/sh
|
||||||
|
printf '%s|%s|%s|%s|%s|%s\n' "$GOTOOLCHAIN" "$GOPROXY" "${CGO_ENABLED-}" "${GOOS-}" "${GOARCH-}" "$*" >> "$CONFIGKIT_TEST_GO_LOG"
|
||||||
|
if [ "${CONFIGKIT_TEST_FAIL-}" = toolchain ] && [ "${1-}" = version ]; then exit 37; fi
|
||||||
|
if [ "${CONFIGKIT_TEST_FAIL-}" = test ] && [ "${1-}" = test ]; then exit 37; fi
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte(goStub), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("/bin/bash", checker)
|
||||||
|
cmd.Dir = filepath.Join(root, "nested")
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||||
|
"CONFIGKIT_BASELINE_TOOLCHAIN=test-baseline",
|
||||||
|
"CONFIGKIT_TEST_GO_LOG="+calls,
|
||||||
|
"GOOS=", "GOARCH=", "CGO_ENABLED=",
|
||||||
|
)
|
||||||
|
switch scenario {
|
||||||
|
case "failed-test":
|
||||||
|
cmd.Env = append(cmd.Env, "CONFIGKIT_TEST_FAIL=test")
|
||||||
|
case "missing-toolchain":
|
||||||
|
cmd.Env = append(cmd.Env, "CONFIGKIT_TEST_FAIL=toolchain")
|
||||||
|
}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if scenario == "success" && err != nil {
|
||||||
|
t.Fatalf("checker: %v\n%s", err, output)
|
||||||
|
}
|
||||||
|
if scenario != "success" && err == nil {
|
||||||
|
t.Fatalf("checker accepted %s", scenario)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(calls)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.HasPrefix(line, "test-baseline|off|") {
|
||||||
|
t.Fatalf("toolchain or offline setting lost: %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scenario == "missing-toolchain" {
|
||||||
|
if len(lines) != 1 || !strings.HasSuffix(lines[0], "|version") {
|
||||||
|
t.Fatalf("checker ran after unavailable toolchain: %q", lines)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(lines) < 2 || lines[1] != "test-baseline|off||||test -mod=readonly -vet=off -race -v ./..." {
|
||||||
|
t.Fatalf("release suite command changed: %q", lines)
|
||||||
|
}
|
||||||
|
if scenario == "failed-test" {
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("checker continued after failure: %q", lines)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
want := []string{
|
||||||
|
"test-baseline|off||||version",
|
||||||
|
"test-baseline|off||||test -mod=readonly -vet=off -race -v ./...",
|
||||||
|
"test-baseline|off||||vet -mod=readonly ./...",
|
||||||
|
"test-baseline|off|0|linux|amd64|build -mod=readonly ./...",
|
||||||
|
"test-baseline|off|0|linux|arm64|build -mod=readonly ./...",
|
||||||
|
}
|
||||||
|
if strings.Join(lines, "\n") != strings.Join(want, "\n") {
|
||||||
|
t.Fatalf("unexpected release gates:\n%s", data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,68 +12,18 @@ run_go() {
|
|||||||
GOTOOLCHAIN="$toolchain" GOPROXY=off go "$@"
|
GOTOOLCHAIN="$toolchain" GOPROXY=off go "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_toolchain() {
|
if ! GOTOOLCHAIN="$baseline_toolchain" GOPROXY=off go version >/dev/null; then
|
||||||
local toolchain=$1
|
printf 'release toolchain %s is unavailable; acquire it before validation\n' "$baseline_toolchain" >&2
|
||||||
GOTOOLCHAIN="$toolchain" GOPROXY=off go version >/dev/null
|
|
||||||
run_go "$toolchain" mod download
|
|
||||||
}
|
|
||||||
|
|
||||||
check_toolchain "$baseline_toolchain"
|
|
||||||
bash -n scripts/release-check.sh
|
|
||||||
|
|
||||||
temporary_dir=$(mktemp -d)
|
|
||||||
trap 'rm -rf -- "$temporary_dir"' EXIT
|
|
||||||
candidate_manifest=$temporary_dir/candidate-files
|
|
||||||
while IFS= read -r -d '' candidate_file; do
|
|
||||||
if [[ -e "$candidate_file" || -L "$candidate_file" ]]; then
|
|
||||||
printf '%s\0' "$candidate_file" >>"$candidate_manifest"
|
|
||||||
fi
|
|
||||||
done < <(git ls-files --cached --others --exclude-standard -z)
|
|
||||||
tar --null --files-from="$candidate_manifest" --create --file=- |
|
|
||||||
tar --extract --file=- --directory="$temporary_dir"
|
|
||||||
(
|
|
||||||
cd "$temporary_dir"
|
|
||||||
GOTOOLCHAIN="$baseline_toolchain" GOPROXY=off go mod tidy
|
|
||||||
)
|
|
||||||
diff -u go.mod "$temporary_dir/go.mod"
|
|
||||||
diff -u go.sum "$temporary_dir/go.sum"
|
|
||||||
|
|
||||||
go_files=()
|
|
||||||
while IFS= read -r -d '' go_file; do
|
|
||||||
if [[ -f "$go_file" ]]; then
|
|
||||||
go_files+=("$go_file")
|
|
||||||
fi
|
|
||||||
done < <(git ls-files --cached --others --exclude-standard -z -- '*.go')
|
|
||||||
if ((${#go_files[@]})); then
|
|
||||||
unformatted=$(gofmt -l -- "${go_files[@]}")
|
|
||||||
if [[ -n "$unformatted" ]]; then
|
|
||||||
printf 'gofmt required for:\n%s\n' "$unformatted" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
git diff --check HEAD --
|
|
||||||
while IFS= read -r -d '' candidate_file; do
|
|
||||||
if [[ ! -f "$candidate_file" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
case "$candidate_file" in
|
|
||||||
*.go|*.md|*.sh|go.mod|go.sum|LICENSE|.gitignore|AGENTS.md)
|
|
||||||
if LC_ALL=C grep -nE '[[:blank:]]+$' -- "$candidate_file"; then
|
|
||||||
printf 'trailing whitespace found in %s\n' "$candidate_file" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done < <(git ls-files --cached --others --exclude-standard -z)
|
|
||||||
|
|
||||||
# Verbose output reports skipped cases without a separate evidence run.
|
# Verbose output reports skipped cases without a separate evidence run.
|
||||||
run_go "$baseline_toolchain" test -race -v ./...
|
run_go "$baseline_toolchain" test -mod=readonly -vet=off -race -v ./...
|
||||||
run_go "$baseline_toolchain" vet ./...
|
run_go "$baseline_toolchain" vet -mod=readonly ./...
|
||||||
|
|
||||||
# Only release-validated targets gate publication; macOS is best-effort.
|
# Only release-validated targets gate publication; macOS is best-effort.
|
||||||
for target in linux/amd64 linux/arm64; do
|
for target in linux/amd64 linux/arm64; do
|
||||||
target_os=${target%/*}
|
target_os=${target%/*}
|
||||||
target_arch=${target#*/}
|
target_arch=${target#*/}
|
||||||
GOTOOLCHAIN="$baseline_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 -mod=readonly ./...
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user