Check committed file sizes
This commit is contained in:
33
check.go
33
check.go
@@ -55,6 +55,19 @@ type ValidationSpec struct {
|
|||||||
// ModuleDistribution selects checks for files that affect module-source
|
// ModuleDistribution selects checks for files that affect module-source
|
||||||
// distribution. Its zero value disables those checks.
|
// distribution. Its zero value disables those checks.
|
||||||
ModuleDistribution ModuleDistributionValidationSpec
|
ModuleDistribution ModuleDistributionValidationSpec
|
||||||
|
// FileSize selects a committed-file size check. Nil disables it; an empty
|
||||||
|
// object selects the 1,000,000-byte default.
|
||||||
|
FileSize *FileSizeValidationSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSizeValidationSpec selects a maximum size for each committed blob.
|
||||||
|
type FileSizeValidationSpec struct {
|
||||||
|
// LimitBytes is the maximum decimal byte count. Nil selects 1,000,000;
|
||||||
|
// zero is a zero-byte limit, and negative values are invalid.
|
||||||
|
LimitBytes *int64
|
||||||
|
// Exclusions are literal repository-relative file or directory paths.
|
||||||
|
// A directory exclusion includes committed descendants.
|
||||||
|
Exclusions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleDistributionValidationSpec selects independent module-source
|
// ModuleDistributionValidationSpec selects independent module-source
|
||||||
@@ -99,6 +112,9 @@ type GoValidationSpec struct {
|
|||||||
// Validation.ModuleDistribution.LocalReplacements parses committed go.mod
|
// Validation.ModuleDistribution.LocalReplacements parses committed go.mod
|
||||||
// declarations, including nested modules, and rejects filesystem replacements
|
// declarations, including nested modules, and rejects filesystem replacements
|
||||||
// after workspace-file inspection. Its zero value disables that check.
|
// after workspace-file inspection. Its zero value disables that check.
|
||||||
|
// Validation.FileSize checks each selected committed blob after module
|
||||||
|
// distribution checks. Its nil value disables size checks; an empty object
|
||||||
|
// enables the default limit.
|
||||||
// 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
|
||||||
@@ -117,6 +133,7 @@ func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func candidateRequest(request ReleaseRequest) candidate.Request {
|
func candidateRequest(request ReleaseRequest) candidate.Request {
|
||||||
|
fileSize := internalFileSizeSettings(request.Validation.FileSize)
|
||||||
result := candidate.Request{
|
result := candidate.Request{
|
||||||
RepositoryDir: request.RepositoryDir,
|
RepositoryDir: request.RepositoryDir,
|
||||||
Product: request.Product,
|
Product: request.Product,
|
||||||
@@ -130,6 +147,7 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles,
|
WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles,
|
||||||
LocalReplacements: request.Validation.ModuleDistribution.LocalReplacements,
|
LocalReplacements: request.Validation.ModuleDistribution.LocalReplacements,
|
||||||
},
|
},
|
||||||
|
FileSize: fileSize,
|
||||||
},
|
},
|
||||||
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
||||||
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
||||||
@@ -141,6 +159,21 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func internalFileSizeSettings(spec *FileSizeValidationSpec) *validation.FileSizeSettings {
|
||||||
|
if spec == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var limitBytes *int64
|
||||||
|
if spec.LimitBytes != nil {
|
||||||
|
limit := *spec.LimitBytes
|
||||||
|
limitBytes = &limit
|
||||||
|
}
|
||||||
|
return &validation.FileSizeSettings{
|
||||||
|
LimitBytes: limitBytes,
|
||||||
|
Exclusions: append([]string(nil), spec.Exclusions...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func candidateChecks(checks func(context.Context, Candidate) error) func(context.Context, candidate.Candidate) error {
|
func candidateChecks(checks func(context.Context, Candidate) error) func(context.Context, candidate.Candidate) error {
|
||||||
if checks == nil {
|
if checks == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ type ValidationSettings struct {
|
|||||||
// 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"`
|
ModuleDistribution ModuleDistributionSettings `json:"module_distribution,omitempty"`
|
||||||
|
// FileSize checks the size of each committed blob. Null or omission disables
|
||||||
|
// the check; an empty object enables the default limit.
|
||||||
|
FileSize *FileSizeSettings `json:"file_size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSizeSettings selects the maximum committed-file size and literal exclusions.
|
||||||
|
type FileSizeSettings struct {
|
||||||
|
LimitBytes *int64 `json:"limit_bytes,omitempty"`
|
||||||
|
Exclusions []string `json:"exclusions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleDistributionSettings selects committed module-source distribution checks.
|
// ModuleDistributionSettings selects committed module-source distribution checks.
|
||||||
@@ -236,6 +245,9 @@ func LoadSettings(repoDir, configPath string) (Settings, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if settings.Validation != nil {
|
if settings.Validation != nil {
|
||||||
|
if fileSize := settings.Validation.FileSize; fileSize != nil && fileSize.LimitBytes != nil && *fileSize.LimitBytes < 0 {
|
||||||
|
return Settings{}, fmt.Errorf("profile %q: validation.file_size.limit_bytes must not be negative", configPath)
|
||||||
|
}
|
||||||
for i, target := range settings.Validation.Go.CompileTargets {
|
for i, target := range settings.Validation.Go.CompileTargets {
|
||||||
if strings.Count(target, "/") != 1 {
|
if strings.Count(target, "/") != 1 {
|
||||||
return Settings{}, fmt.Errorf("profile %q: validation.go.compile_targets[%d] must be GOOS/GOARCH", configPath, i)
|
return Settings{}, fmt.Errorf("profile %q: validation.go.compile_targets[%d] must be GOOS/GOARCH", configPath, i)
|
||||||
|
|||||||
@@ -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,"module_distribution":{"workspace_files":true,"local_replacements":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,"local_replacements":true},"file_size":{"limit_bytes":0,"exclusions":["assets"]},"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +155,9 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.ModuleDistribution.LocalReplacements || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.ModuleDistribution.LocalReplacements || !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)
|
t.Fatalf("validation settings = %#v, want Go formatting, workspace inspection, race, and full vet enabled", settings.Validation)
|
||||||
}
|
}
|
||||||
|
if settings.Validation.FileSize == nil || settings.Validation.FileSize.LimitBytes == nil || *settings.Validation.FileSize.LimitBytes != 0 || len(settings.Validation.FileSize.Exclusions) != 1 || settings.Validation.FileSize.Exclusions[0] != "assets" {
|
||||||
|
t.Fatalf("file size settings = %#v, want explicit zero limit and exclusions", settings.Validation.FileSize)
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -163,6 +166,47 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadSettingsPreservesEmptyAndNullFileSize(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
profile string
|
||||||
|
enabled bool
|
||||||
|
}{
|
||||||
|
{name: "empty object", profile: `{"product":"Example","validation":{"file_size":{}}}`, enabled: true},
|
||||||
|
{name: "default limit", profile: `{"product":"Example","validation":{"file_size":{"limit_bytes":null}}}`, enabled: true},
|
||||||
|
{name: "null object", profile: `{"product":"Example","validation":{"file_size":null}}`},
|
||||||
|
{name: "omitted object", profile: `{"product":"Example","validation":{}}`},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
repoDir := t.TempDir()
|
||||||
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
|
if err := os.WriteFile(profilePath, []byte(test.profile), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
settings, err := LoadSettings(repoDir, profilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := settings.Validation != nil && settings.Validation.FileSize != nil
|
||||||
|
if got != test.enabled {
|
||||||
|
t.Fatalf("FileSize enabled = %t, want %t", got, test.enabled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadSettingsRejectsNegativeFileSizeLimit(t *testing.T) {
|
||||||
|
repoDir := t.TempDir()
|
||||||
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
|
profile := `{"product":"Example","validation":{"file_size":{"limit_bytes":-1}}}`
|
||||||
|
if err := os.WriteFile(profilePath, []byte(profile), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := LoadSettings(repoDir, profilePath); err == nil || !strings.Contains(err.Error(), "validation.file_size.limit_bytes") {
|
||||||
|
t.Fatalf("LoadSettings() error = %v, want invalid negative file-size limit", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExampleProfileLoads(t *testing.T) {
|
func TestExampleProfileLoads(t *testing.T) {
|
||||||
repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -172,6 +172,17 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
|
|||||||
CompileTargets: make([]releasekit.Target, len(settings.Go.CompileTargets)),
|
CompileTargets: make([]releasekit.Target, len(settings.Go.CompileTargets)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if settings.FileSize != nil {
|
||||||
|
var limitBytes *int64
|
||||||
|
if settings.FileSize.LimitBytes != nil {
|
||||||
|
limit := *settings.FileSize.LimitBytes
|
||||||
|
limitBytes = &limit
|
||||||
|
}
|
||||||
|
result.FileSize = &releasekit.FileSizeValidationSpec{
|
||||||
|
LimitBytes: limitBytes,
|
||||||
|
Exclusions: append([]string(nil), settings.FileSize.Exclusions...),
|
||||||
|
}
|
||||||
|
}
|
||||||
for index, target := range settings.Go.CompileTargets {
|
for index, target := range settings.Go.CompileTargets {
|
||||||
goos, goarch, _ := strings.Cut(target, "/")
|
goos, goarch, _ := strings.Cut(target, "/")
|
||||||
result.Go.CompileTargets[index] = releasekit.Target{OS: goos, Arch: goarch}
|
result.Go.CompileTargets[index] = releasekit.Target{OS: goos, Arch: goarch}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
||||||
|
limit := int64(2048)
|
||||||
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
||||||
Product: "Example Product",
|
Product: "Example Product",
|
||||||
ExpectedPushURL: "publication-only value",
|
ExpectedPushURL: "publication-only value",
|
||||||
@@ -120,6 +121,7 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
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, LocalReplacements: true},
|
ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true, LocalReplacements: true},
|
||||||
|
FileSize: &cli.FileSizeSettings{LimitBytes: &limit, Exclusions: []string{"cache"}},
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
||||||
@@ -131,6 +133,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
||||||
t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution)
|
t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution)
|
||||||
}
|
}
|
||||||
|
if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "cache" {
|
||||||
|
t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize)
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ after workspace-file inspection. Filesystem replacements use the
|
|||||||
`local-replacements` identity and include the declaration path, source line,
|
`local-replacements` identity and include the declaration path, source line,
|
||||||
and target; versioned replacements are permitted. Committed module symlinks are
|
and target; versioned replacements are permitted. Committed module symlinks are
|
||||||
resolved within the candidate, regardless of `GOWORK`.
|
resolved within the candidate, regardless of `GOWORK`.
|
||||||
|
When `validation.file_size` is a non-null object, the command checks each
|
||||||
|
committed blob after module-distribution checks. An omitted or null object
|
||||||
|
disables the check; an empty object enables the 1,000,000 decimal-byte default.
|
||||||
|
Oversized files use the `file-size` identity and report their path and measured
|
||||||
|
size against the limit. See the [configuration reference](config.md) for
|
||||||
|
literal exclusions and the current-candidate scope.
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ For example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `validation.file_size` is an object, ReleaseKit compares each committed
|
||||||
|
blob's size with `limit_bytes`. The limit uses decimal bytes and rejects only
|
||||||
|
sizes strictly greater than the limit. An omitted or null object disables the
|
||||||
|
check; `{}` enables it with a default of 1,000,000 bytes. A missing or null
|
||||||
|
`limit_bytes` selects that default, zero is a valid limit, and negative limits
|
||||||
|
are invalid.
|
||||||
|
|
||||||
|
`validation.file_size.exclusions` contains literal repository-relative file or
|
||||||
|
directory paths. Directory exclusions include committed descendants, using path
|
||||||
|
boundaries (`assets` does not exclude `assets-old`). Ordinary relative path
|
||||||
|
normalization applies; metacharacters are literal and unmatched paths have no
|
||||||
|
effect. The check measures regular and symlink blobs, including small binaries;
|
||||||
|
symlink size is the committed link text. Gitlinks have no measured size. Only
|
||||||
|
the captured candidate is checked, one offending file is reported at a time,
|
||||||
|
and historical or aggregate repository size is not measured.
|
||||||
|
|
||||||
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
|
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
|
||||||
`token_env`. Its values are required when running `deliver`, but credentials are
|
`token_env`. Its values are required when running `deliver`, but credentials are
|
||||||
not required for `check`, `publish`, or `build`. `token_env` names the
|
not required for `check`, `publish`, or `build`. `token_env` names the
|
||||||
@@ -111,6 +127,9 @@ validation parses committed `go.mod` files after workspace inspection and report
|
|||||||
filesystem replacements as `local-replacements`, with path, directive line, and
|
filesystem replacements as `local-replacements`, with path, directive line, and
|
||||||
target. Versioned replacements are permitted; parse and inspection failures
|
target. Versioned replacements are permitted; parse and inspection failures
|
||||||
preserve their causes.
|
preserve their causes.
|
||||||
|
When `validation.file_size` is selected, shared validation checks committed
|
||||||
|
blob sizes after module-distribution checks and reports the first oversized
|
||||||
|
path with the `file-size` identity and measured and limit byte counts.
|
||||||
Configured `checks` commands run afterward, once each in order, as additional
|
Configured `checks` commands run afterward, once each in order, as additional
|
||||||
product checks. Artifact builds selected by the separate `build` setting run
|
product checks. Artifact builds selected by the separate `build` setting run
|
||||||
after shared validation and product checks.
|
after shared validation and product checks.
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ path, line, and target as `local-replacements`; versioned replacements are
|
|||||||
permitted. Committed module symlinks are resolved within the candidate. Parse
|
permitted. Committed module symlinks are resolved within the candidate. Parse
|
||||||
and inspection errors retain their causes, and the rule applies even when
|
and inspection errors retain their causes, and the rule applies even when
|
||||||
`GOWORK=off`. Its zero value disables the check.
|
`GOWORK=off`. Its zero value disables the check.
|
||||||
|
Set `Validation.FileSize` to check each committed blob against a byte limit.
|
||||||
|
Nil disables the check; an empty object enables the default of 1,000,000
|
||||||
|
decimal bytes. `LimitBytes` is strict greater-than, accepts zero, and rejects
|
||||||
|
negative values. `Exclusions` contains literal repository-relative files or
|
||||||
|
directories; excluded directories include descendants. Symlink blobs are
|
||||||
|
measured by link-text bytes, and gitlinks are omitted. The check applies to the
|
||||||
|
captured candidate and reports one oversized file with its measurement as
|
||||||
|
`file-size`; it does not measure historical or aggregate size.
|
||||||
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
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ permitted. Committed module symlinks are resolved within the candidate, and
|
|||||||
parse or inspection errors retain their causes. This check is independent of
|
parse or inspection errors retain their causes. This check is independent of
|
||||||
`GOWORK`; its zero value disables it, and a failure stops publication before
|
`GOWORK`; its zero value disables it, and a failure stops publication before
|
||||||
product checks or tag creation.
|
product checks or tag creation.
|
||||||
|
Set `Validation.FileSize` to check each committed blob against a byte limit.
|
||||||
|
Nil disables the check; an empty object enables the default of 1,000,000
|
||||||
|
decimal bytes. `LimitBytes` uses strict greater-than, accepts zero, and rejects
|
||||||
|
negative values. `Exclusions` contains literal repository-relative files or
|
||||||
|
directories, including committed descendants of excluded directories. Symlink
|
||||||
|
blob size is its link text; gitlinks are omitted. This check measures only the
|
||||||
|
captured candidate and reports one oversized file with its measurement as
|
||||||
|
`file-size`; it stops 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
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ these implemented packages:
|
|||||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
| `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/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||||
| `internal/validation` | Shared Go validation with optional race, full-vet, and static package-compilation target selections, formatting, workspace-file and filesystem-replacement checks, structured 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, full-vet, and static package-compilation target selections, formatting, workspace-file and filesystem-replacement checks, per-blob size limits, structured 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/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ with an inspection cause when their selected contents are unavailable. A
|
|||||||
replacement with no parsed version is reported with its go.mod path, line,
|
replacement with no parsed version is reported with its go.mod path, line,
|
||||||
directive, and target; versioned replacements are allowed. It runs after the
|
directive, and target; versioned replacements are allowed. It runs after the
|
||||||
workspace-file check using the same candidate inventory.
|
workspace-file check using the same candidate inventory.
|
||||||
|
The optional file-size check uses each blob's captured Git size, including
|
||||||
|
symlink link-text bytes, and skips entries without a blob size such as gitlinks.
|
||||||
|
It applies normalized literal file/directory exclusions with path-boundary
|
||||||
|
matching, then returns the first oversized path with `FileSizeMeasurement`.
|
||||||
`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.
|
||||||
|
|||||||
@@ -386,6 +386,13 @@ The rule applies to candidate contents even when testing disables workspace use.
|
|||||||
|
|
||||||
## Stage 8: Inspect individual committed file sizes
|
## Stage 8: Inspect individual committed file sizes
|
||||||
|
|
||||||
|
**Completion:** Complete. `Validation.FileSize` and `validation.file_size` enable
|
||||||
|
a per-blob decimal-byte limit with a 1,000,000-byte default, strict greater-than
|
||||||
|
comparison, and literal repository-relative file/directory exclusions. The check
|
||||||
|
uses captured blob sizes, counts symlink link text, omits gitlinks, and reports
|
||||||
|
one oversized file with its measurement. Nil disables the check, and historical
|
||||||
|
or aggregate repository size is outside its scope.
|
||||||
|
|
||||||
**Outcome:** Optional limits and intentional path exceptions are operational.
|
**Outcome:** Optional limits and intentional path exceptions are operational.
|
||||||
|
|
||||||
1. Add `FileSizeValidationSpec`, `ValidationSpec.FileSize`, and matching JSON
|
1. Add `FileSizeValidationSpec`, `ValidationSpec.FileSize`, and matching JSON
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const (
|
|||||||
WorkspaceFiles Check = "workspace-files"
|
WorkspaceFiles Check = "workspace-files"
|
||||||
// LocalReplacements identifies filesystem replacements in committed modules.
|
// LocalReplacements identifies filesystem replacements in committed modules.
|
||||||
LocalReplacements Check = "local-replacements"
|
LocalReplacements Check = "local-replacements"
|
||||||
|
// FileSize identifies committed blobs that exceed a selected size limit.
|
||||||
|
FileSize Check = "file-size"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Finding describes a validation observation at an optional source location.
|
// Finding describes a validation observation at an optional source location.
|
||||||
|
|||||||
52
internal/validation/file_size.go
Normal file
52
internal/validation/file_size.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultFileSizeLimit int64 = 1_000_000
|
||||||
|
|
||||||
|
func checkFileSize(tree *CandidateTree, settings *FileSizeSettings) error {
|
||||||
|
limit := defaultFileSizeLimit
|
||||||
|
if settings.LimitBytes != nil {
|
||||||
|
limit = *settings.LimitBytes
|
||||||
|
}
|
||||||
|
exclusions := normalizedExclusions(settings.Exclusions)
|
||||||
|
for _, entry := range tree.Entries() {
|
||||||
|
if entry.Type != "blob" || !entry.HasSize || excludedByPath(entry.Path, exclusions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.Size > limit {
|
||||||
|
return &Failure{
|
||||||
|
Check: FileSize,
|
||||||
|
Findings: []Finding{{
|
||||||
|
Path: entry.Path,
|
||||||
|
Message: "committed file exceeds the selected size limit",
|
||||||
|
}},
|
||||||
|
Measurement: &FileSizeMeasurement{Bytes: entry.Size, LimitBytes: limit},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedExclusions(exclusions []string) []string {
|
||||||
|
result := make([]string, 0, len(exclusions))
|
||||||
|
for _, exclusion := range exclusions {
|
||||||
|
if exclusion == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, path.Clean(exclusion))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func excludedByPath(filePath string, exclusions []string) bool {
|
||||||
|
for _, exclusion := range exclusions {
|
||||||
|
if exclusion == "." || filePath == exclusion || strings.HasPrefix(filePath, exclusion+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
192
internal/validation/file_size_test.go
Normal file
192
internal/validation/file_size_test.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckFileSizeUsesStrictLimitsAndDefault(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
limit *int64
|
||||||
|
size int64
|
||||||
|
wantLimit int64
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{name: "below", limit: int64Pointer(10), size: 9, wantLimit: 10},
|
||||||
|
{name: "at", limit: int64Pointer(10), size: 10, wantLimit: 10},
|
||||||
|
{name: "above", limit: int64Pointer(10), size: 11, wantLimit: 10, wantError: true},
|
||||||
|
{name: "default at", size: 1_000_000, wantLimit: 1_000_000},
|
||||||
|
{name: "default above", size: 1_000_001, wantLimit: 1_000_000, wantError: true},
|
||||||
|
{name: "zero empty", limit: int64Pointer(0), size: 0},
|
||||||
|
{name: "zero nonempty", limit: int64Pointer(0), size: 1, wantError: true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
tree := &CandidateTree{entries: []git.TreeEntry{{
|
||||||
|
Path: "file.bin", Type: "blob", Mode: "100644", Size: test.size, HasSize: true,
|
||||||
|
}}}
|
||||||
|
err := checkFileSize(tree, &FileSizeSettings{LimitBytes: test.limit})
|
||||||
|
if !test.wantError {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
failure, ok := err.(*Failure)
|
||||||
|
if !ok || failure.Check != FileSize || len(failure.Findings) != 1 || failure.Findings[0].Path != "file.bin" {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want file-size finding", err)
|
||||||
|
}
|
||||||
|
if failure.Measurement == nil || failure.Measurement.Bytes != test.size || failure.Measurement.LimitBytes != test.wantLimit {
|
||||||
|
t.Fatalf("measurement = %#v, want %d bytes against %d", failure.Measurement, test.size, test.wantLimit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFileSizeAppliesLiteralPathExclusions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entries []git.TreeEntry
|
||||||
|
exclusions []string
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "directory boundary and normalized paths",
|
||||||
|
entries: []git.TreeEntry{
|
||||||
|
{Path: "assets/icon.png", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
{Path: "assets-other/icon.png", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
},
|
||||||
|
exclusions: []string{"./assets/"},
|
||||||
|
wantPath: "assets-other/icon.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "glob metacharacters are literal",
|
||||||
|
entries: []git.TreeEntry{
|
||||||
|
{Path: "[ab].dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
{Path: "a.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
},
|
||||||
|
exclusions: []string{"[ab].dat", "literal[set]"},
|
||||||
|
wantPath: "a.dat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "directory metacharacters are literal",
|
||||||
|
entries: []git.TreeEntry{
|
||||||
|
{Path: "literal[set]/file.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
{Path: "literalx/file.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
},
|
||||||
|
exclusions: []string{"literal[set]"},
|
||||||
|
wantPath: "literalx/file.dat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unmatched exclusion",
|
||||||
|
entries: []git.TreeEntry{
|
||||||
|
{Path: "selected.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
},
|
||||||
|
exclusions: []string{"not-present"},
|
||||||
|
wantPath: "selected.dat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exact file exclusion",
|
||||||
|
entries: []git.TreeEntry{
|
||||||
|
{Path: "selected.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
{Path: "other.dat", Type: "blob", Size: 20, HasSize: true},
|
||||||
|
},
|
||||||
|
exclusions: []string{"selected.dat"},
|
||||||
|
wantPath: "other.dat",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
err := checkFileSize(&CandidateTree{entries: test.entries}, &FileSizeSettings{
|
||||||
|
LimitBytes: int64Pointer(10),
|
||||||
|
Exclusions: test.exclusions,
|
||||||
|
})
|
||||||
|
failure, ok := err.(*Failure)
|
||||||
|
if !ok || failure.Check != FileSize || len(failure.Findings) != 1 || failure.Findings[0].Path != test.wantPath {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want only %q to exceed the limit", err, test.wantPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFileSizeUsesCommittedBlobSizes(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "small.bin", []byte{0, 1})
|
||||||
|
writeTreeFile(t, repository, "boundary.bin", []byte{0, 1, 2})
|
||||||
|
if err := os.Symlink("small.bin", filepath.Join(repository, "link")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
submodule := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, submodule, "submodule.txt", []byte("submodule"))
|
||||||
|
gitTree(t, submodule, "add", "--all")
|
||||||
|
gitTree(t, submodule, "commit", "-m", "create nested source")
|
||||||
|
submoduleCommit := strings.TrimSpace(gitTree(t, submodule, "rev-parse", "HEAD"))
|
||||||
|
gitTree(t, repository, "add", "--all")
|
||||||
|
gitTree(t, repository, "update-index", "--add", "--cacheinfo", "160000,"+submoduleCommit+",submodule")
|
||||||
|
commit := commitCandidateTree(t, repository)
|
||||||
|
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkFileSize(tree, &FileSizeSettings{LimitBytes: int64Pointer(3), Exclusions: []string{"link"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want small binary and exact-limit blob to pass", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkFileSize(tree, &FileSizeSettings{LimitBytes: int64Pointer(3), Exclusions: []string{"small.bin", "boundary.bin"}})
|
||||||
|
failure, ok := err.(*Failure)
|
||||||
|
if !ok || failure.Check != FileSize || len(failure.Findings) != 1 || failure.Findings[0].Path != "link" {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want symlink blob to exceed the limit", err)
|
||||||
|
}
|
||||||
|
if failure.Measurement == nil || failure.Measurement.Bytes != int64(len("small.bin")) {
|
||||||
|
t.Fatalf("symlink measurement = %#v, want link-text size %d", failure.Measurement, len("small.bin"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkFileSize(tree, &FileSizeSettings{LimitBytes: int64Pointer(0), Exclusions: []string{"small.bin", "boundary.bin", "link"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want omitted gitlink size", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckFileSizeUsesOnlyCandidateFilesIndividually(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "historical-large.dat", []byte(strings.Repeat("h", 20)))
|
||||||
|
commitCandidateTree(t, repository)
|
||||||
|
if err := os.Remove(filepath.Join(repository, "historical-large.dat")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeTreeFile(t, repository, "one.dat", []byte(strings.Repeat("a", 6)))
|
||||||
|
writeTreeFile(t, repository, "two.dat", []byte(strings.Repeat("b", 6)))
|
||||||
|
commit := commitCandidateTree(t, repository)
|
||||||
|
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := checkFileSize(tree, &FileSizeSettings{LimitBytes: int64Pointer(10)}); err != nil {
|
||||||
|
t.Fatalf("checkFileSize() error = %v, want current individual files to pass", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRejectsNegativeFileSizeLimitBeforeGoChecks(t *testing.T) {
|
||||||
|
negative := int64(-1)
|
||||||
|
err := Run(context.Background(), Request{Settings: Settings{
|
||||||
|
FileSize: &FileSizeSettings{LimitBytes: &negative},
|
||||||
|
}})
|
||||||
|
failure, ok := err.(*Failure)
|
||||||
|
if !ok || failure.Check != FileSize || len(failure.Findings) != 1 || !strings.Contains(failure.Findings[0].Message, "negative") {
|
||||||
|
t.Fatalf("Run() error = %v, want invalid file-size limit failure", err)
|
||||||
|
}
|
||||||
|
if errors.Unwrap(failure) != nil {
|
||||||
|
t.Fatalf("invalid limit failure cause = %v, want none", failure.Cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func int64Pointer(value int64) *int64 { return &value }
|
||||||
@@ -21,6 +21,13 @@ type Settings struct {
|
|||||||
CompileTargets []Target
|
CompileTargets []Target
|
||||||
GoFormat bool
|
GoFormat bool
|
||||||
ModuleDistribution ModuleDistributionSettings
|
ModuleDistribution ModuleDistributionSettings
|
||||||
|
FileSize *FileSizeSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSizeSettings selects a per-blob limit and literal path exclusions.
|
||||||
|
type FileSizeSettings struct {
|
||||||
|
LimitBytes *int64
|
||||||
|
Exclusions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleDistributionSettings selects candidate distribution checks.
|
// ModuleDistributionSettings selects candidate distribution checks.
|
||||||
@@ -51,6 +58,12 @@ func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, e
|
|||||||
|
|
||||||
// Run executes shared validation for the captured candidate.
|
// Run executes shared validation for the captured candidate.
|
||||||
func Run(ctx context.Context, request Request) error {
|
func Run(ctx context.Context, request Request) error {
|
||||||
|
if request.Settings.FileSize != nil && request.Settings.FileSize.LimitBytes != nil && *request.Settings.FileSize.LimitBytes < 0 {
|
||||||
|
return &Failure{
|
||||||
|
Check: FileSize,
|
||||||
|
Findings: []Finding{{Message: "file size limit must not be negative"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, target := range request.Settings.CompileTargets {
|
for _, target := range request.Settings.CompileTargets {
|
||||||
if strings.TrimSpace(target.OS) == "" || strings.TrimSpace(target.Arch) == "" || strings.Contains(target.OS, "/") || strings.Contains(target.Arch, "/") {
|
if strings.TrimSpace(target.OS) == "" || strings.TrimSpace(target.Arch) == "" || strings.Contains(target.OS, "/") || strings.Contains(target.Arch, "/") {
|
||||||
return &Failure{
|
return &Failure{
|
||||||
@@ -99,10 +112,13 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles || request.Settings.ModuleDistribution.LocalReplacements {
|
if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles || request.Settings.ModuleDistribution.LocalReplacements || request.Settings.FileSize != nil {
|
||||||
tree, err := request.ReadCandidateTree(ctx)
|
tree, err := request.ReadCandidateTree(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
check := LocalReplacements
|
check := FileSize
|
||||||
|
if request.Settings.ModuleDistribution.LocalReplacements {
|
||||||
|
check = LocalReplacements
|
||||||
|
}
|
||||||
if request.Settings.ModuleDistribution.WorkspaceFiles {
|
if request.Settings.ModuleDistribution.WorkspaceFiles {
|
||||||
check = WorkspaceFiles
|
check = WorkspaceFiles
|
||||||
}
|
}
|
||||||
@@ -126,6 +142,11 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if request.Settings.FileSize != nil {
|
||||||
|
if err := checkFileSize(tree, request.Settings.FileSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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", "go-format",
|
// Current checks include "go-test", "go-vet", "package-compile", "go-format",
|
||||||
// "workspace-files", and "local-replacements".
|
// "workspace-files", "local-replacements", and "file-size".
|
||||||
type ValidationCheck string
|
type ValidationCheck string
|
||||||
|
|
||||||
// ValidationFinding describes a failed check observation at an optional
|
// ValidationFinding describes a failed check observation at an optional
|
||||||
|
|||||||
@@ -8,15 +8,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
||||||
|
limit := int64(4096)
|
||||||
request := candidateRequest(ReleaseRequest{
|
request := candidateRequest(ReleaseRequest{
|
||||||
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, ModuleDistribution: ModuleDistributionValidationSpec{WorkspaceFiles: true, LocalReplacements: true}},
|
}, GoFormat: true, ModuleDistribution: ModuleDistributionValidationSpec{WorkspaceFiles: true, LocalReplacements: true}, FileSize: &FileSizeValidationSpec{LimitBytes: &limit, Exclusions: []string{"assets"}}},
|
||||||
})
|
})
|
||||||
if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
||||||
t.Fatalf("candidate request validation = %#v, want race, full vet, formatting, and distribution checks enabled", request.Validation)
|
t.Fatalf("candidate request validation = %#v, want race, full vet, formatting, and distribution checks enabled", request.Validation)
|
||||||
}
|
}
|
||||||
|
if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "assets" {
|
||||||
|
t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize)
|
||||||
|
}
|
||||||
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) {
|
||||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.CompileTargets, want)
|
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.CompileTargets, want)
|
||||||
@@ -28,6 +32,17 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCandidateRequestPreservesFileSizeObjectSelection(t *testing.T) {
|
||||||
|
disabled := candidateRequest(ReleaseRequest{})
|
||||||
|
enabled := candidateRequest(ReleaseRequest{Validation: ValidationSpec{FileSize: &FileSizeValidationSpec{}}})
|
||||||
|
if disabled.Validation.FileSize != nil {
|
||||||
|
t.Fatalf("omitted FileSize = %#v, want disabled", disabled.Validation.FileSize)
|
||||||
|
}
|
||||||
|
if enabled.Validation.FileSize == nil || enabled.Validation.FileSize.LimitBytes != nil {
|
||||||
|
t.Fatalf("empty FileSize = %#v, want enabled default selection", enabled.Validation.FileSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidationErrorPreservesLocalReplacementsIdentityAndFinding(t *testing.T) {
|
func TestValidationErrorPreservesLocalReplacementsIdentityAndFinding(t *testing.T) {
|
||||||
cause := errors.New("module declaration could not be read")
|
cause := errors.New("module declaration could not be read")
|
||||||
got := convertValidationError(&validation.Failure{
|
got := convertValidationError(&validation.Failure{
|
||||||
@@ -53,6 +68,21 @@ func TestValidationErrorPreservesLocalReplacementsIdentityAndFinding(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidationErrorPreservesFileSizeMeasurement(t *testing.T) {
|
||||||
|
got := convertValidationError(&validation.Failure{
|
||||||
|
Check: validation.FileSize,
|
||||||
|
Findings: []validation.Finding{{Path: "assets/bundle.bin", Message: "committed file exceeds the selected size limit"}},
|
||||||
|
Measurement: &validation.FileSizeMeasurement{Bytes: 12, LimitBytes: 10},
|
||||||
|
})
|
||||||
|
var validationError *ValidationError
|
||||||
|
if !errors.As(got, &validationError) || validationError.Check != "file-size" {
|
||||||
|
t.Fatalf("converted error = %#v, want file-size identity", got)
|
||||||
|
}
|
||||||
|
if validationError.Measurement == nil || validationError.Measurement.Bytes != 12 || validationError.Measurement.LimitBytes != 10 {
|
||||||
|
t.Fatalf("converted measurement = %#v, want 12 bytes against 10", validationError.Measurement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidationErrorPreservesGoVetIdentity(t *testing.T) {
|
func TestValidationErrorPreservesGoVetIdentity(t *testing.T) {
|
||||||
cause := errors.New("vet failed")
|
cause := errors.New("vet failed")
|
||||||
got := convertValidationError(&validation.Failure{Check: validation.GoVet, Cause: cause})
|
got := convertValidationError(&validation.Failure{Check: validation.GoVet, Cause: cause})
|
||||||
|
|||||||
Reference in New Issue
Block a user