Inspect committed workspace files

This commit is contained in:
2026-10-07 22:45:47 +00:00
parent c5f925c949
commit 45dd827dda
17 changed files with 307 additions and 30 deletions

View File

@@ -52,6 +52,17 @@ type ValidationSpec struct {
// GoFormat checks the formatting of committed regular Go source files. It // GoFormat checks the formatting of committed regular Go source files. It
// does not modify repository contents and is disabled when false. // does not modify repository contents and is disabled when false.
GoFormat bool 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. // 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. // compilation produces no artifacts and does not execute target binaries.
// Validation.GoFormat checks regular committed Go source after those selected // Validation.GoFormat checks regular committed Go source after those selected
// Go checks and before AdditionalChecks. Its zero value disables formatting. // 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 // The zero values of all selections preserve the ordinary suite. If
// Build selects targets, Check builds them after validation in temporary // Build selects targets, Check builds them after validation in temporary
// storage outside the worktree, then rechecks // storage outside the worktree, then rechecks
@@ -106,6 +120,9 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
FullVet: request.Validation.Go.FullVet, FullVet: request.Validation.Go.FullVet,
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)), CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
GoFormat: request.Validation.GoFormat, GoFormat: request.Validation.GoFormat,
ModuleDistribution: validation.ModuleDistributionSettings{
WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles,
},
}, },
AdditionalChecks: candidateChecks(request.AdditionalChecks), AdditionalChecks: candidateChecks(request.AdditionalChecks),
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output), AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),

View File

