diff --git a/app/cli/pkg/action/util.go b/app/cli/pkg/action/util.go index f48d079a7..230d84807 100644 --- a/app/cli/pkg/action/util.go +++ b/app/cli/pkg/action/util.go @@ -54,9 +54,9 @@ func LoadFileOrURL(fileRef string) ([]byte, error) { // ValidateAndExtractName validates and extracts a name from either // an explicit name parameter OR from metadata.name in the file content. -// Ensures exactly one source is provided. Returns error when: +// Returns error when: // - Neither explicit name nor metadata.name is provided -// - Both explicit name and metadata.name are provided (ambiguous) +// - Both are provided and they differ (providing both with the same value is allowed) func ValidateAndExtractName(explicitName, filePath string) (string, error) { // Load file content if provided var content []byte @@ -74,9 +74,9 @@ func ValidateAndExtractName(explicitName, filePath string) (string, error) { return "", fmt.Errorf("parse content: %w", err) } - // Both provided - ambiguous - if explicitName != "" && metadataName != "" { - return "", fmt.Errorf("conflicting names: explicit name (%q) and metadata.name (%q) both provided", explicitName, metadataName) + // Both provided - only an error if they differ + if explicitName != "" && metadataName != "" && explicitName != metadataName { + return "", fmt.Errorf("--name %q and metadata.name %q differ: pass only one, or set them to the same value", explicitName, metadataName) } // Neither provided - missing required name diff --git a/app/cli/pkg/action/util_test.go b/app/cli/pkg/action/util_test.go new file mode 100644 index 000000000..ea7494aaa --- /dev/null +++ b/app/cli/pkg/action/util_test.go @@ -0,0 +1,108 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateAndExtractName(t *testing.T) { + contractWithName := `apiVersion: chainloop.dev/v1 +kind: Contract +metadata: + name: container-image-contract +spec: + materials: + - type: CONTAINER_IMAGE + name: image +` + + contractWithoutName := `schemaVersion: v1 +materials: + - type: CONTAINER_IMAGE + name: image +` + + tests := []struct { + name string + explicitName string + fileContent string + wantName string + wantErr string + }{ + { + name: "explicit name only, no file", + explicitName: "my-contract", + wantName: "my-contract", + }, + { + name: "metadata.name only", + fileContent: contractWithName, + wantName: "container-image-contract", + }, + { + name: "explicit name matching metadata.name is accepted", + explicitName: "container-image-contract", + fileContent: contractWithName, + wantName: "container-image-contract", + }, + { + name: "explicit name differing from metadata.name errors with actionable message", + explicitName: "another-name", + fileContent: contractWithName, + wantErr: `--name "another-name" and metadata.name "container-image-contract" differ: pass only one, or set them to the same value`, + }, + { + name: "neither name nor file", + wantErr: "name is required when no file is provided", + }, + { + name: "file without metadata.name and no explicit name", + fileContent: contractWithoutName, + wantErr: "name is required: either provide explicit name or include metadata.name in the schema", + }, + { + name: "explicit name with file without metadata.name", + explicitName: "my-contract", + fileContent: contractWithoutName, + wantName: "my-contract", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var filePath string + if tc.fileContent != "" { + filePath = filepath.Join(t.TempDir(), "contract.yaml") + require.NoError(t, os.WriteFile(filePath, []byte(tc.fileContent), 0o600)) + } + + got, err := ValidateAndExtractName(tc.explicitName, filePath) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.wantName, got) + }) + } +} diff --git a/docs/examples/contracts/sbom/sbom-quality.yaml b/docs/examples/contracts/sbom/sbom-quality.yaml index 387db3f85..541ba36d7 100644 --- a/docs/examples/contracts/sbom/sbom-quality.yaml +++ b/docs/examples/contracts/sbom/sbom-quality.yaml @@ -1,8 +1,9 @@ -#release-contract +# DO NOT CHANGE metadata.name, it's used in the SSDF and CRA guides +# https://docs.chainloop.dev/guides/ssdf and https://docs.chainloop.dev/guides/cra apiVersion: chainloop.dev/v1 kind: Contract metadata: - name: sbom-quality + name: release-contract description: Contract for SBOM quality checks spec: materials: diff --git a/docs/examples/contracts/slsa/github.yaml b/docs/examples/contracts/slsa/github.yaml index 5545761f0..5e62dab3b 100644 --- a/docs/examples/contracts/slsa/github.yaml +++ b/docs/examples/contracts/slsa/github.yaml @@ -1,8 +1,10 @@ -# Require a container image reference and include SLSA complicance verification +# Require a container image reference and include SLSA compliance verification +# DO NOT CHANGE metadata.name, it's used in the SLSA guide +# https://docs.chainloop.dev/guides/slsa apiVersion: chainloop.dev/v1 kind: Contract metadata: - name: github + name: skynet-build description: Require a container image reference and include SLSA compliance verification spec: materials: diff --git a/docs/examples/contracts/vulnerabilities/vulnerability-management.yaml b/docs/examples/contracts/vulnerabilities/vulnerability-management.yaml index 11dc031e9..ce40a014e 100644 --- a/docs/examples/contracts/vulnerabilities/vulnerability-management.yaml +++ b/docs/examples/contracts/vulnerabilities/vulnerability-management.yaml @@ -1,8 +1,9 @@ -#vuln-scan-contract +# DO NOT CHANGE metadata.name, it's used in the SSDF and CRA guides +# https://docs.chainloop.dev/guides/ssdf and https://docs.chainloop.dev/guides/cra apiVersion: chainloop.dev/v1 kind: Contract metadata: - name: vulnerability-management + name: vuln-scan-contract description: Contract for vulnerability scanning and management spec: materials: