diff --git a/check.go b/check.go index 908dc2a..0109f05 100644 --- a/check.go +++ b/check.go @@ -52,6 +52,17 @@ type ValidationSpec struct { // GoFormat checks the formatting of committed regular Go source files. It // does not modify repository contents and is disabled when false. GoFormat bool + // ModuleDistribution selects checks for files that affect module-source + // distribution. Its zero value disables those checks. + ModuleDistribution ModuleDistributionValidationSpec +} + +// ModuleDistributionValidationSpec selects independent module-source +// distribution checks. +type ModuleDistributionValidationSpec struct { + // WorkspaceFiles rejects committed go.work and go.work.sum blobs, including + // nested paths and symlink entries. It does not select Go's test workspace. + WorkspaceFiles bool } // GoValidationSpec selects checks performed by the Go toolchain. @@ -79,6 +90,9 @@ type GoValidationSpec struct { // compilation produces no artifacts and does not execute target binaries. // Validation.GoFormat checks regular committed Go source after those selected // Go checks and before AdditionalChecks. Its zero value disables formatting. +// Validation.ModuleDistribution.WorkspaceFiles rejects committed workspace +// files after the other selected shared checks. Its zero value disables the +// distribution check and does not change Go's workspace environment. // The zero values of all selections preserve the ordinary suite. If // Build selects targets, Check builds them after validation in temporary // storage outside the worktree, then rechecks @@ -106,6 +120,9 @@ func candidateRequest(request ReleaseRequest) candidate.Request { FullVet: request.Validation.Go.FullVet, CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)), GoFormat: request.Validation.GoFormat, + ModuleDistribution: validation.ModuleDistributionSettings{ + WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles, + }, }, AdditionalChecks: candidateChecks(request.AdditionalChecks), AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output), diff --git a/cmd/releasekit/internal/cli/config.go b/cmd/releasekit/internal/cli/config.go index 51b543d..f09a479 100644 --- a/cmd/releasekit/internal/cli/config.go +++ b/cmd/releasekit/internal/cli/config.go @@ -129,7 +129,15 @@ type ValidationSettings struct { Go GoValidationSettings `json:"go,omitempty"` // GoFormat checks the formatting of committed regular Go source files. // Omitted or false disables the check. - GoFormat bool `json:"go_format,omitempty"` + GoFormat bool `json:"go_format,omitempty"` + ModuleDistribution ModuleDistributionSettings `json:"module_distribution,omitempty"` +} + +// ModuleDistributionSettings selects committed module-source distribution checks. +type ModuleDistributionSettings struct { + // WorkspaceFiles rejects committed go.work and go.work.sum blobs. + // Omitted or false disables the check. + WorkspaceFiles bool `json:"workspace_files,omitempty"` } // GoValidationSettings selects checks performed by the Go toolchain. diff --git a/cmd/releasekit/internal/cli/config_test.go b/cmd/releasekit/internal/cli/config_test.go index 3c7942a..e402503 100644 --- a/cmd/releasekit/internal/cli/config_test.go +++ b/cmd/releasekit/internal/cli/config_test.go @@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(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","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil { + if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"module_distribution":{"workspace_files":true},"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil { t.Fatal(err) } @@ -152,8 +152,8 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) { if err != nil { t.Fatal(err) } - if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet { - t.Fatalf("validation settings = %#v, want Go formatting, race, and full vet enabled", settings.Validation) + if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet { + t.Fatalf("validation settings = %#v, want Go formatting, workspace inspection, race, and full vet enabled", settings.Validation) } if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want { t.Fatalf("compile targets = %q, want %q", got, want) diff --git a/cmd/releasekit/main.go b/cmd/releasekit/main.go index 37e561b..e253c92 100644 --- a/cmd/releasekit/main.go +++ b/cmd/releasekit/main.go @@ -162,6 +162,9 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec } result := releasekit.ValidationSpec{ GoFormat: settings.GoFormat, + ModuleDistribution: releasekit.ModuleDistributionValidationSpec{ + WorkspaceFiles: settings.ModuleDistribution.WorkspaceFiles, + }, 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 55e8f63..ba2561b 100644 --- a/cmd/releasekit/main_test.go +++ b/cmd/releasekit/main_test.go @@ -117,8 +117,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) { Product: "Example Product", ExpectedPushURL: "publication-only value", Validation: &cli.ValidationSettings{ - GoFormat: true, - Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}}, + GoFormat: true, + Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}}, + ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true}, }, }, nil) if !request.Validation.Go.Race || !request.Validation.Go.FullVet { @@ -127,6 +128,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) { if !request.Validation.GoFormat { t.Fatalf("candidate request GoFormat = false, want true") } + if !request.Validation.ModuleDistribution.WorkspaceFiles { + t.Fatalf("candidate request workspace files = false, want true") + } if request.ExpectedPushURL != "" { t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL) } diff --git a/docs/cli.md b/docs/cli.md index 09fac25..8fac261 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -39,8 +39,11 @@ show the selected target. When `validation.go_format` is true, `check` examines regular committed Go files with the Go formatter after tests, vet, and package compilation. It includes vendor and nested-module source, ignores the worktree and symlink blobs, and reports unformatted paths as `go-format` without -modifying files. Any artifact build selected by the separate `build` -setting runs after shared validation and configured checks. +modifying files. When `validation.module_distribution.workspace_files` is true, +`check` reports committed `go.work` and `go.work.sum` blobs, including nested +and symlink entries, as `workspace-files`. This inspection is independent of +`GOWORK` and ignores uncommitted files. Any artifact build selected by the +separate `build` setting runs after shared validation and configured checks. `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, @@ -60,8 +63,9 @@ 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. When `validation.go.full_vet` is true, test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful -tests. Selected package compilation and committed Go formatting follow, then -configured checks run in profile order. It then creates an annotated local tag +tests. Selected package compilation and committed Go formatting follow. The +selected workspace-file check then runs, 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 on success. Validation @@ -77,8 +81,9 @@ after tests and full vet and before configured checks. Each failure is reported as `package-compile` with its selected target; this does not create build artifacts or execute target binaries. When `validation.go_format` is true, committed Go formatting runs after package compilation and before configured -checks. A failure is reported as `go-format` with affected repository paths; -parser and source-inspection causes are retained. +checks. The selected workspace-file check then reports committed go.work and +go.work.sum paths as `workspace-files`. Formatting failures are reported as +`go-format` with affected paths and preserve parser or source-inspection causes. The optional profile field `expected_push_url` applies only to `publish`. A nonempty value must exactly match the effective push URL resolved by Git; diff --git a/docs/config.md b/docs/config.md index c7d98f6..7c2f878 100644 --- a/docs/config.md +++ b/docs/config.md @@ -45,6 +45,12 @@ symlink blobs and the worktree, and reports every selected path whose bytes change under formatting. It never writes formatted bytes back. Omitted or false disables the check. It runs after tests, selected full vet, and selected package compilation, before configured product checks. + +When `validation.module_distribution.workspace_files` is true, it reports +committed blob entries whose basename is `go.work` or `go.work.sum`, including +nested paths and symlinks. It checks candidate distribution contents regardless +of `GOWORK`; it does not select or change the workspace used by Go commands, +and it does not alter existing vendor behavior. Omitted or false disables it. For example: ```json @@ -56,7 +62,10 @@ For example: "full_vet": true, "compile_targets": ["linux/amd64", "linux/arm64"] }, - "go_format": true + "go_format": true, + "module_distribution": { + "workspace_files": true + } } } ``` @@ -84,10 +93,14 @@ run native `go vet -mod=readonly ./...` after successful tests. They then run `validation.go.compile_targets` entry, in profile order. Compile failures use the `package-compile` identity and include the selected target. When selected, `validation.go_format` checks regular committed Go source with go/format after -package compilation; unformatted paths use the `go-format` identity. Configured -`checks` commands run afterward, once each in order, as additional product -checks. Artifact builds selected by the separate `build` setting run after -shared validation and product checks. +package compilation; unformatted paths use the `go-format` identity. When +`validation.module_distribution.workspace_files` is true, shared validation +also reports committed go.work and go.work.sum blobs after Go formatting, using +the workspace-files identity. Uncommitted workspace files are not inspected. +Configured `checks` commands run afterward, once each in order, as additional +product checks. Artifact builds selected by the separate `build` setting 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. diff --git a/docs/consumers/check.md b/docs/consumers/check.md index 076728b..52af16e 100644 --- a/docs/consumers/check.md +++ b/docs/consumers/check.md @@ -41,6 +41,12 @@ formatter built into ReleaseKit. It includes vendor and nested-module source, ignores symlink blobs and worktree changes, and reports unformatted paths as `go-format` without rewriting files. Its zero value disables the check; it runs after tests, selected vet, and package compilation, before `AdditionalChecks`. + +Set `Validation.ModuleDistribution.WorkspaceFiles` to reject committed +`go.work` and `go.work.sum` blobs, including nested and symlink entries. It +examines the captured candidate even when `GOWORK=off`, ignores uncommitted +files, and does not change Go test workspace selection or vendor behavior. The +zero value disables the check; failures use `workspace-files`. ReleaseKit calls `AdditionalChecks` once after shared validation when supplied. It does not download a different toolchain automatically. If race prerequisites are unavailable, the `go-test` check fails rather than being skipped; a selected diff --git a/docs/consumers/publish.md b/docs/consumers/publish.md index 71d1b45..ecdd6e9 100644 --- a/docs/consumers/publish.md +++ b/docs/consumers/publish.md @@ -55,6 +55,13 @@ worktree changes and symlink blobs, and reports unformatted paths as tests, selected vet, and package compilation, and a failure stops publication before product checks or tag creation. +Set `Validation.ModuleDistribution.WorkspaceFiles` to reject committed +`go.work` and `go.work.sum` blobs, including nested paths and symlink entries. +The rule reads the captured candidate regardless of `GOWORK`; uncommitted files +are ignored, and Go workspace selection and vendor behavior are unchanged. Its +zero value disables the check. Findings use `workspace-files` and stop +publication before product checks or 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 candidate and remote guards, creates an annotated tag for the captured commit, diff --git a/docs/internal/validation.md b/docs/internal/validation.md index 8c99636..14e948c 100644 --- a/docs/internal/validation.md +++ b/docs/internal/validation.md @@ -25,6 +25,11 @@ symlink blobs, reports all unformatted paths, and stops with a contextual cause on parse or inspection errors. It runs before product checks and build callbacks. `Request.ReadCandidateTree` lets selected file checks request candidate contents; the Go-only path does not enumerate the tree. + +The workspace-file check uses that same candidate inventory and reports blob +entries named go.work or go.work.sum, including nested paths and symlink blobs. +It does not read workspace contents or depend on the workspace selected for Go +commands. `CandidateTree` gets metadata and bytes from Git objects, reads blob bytes lazily, and resolves destinations without consulting the worktree or neighboring files. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 61d8057..6a63468 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -337,6 +337,12 @@ behavior, API/profile parity, and failure before later product/build work. ## Stage 6: Select committed workspace-file inspection +**Completion:** Complete. `Validation.ModuleDistribution.WorkspaceFiles` and +`validation.module_distribution.workspace_files` report committed blob entries +named `go.work` or `go.work.sum`, including nested paths and symlinks. The rule +inspects the captured candidate independently of `GOWORK` and leaves the Go +workspace environment and vendor behavior unchanged. + **Outcome:** One distribution rule works independently of the Go environment. 1. Add `ModuleDistributionValidationSpec.WorkspaceFiles` and the matching JSON diff --git a/internal/candidate/check_test.go b/internal/candidate/check_test.go index 01a7881..37b98a3 100644 --- a/internal/candidate/check_test.go +++ b/internal/candidate/check_test.go @@ -108,6 +108,52 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T) } } +func TestCheckReportsCommittedWorkspaceFilesWhenGoWorkspaceIsDisabled(t *testing.T) { + t.Setenv("GOWORK", "off") + repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n") + writeTestFile(t, repository, "go.work", "go 1.25.0\n") + writeTestFile(t, repository, "nested/go.work.sum", "workspace-sum\n") + if err := os.MkdirAll(filepath.Join(repository, "linked"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../go.work", filepath.Join(repository, "linked", "go.work")); err != nil { + t.Fatal(err) + } + gitRun(t, repository, "add", "go.work", "nested/go.work.sum", "linked/go.work") + gitRun(t, repository, "commit", "-m", "add workspace files") + _, err := Check(context.Background(), Request{ + RepositoryDir: repository, + Product: "Example Product", + Version: testVersion, + Validation: validation.Settings{ModuleDistribution: validation.ModuleDistributionSettings{ + WorkspaceFiles: true, + }}, + }) + var failure *validation.Failure + if !errors.As(err, &failure) || failure.Check != validation.WorkspaceFiles { + t.Fatalf("Check error = %v, want workspace-files failure", err) + } + want := map[string]bool{ + "go.work": false, + "linked/go.work": false, + "nested/go.work.sum": false, + } + if len(failure.Findings) != len(want) { + t.Fatalf("workspace findings = %#v, want %d paths", failure.Findings, len(want)) + } + for _, finding := range failure.Findings { + if _, ok := want[finding.Path]; !ok { + t.Errorf("unexpected workspace path %q", finding.Path) + } + want[finding.Path] = true + } + for path, found := range want { + if !found { + t.Errorf("workspace path %q was not reported", path) + } + } +} + func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) { repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n") var output bytes.Buffer diff --git a/internal/validation/diagnostic.go b/internal/validation/diagnostic.go index 3203906..9619a72 100644 --- a/internal/validation/diagnostic.go +++ b/internal/validation/diagnostic.go @@ -17,6 +17,8 @@ const ( PackageCompile Check = "package-compile" // GoFormat identifies committed Go source formatting. GoFormat Check = "go-format" + // WorkspaceFiles identifies committed workspace files in module source. + WorkspaceFiles Check = "workspace-files" ) // Finding describes a validation observation at an optional source location. diff --git a/internal/validation/validation.go b/internal/validation/validation.go index 32a6b96..cd1f5ac 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -8,6 +8,7 @@ import ( "go/format" "io" "os" + "path" "strings" "gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" @@ -15,10 +16,16 @@ import ( // Settings selects shared validation behavior. type Settings struct { - Race bool - FullVet bool - CompileTargets []Target - GoFormat bool + Race bool + FullVet bool + CompileTargets []Target + GoFormat bool + ModuleDistribution ModuleDistributionSettings +} + +// ModuleDistributionSettings selects candidate distribution checks. +type ModuleDistributionSettings struct { + WorkspaceFiles bool } // Target selects one package-compilation target. @@ -91,14 +98,45 @@ func Run(ctx context.Context, request Request) error { } } } - if request.Settings.GoFormat { + if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles { tree, err := request.ReadCandidateTree(ctx) if err != nil { - return &Failure{Check: GoFormat, Cause: err} + check := WorkspaceFiles + if request.Settings.GoFormat { + check = GoFormat + } + return &Failure{Check: check, Cause: err} } - if err := checkGoFormat(ctx, tree); err != nil { - return err + if request.Settings.GoFormat { + if err := checkGoFormat(ctx, tree); err != nil { + return err + } } + if request.Settings.ModuleDistribution.WorkspaceFiles { + if err := checkWorkspaceFiles(tree); err != nil { + return err + } + } + } + return nil +} + +func checkWorkspaceFiles(tree *CandidateTree) error { + var findings []Finding + for _, entry := range tree.Entries() { + if entry.Type != "blob" { + continue + } + switch path.Base(entry.Path) { + case "go.work", "go.work.sum": + findings = append(findings, Finding{ + Path: entry.Path, + Message: "workspace file is committed in the candidate", + }) + } + } + if len(findings) != 0 { + return &Failure{Check: WorkspaceFiles, Findings: findings} } return nil } diff --git a/internal/validation/workspace_test.go b/internal/validation/workspace_test.go new file mode 100644 index 0000000..aa54555 --- /dev/null +++ b/internal/validation/workspace_test.go @@ -0,0 +1,100 @@ +package validation + +import ( + "context" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/ReleaseKit/internal/git" +) + +func TestCheckWorkspaceFilesReportsCommittedBlobNames(t *testing.T) { + tree := &CandidateTree{entries: []git.TreeEntry{ + {Path: "go.work", Type: "blob", Mode: "100644"}, + {Path: "nested/go.work.sum", Type: "blob", Mode: "100644"}, + {Path: "vendor/nested/link/go.work", Type: "blob", Mode: "120000"}, + {Path: "nested/not-go.work.backup", Type: "blob", Mode: "100644"}, + {Path: "submodule/go.work", Type: "commit", Mode: "160000"}, + }} + err := checkWorkspaceFiles(tree) + failure, ok := err.(*Failure) + if !ok || failure.Check != WorkspaceFiles { + t.Fatalf("checkWorkspaceFiles error = %v, want workspace-files failure", err) + } + want := map[string]bool{ + "go.work": false, + "nested/go.work.sum": false, + "vendor/nested/link/go.work": false, + } + if len(failure.Findings) != len(want) { + t.Fatalf("workspace findings = %#v, want %d paths", failure.Findings, len(want)) + } + for _, finding := range failure.Findings { + if _, ok := want[finding.Path]; !ok { + t.Errorf("unexpected workspace path %q", finding.Path) + } + want[finding.Path] = true + } + for path, found := range want { + if !found { + t.Errorf("workspace path %q was not reported", path) + } + } +} + +func TestCheckWorkspaceFilesAllowsOtherNames(t *testing.T) { + tree := &CandidateTree{entries: []git.TreeEntry{ + {Path: "go.mod", Type: "blob", Mode: "100644"}, + {Path: "examples/go.work.example", Type: "blob", Mode: "100644"}, + }} + if err := checkWorkspaceFiles(tree); err != nil { + t.Fatalf("checkWorkspaceFiles error = %v, want no findings", err) + } +} + +func TestRunWorkspaceFilesUsesCommittedTreeWithGoWorkspaceDisabled(t *testing.T) { + t.Setenv("GOWORK", "off") + repository := newTreeRepository(t) + writeTreeFile(t, repository, "go.work", []byte("go 1.25.0\n")) + writeTreeFile(t, repository, "nested/go.work.sum", []byte("committed workspace metadata\n")) + gitTree(t, repository, "add", ".") + gitTree(t, repository, "commit", "-m", "capture workspace files") + commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD")) + writeTreeFile(t, repository, "untracked/go.work.sum", []byte("uncommitted workspace metadata\n")) + + logPath := installGoShim(t, "", false) + if err := Run(context.Background(), Request{RepositoryDir: repository, Commit: commit}); err != nil { + t.Fatalf("default validation = %v, want workspace check disabled", err) + } + + logPath = installGoShim(t, "", false) + err := Run(context.Background(), Request{ + RepositoryDir: repository, + Commit: commit, + Settings: Settings{ModuleDistribution: ModuleDistributionSettings{ + WorkspaceFiles: true, + }}, + }) + failure, ok := err.(*Failure) + if !ok || failure.Check != WorkspaceFiles { + t.Fatalf("selected workspace error = %v, want workspace-files failure", err) + } + want := map[string]bool{"go.work": false, "nested/go.work.sum": false} + if len(failure.Findings) != len(want) { + t.Fatalf("workspace findings = %#v, want committed paths only", failure.Findings) + } + for _, finding := range failure.Findings { + if _, ok := want[finding.Path]; !ok { + t.Errorf("unexpected workspace path %q", finding.Path) + } + want[finding.Path] = true + } + for path, found := range want { + if !found { + t.Errorf("committed workspace path %q was not reported", path) + } + } + if got := strings.Join(readGoShimLog(t, logPath), "\n"); !strings.Contains(got, "test -mod=readonly -count=1 ./...") { + t.Fatalf("Go commands = %q, want ordinary test suite with GOWORK=off", got) + } +} diff --git a/validation_error.go b/validation_error.go index 6417ba2..a180a46 100644 --- a/validation_error.go +++ b/validation_error.go @@ -8,8 +8,8 @@ import ( ) // ValidationCheck identifies a shared validation check that failed. -// Current checks include "go-test", "go-vet", "package-compile", and -// "go-format". +// Current checks include "go-test", "go-vet", "package-compile", "go-format", +// and "workspace-files". type ValidationCheck string // ValidationFinding describes a failed check observation at an optional diff --git a/validation_test.go b/validation_test.go index 1557fb8..9d21a72 100644 --- a/validation_test.go +++ b/validation_test.go @@ -12,10 +12,10 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) { Validation: ValidationSpec{Go: GoValidationSpec{ Race: true, FullVet: true, CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}, - }, GoFormat: true}, + }, GoFormat: true, ModuleDistribution: ModuleDistributionValidationSpec{WorkspaceFiles: true}}, }) - if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat { - t.Fatalf("candidate request validation = %#v, want race, full vet, and formatting enabled", request.Validation) + if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles { + t.Fatalf("candidate request validation = %#v, want race, full vet, formatting, and workspace inspection enabled", request.Validation) } want := []validation.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}} if len(request.Validation.CompileTargets) != len(want) { @@ -61,3 +61,20 @@ func TestValidationErrorPreservesGoFormatIdentityAndFindings(t *testing.T) { t.Fatalf("converted error = %v, want parser cause preserved", got) } } + +func TestValidationErrorPreservesWorkspaceFilesIdentity(t *testing.T) { + got := convertValidationError(&validation.Failure{ + Check: validation.WorkspaceFiles, + Findings: []validation.Finding{{ + Path: "nested/go.work.sum", + Message: "workspace file is committed in the candidate", + }}, + }) + var validationError *ValidationError + if !errors.As(got, &validationError) || validationError.Check != "workspace-files" { + t.Fatalf("converted error = %#v, want workspace-files validation identity", got) + } + if len(validationError.Findings) != 1 || validationError.Findings[0].Path != "nested/go.work.sum" { + t.Fatalf("converted findings = %#v, want nested workspace path", validationError.Findings) + } +}