@@ -129,7 +129,15 @@ type ValidationSettings struct {
Go GoValidationSettings `json:"go,omitempty"` Go GoValidationSettings `json:"go,omitempty"`
// GoFormat checks the formatting of committed regular Go source files. // GoFormat checks the formatting of committed regular Go source files.
// Omitted or false disables the check. // 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. // GoValidationSettings selects checks performed by the Go toolchain.

View File

@@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
func TestLoadSettingsReadsGoValidation(t *testing.T) { func TestLoadSettingsReadsGoValidation(t *testing.T) {
repoDir := t.TempDir() repoDir := t.TempDir()
profilePath := filepath.Join(repoDir, "releasekit.json") 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) t.Fatal(err)
} }
@@ -152,8 +152,8 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet { 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, race, and full vet enabled", settings.Validation) 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 { 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) t.Fatalf("compile targets = %q, want %q", got, want)

View File

@@ -162,6 +162,9 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
} }
result := releasekit.ValidationSpec{ result := releasekit.ValidationSpec{
GoFormat: settings.GoFormat, GoFormat: settings.GoFormat,
ModuleDistribution: releasekit.ModuleDistributionValidationSpec{
WorkspaceFiles: settings.ModuleDistribution.WorkspaceFiles,
},
Go: releasekit.GoValidationSpec{ Go: releasekit.GoValidationSpec{
Race: settings.Go.Race, Race: settings.Go.Race,
FullVet: settings.Go.FullVet, FullVet: settings.Go.FullVet,

View File

@@ -117,8 +117,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
Product: "Example Product", Product: "Example Product",
ExpectedPushURL: "publication-only value", ExpectedPushURL: "publication-only value",
Validation: &cli.ValidationSettings{ Validation: &cli.ValidationSettings{
GoFormat: true, GoFormat: true,
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}}, Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true},
}, },
}, nil) }, nil)
if !request.Validation.Go.Race || !request.Validation.Go.FullVet { if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
@@ -127,6 +128,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
if !request.Validation.GoFormat { if !request.Validation.GoFormat {
t.Fatalf("candidate request GoFormat = false, want true") 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 != "" { if request.ExpectedPushURL != "" {
t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL) t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL)
} }

View File

@@ -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 regular committed Go files with the Go formatter after tests, vet, and package
compilation. It includes vendor and nested-module source, ignores the worktree compilation. It includes vendor and nested-module source, ignores the worktree
and symlink blobs, and reports unformatted paths as `go-format` without and symlink blobs, and reports unformatted paths as `go-format` without
modifying files. Any artifact build selected by the separate `build` modifying files. When `validation.module_distribution.workspace_files` is true,
setting runs after shared validation and configured checks. `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 `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, 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` 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, 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 test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful
tests. Selected package compilation and committed Go formatting follow, then tests. Selected package compilation and committed Go formatting follow. The
configured checks run in profile order. It then creates an annotated local tag 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, and pushes only its exact tag object. It prints the confirmed version,
candidate commit, and tag object candidate commit, and tag object
on success. Validation 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 as `package-compile` with its selected target; this does not create build
artifacts or execute target binaries. When `validation.go_format` is true, artifacts or execute target binaries. When `validation.go_format` is true,
committed Go formatting runs after package compilation and before configured committed Go formatting runs after package compilation and before configured
checks. A failure is reported as `go-format` with affected repository paths; checks. The selected workspace-file check then reports committed go.work and
parser and source-inspection causes are retained. 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 The optional profile field `expected_push_url` applies only to `publish`. A
nonempty value must exactly match the effective push URL resolved by Git; nonempty value must exactly match the effective push URL resolved by Git;

View File

@@ -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 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 disables the check. It runs after tests, selected full vet, and selected package
compilation, before configured product checks. 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: For example:
```json ```json
@@ -56,7 +62,10 @@ For example:
"full_vet": true, "full_vet": true,
"compile_targets": ["linux/amd64", "linux/arm64"] "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 `validation.go.compile_targets` entry, in profile order. Compile failures use
the `package-compile` identity and include the selected target. When selected, the `package-compile` identity and include the selected target. When selected,
`validation.go_format` checks regular committed Go source with go/format after `validation.go_format` checks regular committed Go source with go/format after
package compilation; unformatted paths use the `go-format` identity. Configured package compilation; unformatted paths use the `go-format` identity. When
`checks` commands run afterward, once each in order, as additional product `validation.module_distribution.workspace_files` is true, shared validation
checks. Artifact builds selected by the separate `build` setting run after also reports committed go.work and go.work.sum blobs after Go formatting, using
shared validation and product checks. 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 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 `build` command uses the selected build settings and writes binaries to `dist` unless
`--output` selects another path. `--output` selects another path.

View File

@@ -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 ignores symlink blobs and worktree changes, and reports unformatted paths as
`go-format` without rewriting files. Its zero value disables the check; it runs `go-format` without rewriting files. Its zero value disables the check; it runs
after tests, selected vet, and package compilation, before `AdditionalChecks`. 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. ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
It does not download a different toolchain automatically. If race prerequisites It does not download a different toolchain automatically. If race prerequisites
are unavailable, the `go-test` check fails rather than being skipped; a selected are unavailable, the `go-test` check fails rather than being skipped; a selected

View File

@@ -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 tests, selected vet, and package compilation, and a failure stops publication
before product checks or tag creation. 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 If `Build` selects targets, ReleaseKit verifies them after the standard suite
and additional checks, before creating the tag. The operation then rechecks the and additional checks, before creating the tag. The operation then rechecks the
candidate and remote guards, creates an annotated tag for the captured commit, candidate and remote guards, creates an annotated tag for the captured commit,

View File

@@ -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. on parse or inspection errors. It runs before product checks and build callbacks.
`Request.ReadCandidateTree` lets selected file checks request candidate `Request.ReadCandidateTree` lets selected file checks request candidate
contents; the Go-only path does not enumerate the tree. 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 `CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
lazily, and resolves destinations without consulting the worktree or lazily, and resolves destinations without consulting the worktree or
neighboring files. neighboring files.

View File

@@ -337,6 +337,12 @@ behavior, API/profile parity, and failure before later product/build work.
## Stage 6: Select committed workspace-file inspection ## 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. **Outcome:** One distribution rule works independently of the Go environment.
1. Add `ModuleDistributionValidationSpec.WorkspaceFiles` and the matching JSON 1. Add `ModuleDistributionValidationSpec.WorkspaceFiles` and the matching JSON

View File

@@ -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) { func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n") repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
var output bytes.Buffer var output bytes.Buffer

View File

@@ -17,6 +17,8 @@ const (
PackageCompile Check = "package-compile" PackageCompile Check = "package-compile"
// GoFormat identifies committed Go source formatting. // GoFormat identifies committed Go source formatting.
GoFormat Check = "go-format" 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. // Finding describes a validation observation at an optional source location.

View File

@@ -8,6 +8,7 @@ import (
"go/format" "go/format"
"io" "io"
"os" "os"
"path"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
@@ -15,10 +16,16 @@ import (
// Settings selects shared validation behavior. // Settings selects shared validation behavior.
type Settings struct { type Settings struct {
Race bool Race bool
FullVet bool FullVet bool
CompileTargets []Target CompileTargets []Target
GoFormat bool GoFormat bool
ModuleDistribution ModuleDistributionSettings
}
// ModuleDistributionSettings selects candidate distribution checks.
type ModuleDistributionSettings struct {
WorkspaceFiles bool
} }
// Target selects one package-compilation target. // 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) tree, err := request.ReadCandidateTree(ctx)
if err != nil { 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 { if request.Settings.GoFormat {
return err 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 return nil
} }

View File

@@ -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)
}
}

View File

@@ -8,8 +8,8 @@ import (
) )
// ValidationCheck identifies a shared validation check that failed. // ValidationCheck identifies a shared validation check that failed.
// Current checks include "go-test", "go-vet", "package-compile", and // Current checks include "go-test", "go-vet", "package-compile", "go-format",
// "go-format". // and "workspace-files".
type ValidationCheck string type ValidationCheck string
// ValidationFinding describes a failed check observation at an optional // ValidationFinding describes a failed check observation at an optional

View File

@@ -12,10 +12,10 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
Validation: ValidationSpec{Go: GoValidationSpec{ Validation: ValidationSpec{Go: GoValidationSpec{
Race: true, FullVet: true, Race: true, FullVet: true,
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}, 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 { if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles {
t.Fatalf("candidate request validation = %#v, want race, full vet, and formatting enabled", request.Validation) 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"}} want := []validation.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
if len(request.Validation.CompileTargets) != len(want) { 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) 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)
}
}