From 9d4a1046a33f2a8b038878e55b2f47762169808c Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 14 Apr 2026 22:00:40 +0200 Subject: [PATCH 1/5] feat: resolve "latest" magic constant for project versions Allow clients to pass "latest" as the projectVersion during attestation init. The control plane resolves it to the project version with latest=true in the database, returning a validation error if no version exists. Also blocks "latest" as a literal version name to prevent ambiguity. Closes #3034 Signed-off-by: Jose I. Paris --- app/controlplane/pkg/biz/biz.go | 8 ++++ app/controlplane/pkg/biz/version_test.go | 7 ++- app/controlplane/pkg/biz/workflowrun.go | 2 +- .../pkg/biz/workflowrun_integration_test.go | 47 ++++++++++++++++++- app/controlplane/pkg/data/workflowrun.go | 34 ++++++++++---- 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/app/controlplane/pkg/biz/biz.go b/app/controlplane/pkg/biz/biz.go index 497db0400..cd42048ab 100644 --- a/app/controlplane/pkg/biz/biz.go +++ b/app/controlplane/pkg/biz/biz.go @@ -65,6 +65,10 @@ var ProviderSet = wire.NewSet( wire.Struct(new(NewUserUseCaseParams), "*"), ) +// LatestVersionMagicConstant is a reserved version identifier that resolves to +// the project version with latest=true in the database. +const LatestVersionMagicConstant = "latest" + var ( // versionRegexp allows alphanumeric, dots, hyphens, underscores, plus signs, and build metadata versionRegexp = regexp.MustCompile(`^[a-zA-Z0-9.\-_+]+(?:\+[a-zA-Z0-9.\-_]+)?$`) @@ -117,6 +121,10 @@ func ValidateIsDNS1123(name string) error { // The version string must match the following regular expression: ^[a-zA-Z0-9.\-]+$ // This ensures the version only contains alphanumeric characters, dots, and hyphens. func ValidateVersion(version string) error { + if version == LatestVersionMagicConstant { + return NewErrValidationStr("'latest' is a reserved version identifier") + } + if !versionRegexp.MatchString(version) { return NewErrValidationStr(fmt.Sprintf("invalid version format: %s. Valid examples: '1.0.0', 'v2.1-alpha', '3.0.0+build.123', '2024.3.12', 'v1.0_beta'", version)) } diff --git a/app/controlplane/pkg/biz/version_test.go b/app/controlplane/pkg/biz/version_test.go index 5ba65af9a..5fa52bbba 100644 --- a/app/controlplane/pkg/biz/version_test.go +++ b/app/controlplane/pkg/biz/version_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-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. @@ -82,6 +82,11 @@ func (s *versionTestSuite) TestValidateVersion() { version: "release-20230615", wantError: false, }, + { + name: "reserved latest keyword", + version: "latest", + wantError: true, + }, { name: "invalid version with spaces", version: "version 1.0", diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 20e0a4b98..a9461a664 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -249,7 +249,7 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat contractRevision := opts.ContractRevision - if opts.ProjectVersion != "" { + if opts.ProjectVersion != "" && opts.ProjectVersion != LatestVersionMagicConstant { if err := ValidateVersion(opts.ProjectVersion); err != nil { return nil, err } diff --git a/app/controlplane/pkg/biz/workflowrun_integration_test.go b/app/controlplane/pkg/biz/workflowrun_integration_test.go index 843f24085..124f3aacd 100644 --- a/app/controlplane/pkg/biz/workflowrun_integration_test.go +++ b/app/controlplane/pkg/biz/workflowrun_integration_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-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. @@ -26,6 +26,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers" attestation2 "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" + entProjectVersion "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/attestation" "github.com/chainloop-dev/chainloop/pkg/credentials" @@ -339,6 +340,50 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() { s.Equal(pv.ID, run.ProjectVersion.ID) } }) + + s.T().Run("latest resolves to version with latest=true", func(_ *testing.T) { + // workflowOrg1 already has versions from previous test runs. + // The last created version should have latest=true. + // Create a named version first so we know which one is latest. + namedRun, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "latest-target", + }) + s.Require().NoError(err) + latestVersion := namedRun.ProjectVersion + + // Now create a run with "latest" — should resolve to "latest-target" + run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "latest", + }) + s.Require().NoError(err) + s.Equal(latestVersion.ID, run.ProjectVersion.ID) + s.Equal("latest-target", run.ProjectVersion.Version) + }) + + s.T().Run("latest with no versions returns error", func(_ *testing.T) { + // Create a new workflow in a fresh project (which auto-creates a default version) + wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{ + Name: "no-versions-workflow", OrgID: s.org.ID, Project: "empty-project", + }) + s.Require().NoError(err) + + // Soft-delete all versions for this project so "latest" resolution fails + _, err = s.Data.DB.ProjectVersion.Delete(). + Where( + entProjectVersion.ProjectID(wf.ProjectID), + ).Exec(ctx) + s.Require().NoError(err) + + _, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "latest", + }) + s.Require().Error(err) + s.True(biz.IsErrValidation(err)) + s.Contains(err.Error(), "no project version exists") + }) } func (s *workflowRunIntegrationTestSuite) TestContractInformation() { diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 64183f872..b981e52e6 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-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. @@ -55,16 +55,30 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC return nil, fmt.Errorf("getting workflow: %w", err) } - // load the version in advance to prevent locking if it already exists - version, err := r.data.DB.ProjectVersion.Query(). - Where(projectversion.Version(opts.ProjectVersion), projectversion.ProjectID(wf.ProjectID), projectversion.DeletedAtIsNil()).First(ctx) - if err != nil && !ent.IsNotFound(err) { - return nil, fmt.Errorf("checking existing version: %w", err) - } + var version *ent.ProjectVersion + if opts.ProjectVersion == biz.LatestVersionMagicConstant { + // Resolve "latest" to the project version with latest=true + version, err = r.data.DB.ProjectVersion.Query(). + Where(projectversion.ProjectID(wf.ProjectID), projectversion.DeletedAtIsNil(), projectversion.Latest(true)). + First(ctx) + if err != nil && !ent.IsNotFound(err) { + return nil, fmt.Errorf("resolving latest version: %w", err) + } + if version == nil { + return nil, biz.NewErrValidationStr("no project version exists; create one before attesting with 'latest'") + } + } else { + // load the version in advance to prevent locking if it already exists + version, err = r.data.DB.ProjectVersion.Query(). + Where(projectversion.Version(opts.ProjectVersion), projectversion.ProjectID(wf.ProjectID), projectversion.DeletedAtIsNil()).First(ctx) + if err != nil && !ent.IsNotFound(err) { + return nil, fmt.Errorf("checking existing version: %w", err) + } - // If RequireExistingVersion is set, fail if the version doesn't exist - if opts.RequireExistingVersion && version == nil { - return nil, biz.NewErrValidationStr(fmt.Errorf("project version %q not found", opts.ProjectVersion).Error()) + // If RequireExistingVersion is set, fail if the version doesn't exist + if opts.RequireExistingVersion && version == nil { + return nil, biz.NewErrValidationStr(fmt.Errorf("project version %q not found", opts.ProjectVersion).Error()) + } } var p *ent.WorkflowRun From 8bcf68f5edf0942172278e2afcc287354321d729 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 14 Apr 2026 23:02:23 +0200 Subject: [PATCH 2/5] fix: update CLI to display resolved version name after att init with "latest" Signed-off-by: Jose I. Paris --- app/cli/pkg/action/attestation_init.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/cli/pkg/action/attestation_init.go b/app/cli/pkg/action/attestation_init.go index 6595014e8..b66ed0fc6 100644 --- a/app/cli/pkg/action/attestation_init.go +++ b/app/cli/pkg/action/attestation_init.go @@ -235,6 +235,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun uiDashboardURL = result.GetUiDashboardUrl() if v := workflowMeta.Version; v != nil && workflowRun.GetVersion() != nil { + v.Version = workflowRun.GetVersion().GetVersion() v.Prerelease = workflowRun.GetVersion().GetPrerelease() } From ed55823a9ebb06e56c2b75b867ca8c1fa908e0b9 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 14 Apr 2026 23:21:14 +0200 Subject: [PATCH 3/5] feat: replace magic version constant with --latest-version flag Replace the "latest" magic string approach with a dedicated boolean use_latest_version field in the API and a --latest-version CLI flag. This avoids any collision with existing version names and is cleaner. The flag is mutually exclusive with --version. When set, the control plane resolves to the project version with latest=true in the database. Signed-off-by: Jose I. Paris --- app/cli/cmd/attestation_init.go | 7 ++++++ app/cli/pkg/action/attestation_init.go | 4 +++- .../api/controlplane/v1/workflow_run.pb.go | 19 +++++++++++---- .../api/controlplane/v1/workflow_run.proto | 3 +++ .../frontend/controlplane/v1/workflow_run.ts | 19 +++++++++++++++ ...estationServiceInitRequest.jsonschema.json | 8 +++++++ ....AttestationServiceInitRequest.schema.json | 8 +++++++ .../internal/service/attestation.go | 1 + app/controlplane/pkg/biz/biz.go | 8 ------- app/controlplane/pkg/biz/version_test.go | 5 ---- app/controlplane/pkg/biz/workflowrun.go | 9 ++++++- .../pkg/biz/workflowrun_integration_test.go | 24 ++++++++++++------- app/controlplane/pkg/data/workflowrun.go | 6 ++--- 13 files changed, 91 insertions(+), 30 deletions(-) diff --git a/app/cli/cmd/attestation_init.go b/app/cli/cmd/attestation_init.go index e346ed8fe..e56405caa 100644 --- a/app/cli/cmd/attestation_init.go +++ b/app/cli/cmd/attestation_init.go @@ -35,6 +35,7 @@ func newAttestationInitCmd() *cobra.Command { workflowName string projectName string projectVersion string + useLatestVersion bool projectVersionRelease bool existingVersion bool newWorkflowcontract string @@ -69,6 +70,10 @@ func newAttestationInitCmd() *cobra.Command { projectVersion = cfg.ProjectVersion } + if useLatestVersion && projectVersion != "" { + return errors.New("--latest-version and --version are mutually exclusive") + } + if projectVersion == "" && projectVersionRelease { return errors.New("project version is required when using --release") } @@ -110,6 +115,7 @@ func newAttestationInitCmd() *cobra.Command { ContractRevision: contractRevision, ProjectName: projectName, ProjectVersion: projectVersion, + UseLatestVersion: useLatestVersion, WorkflowName: workflowName, NewWorkflowContractRef: newWorkflowcontract, ProjectVersionMarkAsReleased: projectVersionRelease, @@ -172,6 +178,7 @@ func newAttestationInitCmd() *cobra.Command { cmd.Flags().StringVar(&newWorkflowcontract, "contract", "", "name of an existing contract or the path/URL to a contract file, to attach it to the auto-created workflow (it doesn't update an existing one)") cmd.Flags().StringVar(&projectVersion, "version", "", "project version, i.e 0.1.0") + cmd.Flags().BoolVar(&useLatestVersion, "latest-version", false, "use the latest existing project version instead of specifying one") cmd.Flags().BoolVar(&projectVersionRelease, "release", false, "promote the provided version as a release") cmd.Flags().BoolVar(&existingVersion, "existing-version", false, "return an error if the version doesn't exist in the project") cmd.Flags().StringSliceVar(&collectors, "collectors", nil, "comma-separated list of additional collectors to enable (e.g. aiconfig)") diff --git a/app/cli/pkg/action/attestation_init.go b/app/cli/pkg/action/attestation_init.go index b66ed0fc6..ddad251d2 100644 --- a/app/cli/pkg/action/attestation_init.go +++ b/app/cli/pkg/action/attestation_init.go @@ -97,6 +97,7 @@ type AttestationInitRunOpts struct { ContractRevision int ProjectName string ProjectVersion string + UseLatestVersion bool ProjectVersionMarkAsReleased bool RequireExistingVersion bool WorkflowName string @@ -170,7 +171,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun ContractName: workflow.ContractName, } - if opts.ProjectVersion != "" { + if opts.ProjectVersion != "" || opts.UseLatestVersion { workflowMeta.Version = &clientAPI.ProjectVersion{ Version: opts.ProjectVersion, MarkAsReleased: opts.ProjectVersionMarkAsReleased, @@ -215,6 +216,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun WorkflowName: opts.WorkflowName, ProjectName: opts.ProjectName, ProjectVersion: opts.ProjectVersion, + UseLatestVersion: opts.UseLatestVersion, RequireExistingVersion: opts.RequireExistingVersion, }, ) diff --git a/app/controlplane/api/controlplane/v1/workflow_run.pb.go b/app/controlplane/api/controlplane/v1/workflow_run.pb.go index 38f48d79d..bc1a9f373 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run.pb.go @@ -600,8 +600,11 @@ type AttestationServiceInitRequest struct { ProjectVersion string `protobuf:"bytes,6,opt,name=project_version,json=projectVersion,proto3" json:"project_version,omitempty"` // Optional flag to require that the project version already exists RequireExistingVersion bool `protobuf:"varint,7,opt,name=require_existing_version,json=requireExistingVersion,proto3" json:"require_existing_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Use the latest project version instead of specifying one explicitly. + // Mutually exclusive with project_version. + UseLatestVersion bool `protobuf:"varint,8,opt,name=use_latest_version,json=useLatestVersion,proto3" json:"use_latest_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttestationServiceInitRequest) Reset() { @@ -683,6 +686,13 @@ func (x *AttestationServiceInitRequest) GetRequireExistingVersion() bool { return false } +func (x *AttestationServiceInitRequest) GetUseLatestVersion() bool { + if x != nil { + return x.UseLatestVersion + } + return false +} + type AttestationServiceInitResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Result *AttestationServiceInitResponse_Result `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` @@ -1784,7 +1794,7 @@ const file_controlplane_v1_workflow_run_proto_rawDesc = "" + "\x06result\x18\x01 \x01(\v2=.controlplane.v1.AttestationServiceGetContractResponse.ResultR\x06result\x1a\x8d\x01\n" + "\x06Result\x129\n" + "\bworkflow\x18\x01 \x01(\v2\x1d.controlplane.v1.WorkflowItemR\bworkflow\x12H\n" + - "\bcontract\x18\x02 \x01(\v2,.controlplane.v1.WorkflowContractVersionItemR\bcontract\"\xf1\x02\n" + + "\bcontract\x18\x02 \x01(\v2,.controlplane.v1.WorkflowContractVersionItemR\bcontract\"\x9f\x03\n" + "\x1dAttestationServiceInitRequest\x12+\n" + "\x11contract_revision\x18\x01 \x01(\x05R\x10contractRevision\x12\x17\n" + "\ajob_url\x18\x02 \x01(\tR\x06jobUrl\x12M\n" + @@ -1792,7 +1802,8 @@ const file_controlplane_v1_workflow_run_proto_rawDesc = "" + "\rworkflow_name\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\fworkflowName\x12*\n" + "\fproject_name\x18\x05 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\vprojectName\x12'\n" + "\x0fproject_version\x18\x06 \x01(\tR\x0eprojectVersion\x128\n" + - "\x18require_existing_version\x18\a \x01(\bR\x16requireExistingVersion\"\x94\x05\n" + + "\x18require_existing_version\x18\a \x01(\bR\x16requireExistingVersion\x12,\n" + + "\x12use_latest_version\x18\b \x01(\bR\x10useLatestVersion\"\x94\x05\n" + "\x1eAttestationServiceInitResponse\x12N\n" + "\x06result\x18\x01 \x01(\v26.controlplane.v1.AttestationServiceInitResponse.ResultR\x06result\x1a\xb8\x03\n" + "\x06Result\x12C\n" + diff --git a/app/controlplane/api/controlplane/v1/workflow_run.proto b/app/controlplane/api/controlplane/v1/workflow_run.proto index 7b1decfe3..76aba91aa 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.proto +++ b/app/controlplane/api/controlplane/v1/workflow_run.proto @@ -124,6 +124,9 @@ message AttestationServiceInitRequest { string project_version = 6; // Optional flag to require that the project version already exists bool require_existing_version = 7; + // Use the latest project version instead of specifying one explicitly. + // Mutually exclusive with project_version. + bool use_latest_version = 8; } message AttestationServiceInitResponse { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts index aabc5cacc..e93832f03 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts @@ -99,6 +99,11 @@ export interface AttestationServiceInitRequest { projectVersion: string; /** Optional flag to require that the project version already exists */ requireExistingVersion: boolean; + /** + * Use the latest project version instead of specifying one explicitly. + * Mutually exclusive with project_version. + */ + useLatestVersion: boolean; } export interface AttestationServiceInitResponse { @@ -1081,6 +1086,7 @@ function createBaseAttestationServiceInitRequest(): AttestationServiceInitReques projectName: "", projectVersion: "", requireExistingVersion: false, + useLatestVersion: false, }; } @@ -1107,6 +1113,9 @@ export const AttestationServiceInitRequest = { if (message.requireExistingVersion === true) { writer.uint32(56).bool(message.requireExistingVersion); } + if (message.useLatestVersion === true) { + writer.uint32(64).bool(message.useLatestVersion); + } return writer; }, @@ -1166,6 +1175,13 @@ export const AttestationServiceInitRequest = { message.requireExistingVersion = reader.bool(); continue; + case 8: + if (tag !== 64) { + break; + } + + message.useLatestVersion = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1184,6 +1200,7 @@ export const AttestationServiceInitRequest = { projectName: isSet(object.projectName) ? String(object.projectName) : "", projectVersion: isSet(object.projectVersion) ? String(object.projectVersion) : "", requireExistingVersion: isSet(object.requireExistingVersion) ? Boolean(object.requireExistingVersion) : false, + useLatestVersion: isSet(object.useLatestVersion) ? Boolean(object.useLatestVersion) : false, }; }, @@ -1196,6 +1213,7 @@ export const AttestationServiceInitRequest = { message.projectName !== undefined && (obj.projectName = message.projectName); message.projectVersion !== undefined && (obj.projectVersion = message.projectVersion); message.requireExistingVersion !== undefined && (obj.requireExistingVersion = message.requireExistingVersion); + message.useLatestVersion !== undefined && (obj.useLatestVersion = message.useLatestVersion); return obj; }, @@ -1214,6 +1232,7 @@ export const AttestationServiceInitRequest = { message.projectName = object.projectName ?? ""; message.projectVersion = object.projectVersion ?? ""; message.requireExistingVersion = object.requireExistingVersion ?? false; + message.useLatestVersion = object.useLatestVersion ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json index 85c42f1fc..5e6e99f59 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.jsonschema.json @@ -23,6 +23,10 @@ "description": "Optional flag to require that the project version already exists", "type": "boolean" }, + "^(use_latest_version)$": { + "description": "Use the latest project version instead of specifying one explicitly.\n Mutually exclusive with project_version.", + "type": "boolean" + }, "^(workflow_name)$": { "minLength": 1, "type": "string" @@ -73,6 +77,10 @@ } ] }, + "useLatestVersion": { + "description": "Use the latest project version instead of specifying one explicitly.\n Mutually exclusive with project_version.", + "type": "boolean" + }, "workflowName": { "minLength": 1, "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json index 707469a66..51049ed78 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceInitRequest.schema.json @@ -23,6 +23,10 @@ "description": "Optional flag to require that the project version already exists", "type": "boolean" }, + "^(useLatestVersion)$": { + "description": "Use the latest project version instead of specifying one explicitly.\n Mutually exclusive with project_version.", + "type": "boolean" + }, "^(workflowName)$": { "minLength": 1, "type": "string" @@ -73,6 +77,10 @@ } ] }, + "use_latest_version": { + "description": "Use the latest project version instead of specifying one explicitly.\n Mutually exclusive with project_version.", + "type": "boolean" + }, "workflow_name": { "minLength": 1, "type": "string" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index ece70348e..856cffc32 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -201,6 +201,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer RunnerType: req.GetRunner().String(), CASBackendID: backend.ID, ProjectVersion: req.GetProjectVersion(), + UseLatestVersion: req.GetUseLatestVersion(), RequireExistingVersion: req.GetRequireExistingVersion(), } diff --git a/app/controlplane/pkg/biz/biz.go b/app/controlplane/pkg/biz/biz.go index cd42048ab..497db0400 100644 --- a/app/controlplane/pkg/biz/biz.go +++ b/app/controlplane/pkg/biz/biz.go @@ -65,10 +65,6 @@ var ProviderSet = wire.NewSet( wire.Struct(new(NewUserUseCaseParams), "*"), ) -// LatestVersionMagicConstant is a reserved version identifier that resolves to -// the project version with latest=true in the database. -const LatestVersionMagicConstant = "latest" - var ( // versionRegexp allows alphanumeric, dots, hyphens, underscores, plus signs, and build metadata versionRegexp = regexp.MustCompile(`^[a-zA-Z0-9.\-_+]+(?:\+[a-zA-Z0-9.\-_]+)?$`) @@ -121,10 +117,6 @@ func ValidateIsDNS1123(name string) error { // The version string must match the following regular expression: ^[a-zA-Z0-9.\-]+$ // This ensures the version only contains alphanumeric characters, dots, and hyphens. func ValidateVersion(version string) error { - if version == LatestVersionMagicConstant { - return NewErrValidationStr("'latest' is a reserved version identifier") - } - if !versionRegexp.MatchString(version) { return NewErrValidationStr(fmt.Sprintf("invalid version format: %s. Valid examples: '1.0.0', 'v2.1-alpha', '3.0.0+build.123', '2024.3.12', 'v1.0_beta'", version)) } diff --git a/app/controlplane/pkg/biz/version_test.go b/app/controlplane/pkg/biz/version_test.go index 5fa52bbba..e4ba5affb 100644 --- a/app/controlplane/pkg/biz/version_test.go +++ b/app/controlplane/pkg/biz/version_test.go @@ -82,11 +82,6 @@ func (s *versionTestSuite) TestValidateVersion() { version: "release-20230615", wantError: false, }, - { - name: "reserved latest keyword", - version: "latest", - wantError: true, - }, { name: "invalid version with spaces", version: "version 1.0", diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index a9461a664..56228b55a 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -220,6 +220,7 @@ type WorkflowRunCreateOpts struct { RunnerType string CASBackendID uuid.UUID ProjectVersion string + UseLatestVersion bool RequireExistingVersion bool } @@ -229,6 +230,7 @@ type WorkflowRunRepoCreateOpts struct { Backends []uuid.UUID LatestRevision, UsedRevision int ProjectVersion string + UseLatestVersion bool RequireExistingVersion bool } @@ -249,7 +251,11 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat contractRevision := opts.ContractRevision - if opts.ProjectVersion != "" && opts.ProjectVersion != LatestVersionMagicConstant { + if opts.UseLatestVersion && opts.ProjectVersion != "" { + return nil, NewErrValidationStr("cannot specify both a project version and use-latest-version") + } + + if opts.ProjectVersion != "" { if err := ValidateVersion(opts.ProjectVersion); err != nil { return nil, err } @@ -268,6 +274,7 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat LatestRevision: contractRevision.Contract.LatestRevision, UsedRevision: contractRevision.Version.Revision, ProjectVersion: opts.ProjectVersion, + UseLatestVersion: opts.UseLatestVersion, RequireExistingVersion: opts.RequireExistingVersion, }) if err != nil { diff --git a/app/controlplane/pkg/biz/workflowrun_integration_test.go b/app/controlplane/pkg/biz/workflowrun_integration_test.go index 124f3aacd..68041ca0d 100644 --- a/app/controlplane/pkg/biz/workflowrun_integration_test.go +++ b/app/controlplane/pkg/biz/workflowrun_integration_test.go @@ -341,9 +341,7 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() { } }) - s.T().Run("latest resolves to version with latest=true", func(_ *testing.T) { - // workflowOrg1 already has versions from previous test runs. - // The last created version should have latest=true. + s.T().Run("use-latest-version resolves to version with latest=true", func(_ *testing.T) { // Create a named version first so we know which one is latest. namedRun, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, @@ -352,24 +350,24 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() { s.Require().NoError(err) latestVersion := namedRun.ProjectVersion - // Now create a run with "latest" — should resolve to "latest-target" + // Now create a run with UseLatestVersion — should resolve to "latest-target" run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, - RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "latest", + RunnerType: "runnerType", RunnerRunURL: "runURL", UseLatestVersion: true, }) s.Require().NoError(err) s.Equal(latestVersion.ID, run.ProjectVersion.ID) s.Equal("latest-target", run.ProjectVersion.Version) }) - s.T().Run("latest with no versions returns error", func(_ *testing.T) { + s.T().Run("use-latest-version with no versions returns error", func(_ *testing.T) { // Create a new workflow in a fresh project (which auto-creates a default version) wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{ Name: "no-versions-workflow", OrgID: s.org.ID, Project: "empty-project", }) s.Require().NoError(err) - // Soft-delete all versions for this project so "latest" resolution fails + // Delete all versions for this project so resolution fails _, err = s.Data.DB.ProjectVersion.Delete(). Where( entProjectVersion.ProjectID(wf.ProjectID), @@ -378,12 +376,22 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() { _, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, - RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "latest", + RunnerType: "runnerType", RunnerRunURL: "runURL", UseLatestVersion: true, }) s.Require().Error(err) s.True(biz.IsErrValidation(err)) s.Contains(err.Error(), "no project version exists") }) + + s.T().Run("use-latest-version and project-version are mutually exclusive", func(_ *testing.T) { + _, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "v1.0", UseLatestVersion: true, + }) + s.Require().Error(err) + s.True(biz.IsErrValidation(err)) + s.Contains(err.Error(), "cannot specify both") + }) } func (s *workflowRunIntegrationTestSuite) TestContractInformation() { diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index b981e52e6..d39f2b6e0 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -56,8 +56,8 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC } var version *ent.ProjectVersion - if opts.ProjectVersion == biz.LatestVersionMagicConstant { - // Resolve "latest" to the project version with latest=true + if opts.UseLatestVersion { + // Resolve to the project version with latest=true version, err = r.data.DB.ProjectVersion.Query(). Where(projectversion.ProjectID(wf.ProjectID), projectversion.DeletedAtIsNil(), projectversion.Latest(true)). First(ctx) @@ -65,7 +65,7 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC return nil, fmt.Errorf("resolving latest version: %w", err) } if version == nil { - return nil, biz.NewErrValidationStr("no project version exists; create one before attesting with 'latest'") + return nil, biz.NewErrValidationStr("no project version exists; create one before attesting with --latest-version") } } else { // load the version in advance to prevent locking if it already exists From 0fb9213bba3964819744f88c112936b7df7bf4bc Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 14 Apr 2026 23:26:11 +0200 Subject: [PATCH 4/5] fix: skip loading version from .chainloop.yml when --latest-version is set Signed-off-by: Jose I. Paris --- app/cli/cmd/attestation_init.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/cmd/attestation_init.go b/app/cli/cmd/attestation_init.go index e56405caa..ed4525ee8 100644 --- a/app/cli/cmd/attestation_init.go +++ b/app/cli/cmd/attestation_init.go @@ -55,8 +55,8 @@ func newAttestationInitCmd() *cobra.Command { return errors.New("workflow name is required, set it via --workflow flag") } - // load version from the file if not set - if projectVersion == "" { + // load version from the file if not set and not using --latest-version + if projectVersion == "" && !useLatestVersion { // load the cfg from the file cfg, path, err := loadDotChainloopConfigWithParentTraversal() // we do gracefully load, if not found, or any other error we continue From 071c855bb6aa1e3029c5e009c6fd8aa58b7287ee Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 14 Apr 2026 23:31:00 +0200 Subject: [PATCH 5/5] gnerate doc Signed-off-by: Jose I. Paris --- app/cli/documentation/cli-reference.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 1262ba788..c8d3914c6 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -327,6 +327,7 @@ Options --dry-run do not record attestation in the control plane, useful for development --existing-version return an error if the version doesn't exist in the project -h, --help help for init +--latest-version use the latest existing project version instead of specifying one --project string name of the project of this workflow --release promote the provided version as a release --remote-state Store the attestation state remotely