From 1c41f7a0d16957e7bec73857ef51d4c80dae8f9c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 7 Oct 2026 22:18:47 +0000 Subject: [PATCH] Coordinate separate full vet pass --- check.go | 17 ++- cmd/releasekit/internal/cli/config.go | 3 +- cmd/releasekit/internal/cli/config_test.go | 8 +- cmd/releasekit/main.go | 5 +- cmd/releasekit/main_test.go | 8 +- docs/cli.md | 17 ++- docs/config.md | 17 ++- docs/consumers/check.md | 15 ++- docs/consumers/publish.md | 11 +- docs/internal/overview.md | 2 +- docs/internal/validation.md | 8 +- docs/roadmap/implementation.md | 13 +- docs/roadmap/model.md | 27 ++-- internal/validation/diagnostic.go | 2 + internal/validation/race_test.go | 140 ++++++++++++++++++--- internal/validation/validation.go | 12 +- publish.go | 7 +- validation_error.go | 2 +- validation_test.go | 27 +++- 19 files changed, 261 insertions(+), 80 deletions(-) diff --git a/check.go b/check.go index 1ab2010..44ee0ee 100644 --- a/check.go +++ b/check.go @@ -46,6 +46,9 @@ type ValidationSpec struct { type GoValidationSpec struct { // Race runs the one standard Go test suite with the race detector enabled. Race bool + // FullVet runs go vet after the suite instead of the test-time vet pass. + // Failures use the go-vet validation check identity. + FullVet bool } // Check validates one local, committed release candidate. It requires a clean @@ -53,7 +56,8 @@ type GoValidationSpec struct { // and an unused local tag. It always runs the offline Go test suite with the // available compatible local Go toolchain and Go host target, then calls // AdditionalChecks when supplied. Validation.Go.Race runs that one suite with -// the race detector; the zero value preserves the ordinary native suite. If +// the race detector, and Validation.Go.FullVet runs go vet after the suite +// instead of test-time vet. Their zero values preserve the ordinary suite. If // Build selects targets, Check builds them after validation in temporary // storage outside the worktree, then rechecks // branch, HEAD, worktree, index, and local tag state. Success establishes local @@ -72,10 +76,13 @@ func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) { func candidateRequest(request ReleaseRequest) candidate.Request { return candidate.Request{ - RepositoryDir: request.RepositoryDir, - Product: request.Product, - Version: request.Version, - Validation: validation.Settings{Race: request.Validation.Go.Race}, + RepositoryDir: request.RepositoryDir, + Product: request.Product, + Version: request.Version, + Validation: validation.Settings{ + Race: request.Validation.Go.Race, + FullVet: request.Validation.Go.FullVet, + }, AdditionalChecks: candidateChecks(request.AdditionalChecks), AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output), Output: request.Output, diff --git a/cmd/releasekit/internal/cli/config.go b/cmd/releasekit/internal/cli/config.go index dc90d89..ca29d96 100644 --- a/cmd/releasekit/internal/cli/config.go +++ b/cmd/releasekit/internal/cli/config.go @@ -130,7 +130,8 @@ type ValidationSettings struct { // GoValidationSettings selects checks performed by the Go toolchain. type GoValidationSettings struct { - Race bool `json:"race,omitempty"` + Race bool `json:"race,omitempty"` + FullVet bool `json:"full_vet,omitempty"` } // DeliverySettings selects the hosted Gitea release destination. diff --git a/cmd/releasekit/internal/cli/config_test.go b/cmd/releasekit/internal/cli/config_test.go index d5ad349..d41eed8 100644 --- a/cmd/releasekit/internal/cli/config_test.go +++ b/cmd/releasekit/internal/cli/config_test.go @@ -141,10 +141,10 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) { } } -func TestLoadSettingsReadsRaceValidation(t *testing.T) { +func TestLoadSettingsReadsGoValidation(t *testing.T) { repoDir := t.TempDir() profilePath := filepath.Join(repoDir, "releasekit.json") - if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true}}}`), 0o600); err != nil { + if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true,"full_vet":true}}}`), 0o600); err != nil { t.Fatal(err) } @@ -152,8 +152,8 @@ func TestLoadSettingsReadsRaceValidation(t *testing.T) { if err != nil { t.Fatal(err) } - if settings.Validation == nil || !settings.Validation.Go.Race { - t.Fatalf("validation settings = %#v, want Go race enabled", settings.Validation) + if settings.Validation == nil || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet { + t.Fatalf("validation settings = %#v, want Go race and full vet enabled", settings.Validation) } } diff --git a/cmd/releasekit/main.go b/cmd/releasekit/main.go index eeac378..6ecfc47 100644 --- a/cmd/releasekit/main.go +++ b/cmd/releasekit/main.go @@ -160,7 +160,10 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec return releasekit.ValidationSpec{} } return releasekit.ValidationSpec{ - Go: releasekit.GoValidationSpec{Race: settings.Go.Race}, + Go: releasekit.GoValidationSpec{ + Race: settings.Go.Race, + FullVet: settings.Go.FullVet, + }, } } diff --git a/cmd/releasekit/main_test.go b/cmd/releasekit/main_test.go index e18c241..c0018d0 100644 --- a/cmd/releasekit/main_test.go +++ b/cmd/releasekit/main_test.go @@ -112,15 +112,15 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) { } } -func TestCandidateRequestMapsRaceValidation(t *testing.T) { +func TestCandidateRequestMapsGoValidation(t *testing.T) { request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{ Product: "Example Product", Validation: &cli.ValidationSettings{ - Go: cli.GoValidationSettings{Race: true}, + Go: cli.GoValidationSettings{Race: true, FullVet: true}, }, }, nil) - if !request.Validation.Go.Race { - t.Fatalf("candidate request validation = %#v, want Go race enabled", request.Validation) + if !request.Validation.Go.Race || !request.Validation.Go.FullVet { + t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation) } } diff --git a/docs/cli.md b/docs/cli.md index 6b5ac8a..7d9f4fd 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,13 +25,16 @@ version. The repository defaults to the current directory, and the profile defaults to `/releasekit.json`. A successful command prints the captured local commit and version. It checks the committed release note and local Git state, runs the standard offline Go test suite (with `-race` when -`validation.go.race` is true), and then runs any configured additional checks -in profile order. It does not inspect remote availability or publish a tag. +`validation.go.race` is true), and runs a separate `go vet -mod=readonly ./...` +pass after successful tests when `validation.go.full_vet` is true. It then runs +configured additional checks in profile order. It does not inspect remote +availability or publish a tag. `check` writes errors and diagnostics to standard error. It returns status 0 after local validation, 1 after a candidate validation failure, and 2 for usage, profile, or invalid-version errors. Shared validation failures identify the -failed check and retain useful findings and cause context in one diagnostic. +failed check and retain useful findings and cause context in one diagnostic; +separate full-vet failures use the `go-vet` identity. Unsupported argument sequences print usage to standard error. Publish a source tag: @@ -43,9 +46,11 @@ go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION `publish` reads the same product, remote, validation, and additional-check settings as `check`. It checks the authoritative remote and validates the candidate: the single standard offline Go test suite runs first, with `-race` -when `validation.go.race` is true, followed by configured checks in profile -order. It then creates an annotated local tag and pushes only its exact -tag object. It prints the confirmed version, candidate commit, and tag object +when `validation.go.race` is true. When `validation.go.full_vet` is true, +test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful +tests. Configured checks then run in profile order. It then creates an annotated +local tag and pushes only its exact tag object. It prints the confirmed version, +candidate commit, and tag object on success. Validation progress and failed publication state, including the captured publication endpoint for recovery, are written to standard error. A signal diff --git a/docs/config.md b/docs/config.md index 8a4d7e8..2a124e3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -28,12 +28,15 @@ target requires the package and binary name; each target is written as one The optional `validation` object selects shared checks implemented by ReleaseKit. `validation.go.race` enables the race detector for the single standard Go test -suite. When omitted or false, the ordinary native suite runs. For example: +suite. `validation.go.full_vet` runs a separate native `go vet` pass after that +suite succeeds; ReleaseKit disables test-time vet for that suite to avoid +running vet twice. When `full_vet` is omitted or false, the test command keeps +its existing vet behavior. Both settings are independent. For example: ```json { "product": "Example", - "validation": {"go": {"race": true}} + "validation": {"go": {"race": true, "full_vet": true}} } ``` @@ -46,11 +49,13 @@ passes its value explicitly to the library operation. The `check` and `publish` commands run one `go test -mod=readonly -count=1 ./...` suite with Go module and toolchain network -access disabled, adding `-race` when `validation.go.race` is true. They then run +access disabled, adding `-race` when `validation.go.race` is true. When +`validation.go.full_vet` is true, they add `-vet=off` to that test command and +run native `go vet -mod=readonly ./...` after successful tests. They then run each configured `checks` command once, in order, as an additional product -check. Selected-target builds run after the suite and product checks. Do not -repeat the standard suite in `checks`; doing so runs it twice. The `build` -command uses the selected build settings and writes binaries to `dist` unless +check. Selected-target builds run after shared validation and product checks. +Do not repeat the standard suite or vet in `checks`; doing so repeats work. The +`build` command uses the selected build settings and writes binaries to `dist` unless `--output` selects another path. The `deliver` command uses the delivery destination and token. By default it passes selected targets to the delivery facade, which builds tagged-source diff --git a/docs/consumers/check.md b/docs/consumers/check.md index 70284bd..2eaa8ac 100644 --- a/docs/consumers/check.md +++ b/docs/consumers/check.md @@ -9,7 +9,7 @@ result, err := releasekit.Check(ctx, releasekit.ReleaseRequest{ Product: "Example", Version: "v1.2.3", Validation: releasekit.ValidationSpec{ - Go: releasekit.GoValidationSpec{Race: true}, + Go: releasekit.GoValidationSpec{Race: true, FullVet: true}, }, AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error { return runProjectChecks(ctx, candidate.RepositoryDir) @@ -28,11 +28,14 @@ product name and exact version as complete title elements. No local tag with that version may already exist. ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite with the -available compatible local Go toolchain, then calls `AdditionalChecks` once -when supplied. Set `Validation.Go.Race` to run that suite with `-race`; the zero -value keeps the ordinary native suite. ReleaseKit does not download a different -toolchain automatically. If the race prerequisite is unavailable, the `go-test` -check fails rather than being skipped. The suite uses the Go host target, +available compatible local Go toolchain. Set `Validation.Go.Race` to run that +suite with `-race`; set `Validation.Go.FullVet` to run +`go vet -mod=readonly ./...` after successful tests, with test-time vet disabled +for the suite. It calls `AdditionalChecks` once after shared validation when +supplied. Their zero values retain the ordinary native test behavior. ReleaseKit +does not download a different toolchain automatically. If race prerequisites +are unavailable, the `go-test` check fails rather than being skipped; a selected +full-vet failure is reported as `go-vet`. The suite uses the Go host target, disables workspace and module network access, and ignores ambient `GOFLAGS`, experiment, and CPU feature overrides. Effective cache and native cgo/C-tool settings are preserved, including values from Go's persistent environment diff --git a/docs/consumers/publish.md b/docs/consumers/publish.md index 25b03f3..ac0f310 100644 --- a/docs/consumers/publish.md +++ b/docs/consumers/publish.md @@ -9,7 +9,7 @@ result, err := releasekit.Publish(ctx, releasekit.ReleaseRequest{ Product: "Example", Version: "v1.2.3", Validation: releasekit.ValidationSpec{ - Go: releasekit.GoValidationSpec{Race: true}, + Go: releasekit.GoValidationSpec{Race: true, FullVet: true}, }, AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error { return runProjectChecks(ctx, candidate.RepositoryDir) @@ -25,13 +25,16 @@ Publication captures a clean candidate on `main`, validates its committed release note, and checks the selected remote before running candidate checks. It runs one `go test -mod=readonly -count=1 ./...` suite with the available compatible local Go toolchain. Set `Validation.Go.Race` to run that suite with -`-race`; its zero value keeps the ordinary native suite. It then calls -`AdditionalChecks` once when supplied. The standard suite cannot be replaced; +`-race`. Set `Validation.Go.FullVet` to run `go vet -mod=readonly ./...` after +successful tests, with test-time vet disabled for that suite. The zero values +keep ordinary test-time vet behavior. It then calls `AdditionalChecks` once +when supplied. The standard suite cannot be replaced; move product-specific behavior into `AdditionalChecks` and remove repeated standard-suite commands from custom checks. A failed suite or additional check prevents tag creation, and wrapped errors preserve their causes. If race prerequisites are unavailable, the selected suite fails as `go-test` -instead of silently running without the race detector. +instead of silently running without the race detector. Full-vet failures are +reported as `go-vet` and stop publication before tag creation. If `Build` selects targets, ReleaseKit verifies them after the standard suite and additional checks, before creating the tag. The operation then rechecks the diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 5b506a0..0d0615a 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -46,7 +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 Go validation with optional race selection, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. | +| `internal/validation` | Shared Go validation with optional race and full-vet selections, structured check failures, and lazy committed-file access 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. | diff --git a/docs/internal/validation.md b/docs/internal/validation.md index 2c46ff7..3e81746 100644 --- a/docs/internal/validation.md +++ b/docs/internal/validation.md @@ -9,8 +9,12 @@ adds its endpoint guards, tag creation, push, and recovery around that same candidate path. The executor runs one offline Go suite, optionally with the race detector when -selected. Its request carries the captured repository directory and commit, -output writer, and internal settings. `Request.ReadCandidateTree` lets a +selected. When full vet is selected, it disables test-time vet for that suite +and runs native `go vet -mod=readonly ./...` after the suite succeeds, using the +same discovered Go tool and native environment. Test and vet failures retain +their separate `go-test` and `go-vet` identities and underlying causes. Its +request carries the captured repository directory and commit, output writer, +and internal settings. `Request.ReadCandidateTree` lets a selected file check request candidate contents; the Go-only path does not enumerate the tree. `CandidateTree` gets metadata and bytes from Git objects, reads blob bytes diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4e7056e..9a2ce5d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -18,9 +18,11 @@ handles symlinks, parent traversal, cancellation, and absolute in-checkout paths The root error types and CLI presentation preserve findings and cause context. `buildinfo` retains its independent runtime dependency graph. -The public validation specification and its JSON settings remain unimplemented. -The consumer CI template still selects an independent tool version. This sequence -completes every remaining release-roadmap capability and Audita's CI migration. +The shared validation specification and profile settings are implemented +incrementally with each capability; remaining selections are not yet +implemented. The consumer CI template still selects an independent tool +version. This sequence completes every remaining release-roadmap capability and +Audita's CI migration. It ends with local verification and committed changes. Architecture review, hosted deployment, release tagging, and decisions about further testing follow this implementation. @@ -250,6 +252,11 @@ hosts and a controlled Go command for portable prerequisite-failure coverage. ## Stage 2: Coordinate a separate full vet pass +**Completion:** Complete. `ReleaseRequest.Validation.Go.FullVet` and profile +`validation.go.full_vet` disable test-time vet for the one native test suite and +run native `go vet -mod=readonly ./...` afterward. Vet failures retain the +`go-vet` identity, and the selection is shared by the API and profile paths. + **Outcome:** Full vet replaces test-time vet overlap through one declaration. 1. Add `Go.FullVet` and `validation.go.full_vet` through the same conversion path. diff --git a/docs/roadmap/model.md b/docs/roadmap/model.md index e6fd11f..47ff424 100644 --- a/docs/roadmap/model.md +++ b/docs/roadmap/model.md @@ -1,7 +1,7 @@ # Validation API and profile model -Status: Shared validation foundation and Go race selection implemented; -remaining v0.2.0 capabilities and their settings are future work. +Status: Shared validation foundation, Go race selection, and separate full vet +implemented; remaining v0.2.0 capabilities and their settings are future work. This roadmap describes the shared model that enables the optional validation capabilities in the [v0.2.0 roadmap](v0.2.0.md). It owns their common API, @@ -18,9 +18,9 @@ Selected executable builds follow validation. The CLI maps profile `checks` commands to the additional-check callback. This provides a common procedure and a product-specific extension point. The -request and profile now have a structured selection for race testing; full vet, -package compilation, formatting, Markdown, module-distribution, and file-size -checks will use the same model as their capabilities are added. +request and profile have structured selections for race testing and a separate +full vet pass. Package compilation, formatting, Markdown, module-distribution, +and file-size checks will use the same model as their capabilities are added. The model should let consumers declare their choices once while ReleaseKit owns execution and coordination. This follows [ADR 0002](../adr/0002-routine-releases.md): @@ -125,15 +125,18 @@ failure-stopping behavior of the candidate workflow. ## Implementation The [implementation plan](implementation.md) records the completed foundation -and current rollout. The facade exposes `ReleaseRequest.Validation.Go.Race`, and -profiles accept `validation.go.race`; both select `-race` for the one base Go -suite. Each remaining setting will be introduced with its first working -capability. Individual checkers, module-selected CI tooling, and the publication -endpoint expectation remain future work in the [v0.2.0 roadmap](v0.2.0.md). +and current rollout. The facade exposes `ReleaseRequest.Validation.Go.Race` and +`FullVet`, and profiles accept `validation.go.race` and +`validation.go.full_vet`. Race selection adds `-race` to the one base Go suite. +Full-vet selection disables test-time vet and runs native +`go vet -mod=readonly ./...` after successful tests. Each remaining setting +will be introduced with its first working capability. Individual checkers, +module-selected CI tooling, and the publication endpoint expectation remain +future work in the [v0.2.0 roadmap](v0.2.0.md). The target state above describes the complete model across those capabilities. -Foundation completion establishes their shared owners and conversion boundaries, -but does not mean the optional checks are implemented or that v0.2.0 is complete. +The rollout establishes shared owners and conversion boundaries incrementally; +not all optional checks are implemented, and v0.2.0 is not complete. ## Adoption evidence diff --git a/internal/validation/diagnostic.go b/internal/validation/diagnostic.go index eddc834..9f33f19 100644 --- a/internal/validation/diagnostic.go +++ b/internal/validation/diagnostic.go @@ -11,6 +11,8 @@ type Check string const ( // GoTest identifies the base Go test suite. GoTest Check = "go-test" + // GoVet identifies the separate full vet pass. + GoVet Check = "go-vet" ) // Finding describes a validation observation at an optional source location. diff --git a/internal/validation/race_test.go b/internal/validation/race_test.go index dd4d1fe..b273e94 100644 --- a/internal/validation/race_test.go +++ b/internal/validation/race_test.go @@ -11,31 +11,41 @@ import ( "runtime" "strings" "testing" + "time" ) -func TestRunUsesOneSuiteWithSelectedRaceSetting(t *testing.T) { +func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) { for _, test := range []struct { - name string - race bool + name string + race bool + fullVet bool }{ {name: "default"}, {name: "race", race: true}, + {name: "full vet", fullVet: true}, + {name: "race and full vet", race: true, fullVet: true}, } { t.Run(test.name, func(t *testing.T) { - logPath := installGoShim(t, false) + logPath := installGoShim(t, "", false) err := Run(context.Background(), Request{ RepositoryDir: t.TempDir(), - Settings: Settings{Race: test.race}, + Settings: Settings{Race: test.race, FullVet: test.fullVet}, }) if err != nil { t.Fatalf("Run() error = %v", err) } commands := readGoShimLog(t, logPath) - var tests []string - for _, command := range commands { + var tests, vets []string + var testPosition, vetPosition int + for i, command := range commands { if strings.HasPrefix(command, "test ") { tests = append(tests, command) + testPosition = i + } + if strings.HasPrefix(command, "vet ") { + vets = append(vets, command) + vetPosition = i } } if len(tests) != 1 { @@ -50,12 +60,33 @@ func TestRunUsesOneSuiteWithSelectedRaceSetting(t *testing.T) { if got := strings.Contains(command, " -race "); got != test.race { t.Errorf("Go test command %q has race=%t, want %t", tests[0], got, test.race) } + if got := strings.Contains(command, " -vet=off "); got != test.fullVet { + t.Errorf("Go test command %q disables test-time vet=%t, want %t", tests[0], got, test.fullVet) + } + wantVets := 0 + if test.fullVet { + wantVets = 1 + } + if len(vets) != wantVets { + t.Fatalf("Go vet invocations = %q, want %d", vets, wantVets) + } + if test.fullVet { + vetCommand := " " + vets[0] + " " + for _, argument := range []string{"vet", "-mod=readonly", "./..."} { + if !strings.Contains(vetCommand, " "+argument+" ") { + t.Errorf("Go vet command %q does not contain %q", vets[0], argument) + } + } + if vetPosition <= testPosition { + t.Errorf("Go vet command %q did not follow the test suite %q", vets[0], tests[0]) + } + } }) } } func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) { - logPath := installGoShim(t, true) + logPath := installGoShim(t, "race", false) err := Run(context.Background(), Request{ RepositoryDir: t.TempDir(), Settings: Settings{Race: true}, @@ -80,6 +111,69 @@ func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) { } } +func TestRunStopsAfterGoTestFailure(t *testing.T) { + logPath := installGoShim(t, "test", false) + err := Run(context.Background(), Request{ + RepositoryDir: t.TempDir(), + Settings: Settings{FullVet: true}, + }) + var failure *Failure + if !errors.As(err, &failure) || failure.Check != GoTest { + t.Fatalf("Run() error = %v, want a go-test validation failure", err) + } + for _, command := range readGoShimLog(t, logPath) { + if strings.HasPrefix(command, "vet ") { + t.Fatalf("Go vet ran after Go test failed: %q", command) + } + } +} + +func TestRunAttributesFullVetFailure(t *testing.T) { + logPath := installGoShim(t, "vet", false) + err := Run(context.Background(), Request{ + RepositoryDir: t.TempDir(), + Settings: Settings{FullVet: true}, + }) + var failure *Failure + if !errors.As(err, &failure) || failure.Check != GoVet { + t.Fatalf("Run() error = %v, want a go-vet validation failure", err) + } + var exitError *exec.ExitError + if !errors.As(err, &exitError) { + t.Fatalf("Run() error = %v, want the command failure preserved", err) + } + commands := readGoShimLog(t, logPath) + var validationCommands []string + for _, command := range commands { + if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") { + validationCommands = append(validationCommands, command) + } + } + if len(validationCommands) != 2 || !strings.HasPrefix(validationCommands[0], "test ") || !strings.HasPrefix(validationCommands[1], "vet ") { + t.Fatalf("validation command sequence = %q, want test followed by vet", validationCommands) + } +} + +func TestRunPreservesFullVetCancellation(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("the controlled Go command fixture uses a POSIX shell") + } + installGoShim(t, "", true) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err := Run(ctx, Request{ + RepositoryDir: t.TempDir(), + Settings: Settings{FullVet: true}, + }) + var failure *Failure + if !errors.As(err, &failure) || failure.Check != GoVet { + t.Fatalf("Run() error = %v, want a go-vet validation failure", err) + } + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("Run() error = %v, want deadline cause preserved", err) + } +} + func TestRunExecutesRaceEnabledSuiteOnHost(t *testing.T) { if !raceSupported(runtime.GOOS, runtime.GOARCH) { t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH) @@ -112,7 +206,7 @@ func TestRaceFixture(t *testing.T) {} } } -func installGoShim(t *testing.T, failRace bool) string { +func installGoShim(t *testing.T, failure string, delayVet bool) string { t.Helper() if runtime.GOOS == "windows" { t.Skip("the controlled Go command fixture uses a POSIX shell") @@ -131,17 +225,33 @@ func installGoShim(t *testing.T, failRace bool) string { if err != nil { t.Fatal(err) } - failBlock := "" - if failRace { - failBlock = ` + vetDelay := "" + if delayVet { + vetDelay = ` +if [ "${1-}" = "vet" ]; then + exec sleep 5 +fi +` + } + failBlock := fmt.Sprintf(` +if [ "${1-}" = "test" ]; then for argument in "$@"; do - if [ "$argument" = "-race" ]; then + if [ "%s" = "race" ] && [ "$argument" = "-race" ]; then echo "race detector prerequisite unavailable" >&2 exit 2 fi done -` - } + if [ "%s" = "test" ]; then + echo "Go test failed" >&2 + exit 2 + fi +fi +if [ "${1-}" = "vet" ] && [ "%s" = "vet" ]; then + echo "Go vet failed" >&2 + exit 2 +fi +%s +`, failure, failure, failure, vetDelay) script := fmt.Sprintf(`#!/bin/sh set -eu printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG" diff --git a/internal/validation/validation.go b/internal/validation/validation.go index 8cbe804..138bfaf 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -10,7 +10,8 @@ import ( // Settings selects shared validation behavior. type Settings struct { - Race bool + Race bool + FullVet bool } // Request identifies the captured source and output used by shared validation. @@ -37,10 +38,19 @@ func Run(ctx context.Context, request Request) error { if request.Settings.Race { args = append(args, "-race") } + if request.Settings.FullVet { + args = append(args, "-vet=off") + } args = append(args, "./...") _, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, args...) if err != nil { return &Failure{Check: GoTest, Cause: err} } + if request.Settings.FullVet { + _, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, "vet", "-mod=readonly", "./...") + if err != nil { + return &Failure{Check: GoVet, Cause: err} + } + } return nil } diff --git a/publish.go b/publish.go index 597146c..8e5a565 100644 --- a/publish.go +++ b/publish.go @@ -48,9 +48,10 @@ type PublicationResult struct { State PublicationState // publication outcome for this invocation } -// Publish always runs one offline Go test suite for the captured candidate; -// Validation.Go.Race enables the race detector for that suite. It then calls -// AdditionalChecks when supplied. It optionally builds selected +// Publish always runs one offline Go test suite for the captured candidate. +// Validation.Go.Race enables the race detector for that suite, and +// Validation.Go.FullVet runs go vet afterward instead of test-time vet. It then +// calls AdditionalChecks when supplied. It optionally builds selected // targets in temporary storage outside the worktree and pushes the exact // annotated tag object to the selected remote. It returns a result with errors: preflight // failures are not_attempted, failed authoritative guards are conflict, and diff --git a/validation_error.go b/validation_error.go index c69e2bf..ae94f23 100644 --- a/validation_error.go +++ b/validation_error.go @@ -8,7 +8,7 @@ import ( ) // ValidationCheck identifies a shared validation check that failed. -// Current checks include "go-test". +// Current checks include "go-test" and "go-vet". type ValidationCheck string // ValidationFinding describes a failed check observation at an optional diff --git a/validation_test.go b/validation_test.go index a4e19da..9959187 100644 --- a/validation_test.go +++ b/validation_test.go @@ -1,12 +1,29 @@ package releasekit -import "testing" +import ( + "errors" + "testing" -func TestCandidateRequestMapsRaceValidation(t *testing.T) { + "gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" +) + +func TestCandidateRequestMapsGoValidation(t *testing.T) { request := candidateRequest(ReleaseRequest{ - Validation: ValidationSpec{Go: GoValidationSpec{Race: true}}, + Validation: ValidationSpec{Go: GoValidationSpec{Race: true, FullVet: true}}, }) - if !request.Validation.Race { - t.Fatalf("candidate request validation = %#v, want race enabled", request.Validation) + if !request.Validation.Race || !request.Validation.FullVet { + t.Fatalf("candidate request validation = %#v, want race and full vet enabled", request.Validation) + } +} + +func TestValidationErrorPreservesGoVetIdentity(t *testing.T) { + cause := errors.New("vet failed") + got := convertValidationError(&validation.Failure{Check: validation.GoVet, Cause: cause}) + var validationError *ValidationError + if !errors.As(got, &validationError) || validationError.Check != "go-vet" { + t.Fatalf("converted error = %#v, want go-vet validation identity", got) + } + if !errors.Is(got, cause) { + t.Fatalf("converted error = %v, want cause preserved", got) } }