Share artifact declaration validation
This commit is contained in:
@@ -182,32 +182,15 @@ func validateBuildReceipt(receipt BuildReceipt) error {
|
||||
if len(receipt.Targets) == 0 || len(receipt.Artifacts) != len(receipt.Targets) {
|
||||
return errors.New("build receipt target and artifact counts do not match")
|
||||
}
|
||||
seenTargets := make(map[ReceiptTarget]bool, len(receipt.Targets))
|
||||
seenArtifacts := make(map[ReceiptTarget]bool, len(receipt.Artifacts))
|
||||
targets := make([]artifact.Target, 0, len(receipt.Targets))
|
||||
for _, target := range receipt.Targets {
|
||||
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||
return fmt.Errorf("build receipt has unsafe target %q/%q", target.OS, target.Arch)
|
||||
}
|
||||
if seenTargets[target] {
|
||||
return fmt.Errorf("build receipt repeats target %s/%s", target.OS, target.Arch)
|
||||
}
|
||||
seenTargets[target] = true
|
||||
targets = append(targets, artifact.Target{OS: target.OS, Arch: target.Arch})
|
||||
declarations := make([]artifact.Declaration, 0, len(receipt.Artifacts))
|
||||
for _, item := range receipt.Artifacts {
|
||||
declarations = append(declarations, artifact.Declaration{Target: artifact.Target{OS: item.Target.OS, Arch: item.Target.Arch}, Filename: item.Filename})
|
||||
}
|
||||
declarations := artifact.ExpectedBuild(receipt.BinaryName, receipt.Version, targets)
|
||||
wantByTarget := make(map[ReceiptTarget]string, len(declarations))
|
||||
for _, declaration := range declarations {
|
||||
target := ReceiptTarget{OS: declaration.Target.OS, Arch: declaration.Target.Arch}
|
||||
wantByTarget[target] = declaration.Filename
|
||||
if err := artifact.ValidateDeclarations(receipt.BinaryName, receipt.Version, parseStoredTargets(receipt.Targets), declarations); err != nil {
|
||||
return fmt.Errorf("build receipt: %w", err)
|
||||
}
|
||||
for _, artifact := range receipt.Artifacts {
|
||||
if !seenTargets[artifact.Target] || seenArtifacts[artifact.Target] {
|
||||
return fmt.Errorf("build receipt has an unexpected artifact target %s/%s", artifact.Target.OS, artifact.Target.Arch)
|
||||
}
|
||||
seenArtifacts[artifact.Target] = true
|
||||
wantName := wantByTarget[artifact.Target]
|
||||
if artifact.Filename != wantName || filepath.Base(artifact.Filename) != artifact.Filename {
|
||||
if filepath.Base(artifact.Filename) != artifact.Filename {
|
||||
return fmt.Errorf("build receipt has invalid artifact filename %q", artifact.Filename)
|
||||
}
|
||||
wantPath := filepath.Join(receipt.OutputDir, artifact.Filename)
|
||||
|
||||
@@ -44,7 +44,7 @@ the agent policy; an intermediate compilation dependency is recorded accurately.
|
||||
| 6 | A04 | Complete — Git display, endpoint change, public mismatch, CLI, publication, and Git failure regressions passed. |
|
||||
| 7 | A05 | Complete — real-repository inspection failures preserve the local ref, available object ID, and Git cause without a push. |
|
||||
| 8 | A06, A14 | Complete — public build and prebuilt-delivery checks and CLI receipt/partial-artifact regressions passed. |
|
||||
| 9 | A16 | Pending |
|
||||
| 9 | A16 | Complete — shared declaration, receipt, delivery, prebuilt, and source-only regressions passed. |
|
||||
| 10 | A07: allocation and temporary metadata | Pending |
|
||||
| 11 | A07: build and delivery outcomes | Pending |
|
||||
| 12 | A08 | Pending |
|
||||
|
||||
@@ -90,3 +90,30 @@ func ExpectedBuild(binaryName, version string, targets []Target) []Declaration {
|
||||
}
|
||||
return declarations
|
||||
}
|
||||
|
||||
// ValidateDeclarations matches recorded artifact identities to selected targets.
|
||||
func ValidateDeclarations(binaryName, version string, targets []Target, recorded []Declaration) error {
|
||||
if len(recorded) != len(targets) {
|
||||
return fmt.Errorf("artifact and target counts do not match")
|
||||
}
|
||||
want := make(map[Target]string, len(targets))
|
||||
for _, declaration := range ExpectedBuild(binaryName, version, targets) {
|
||||
target := declaration.Target
|
||||
if !ValidComponent(target.OS) || !ValidComponent(target.Arch) {
|
||||
return fmt.Errorf("invalid target %q/%q", target.OS, target.Arch)
|
||||
}
|
||||
if _, exists := want[target]; exists {
|
||||
return fmt.Errorf("repeats target %q/%q", target.OS, target.Arch)
|
||||
}
|
||||
want[target] = declaration.Filename
|
||||
}
|
||||
seen := make(map[Target]bool, len(recorded))
|
||||
for _, declaration := range recorded {
|
||||
name, ok := want[declaration.Target]
|
||||
if !ok || seen[declaration.Target] || declaration.Filename != name {
|
||||
return fmt.Errorf("artifact %q does not match a selected target and its declared name", declaration.Filename)
|
||||
}
|
||||
seen[declaration.Target] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,3 +56,31 @@ func TestValidComponentPreservesBuildAndReceiptRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDeclarationsMatchesTargetsAndNames(t *testing.T) {
|
||||
linux := Target{OS: "linux", Arch: "amd64"}
|
||||
windows := Target{OS: "windows", Arch: "arm64"}
|
||||
first := Declaration{Target: linux, Filename: "app-v1.2.3-linux-amd64"}
|
||||
second := Declaration{Target: windows, Filename: "app-v1.2.3-windows-arm64.exe"}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
targets []Target
|
||||
recorded []Declaration
|
||||
valid bool
|
||||
}{
|
||||
{"reordered", []Target{linux, windows}, []Declaration{second, first}, true},
|
||||
{"count", []Target{linux, windows}, []Declaration{first}, false},
|
||||
{"duplicate target", []Target{linux, linux}, []Declaration{first, first}, false},
|
||||
{"duplicate artifact", []Target{linux, windows}, []Declaration{first, first}, false},
|
||||
{"incorrect name", []Target{linux}, []Declaration{{Target: linux, Filename: "other"}}, false},
|
||||
{"unknown target", []Target{linux}, []Declaration{second}, false},
|
||||
{"unsafe target", []Target{{OS: "linux/other", Arch: "amd64"}}, []Declaration{{Target: linux, Filename: first.Filename}}, false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := ValidateDeclarations("app", "v1.2.3", test.targets, test.recorded)
|
||||
if (err == nil) != test.valid {
|
||||
t.Fatalf("ValidateDeclarations() error = %v, valid = %t", err, test.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,29 +17,15 @@ func declaredBuildAssets(result internalbuild.Result) ([]artifact.ExpectedAsset,
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if len(result.Artifacts) != len(settings.Targets) {
|
||||
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
||||
declarations := make([]artifact.Declaration, 0, len(result.Artifacts))
|
||||
for _, built := range result.Artifacts {
|
||||
declarations = append(declarations, artifact.Declaration{Target: built.Target, Filename: built.Filename})
|
||||
}
|
||||
declarations := artifact.ExpectedBuild(settings.BinaryName, result.Version, settings.Targets)
|
||||
wantByTarget := make(map[artifact.Target]string, len(declarations))
|
||||
for _, declaration := range declarations {
|
||||
target := declaration.Target
|
||||
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||
return nil, fmt.Errorf("build result contains an invalid target")
|
||||
}
|
||||
if _, exists := wantByTarget[target]; exists {
|
||||
return nil, fmt.Errorf("build result repeats target %q", target.OS+"/"+target.Arch)
|
||||
}
|
||||
wantByTarget[target] = declaration.Filename
|
||||
if err := artifact.ValidateDeclarations(settings.BinaryName, result.Version, settings.Targets, declarations); err != nil {
|
||||
return nil, fmt.Errorf("build result: %w", err)
|
||||
}
|
||||
assets := make([]artifact.ExpectedAsset, 0, len(result.Artifacts)+1)
|
||||
seenTargets := make(map[artifact.Target]bool, len(result.Artifacts))
|
||||
for _, builtArtifact := range result.Artifacts {
|
||||
wantName, ok := wantByTarget[builtArtifact.Target]
|
||||
if !ok || seenTargets[builtArtifact.Target] || builtArtifact.Filename != wantName {
|
||||
return nil, fmt.Errorf("build artifact %q does not match a selected target and its declared name", builtArtifact.Filename)
|
||||
}
|
||||
seenTargets[builtArtifact.Target] = true
|
||||
if filepath.Base(builtArtifact.Path) != builtArtifact.Filename {
|
||||
return nil, fmt.Errorf("build artifact path %q does not end with filename %q", builtArtifact.Path, builtArtifact.Filename)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user