From 73c8b5d2260fe12e3890e422f44e3f5deb953441 Mon Sep 17 00:00:00 2001 From: Tomasz Knura Date: Fri, 10 Jul 2026 14:53:29 +0200 Subject: [PATCH] fix(cli): accept matching --name and metadata.name in contract commands When both an explicit --name flag and metadata.name in the contract file were provided, the CLI rejected the command even if both values were identical. Accept the command when they match and, when they differ, emit an actionable error that tells the user how to resolve it. Also align the metadata.name of the example contracts with the names used in the SSDF, CRA and SLSA guides so the documented commands work as written. Fixes SOL-58 Co-Authored-By: Claude Fable 5 Signed-off-by: Tomasz Knura --- app/cli/pkg/action/util.go | 10 +- app/cli/pkg/action/util_test.go | 108 ++++++++++++++++++ .../examples/contracts/sbom/sbom-quality.yaml | 5 +- docs/examples/contracts/slsa/github.yaml | 6 +- .../vulnerability-management.yaml | 5 +- 5 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 app/cli/pkg/action/util_test.go 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: