From 1de7555b3e018294698105d3942962c54fd508bd Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 7 Oct 2026 18:26:36 +0000 Subject: [PATCH] Move shared checks into validation package --- check.go | 35 +++++++++++++++++-------------- docs/internal/overview.md | 10 ++++++++- docs/roadmap/implementation.md | 7 ++++++- internal/candidate/check.go | 18 ++++++---------- internal/validation/validation.go | 33 +++++++++++++++++++++++++++++ publish.go | 13 +----------- 6 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 internal/validation/validation.go diff --git a/check.go b/check.go index d50b86c..4200136 100644 --- a/check.go +++ b/check.go @@ -23,13 +23,13 @@ type Candidate struct { // local candidate. Remote is retained for publication operations; Check makes // no remote availability claim. type ReleaseRequest struct { - RepositoryDir string - Product string - Version string - Remote string + RepositoryDir string + Product string + Version string + Remote string // AdditionalChecks optionally runs product checks after the standard suite. AdditionalChecks func(context.Context, Candidate) error - Build *BuildSpec // optional selected-target build after validation. + Build *BuildSpec // optional selected-target build after validation. Output io.Writer } @@ -42,21 +42,11 @@ type ReleaseRequest struct { // branch, HEAD, worktree, index, and local tag state. Success establishes local // candidate evidence only; it does not establish remote availability. func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) { - var validate func(context.Context, candidate.Candidate) error - if request.AdditionalChecks != nil { - validate = func(ctx context.Context, captured candidate.Candidate) error { - return request.AdditionalChecks(ctx, Candidate{ - RepositoryDir: captured.RepositoryDir, - Commit: captured.Commit, - Version: captured.Version, - }) - } - } checked, err := candidate.Check(ctx, candidate.Request{ RepositoryDir: request.RepositoryDir, Product: request.Product, Version: request.Version, - AdditionalChecks: validate, + AdditionalChecks: candidateChecks(request.AdditionalChecks), AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output), Output: request.Output, }) @@ -69,3 +59,16 @@ func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) { Version: checked.Version, }, nil } + +func candidateChecks(checks func(context.Context, Candidate) error) func(context.Context, candidate.Candidate) error { + if checks == nil { + return nil + } + return func(ctx context.Context, captured candidate.Candidate) error { + return checks(ctx, Candidate{ + RepositoryDir: captured.RepositoryDir, + Commit: captured.Commit, + Version: captured.Version, + }) + } +} diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 4a04e50..0e4e120 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -46,6 +46,7 @@ these implemented packages: | `internal/tempdir` | Disposable system temporary storage outside source worktrees. | | `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. | | `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. | +| `internal/validation` | Shared default Go validation for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. | | `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. | | `internal/runtimeinfo` | Private version lookup used by the runtime facade. | | `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. | @@ -58,6 +59,13 @@ dependency direction with `go run ./internal/apicheck`. These constraints are also recorded in [ADR 0001](../adr/0001-public-facade.md) and the [architecture policy](../policy/architecture.md). +Candidate checking captures and validates one clean committed source revision +in `internal/candidate`. Its validation bridge calls `internal/validation` for +the default offline Go suite, then runs product-specific checks once. The +candidate package owns post-validation builds and local-state rechecks; +publication owns endpoint guards, tag creation, push, and recovery around that +same candidate validation path. + ## Capability owners and behavioral evidence The following tests are representative evidence from the current offline suite, @@ -66,7 +74,7 @@ not a separate release matrix: | Capability | Implementation owner | Existing evidence | | --- | --- | --- | | Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` | -| Candidate capture, standard suite, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate` | +| Candidate capture, shared validation, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate` | | Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` | | Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` | | Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` | diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 842aee2..2384b26 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Validation model implementation plan -Status: Ready for implementation using incremental API rollout. +Status: In progress; the shared validation executor is complete. This plan implements the enabling work in [model.md](model.md), guided by [ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna @@ -178,6 +178,11 @@ filesystem. Their tests use synthetic trees and local Git repositories. ## Stage 1: Extract the shared validation executor +**Completion:** Complete. The default Go suite now runs through +`internal/validation`, candidate capture and callbacks remain in +`internal/candidate`, and `Check` and `Publish` share the public callback +adapter. + **Outcome:** One internal owner runs current default validation, with current candidate and publication behavior preserved. diff --git a/internal/candidate/check.go b/internal/candidate/check.go index 9079d88..6d30f21 100644 --- a/internal/candidate/check.go +++ b/internal/candidate/check.go @@ -11,8 +11,8 @@ import ( "path/filepath" "strings" - "gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "gitea.maximumdirect.net/eric/ReleaseKit/internal/process" + "gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" ) @@ -98,7 +98,11 @@ func Capture(ctx context.Context, request Request) (Candidate, error) { // Validate runs the standard offline Go suite, followed by AdditionalChecks // when supplied, for the captured candidate. func Validate(ctx context.Context, request Request, captured Candidate) error { - if err := runDefaultValidation(ctx, captured.RepositoryDir, request.Output); err != nil { + if err := validation.Run(ctx, validation.Request{ + RepositoryDir: captured.RepositoryDir, + Commit: captured.Commit, + Output: request.Output, + }); err != nil { return fmt.Errorf("validate candidate %s: %w", captured.Commit, err) } if request.AdditionalChecks != nil { @@ -214,16 +218,6 @@ func hasLocalTag(ctx context.Context, repositoryDir, version string) (bool, erro return false, err } -func runDefaultValidation(ctx context.Context, repositoryDir string, output io.Writer) error { - tool, err := gotool.New(ctx, repositoryDir) - if err != nil { - return fmt.Errorf("prepare Go toolchain: %w", err) - } - _, err = tool.Run(ctx, repositoryDir, gotool.Native, gotool.Target{}, output, - "test", "-mod=readonly", "-count=1", "./...") - return err -} - func validStableVersion(version string) bool { return versioncontract.ValidStableVersion(version) } diff --git a/internal/validation/validation.go b/internal/validation/validation.go new file mode 100644 index 0000000..0b1ee6b --- /dev/null +++ b/internal/validation/validation.go @@ -0,0 +1,33 @@ +// Package validation owns the shared checks run against a captured candidate. +package validation + +import ( + "context" + "fmt" + "io" + + "gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" +) + +// Settings selects shared validation behavior. Its zero value runs the current +// default Go suite. +type Settings struct{} + +// Request identifies the captured source and output used by shared validation. +type Request struct { + RepositoryDir string + Commit string + Output io.Writer + Settings Settings +} + +// Run executes shared validation for the captured candidate. +func Run(ctx context.Context, request Request) error { + tool, err := gotool.New(ctx, request.RepositoryDir) + if err != nil { + return fmt.Errorf("prepare Go toolchain: %w", err) + } + _, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, + "test", "-mod=readonly", "-count=1", "./...") + return err +} diff --git a/publish.go b/publish.go index 5d3ee3b..eb8988a 100644 --- a/publish.go +++ b/publish.go @@ -4,7 +4,6 @@ import ( "context" internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" - "gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "gitea.maximumdirect.net/eric/ReleaseKit/internal/publish" ) @@ -61,22 +60,12 @@ type PublicationResult struct { // an unreadable result is unknown. A nil error means publication is confirmed // by the successful push or exact remote object match. func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, error) { - var validate func(context.Context, candidate.Candidate) error - if request.AdditionalChecks != nil { - validate = func(ctx context.Context, captured candidate.Candidate) error { - return request.AdditionalChecks(ctx, Candidate{ - RepositoryDir: captured.RepositoryDir, - Commit: captured.Commit, - Version: captured.Version, - }) - } - } result, err := publish.Publish(ctx, publish.Request{ RepositoryDir: request.RepositoryDir, Product: request.Product, Version: request.Version, Remote: request.Remote, - AdditionalChecks: validate, + AdditionalChecks: candidateChecks(request.AdditionalChecks), AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output), Output: request.Output, })