Delegate release file checks to ReleaseKit

This commit is contained in:
2026-11-03 01:32:05 +00:00
parent f7044cf695
commit 7e53863adb
6 changed files with 56 additions and 185 deletions

View File

@@ -251,15 +251,19 @@ dependency downloads. Acquire dependencies and any selected toolchain beforehand
Format reported Go files with `gofmt -w <files>`.
The default suite includes executable examples, maintained consumer fixtures,
local documentation-link checks, and the dependency import-boundary check.
and the dependency/public boundary checks.
It must stay deterministic and runnable without network access or credentials.
Still inspect new/untracked content and review paths, examples, ownership, and
current-versus-planned claims; automated checks do not replace content review.
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.
under the testing policy. Documentation-only work normally needs review of
affected links and whitespace; behavior-bearing examples and commands also
need execution. Automated Markdown link checking runs against the committed
candidate through ReleaseKit, not during ordinary Go tests or the everyday
checker. See the [release procedure](release.md#4-publish-the-committed-candidate)
for the selected checks and optional diagnostic command. Use
`go test -race ./...` for ownership or concurrency changes.
The [release procedure](release.md#4-publish-the-committed-candidate) performs
its selected validation through one `publish` invocation. The everyday checker
remains a development gate and is not a separate release prerequisite. CI never
@@ -295,7 +299,7 @@ code changes. Use the component overview to find the corresponding implementatio
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
| Identified-sequence scaling evidence | [Mixed-upsert benchmark](../internal/engine/sequence_combine_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkSequenceMixedUpsert$' -benchmem` |
| Inheritance scaling evidence | [Expansion benchmarks](../internal/engine/inheritance_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkInheritance' -benchmem` |
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
| Dependency and public API isolation | [Import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
| Development check behavior | [Development checker test](../development_check_test.go) | `go test . -run '^TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure$'` |
## Fuzz validation

View File

@@ -50,7 +50,7 @@ Implemented component owners:
- Engine mechanism tests own source, composition, inheritance, decoding,
callback, provenance,
source-context, secret, history, rendering, and bootstrap behavior. Root tests
own public API compatibility, facade integration, documentation links, and
own public API compatibility, facade integration, and
dependency/public boundary checks. Consumer examples remain under
`examples/consumers`.

View File

@@ -213,8 +213,8 @@ test or CI run required before it.
The selected profile runs the complete deterministic, offline default suite
once with the race detector and test-time vet disabled. The suite already
includes examples, consumer fixtures, documentation links, and package
boundaries. One full vet pass adds static checks beyond the test-time subset.
includes examples, consumer fixtures, and package boundaries. One full vet
pass adds static checks beyond the test-time subset.
CGO-disabled Linux amd64 and arm64 package compilation covers supported
configurations distinct from native race compilation. macOS and newer-toolchain
checks are optional.
@@ -231,6 +231,17 @@ delivery token path out of the committed candidate. The profile owns the path;
the check does not inspect file contents or Git history. Hosted credential
preflight belongs to delivery and adds no source-publication gate.
ConfigKit delegates committed Markdown link validation to ReleaseKit rather
than maintaining a documentation-link test in the Go suite. Links outside the
checkout remain out of scope. This moves automated link feedback to candidate
validation; ordinary development review still covers edited links.
ConfigKit also selects ReleaseKit's local-replacement check for committed module
declarations. Filesystem replacements can make local validation succeed with
dependencies unavailable to consumers of the published module. Versioned module
replacements remain permitted. This check does not validate remote dependency
availability or require consumer retrieval.
ReleaseKit runs its selected Go commands with readonly module mode and
`GOPROXY=off`; acquire dependencies and build the tool before candidate
validation. Formatting and whitespace remain ordinary development checks in

View File

@@ -31,7 +31,8 @@ version supported for applications using ConfigKit.
Release validation runs the offline Go test suite once with the race detector,
then a full vet pass and package compilation for both Linux targets with cgo
disabled (`CGO_ENABLED=0`). macOS and CI are
disabled (`CGO_ENABLED=0`), followed by the selected committed-file checks
described in [publication](#4-publish-the-committed-candidate). macOS and CI are
not release gates; do not wait for or poll CI.
The vanity module path is the name applications put in Go imports. Source/tag
@@ -93,22 +94,18 @@ Prepare dependencies whether or not the minimum changes:
go mod download
```
Build the ReleaseKit version recorded in `go.mod` into temporary storage outside
the checkout.
Ensure `$TMPDIR` names a system temporary location, not the repository. These
commands never install another ReleaseKit version:
Run the module-selected ReleaseKit tool from the repository root. Go builds
and caches the executable; no temporary executable or launcher is needed.
These commands use the version recorded in `go.mod`:
```sh
releasekit_tmp=$(mktemp -d "${TMPDIR:-/tmp}/configkit-releasekit.XXXXXX")
releasekit_bin="$releasekit_tmp/releasekit"
go build -o "$releasekit_bin" go.maximumdirect.net/releasekit/cmd/releasekit
go list -m go.maximumdirect.net/releasekit
"$releasekit_bin" version
go tool releasekit version
```
This repository selects ReleaseKit v0.8.0. Use the compiled program above for
publication so its exit status and publication report reach you unchanged.
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
This repository selects ReleaseKit v0.8.0. `go tool releasekit` preserves the
tool's exit status and publication report; `go run` maps a nonzero child exit
status to 1. ReleaseKit's child Go commands
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
executable first on `PATH` through the publication command.
@@ -174,10 +171,10 @@ candidate to `main`, and invoke validation again.
## 4. Publish the committed candidate
Once publication is authorized and the candidate is on the publication remote's
`main`, run the compiled ReleaseKit program once to validate and publish it:
`main`, run the module-selected ReleaseKit tool once to validate and publish it:
```sh
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
go tool releasekit publish vMAJOR.MINOR.PATCH
```
Replace the placeholder with the selected version. ReleaseKit checks the
@@ -186,14 +183,20 @@ It verifies the candidate on
`main`, and checks the release-note H1 for the product and version. It then
runs the selected race-enabled standard suite once with test-time vet disabled,
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
arm64. It publishes only the annotated tag for that candidate. Confirmed
arm64. It also checks the committed candidate for the forbidden token path,
filesystem replacements in module declarations, and local Markdown destinations
and supported heading fragments. Markdown links outside the checkout are
reported as out of scope without failing validation. These file checks inspect
the committed candidate, not uncommitted or ignored working-tree content.
It publishes only the annotated tag for that candidate. Confirmed
publication of the intended tag completes the release. The tag points to the
captured commit and uses the complete checked-in note as its annotation message.
Use the optional `check` command for diagnosis when useful. It validates without
publishing and is not required before `publish`, which performs its own checks:
```sh
"$releasekit_bin" check vMAJOR.MINOR.PATCH
go tool releasekit check vMAJOR.MINOR.PATCH
```
For `publish`, interpret the program's exit status as follows:
@@ -215,11 +218,11 @@ remote read-back or consumer retrieval; those checks remain optional.
## 5. Deliver the hosted release page
After source publication succeeds, run local hosted delivery from the repository
root. Keep the worktree clean and `HEAD` at the published tag's commit. Reuse the
compiled executable from step 1. For the first release using this workflow:
root. Keep the worktree clean and `HEAD` at the published tag's commit. Use the
module-selected tool as in step 1. For the first release using this workflow:
```sh
"$releasekit_bin" deliver v1.0.0
go tool releasekit deliver v1.0.0
```
For subsequent releases, substitute the selected version. The profile selects
@@ -247,8 +250,8 @@ separately.
A delivery problem leaves the successfully published source tag intact. Retry
delivery with the same immutable version after correcting the cause and
resolving any uncertain write. For a later retry, use a clean checkout of the
original tag, fetching that tag if needed. Build the module-selected CLI from
that checkout using step 1's tool-build commands, without changing the tagged
original tag, fetching that tag if needed. Use that checkout's module-selected
CLI, prepare its dependencies and tool as in step 1 without changing the tagged
module files, and make the token available there. For example, with the tag
available locally:
@@ -260,7 +263,7 @@ git switch --detach "$release_version"
After preparing the tool in that tagged checkout:
```sh
"$releasekit_bin" deliver "$release_version"
go tool releasekit deliver "$release_version"
```
Do not rerun `publish`, move a tag, or replace a hosted page to repair delivery.
@@ -285,17 +288,17 @@ credential-preflight gate, or required recording-success check.
After a failed push, ReleaseKit makes one bounded observation of the original
publication endpoint. If the outcome remains unknown, use the explicitly saved
record rather than rerunning publication. Copy the recovery command printed by
ReleaseKit, or use the same compiled executable:
ReleaseKit, or use the module-selected tool:
```sh
publication_record='/absolute/path/from/the/publication/report.json'
"$releasekit_bin" reconcile --record "$publication_record"
go tool releasekit reconcile --record "$publication_record"
```
If the path was lost, discover local attempts without contacting a remote:
```sh
"$releasekit_bin" publication-attempts
go tool releasekit publication-attempts
```
Select the record for the intended attempt from its reported version, candidate,
@@ -334,7 +337,7 @@ by another release channel may it be removed before retrying publication:
```sh
release_version='vMAJOR.MINOR.PATCH'
git tag -d "$release_version"
"$releasekit_bin" publish "$release_version"
go tool releasekit publish "$release_version"
```
Replace the placeholder with the original attempt's version.

View File

@@ -1,151 +0,0 @@
package configkit
import (
"bufio"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"unicode"
)
var markdownLinkPattern = regexp.MustCompile(`(?m)(?:^|[[:space:]])\[[^\]]*\]\(([^)[:space:]]+)`)
func TestLocalDocumentationLinks(t *testing.T) {
repository, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
var failures []string
err = filepath.WalkDir(repository, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
if entry.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
if filepath.Ext(path) != ".md" {
return nil
}
content, readErr := os.ReadFile(path)
if readErr != nil {
return readErr
}
for _, match := range markdownLinkPattern.FindAllStringSubmatch(string(content), -1) {
target := strings.Trim(match[1], "<>")
if strings.Contains(target, "://") || strings.HasPrefix(target, "mailto:") {
continue
}
filePart, fragment, _ := strings.Cut(target, "#")
decoded, decodeErr := url.PathUnescape(filePart)
if decodeErr != nil {
failures = append(failures, fmt.Sprintf("%s: invalid escaped link %q", relativePath(repository, path), target))
continue
}
linked := path
if decoded != "" {
linked = filepath.Clean(filepath.Join(filepath.Dir(path), filepath.FromSlash(decoded)))
}
_, statErr := os.Stat(linked)
outside := linked != repository && !strings.HasPrefix(linked, repository+string(filepath.Separator))
if os.IsNotExist(statErr) && outside && optionalSiblingSource(repository, linked) {
continue // Recorded sibling-source links are optional in isolated clones.
}
if statErr != nil {
failures = append(failures, fmt.Sprintf("%s: missing local link %q", relativePath(repository, path), target))
continue
}
if fragment != "" && filepath.Ext(linked) == ".md" && !markdownHasFragment(linked, fragment) {
failures = append(failures, fmt.Sprintf("%s: missing fragment in %q", relativePath(repository, path), target))
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
if len(failures) != 0 {
t.Fatalf("documentation link failures:\n%s", strings.Join(failures, "\n"))
}
}
func optionalSiblingSource(repository, linked string) bool {
relative, err := filepath.Rel(filepath.Dir(repository), linked)
if err != nil || relative == "." || filepath.IsAbs(relative) {
return false
}
parts := strings.Split(filepath.ToSlash(relative), "/")
if len(parts) == 0 {
return false
}
switch strings.ToLower(parts[0]) {
case "narratio", "notarius", "weatherreporter", "seriatim":
return true
default:
return false
}
}
func markdownHasFragment(path, wanted string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()
wanted, err = url.PathUnescape(wanted)
if err != nil {
return false
}
seen := make(map[string]int)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "#") {
continue
}
heading := strings.TrimSpace(strings.TrimLeft(line, "#"))
base := markdownFragment(heading)
count := seen[base]
seen[base]++
fragment := base
if count > 0 {
fragment = fmt.Sprintf("%s-%d", base, count)
}
if fragment == wanted {
return true
}
}
return false
}
func markdownFragment(heading string) string {
var result strings.Builder
previousSpace := false
for _, character := range strings.ToLower(heading) {
switch {
case unicode.IsLetter(character), unicode.IsDigit(character), character == '_', character == '-':
result.WriteRune(character)
previousSpace = false
case unicode.IsSpace(character):
if result.Len() > 0 && !previousSpace {
result.WriteByte('-')
previousSpace = true
}
}
}
return strings.TrimRight(result.String(), "-")
}
func relativePath(root, path string) string {
relative, err := filepath.Rel(root, path)
if err != nil {
return path
}
return filepath.ToSlash(relative)
}

View File

@@ -4,6 +4,10 @@
"expected_push_url": "ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git",
"validation": {
"forbidden_files": [".releasekit/gitea-token"],
"markdown": {},
"module_distribution": {
"local_replacements": true
},
"go": {
"race": true,
"full_vet": true,