From 647c8466a53a00ab58c89c850019ac8237c66208 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 15 Oct 2025 23:24:53 +0200 Subject: [PATCH 1/7] add apply command to wf contracts Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract.go | 1 + app/cli/cmd/workflow_contract_apply.go | 67 +++++++++ app/cli/pkg/action/workflow_contract_apply.go | 131 ++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 app/cli/cmd/workflow_contract_apply.go create mode 100644 app/cli/pkg/action/workflow_contract_apply.go diff --git a/app/cli/cmd/workflow_contract.go b/app/cli/cmd/workflow_contract.go index 6d4d2a8c9..2bc0569ee 100644 --- a/app/cli/cmd/workflow_contract.go +++ b/app/cli/cmd/workflow_contract.go @@ -28,6 +28,7 @@ func newWorkflowContractCmd() *cobra.Command { cmd.AddCommand( newWorkflowContractListCmd(), newWorkflowContractCreateCmd(), + newWorkflowContractApplyCmd(), newWorkflowContractDescribeCmd(), newWorkflowContractUpdateCmd(), newWorkflowContractDeleteCmd(), diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go new file mode 100644 index 000000000..24977e81c --- /dev/null +++ b/app/cli/cmd/workflow_contract_apply.go @@ -0,0 +1,67 @@ +// +// Copyright 2024-2025 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 cmd + +import ( + "fmt" + + "github.com/chainloop-dev/chainloop/app/cli/cmd/output" + "github.com/chainloop-dev/chainloop/app/cli/pkg/action" + "github.com/spf13/cobra" +) + +func newWorkflowContractApplyCmd() *cobra.Command { + var filePath, name, description, projectName string + + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply a contract (create or update)", + Long: `Apply a contract from a file. This command will create the contract if it doesn't exist, +or update it if it already exists.`, + Example: ` # Apply a contract from file + chainloop workflow contract apply --contract my-contract.yaml + + # Apply to a specific project + chainloop workflow contract apply --contract my-contract.yaml --project my-project`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if filePath == "" && name == "" { + return fmt.Errorf("either --contract file or --name must be provided") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var desc *string + if cmd.Flags().Changed("description") { + desc = &description + } + + res, err := action.NewWorkflowContractApply(ActionOpts).Run(filePath, name, desc, projectName) + if err != nil { + return err + } + + logger.Info().Msg("Contract applied!") + return output.EncodeOutput(flagOutputFormat, res, contractItemTableOutput) + }, + } + + cmd.Flags().StringVarP(&filePath, "contract", "f", "", "workflow contract file path (optional)") + cmd.Flags().StringVar(&name, "name", "", "contract name (required if no contract file provided)") + cmd.Flags().StringVar(&description, "description", "", "contract description") + cmd.Flags().StringVar(&projectName, "project", "", "project name to scope the contract") + + return cmd +} diff --git a/app/cli/pkg/action/workflow_contract_apply.go b/app/cli/pkg/action/workflow_contract_apply.go new file mode 100644 index 000000000..6ef5b354f --- /dev/null +++ b/app/cli/pkg/action/workflow_contract_apply.go @@ -0,0 +1,131 @@ +// +// Copyright 2024-2025 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 ( + "context" + "fmt" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" +) + +type WorkflowContractApply struct { + cfg *ActionsOpts +} + +func NewWorkflowContractApply(cfg *ActionsOpts) *WorkflowContractApply { + return &WorkflowContractApply{cfg} +} + +func (action *WorkflowContractApply) Run(filePath, name string, description *string, projectName string) (*WorkflowContractItem, error) { + var rawContract []byte + var err error + contractName := name + + // Load contract from file if provided + if filePath != "" { + rawContract, err = LoadFileOrURL(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read contract file: %w", err) + } + + // Extract name from the contract file content + extractedName, err := extractContractNameFromRawSchema(rawContract) + if err != nil { + return nil, err + } + + // For v2 schemas, use the extracted name. For v1 schemas, extractedName will be empty + if extractedName == "" && name == "" { + return nil, fmt.Errorf("contracts require --name flag to specify the contract name") + } else if extractedName != "" { + contractName = extractedName + } + } + + client := pb.NewWorkflowContractServiceClient(action.cfg.CPConnection) + + // Try to describe the specific contract first to determine if we should create or update + describeReq := &pb.WorkflowContractServiceDescribeRequest{ + Name: contractName, + } + + _, err = client.Describe(context.Background(), describeReq) + if err == nil { + // Contract exists, perform update + updateReq := &pb.WorkflowContractServiceUpdateRequest{ + Name: contractName, + RawContract: rawContract, + } + + if description != nil { + updateReq.Description = description + } + + res, err := client.Update(context.Background(), updateReq) + if err != nil { + return nil, fmt.Errorf("failed to update existing contract '%s': %w", contractName, err) + } + + return pbWorkflowContractItemToAction(res.Result.Contract), nil + } + + // Contract doesn't exist, perform create + createReq := &pb.WorkflowContractServiceCreateRequest{ + Name: contractName, + RawContract: rawContract, + } + + if description != nil { + createReq.Description = description + } + + if projectName != "" { + createReq.ProjectReference = &pb.IdentityReference{ + Name: &projectName, + } + } + + res, err := client.Create(context.Background(), createReq) + if err != nil { + return nil, fmt.Errorf("failed to create new contract '%s': %w", contractName, err) + } + + return pbWorkflowContractItemToAction(res.Result), nil +} + +// extractContractNameFromContent tries to extract the contract name from the contract file content +func extractContractNameFromRawSchema(content []byte) (string, error) { + // Identify format first + format, err := unmarshal.IdentifyFormat(content) + if err != nil { + return "", fmt.Errorf("failed to identify contract format: %w", err) + } + + // Unmarshal as v2 contract + // If it fails, we assume it's a v1 contract which doesn't have a name in the content + v2Contract := &schemaapi.CraftingSchemaV2{} + if err := unmarshal.FromRaw(content, format, v2Contract, false); err == nil { + // Successfully parsed as v2, extract name from metadata + if v2Contract.Metadata != nil && v2Contract.Metadata.Name != "" { + return v2Contract.Metadata.Name, nil + } + return "", fmt.Errorf("missing name in metadata section of the contract") + } + return "", nil +} From 9623920dddb4b75ab1cd0d369ca5cf5484add1bd Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 15 Oct 2025 23:32:47 +0200 Subject: [PATCH 2/7] lint Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract_apply.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go index 24977e81c..b37532472 100644 --- a/app/cli/cmd/workflow_contract_apply.go +++ b/app/cli/cmd/workflow_contract_apply.go @@ -36,13 +36,13 @@ or update it if it already exists.`, # Apply to a specific project chainloop workflow contract apply --contract my-contract.yaml --project my-project`, - PreRunE: func(cmd *cobra.Command, args []string) error { + PreRunE: func(_ *cobra.Command, _ []string) error { if filePath == "" && name == "" { return fmt.Errorf("either --contract file or --name must be provided") } return nil }, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { var desc *string if cmd.Flags().Changed("description") { desc = &description From ffdfb2acf1fd8966ed7e26465710b0b3c5e38938 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 15 Oct 2025 23:42:39 +0200 Subject: [PATCH 3/7] gen code Signed-off-by: Sylwester Piskozub --- app/cli/documentation/cli-reference.mdx | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 3926bdf32..1a6fef460 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -3291,6 +3291,55 @@ Options inherited from parent commands -y, --yes Skip confirmation ``` +#### chainloop workflow contract apply + +Apply a contract (create or update) + +Synopsis + +Apply a contract from a file. This command will create the contract if it doesn't exist, +or update it if it already exists. + +``` +chainloop workflow contract apply [flags] +``` + +Examples + +``` +Apply a contract from file +chainloop workflow contract apply --contract my-contract.yaml + +Apply to a specific project +chainloop workflow contract apply --contract my-contract.yaml --project my-project +``` + +Options + +``` +-f, --contract string workflow contract file path (optional) +--description string contract description +-h, --help help for apply +--name string contract name (required if no contract file provided) +--project string project name to scope the contract +``` + +Options inherited from parent commands + +``` +--artifact-cas string URL for the Artifacts Content Addressable Storage API ($CHAINLOOP_ARTIFACT_CAS_API) (default "api.cas.chainloop.dev:443") +--artifact-cas-ca string CUSTOM CA file for the Artifacts CAS API (optional) ($CHAINLOOP_ARTIFACT_CAS_API_CA) +-c, --config string Path to an existing config file (default is $HOME/.config/chainloop/config.toml) +--control-plane string URL for the Control Plane API ($CHAINLOOP_CONTROL_PLANE_API) (default "api.cp.chainloop.dev:443") +--control-plane-ca string CUSTOM CA file for the Control Plane API (optional) ($CHAINLOOP_CONTROL_PLANE_API_CA) +--debug Enable debug/verbose logging mode +-i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE) +-n, --org string organization name +-o, --output string Output format, valid options are json and table (default "table") +-t, --token string API token. NOTE: Alternatively use the env variable CHAINLOOP_TOKEN +-y, --yes Skip confirmation +``` + #### chainloop workflow contract create Create a new contract From cd0ed0dce0c0720e9a2946f81842f6feb893a5ca Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 17 Oct 2025 12:02:31 +0200 Subject: [PATCH 4/7] fix year; move name extraction to pre run; make name extraction generic Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract_apply.go | 29 ++++++++-- app/cli/pkg/action/util.go | 35 +++++++++++ app/cli/pkg/action/workflow_contract_apply.go | 58 ++----------------- 3 files changed, 65 insertions(+), 57 deletions(-) diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go index b37532472..349880fa8 100644 --- a/app/cli/cmd/workflow_contract_apply.go +++ b/app/cli/cmd/workflow_contract_apply.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2025 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. @@ -25,6 +25,8 @@ import ( func newWorkflowContractApplyCmd() *cobra.Command { var filePath, name, description, projectName string + var contractName string + var rawContract []byte cmd := &cobra.Command{ Use: "apply", @@ -37,8 +39,27 @@ or update it if it already exists.`, # Apply to a specific project chainloop workflow contract apply --contract my-contract.yaml --project my-project`, PreRunE: func(_ *cobra.Command, _ []string) error { - if filePath == "" && name == "" { - return fmt.Errorf("either --contract file or --name must be provided") + contractName = name + + if filePath != "" { + var err error + rawContract, err = action.LoadFileOrURL(filePath) + if err != nil { + return fmt.Errorf("failed to read contract file: %w", err) + } + + // Extract name from the contract file content + extractedName, err := action.ExtractNameFromRawSchema(rawContract) + if err != nil { + return err + } + + // For v2 schemas, use the extracted name. For v1 schemas, extractedName will be empty + if extractedName == "" && name == "" { + return fmt.Errorf("contracts require --name flag to specify the contract name") + } else if extractedName != "" { + contractName = extractedName + } } return nil }, @@ -48,7 +69,7 @@ or update it if it already exists.`, desc = &description } - res, err := action.NewWorkflowContractApply(ActionOpts).Run(filePath, name, desc, projectName) + res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), rawContract, contractName, desc, projectName) if err != nil { return err } diff --git a/app/cli/pkg/action/util.go b/app/cli/pkg/action/util.go index ebe7f7855..ce3180326 100644 --- a/app/cli/pkg/action/util.go +++ b/app/cli/pkg/action/util.go @@ -16,12 +16,16 @@ package action import ( + "encoding/json" "errors" + "fmt" "io" "net/http" "os" "path/filepath" "strings" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" ) // LoadFileOrURL loads a file from a local path or a URL @@ -47,3 +51,34 @@ func LoadFileOrURL(fileRef string) ([]byte, error) { return os.ReadFile(filepath.Clean(fileRef)) } + +// SchemaBase represents the minimal structure of a schema with metadata containing a name +type SchemaBase struct { + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` +} + +// ExtractNameFromRawSchema tries to extract the name from any schema with metadata +func ExtractNameFromRawSchema(content []byte) (string, error) { + // Identify format + format, err := unmarshal.IdentifyFormat(content) + if err != nil { + return "", fmt.Errorf("failed to identify schema format: %w", err) + } + + // Convert to JSON for consistent parsing + jsonData, err := unmarshal.LoadJSONBytes(content, "."+string(format)) + if err != nil { + return "", fmt.Errorf("failed to convert to JSON: %w", err) + } + + // Unmarshal to extract name + var schemaBase SchemaBase + if err := json.Unmarshal(jsonData, &schemaBase); err != nil { + // Unmarshalling error, don't fail, return empty name, + return "", nil + } + + return schemaBase.Metadata.Name, nil +} diff --git a/app/cli/pkg/action/workflow_contract_apply.go b/app/cli/pkg/action/workflow_contract_apply.go index 6ef5b354f..5a9d91dae 100644 --- a/app/cli/pkg/action/workflow_contract_apply.go +++ b/app/cli/pkg/action/workflow_contract_apply.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2025 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. @@ -20,8 +20,6 @@ import ( "fmt" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" - schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" ) type WorkflowContractApply struct { @@ -32,32 +30,7 @@ func NewWorkflowContractApply(cfg *ActionsOpts) *WorkflowContractApply { return &WorkflowContractApply{cfg} } -func (action *WorkflowContractApply) Run(filePath, name string, description *string, projectName string) (*WorkflowContractItem, error) { - var rawContract []byte - var err error - contractName := name - - // Load contract from file if provided - if filePath != "" { - rawContract, err = LoadFileOrURL(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read contract file: %w", err) - } - - // Extract name from the contract file content - extractedName, err := extractContractNameFromRawSchema(rawContract) - if err != nil { - return nil, err - } - - // For v2 schemas, use the extracted name. For v1 schemas, extractedName will be empty - if extractedName == "" && name == "" { - return nil, fmt.Errorf("contracts require --name flag to specify the contract name") - } else if extractedName != "" { - contractName = extractedName - } - } - +func (action *WorkflowContractApply) Run(ctx context.Context, rawContract []byte, contractName string, description *string, projectName string) (*WorkflowContractItem, error) { client := pb.NewWorkflowContractServiceClient(action.cfg.CPConnection) // Try to describe the specific contract first to determine if we should create or update @@ -65,7 +38,7 @@ func (action *WorkflowContractApply) Run(filePath, name string, description *str Name: contractName, } - _, err = client.Describe(context.Background(), describeReq) + _, err := client.Describe(ctx, describeReq) if err == nil { // Contract exists, perform update updateReq := &pb.WorkflowContractServiceUpdateRequest{ @@ -77,7 +50,7 @@ func (action *WorkflowContractApply) Run(filePath, name string, description *str updateReq.Description = description } - res, err := client.Update(context.Background(), updateReq) + res, err := client.Update(ctx, updateReq) if err != nil { return nil, fmt.Errorf("failed to update existing contract '%s': %w", contractName, err) } @@ -101,31 +74,10 @@ func (action *WorkflowContractApply) Run(filePath, name string, description *str } } - res, err := client.Create(context.Background(), createReq) + res, err := client.Create(ctx, createReq) if err != nil { return nil, fmt.Errorf("failed to create new contract '%s': %w", contractName, err) } return pbWorkflowContractItemToAction(res.Result), nil } - -// extractContractNameFromContent tries to extract the contract name from the contract file content -func extractContractNameFromRawSchema(content []byte) (string, error) { - // Identify format first - format, err := unmarshal.IdentifyFormat(content) - if err != nil { - return "", fmt.Errorf("failed to identify contract format: %w", err) - } - - // Unmarshal as v2 contract - // If it fails, we assume it's a v1 contract which doesn't have a name in the content - v2Contract := &schemaapi.CraftingSchemaV2{} - if err := unmarshal.FromRaw(content, format, v2Contract, false); err == nil { - // Successfully parsed as v2, extract name from metadata - if v2Contract.Metadata != nil && v2Contract.Metadata.Name != "" { - return v2Contract.Metadata.Name, nil - } - return "", fmt.Errorf("missing name in metadata section of the contract") - } - return "", nil -} From 9d900f122314fa32a06e6f838603df60099674e9 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 17 Oct 2025 12:20:19 +0200 Subject: [PATCH 5/7] extract helper Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract_apply.go | 29 +++++------------------- app/cli/pkg/action/util.go | 31 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go index 349880fa8..93501b15a 100644 --- a/app/cli/cmd/workflow_contract_apply.go +++ b/app/cli/cmd/workflow_contract_apply.go @@ -16,8 +16,6 @@ package cmd import ( - "fmt" - "github.com/chainloop-dev/chainloop/app/cli/cmd/output" "github.com/chainloop-dev/chainloop/app/cli/pkg/action" "github.com/spf13/cobra" @@ -39,27 +37,10 @@ or update it if it already exists.`, # Apply to a specific project chainloop workflow contract apply --contract my-contract.yaml --project my-project`, PreRunE: func(_ *cobra.Command, _ []string) error { - contractName = name - - if filePath != "" { - var err error - rawContract, err = action.LoadFileOrURL(filePath) - if err != nil { - return fmt.Errorf("failed to read contract file: %w", err) - } - - // Extract name from the contract file content - extractedName, err := action.ExtractNameFromRawSchema(rawContract) - if err != nil { - return err - } - - // For v2 schemas, use the extracted name. For v1 schemas, extractedName will be empty - if extractedName == "" && name == "" { - return fmt.Errorf("contracts require --name flag to specify the contract name") - } else if extractedName != "" { - contractName = extractedName - } + var err error + rawContract, contractName, err = action.LoadSchemaAndExtractName(filePath, name) + if err != nil { + return err } return nil }, @@ -84,5 +65,7 @@ or update it if it already exists.`, cmd.Flags().StringVar(&description, "description", "", "contract description") cmd.Flags().StringVar(&projectName, "project", "", "project name to scope the contract") + cmd.MarkFlagsOneRequired("contract", "name") + return cmd } diff --git a/app/cli/pkg/action/util.go b/app/cli/pkg/action/util.go index ce3180326..3c41fbe09 100644 --- a/app/cli/pkg/action/util.go +++ b/app/cli/pkg/action/util.go @@ -60,7 +60,7 @@ type SchemaBase struct { } // ExtractNameFromRawSchema tries to extract the name from any schema with metadata -func ExtractNameFromRawSchema(content []byte) (string, error) { +func extractNameFromRawSchema(content []byte) (string, error) { // Identify format format, err := unmarshal.IdentifyFormat(content) if err != nil { @@ -82,3 +82,32 @@ func ExtractNameFromRawSchema(content []byte) (string, error) { return schemaBase.Metadata.Name, nil } + +// LoadSchemaAndExtractName loads a schema from file path and extracts name from metadata if available +func LoadSchemaAndExtractName(filePath, explicitName string) ([]byte, string, error) { + finalName := explicitName + + if filePath != "" { + rawSchema, err := LoadFileOrURL(filePath) + if err != nil { + return nil, "", fmt.Errorf("failed to load schema file: %w", err) + } + + // Extract name from the schema file content + extractedName, err := extractNameFromRawSchema(rawSchema) + if err != nil { + return nil, "", err + } + + // Name is required + if extractedName == "" && explicitName == "" { + return nil, "", fmt.Errorf("name in schema not found, --name flag is required") + } else if extractedName != "" { + finalName = extractedName + } + + return rawSchema, finalName, nil + } + + return nil, finalName, nil +} From 9e4addf8a2759734dc184db1ca51b35cf3bc3171 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Mon, 20 Oct 2025 14:16:36 +0200 Subject: [PATCH 6/7] remove validation related to schema v2 Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract_apply.go | 26 ++++------ app/cli/documentation/cli-reference.mdx | 13 ++--- app/cli/pkg/action/util.go | 64 ------------------------- 3 files changed, 13 insertions(+), 90 deletions(-) diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go index 93501b15a..98e9c8df7 100644 --- a/app/cli/cmd/workflow_contract_apply.go +++ b/app/cli/cmd/workflow_contract_apply.go @@ -22,7 +22,7 @@ import ( ) func newWorkflowContractApplyCmd() *cobra.Command { - var filePath, name, description, projectName string + var contractPath, name, description, projectName string var contractName string var rawContract []byte @@ -32,18 +32,7 @@ func newWorkflowContractApplyCmd() *cobra.Command { Long: `Apply a contract from a file. This command will create the contract if it doesn't exist, or update it if it already exists.`, Example: ` # Apply a contract from file - chainloop workflow contract apply --contract my-contract.yaml - - # Apply to a specific project - chainloop workflow contract apply --contract my-contract.yaml --project my-project`, - PreRunE: func(_ *cobra.Command, _ []string) error { - var err error - rawContract, contractName, err = action.LoadSchemaAndExtractName(filePath, name) - if err != nil { - return err - } - return nil - }, + chainloop workflow contract apply --contract my-contract.yaml --name my-contract --project my-project`, RunE: func(cmd *cobra.Command, _ []string) error { var desc *string if cmd.Flags().Changed("description") { @@ -60,12 +49,13 @@ or update it if it already exists.`, }, } - cmd.Flags().StringVarP(&filePath, "contract", "f", "", "workflow contract file path (optional)") - cmd.Flags().StringVar(&name, "name", "", "contract name (required if no contract file provided)") - cmd.Flags().StringVar(&description, "description", "", "contract description") - cmd.Flags().StringVar(&projectName, "project", "", "project name to scope the contract") + cmd.Flags().StringVar(&name, "name", "", "contract name") + err := cmd.MarkFlagRequired("name") + cobra.CheckErr(err) - cmd.MarkFlagsOneRequired("contract", "name") + cmd.Flags().StringVarP(&contractPath, "contract", "f", "", "path or URL to the contract schema") + cmd.Flags().StringVar(&description, "description", "", "description of the contract") + cmd.Flags().StringVar(&projectName, "project", "", "project name used to scope the contract, if not set the contract will be created in the organization") return cmd } diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 1a6fef460..019073e82 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -3308,20 +3308,17 @@ Examples ``` Apply a contract from file -chainloop workflow contract apply --contract my-contract.yaml - -Apply to a specific project -chainloop workflow contract apply --contract my-contract.yaml --project my-project +chainloop workflow contract apply --contract my-contract.yaml --name my-contract --project my-project ``` Options ``` --f, --contract string workflow contract file path (optional) ---description string contract description +-f, --contract string path or URL to the contract schema +--description string description of the contract -h, --help help for apply ---name string contract name (required if no contract file provided) ---project string project name to scope the contract +--name string contract name +--project string project name used to scope the contract, if not set the contract will be created in the organization ``` Options inherited from parent commands diff --git a/app/cli/pkg/action/util.go b/app/cli/pkg/action/util.go index 3c41fbe09..ebe7f7855 100644 --- a/app/cli/pkg/action/util.go +++ b/app/cli/pkg/action/util.go @@ -16,16 +16,12 @@ package action import ( - "encoding/json" "errors" - "fmt" "io" "net/http" "os" "path/filepath" "strings" - - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" ) // LoadFileOrURL loads a file from a local path or a URL @@ -51,63 +47,3 @@ func LoadFileOrURL(fileRef string) ([]byte, error) { return os.ReadFile(filepath.Clean(fileRef)) } - -// SchemaBase represents the minimal structure of a schema with metadata containing a name -type SchemaBase struct { - Metadata struct { - Name string `json:"name"` - } `json:"metadata"` -} - -// ExtractNameFromRawSchema tries to extract the name from any schema with metadata -func extractNameFromRawSchema(content []byte) (string, error) { - // Identify format - format, err := unmarshal.IdentifyFormat(content) - if err != nil { - return "", fmt.Errorf("failed to identify schema format: %w", err) - } - - // Convert to JSON for consistent parsing - jsonData, err := unmarshal.LoadJSONBytes(content, "."+string(format)) - if err != nil { - return "", fmt.Errorf("failed to convert to JSON: %w", err) - } - - // Unmarshal to extract name - var schemaBase SchemaBase - if err := json.Unmarshal(jsonData, &schemaBase); err != nil { - // Unmarshalling error, don't fail, return empty name, - return "", nil - } - - return schemaBase.Metadata.Name, nil -} - -// LoadSchemaAndExtractName loads a schema from file path and extracts name from metadata if available -func LoadSchemaAndExtractName(filePath, explicitName string) ([]byte, string, error) { - finalName := explicitName - - if filePath != "" { - rawSchema, err := LoadFileOrURL(filePath) - if err != nil { - return nil, "", fmt.Errorf("failed to load schema file: %w", err) - } - - // Extract name from the schema file content - extractedName, err := extractNameFromRawSchema(rawSchema) - if err != nil { - return nil, "", err - } - - // Name is required - if extractedName == "" && explicitName == "" { - return nil, "", fmt.Errorf("name in schema not found, --name flag is required") - } else if extractedName != "" { - finalName = extractedName - } - - return rawSchema, finalName, nil - } - - return nil, finalName, nil -} From a544336ba861bae543a927956367e10943d77f33 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Mon, 20 Oct 2025 14:56:40 +0200 Subject: [PATCH 7/7] fix contract loading Signed-off-by: Sylwester Piskozub --- app/cli/cmd/workflow_contract_apply.go | 4 +--- app/cli/pkg/action/workflow_contract_apply.go | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/cli/cmd/workflow_contract_apply.go b/app/cli/cmd/workflow_contract_apply.go index 98e9c8df7..98e95976d 100644 --- a/app/cli/cmd/workflow_contract_apply.go +++ b/app/cli/cmd/workflow_contract_apply.go @@ -23,8 +23,6 @@ import ( func newWorkflowContractApplyCmd() *cobra.Command { var contractPath, name, description, projectName string - var contractName string - var rawContract []byte cmd := &cobra.Command{ Use: "apply", @@ -39,7 +37,7 @@ or update it if it already exists.`, desc = &description } - res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), rawContract, contractName, desc, projectName) + res, err := action.NewWorkflowContractApply(ActionOpts).Run(cmd.Context(), name, contractPath, desc, projectName) if err != nil { return err } diff --git a/app/cli/pkg/action/workflow_contract_apply.go b/app/cli/pkg/action/workflow_contract_apply.go index 5a9d91dae..63a6f9335 100644 --- a/app/cli/pkg/action/workflow_contract_apply.go +++ b/app/cli/pkg/action/workflow_contract_apply.go @@ -30,7 +30,7 @@ func NewWorkflowContractApply(cfg *ActionsOpts) *WorkflowContractApply { return &WorkflowContractApply{cfg} } -func (action *WorkflowContractApply) Run(ctx context.Context, rawContract []byte, contractName string, description *string, projectName string) (*WorkflowContractItem, error) { +func (action *WorkflowContractApply) Run(ctx context.Context, contractName string, contractPath string, description *string, projectName string) (*WorkflowContractItem, error) { client := pb.NewWorkflowContractServiceClient(action.cfg.CPConnection) // Try to describe the specific contract first to determine if we should create or update @@ -38,18 +38,25 @@ func (action *WorkflowContractApply) Run(ctx context.Context, rawContract []byte Name: contractName, } + var rawContract []byte + if contractPath != "" { + raw, err := LoadFileOrURL(contractPath) + if err != nil { + action.cfg.Logger.Debug().Err(err).Msg("loading the contract") + return nil, err + } + rawContract = raw + } + _, err := client.Describe(ctx, describeReq) if err == nil { // Contract exists, perform update updateReq := &pb.WorkflowContractServiceUpdateRequest{ Name: contractName, + Description: description, RawContract: rawContract, } - if description != nil { - updateReq.Description = description - } - res, err := client.Update(ctx, updateReq) if err != nil { return nil, fmt.Errorf("failed to update existing contract '%s': %w", contractName, err) @@ -61,13 +68,10 @@ func (action *WorkflowContractApply) Run(ctx context.Context, rawContract []byte // Contract doesn't exist, perform create createReq := &pb.WorkflowContractServiceCreateRequest{ Name: contractName, + Description: description, RawContract: rawContract, } - if description != nil { - createReq.Description = description - } - if projectName != "" { createReq.ProjectReference = &pb.IdentityReference{ Name: &projectName,