From 012eb62b1dbe3fe82b170b3aa6f70733ef8ca22e Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 18:13:12 +0200 Subject: [PATCH 1/6] refactor(api): remove deprecated attestation and bundle fields from Store Closes #3055 The AttestationServiceStoreRequest previously carried three representations of the same payload: the raw DSSE envelope, the original Sigstore bundle (with the signature bug from #1832), and the fixed attestation bundle. Only attestation_bundle is consumed today; the other two have been deprecated since June 2025 and the fixed bundle has been available since February 2025. This removes the deprecated fields and their fallback paths in the CLI, controlplane service/biz/data layers, and the attestation helper package, cutting the gRPC request size by ~3x for the same logical payload. Signed-off-by: Miguel Martinez Trivino --- app/cli/pkg/action/attestation_push.go | 29 ++-------- .../api/controlplane/v1/workflow_run.pb.go | 34 ++--------- .../api/controlplane/v1/workflow_run.proto | 9 ++- .../controlplane/v1/workflow_run_grpc.pb.go | 2 +- .../frontend/controlplane/v1/workflow_run.ts | 48 +--------------- ...stationServiceStoreRequest.jsonschema.json | 10 ---- ...AttestationServiceStoreRequest.schema.json | 10 ---- .../internal/service/attestation.go | 24 +++----- .../pkg/biz/mocks/WorkflowRunRepo.go | 27 ++++----- app/controlplane/pkg/biz/workflowrun.go | 31 ++++------ .../pkg/biz/workflowrun_integration_test.go | 57 ++++++++----------- app/controlplane/pkg/data/workflowrun.go | 17 ++---- pkg/attestation/attestations.go | 24 +++----- 13 files changed, 78 insertions(+), 244 deletions(-) diff --git a/app/cli/pkg/action/attestation_push.go b/app/cli/pkg/action/attestation_push.go index cb6845596..07aab5d66 100644 --- a/app/cli/pkg/action/attestation_push.go +++ b/app/cli/pkg/action/attestation_push.go @@ -263,7 +263,7 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru workflow := crafter.CraftingState.Attestation.GetWorkflow() - attestationResult.Digest, err = pushToControlPlane(ctx, action.CPConnection, envelope, bundle, workflow.GetWorkflowRunId(), workflow.GetVersion().GetMarkAsReleased()) + attestationResult.Digest, err = pushToControlPlane(ctx, action.CPConnection, bundle, workflow.GetWorkflowRunId(), workflow.GetVersion().GetMarkAsReleased()) if err != nil { return nil, fmt.Errorf("pushing to control plane: %w", err) } @@ -303,32 +303,17 @@ func (action *AttestationPush) saveBundle(bundle *protobundle.Bundle) error { return nil } -func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, envelope *dsse.Envelope, bundle *protobundle.Bundle, workflowRunID string, markVersionAsReleased bool) (string, error) { - encodedBundle, err := encodeBundle(bundle) - if err != nil { - return "", fmt.Errorf("encoding attestation: %w", err) - } - - client := pb.NewAttestationServiceClient(conn) - - // if endpoint doesn't accept the bundle, we still send the plain attestation for backwards compatibility - encodedAttestation, err := encodeEnvelope(envelope) - if err != nil { - return "", fmt.Errorf("encoding attestation: %w", err) - } - +func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, bundle *protobundle.Bundle, workflowRunID string, markVersionAsReleased bool) (string, error) { // remove additional base64 encoding in signature. See https://github.com/chainloop-dev/chainloop/issues/1832 attestation.FixSignatureInBundle(bundle) - encodedFixedBundle, err := encodeBundle(bundle) + encodedBundle, err := encodeBundle(bundle) if err != nil { return "", fmt.Errorf("encoding attestation: %w", err) } - // Store bundle next versions will perform this in a single call) + client := pb.NewAttestationServiceClient(conn) resp, err := client.Store(ctx, &pb.AttestationServiceStoreRequest{ - Attestation: encodedAttestation, - Bundle: encodedBundle, - AttestationBundle: encodedFixedBundle, + AttestationBundle: encodedBundle, WorkflowRunId: workflowRunID, MarkVersionAsReleased: &markVersionAsReleased, }) @@ -339,10 +324,6 @@ func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, envelope *ds return resp.Result.Digest, nil } -func encodeEnvelope(e *dsse.Envelope) ([]byte, error) { - return json.Marshal(e) -} - func encodeBundle(b *protobundle.Bundle) ([]byte, error) { return protojson.Marshal(b) } diff --git a/app/controlplane/api/controlplane/v1/workflow_run.pb.go b/app/controlplane/api/controlplane/v1/workflow_run.pb.go index bc1a9f373..86e76a8fa 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run.pb.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. @@ -739,14 +739,6 @@ func (x *AttestationServiceInitResponse) GetResult() *AttestationServiceInitResp type AttestationServiceStoreRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // encoded DSEE envelope - // - // Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. - Attestation []byte `protobuf:"bytes,1,opt,name=attestation,proto3" json:"attestation,omitempty"` - // deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - // - // Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. - Bundle []byte `protobuf:"bytes,4,opt,name=bundle,proto3" json:"bundle,omitempty"` // encoded Sigstore attestation bundle AttestationBundle []byte `protobuf:"bytes,5,opt,name=attestation_bundle,json=attestationBundle,proto3" json:"attestation_bundle,omitempty"` WorkflowRunId string `protobuf:"bytes,2,opt,name=workflow_run_id,json=workflowRunId,proto3" json:"workflow_run_id,omitempty"` @@ -786,22 +778,6 @@ func (*AttestationServiceStoreRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_workflow_run_proto_rawDescGZIP(), []int{11} } -// Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. -func (x *AttestationServiceStoreRequest) GetAttestation() []byte { - if x != nil { - return x.Attestation - } - return nil -} - -// Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. -func (x *AttestationServiceStoreRequest) GetBundle() []byte { - if x != nil { - return x.Bundle - } - return nil -} - func (x *AttestationServiceStoreRequest) GetAttestationBundle() []byte { if x != nil { return x.AttestationBundle @@ -1817,14 +1793,12 @@ const file_controlplane_v1_workflow_run_proto_rawDesc = "" + "\x0eSigningOptions\x126\n" + "\x17timestamp_authority_url\x18\x01 \x01(\tR\x15timestampAuthorityUrl\x12\x1d\n" + "\n" + - "signing_ca\x18\x02 \x01(\tR\tsigningCa\"\x9d\x02\n" + - "\x1eAttestationServiceStoreRequest\x12$\n" + - "\vattestation\x18\x01 \x01(\fB\x02\x18\x01R\vattestation\x12\x1a\n" + - "\x06bundle\x18\x04 \x01(\fB\x02\x18\x01R\x06bundle\x12-\n" + + "signing_ca\x18\x02 \x01(\tR\tsigningCa\"\xfc\x01\n" + + "\x1eAttestationServiceStoreRequest\x12-\n" + "\x12attestation_bundle\x18\x05 \x01(\fR\x11attestationBundle\x12/\n" + "\x0fworkflow_run_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\rworkflowRunId\x12<\n" + "\x18mark_version_as_released\x18\x03 \x01(\bH\x00R\x15markVersionAsReleased\x88\x01\x01B\x1b\n" + - "\x19_mark_version_as_released\"\x94\x01\n" + + "\x19_mark_version_as_releasedJ\x04\b\x01\x10\x02J\x04\b\x04\x10\x05R\vattestationR\x06bundle\"\x94\x01\n" + "\x1fAttestationServiceStoreResponse\x12O\n" + "\x06result\x18\x01 \x01(\v27.controlplane.v1.AttestationServiceStoreResponse.ResultR\x06result\x1a \n" + "\x06Result\x12\x16\n" + diff --git a/app/controlplane/api/controlplane/v1/workflow_run.proto b/app/controlplane/api/controlplane/v1/workflow_run.proto index 76aba91aa..48cabc315 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.proto +++ b/app/controlplane/api/controlplane/v1/workflow_run.proto @@ -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. @@ -157,16 +157,15 @@ message AttestationServiceInitResponse { } message AttestationServiceStoreRequest { - // encoded DSEE envelope - bytes attestation = 1 [deprecated = true]; - // deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - bytes bundle = 4 [deprecated = true]; // encoded Sigstore attestation bundle bytes attestation_bundle = 5; string workflow_run_id = 2 [(buf.validate.field).string = {min_len: 1}]; // mark the associated version as released optional bool mark_version_as_released = 3; + + reserved 1, 4; + reserved "attestation", "bundle"; } message AttestationServiceStoreResponse { diff --git a/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go b/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go index e3618133f..216637d69 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.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. 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 e93832f03..eaa841007 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts @@ -134,18 +134,6 @@ export interface AttestationServiceInitResponse_SigningOptions { } export interface AttestationServiceStoreRequest { - /** - * encoded DSEE envelope - * - * @deprecated - */ - attestation: Uint8Array; - /** - * deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - * - * @deprecated - */ - bundle: Uint8Array; /** encoded Sigstore attestation bundle */ attestationBundle: Uint8Array; workflowRunId: string; @@ -1537,23 +1525,11 @@ export const AttestationServiceInitResponse_SigningOptions = { }; function createBaseAttestationServiceStoreRequest(): AttestationServiceStoreRequest { - return { - attestation: new Uint8Array(0), - bundle: new Uint8Array(0), - attestationBundle: new Uint8Array(0), - workflowRunId: "", - markVersionAsReleased: undefined, - }; + return { attestationBundle: new Uint8Array(0), workflowRunId: "", markVersionAsReleased: undefined }; } export const AttestationServiceStoreRequest = { encode(message: AttestationServiceStoreRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.attestation.length !== 0) { - writer.uint32(10).bytes(message.attestation); - } - if (message.bundle.length !== 0) { - writer.uint32(34).bytes(message.bundle); - } if (message.attestationBundle.length !== 0) { writer.uint32(42).bytes(message.attestationBundle); } @@ -1573,20 +1549,6 @@ export const AttestationServiceStoreRequest = { while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.attestation = reader.bytes(); - continue; - case 4: - if (tag !== 34) { - break; - } - - message.bundle = reader.bytes(); - continue; case 5: if (tag !== 42) { break; @@ -1619,8 +1581,6 @@ export const AttestationServiceStoreRequest = { fromJSON(object: any): AttestationServiceStoreRequest { return { - attestation: isSet(object.attestation) ? bytesFromBase64(object.attestation) : new Uint8Array(0), - bundle: isSet(object.bundle) ? bytesFromBase64(object.bundle) : new Uint8Array(0), attestationBundle: isSet(object.attestationBundle) ? bytesFromBase64(object.attestationBundle) : new Uint8Array(0), @@ -1631,10 +1591,6 @@ export const AttestationServiceStoreRequest = { toJSON(message: AttestationServiceStoreRequest): unknown { const obj: any = {}; - message.attestation !== undefined && - (obj.attestation = base64FromBytes(message.attestation !== undefined ? message.attestation : new Uint8Array(0))); - message.bundle !== undefined && - (obj.bundle = base64FromBytes(message.bundle !== undefined ? message.bundle : new Uint8Array(0))); message.attestationBundle !== undefined && (obj.attestationBundle = base64FromBytes( message.attestationBundle !== undefined ? message.attestationBundle : new Uint8Array(0), @@ -1652,8 +1608,6 @@ export const AttestationServiceStoreRequest = { object: I, ): AttestationServiceStoreRequest { const message = createBaseAttestationServiceStoreRequest(); - message.attestation = object.attestation ?? new Uint8Array(0); - message.bundle = object.bundle ?? new Uint8Array(0); message.attestationBundle = object.attestationBundle ?? new Uint8Array(0); message.workflowRunId = object.workflowRunId ?? ""; message.markVersionAsReleased = object.markVersionAsReleased ?? undefined; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json index 737e89d06..1a026e49f 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json @@ -18,21 +18,11 @@ } }, "properties": { - "attestation": { - "description": "encoded DSEE envelope", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "attestationBundle": { "description": "encoded Sigstore attestation bundle", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "type": "string" }, - "bundle": { - "description": "deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "markVersionAsReleased": { "description": "mark the associated version as released", "type": "boolean" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json index 44574d5da..12abf0571 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json @@ -18,21 +18,11 @@ } }, "properties": { - "attestation": { - "description": "encoded DSEE envelope", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "attestation_bundle": { "description": "encoded Sigstore attestation bundle", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "type": "string" }, - "bundle": { - "description": "deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "mark_version_as_released": { "description": "mark the associated version as released", "type": "boolean" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 35e49ec00..9feeca93c 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -245,11 +245,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe bundle := req.GetAttestationBundle() if bundle == nil { - bundle = req.GetBundle() - } - - if req.GetAttestation() == nil && bundle == nil { - return nil, errors.BadRequest("input required", "DSSE envelope or attestation bundle is required") + return nil, errors.BadRequest("input required", "attestation bundle is required") } // This will make sure the provided workflowRunID belongs to the org encoded in the robot account @@ -274,7 +270,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe return nil, errors.NotFound("not found", "workflow run has no CAS backend") } - digest, err := s.storeAttestation(ctx, req.GetAttestation(), bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased) + digest, err := s.storeAttestation(ctx, bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -284,19 +280,19 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe }, nil } -// Stores and process a DSSE Envelope with a Chainloop attestation -func (s *AttestationService) storeAttestation(ctx context.Context, envelope []byte, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) { +// storeAttestation stores and processes a Sigstore attestation bundle. +func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) { workflowRunID := wfRun.ID.String() casBackend := wfRun.CASBackends[0] // extract structured envelope for integrations - dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope) + dsseEnv, err := attestation.DSSEEnvelopeFromBundleBytes(bundle) if err != nil { return nil, handleUseCaseErr(err, s.log) } // Store the attestation - digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, envelope, bundle) + digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, bundle) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -315,15 +311,9 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope []by b.MaxElapsedTime = 1 * time.Minute err := backoff.Retry( func() error { - rawContent := bundle - if rawContent == nil { - rawContent = envelope - } - // reset context ctx := context.Background() - var err error - if err = s.attestationUseCase.UploadAttestationToCAS(ctx, rawContent, casBackend, workflowRunID, *digest); err != nil { + if err := s.attestationUseCase.UploadAttestationToCAS(ctx, bundle, casBackend, workflowRunID, *digest); err != nil { return err } diff --git a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go index e74004195..be01a87ac 100644 --- a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go +++ b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go @@ -11,7 +11,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/google/uuid" - "github.com/secure-systems-lab/go-securesystemslib/dsse" mock "github.com/stretchr/testify/mock" ) @@ -675,16 +674,16 @@ func (_c *WorkflowRunRepo_MarkAsFinished_Call) RunAndReturn(run func(ctx context } // SaveAttestation provides a mock function for the type WorkflowRunRepo -func (_mock *WorkflowRunRepo) SaveAttestation(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error { - ret := _mock.Called(ctx, ID, att, digest) +func (_mock *WorkflowRunRepo) SaveAttestation(ctx context.Context, ID uuid.UUID, digest string) error { + ret := _mock.Called(ctx, ID, digest) if len(ret) == 0 { panic("no return value specified for SaveAttestation") } var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *dsse.Envelope, string) error); ok { - r0 = returnFunc(ctx, ID, att, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, string) error); ok { + r0 = returnFunc(ctx, ID, digest) } else { r0 = ret.Error(0) } @@ -699,13 +698,12 @@ type WorkflowRunRepo_SaveAttestation_Call struct { // SaveAttestation is a helper method to define mock.On call // - ctx context.Context // - ID uuid.UUID -// - att *dsse.Envelope // - digest string -func (_e *WorkflowRunRepo_Expecter) SaveAttestation(ctx interface{}, ID interface{}, att interface{}, digest interface{}) *WorkflowRunRepo_SaveAttestation_Call { - return &WorkflowRunRepo_SaveAttestation_Call{Call: _e.mock.On("SaveAttestation", ctx, ID, att, digest)} +func (_e *WorkflowRunRepo_Expecter) SaveAttestation(ctx interface{}, ID interface{}, digest interface{}) *WorkflowRunRepo_SaveAttestation_Call { + return &WorkflowRunRepo_SaveAttestation_Call{Call: _e.mock.On("SaveAttestation", ctx, ID, digest)} } -func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string)) *WorkflowRunRepo_SaveAttestation_Call { +func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context, ID uuid.UUID, digest string)) *WorkflowRunRepo_SaveAttestation_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -715,19 +713,14 @@ func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context if args[1] != nil { arg1 = args[1].(uuid.UUID) } - var arg2 *dsse.Envelope + var arg2 string if args[2] != nil { - arg2 = args[2].(*dsse.Envelope) - } - var arg3 string - if args[3] != nil { - arg3 = args[3].(string) + arg2 = args[2].(string) } run( arg0, arg1, arg2, - arg3, ) }) return _c @@ -738,7 +731,7 @@ func (_c *WorkflowRunRepo_SaveAttestation_Call) Return(err error) *WorkflowRunRe return _c } -func (_c *WorkflowRunRepo_SaveAttestation_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error) *WorkflowRunRepo_SaveAttestation_Call { +func (_c *WorkflowRunRepo_SaveAttestation_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, digest string) error) *WorkflowRunRepo_SaveAttestation_Call { _c.Call.Return(run) return _c } diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 5947a3578..13131f060 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -93,7 +93,7 @@ type WorkflowRunRepo interface { FindByAttestationDigest(ctx context.Context, digest string) (*WorkflowRun, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*WorkflowRun, error) MarkAsFinished(ctx context.Context, ID uuid.UUID, status WorkflowRunStatus, reason string) error - SaveAttestation(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error + SaveAttestation(ctx context.Context, ID uuid.UUID, digest string) error SaveBundle(ctx context.Context, ID uuid.UUID, bundle []byte) error GetBundle(ctx context.Context, wrID uuid.UUID) ([]byte, error) UpdatePolicyViolationsStatus(ctx context.Context, ID uuid.UUID, hasPolicyViolations bool) error @@ -326,25 +326,20 @@ func (uc *WorkflowRunUseCase) MarkAsFinished(ctx context.Context, id string, sta return uc.wfRunRepo.MarkAsFinished(ctx, runID, status, reason) } -func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, envelope, bundle []byte) (*v1.Hash, error) { +func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, bundle []byte) (*v1.Hash, error) { runID, err := uuid.Parse(id) if err != nil { return nil, NewErrInvalidUUID(err) } - rawContent := bundle - if rawContent == nil { - rawContent = envelope - } - // calculate the content digest // Todo: this should be calculated in the use case - digest, _, err := v1.SHA256(bytes.NewReader(rawContent)) + digest, _, err := v1.SHA256(bytes.NewReader(bundle)) if err != nil { return nil, fmt.Errorf("failed to calculate SHA256 of attestation: %w", err) } - dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope) + dsseEnv, err := attestation.DSSEEnvelopeFromBundleBytes(bundle) if err != nil { return nil, fmt.Errorf("extracting DSSE envelope: %w", err) } @@ -356,7 +351,7 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } // verify attestation (only if chainloop is the signer) - validation, err := uc.verifyBundle(ctx, rawContent) + validation, err := uc.verifyBundle(ctx, bundle) if err != nil { if !errors.Is(err, verifier.ErrInvalidBundle) { return nil, err @@ -384,18 +379,14 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } } - // Save bundle if provided, as it might come as an empty struct - if bundle != nil { - // Save bundle - if err = uc.wfRunRepo.SaveBundle(ctx, runID, bundle); err != nil { - return nil, fmt.Errorf("saving bundle: %w", err) - } - // do not store the envelope if bundle is already stored - dsseEnv = nil + // Update the workflow run first so a missing runID surfaces as NotFound before the bundle insert, + // which would otherwise fail with a foreign-key violation. + if err := uc.wfRunRepo.SaveAttestation(ctx, runID, digest.String()); err != nil { + return nil, fmt.Errorf("saving attestation: %w", err) } - if err := uc.wfRunRepo.SaveAttestation(ctx, runID, dsseEnv, digest.String()); err != nil { - return nil, fmt.Errorf("saving attestation: %w", err) + if err = uc.wfRunRepo.SaveBundle(ctx, runID, bundle); err != nil { + return nil, fmt.Errorf("saving bundle: %w", err) } // Extract and save policy violations status from the predicate diff --git a/app/controlplane/pkg/biz/workflowrun_integration_test.go b/app/controlplane/pkg/biz/workflowrun_integration_test.go index e3880b47a..9900669c5 100644 --- a/app/controlplane/pkg/biz/workflowrun_integration_test.go +++ b/app/controlplane/pkg/biz/workflowrun_integration_test.go @@ -134,44 +134,23 @@ func (s *workflowRunIntegrationTestSuite) TestSaveAttestation() { assert := assert.New(s.T()) ctx := context.Background() - validEnvelope, envelopeBytes := testEnvelope(s.T(), "testdata/attestations/full.json") - h, _, err := v2.SHA256(bytes.NewReader(envelopeBytes)) + bundle, bundleBytes := testBundle(s.T(), "testdata/attestations/bundle.json") + bundleHash, _, err := v2.SHA256(bytes.NewReader(bundleBytes)) require.NoError(s.T(), err) - s.T().Run("non existing workflowRun", func(t *testing.T) { - _, err := s.WorkflowRun.SaveAttestation(ctx, uuid.NewString(), envelopeBytes, nil) + s.T().Run("non existing workflowRun", func(_ *testing.T) { + _, err := s.WorkflowRun.SaveAttestation(ctx, uuid.NewString(), bundleBytes) assert.Error(err) assert.True(biz.IsNotFound(err)) }) - s.T().Run("valid workflowRun with envelope", func(_ *testing.T) { - run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ - WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, - }) - assert.NoError(err) - - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, nil) - assert.NoError(err) - assert.Equal(h, *d) - - // Retrieve attestation ref from storage and compare - r, err := s.WorkflowRun.GetByIDInOrgOrPublic(ctx, s.org.ID, run.ID.String()) - assert.NoError(err) - assert.Equal(h.String(), r.Attestation.Digest) - assert.Equal(&biz.Attestation{Envelope: validEnvelope, Digest: h.String()}, r.Attestation) - }) - - bundle, bundleBytes := testBundle(s.T(), "testdata/attestations/bundle.json") - bundleHash, _, err := v2.SHA256(bytes.NewReader(bundleBytes)) - require.NoError(s.T(), err) - s.T().Run("succeeded workflowRun with bundle", func(_ *testing.T) { run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, }) assert.NoError(err) - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, bundleBytes) + d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), bundleBytes) assert.NoError(err) assert.Equal(bundleHash, *d) @@ -192,7 +171,7 @@ func (s *workflowRunIntegrationTestSuite) TestSaveAttestation() { }) assert.NoError(err) - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, bundleBytes) + d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), bundleBytes) assert.NoError(err) assert.Equal(bundleHash, *d) exists, err := s.Data.DB.Attestation.Query().Where(attestation2.WorkflowrunID(run.ID)).Exist(ctx) @@ -489,6 +468,18 @@ func testBundle(t *testing.T, path string) (*v1.Bundle, []byte) { return &bundle, bundleJSON } +// testBundleBytesFromEnvelope wraps a DSSE envelope file into a Sigstore bundle and returns its protojson bytes. +func testBundleBytesFromEnvelope(t *testing.T, path string) []byte { + _, envBytes := testEnvelope(t, path) + var env dsse.Envelope + require.NoError(t, json.Unmarshal(envBytes, &env)) + b, err := attestation.BundleFromDSSEEnvelope(&env) + require.NoError(t, err) + out, err := protojson.Marshal(b) + require.NoError(t, err) + return out +} + const ( version1 = "v1" version2 = "v2" @@ -528,8 +519,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version1, }) assert.NoError(err) - _, envBytes := testEnvelope(t, "testdata/attestations/full.json") - d, err := suite.WorkflowRun.SaveAttestation(ctx, s.runOrg1.ID.String(), envBytes, nil) + bundleBytes := testBundleBytesFromEnvelope(t, "testdata/attestations/full.json") + d, err := suite.WorkflowRun.SaveAttestation(ctx, s.runOrg1.ID.String(), bundleBytes) assert.NoError(err) s.digestAtt1 = d.String() @@ -539,8 +530,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version1, }) assert.NoError(err) - _, envBytes = testEnvelope(t, "testdata/attestations/empty.json") - d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2.ID.String(), envBytes, nil) + bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/empty.json") + d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2.ID.String(), bundleBytes) assert.NoError(err) s.digestAttOrg2 = d.String() @@ -550,8 +541,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version2, }) assert.NoError(err) - _, envBytes = testEnvelope(t, "testdata/attestations/with-string.json") - d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2Public.ID.String(), envBytes, nil) + bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/with-string.json") + d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2Public.ID.String(), bundleBytes) assert.NoError(err) s.digestAttPublic = d.String() diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index d39f2b6e0..d3eea996c 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -33,7 +33,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" - "github.com/secure-systems-lab/go-securesystemslib/dsse" ) type WorkflowRunRepo struct { @@ -205,18 +204,10 @@ func (r *WorkflowRunRepo) FindByIDInOrg(ctx context.Context, orgID, id uuid.UUID } // SaveAttestation Saves the attestation for a workflow run in the database -func (r *WorkflowRunRepo) SaveAttestation(ctx context.Context, id uuid.UUID, att *dsse.Envelope, digest string) error { - q := r.data.DB.WorkflowRun.UpdateOneID(id). - SetAttestationDigest(digest) - - // the envelope will come empty in normal attestations, since bundles are stored separately - // But old CLIs might still send the envelope instead of the bundle. In those cases, we store it - // as before. But this is a DEPRECATED behaviour that will be removed eventually. - if att != nil { - // Set attestation when using old CLI versions - q.SetAttestation(att) - } - run, err := q.Save(ctx) +func (r *WorkflowRunRepo) SaveAttestation(ctx context.Context, id uuid.UUID, digest string) error { + run, err := r.data.DB.WorkflowRun.UpdateOneID(id). + SetAttestationDigest(digest). + Save(ctx) if err != nil && !ent.IsNotFound(err) { return err } else if run == nil { diff --git a/pkg/attestation/attestations.go b/pkg/attestation/attestations.go index fb53ffc8f..dacad4301 100644 --- a/pkg/attestation/attestations.go +++ b/pkg/attestation/attestations.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. @@ -91,25 +91,15 @@ func BundleFromDSSEEnvelope(dsseEnvelope *dsse.Envelope) (*protobundle.Bundle, e }, nil } -func DSSEEnvelopeFromRaw(bundle, envelope []byte) (*dsse.Envelope, error) { - var dsseEnv dsse.Envelope - if bundle != nil { - var attBundle protobundle.Bundle - if err := protojson.Unmarshal(bundle, &attBundle); err != nil { - return nil, fmt.Errorf("unmarshalling bundle: %w", err) - } - dsseEnv = *DSSEEnvelopeFromBundle(&attBundle) - } else { - if err := json.Unmarshal(envelope, &dsseEnv); err != nil { - return nil, fmt.Errorf("unmarshalling envelope: %w", err) - } +// DSSEEnvelopeFromBundleBytes extracts a DSSE envelope from the protojson-encoded bytes of a Sigstore bundle. +func DSSEEnvelopeFromBundleBytes(bundle []byte) (*dsse.Envelope, error) { + var attBundle protobundle.Bundle + if err := protojson.Unmarshal(bundle, &attBundle); err != nil { + return nil, fmt.Errorf("unmarshalling bundle: %w", err) } - return &dsseEnv, nil + return DSSEEnvelopeFromBundle(&attBundle), nil } -// TODO: remove this fix once `AttestationServiceStoreRequest.Bundle` is fully removed, -// and move the logic to BundleFromDSSEEnvelope method instead (where the bug is originated) - // FixSignatureInBundle removes any additional base64 encoding from the signature in the bundle. // Old attestations have signatures base64 encoded twice, see https://github.com/chainloop-dev/chainloop/issues/1832 func FixSignatureInBundle(bundle *protobundle.Bundle) { From 84bf80c7a13efc5aa043c98408bf7b681cf29cb2 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 18:37:41 +0200 Subject: [PATCH 2/6] refactor(api): allow deleting reserved attestation/bundle fields Ignore the `FIELD_NO_DELETE` breaking-change rule for `workflow_run.proto`. The deprecated `attestation` and `bundle` fields are removed in this change; their tag numbers and names are reserved in the proto to prevent accidental reuse. Signed-off-by: Miguel Martinez Trivino --- buf.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/buf.yaml b/buf.yaml index 1375c638d..6ac0cff6d 100644 --- a/buf.yaml +++ b/buf.yaml @@ -52,6 +52,11 @@ modules: except: - EXTENSION_NO_DELETE - FIELD_SAME_DEFAULT + ignore_only: + # Deprecated `attestation` and `bundle` fields were removed in favor of `attestation_bundle`; + # their tag numbers and names are `reserved` in the proto to prevent accidental reuse. + FIELD_NO_DELETE: + - app/controlplane/api/controlplane/v1/workflow_run.proto - path: app/controlplane/internal/conf lint: use: From a87928bb0b19d9d0a567f90680a1df9fed600322 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 23:16:57 +0200 Subject: [PATCH 3/6] fix(controlplane): guard against bundles without DSSE signatures Validate that the incoming bundle carries a DSSE envelope with at least one signature before extracting it, so malformed-but-valid-JSON bundles return a typed error instead of panicking on out-of-range access inside DSSEEnvelopeFromBundle. Signed-off-by: Miguel Martinez Trivino --- .claude/scheduled_tasks.lock | 1 + pkg/attestation/attestations.go | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 000000000..cf59b2946 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"1f4e5cbe-487f-4fc6-97dd-c1a681f1294b","pid":1224094,"acquiredAt":1776443905793} \ No newline at end of file diff --git a/pkg/attestation/attestations.go b/pkg/attestation/attestations.go index dacad4301..2a03fd3ef 100644 --- a/pkg/attestation/attestations.go +++ b/pkg/attestation/attestations.go @@ -92,11 +92,16 @@ func BundleFromDSSEEnvelope(dsseEnvelope *dsse.Envelope) (*protobundle.Bundle, e } // DSSEEnvelopeFromBundleBytes extracts a DSSE envelope from the protojson-encoded bytes of a Sigstore bundle. +// It validates that the bundle carries a DSSE envelope with at least one signature, since callers +// (and DSSEEnvelopeFromBundle) assume that invariant. func DSSEEnvelopeFromBundleBytes(bundle []byte) (*dsse.Envelope, error) { var attBundle protobundle.Bundle if err := protojson.Unmarshal(bundle, &attBundle); err != nil { return nil, fmt.Errorf("unmarshalling bundle: %w", err) } + if len(attBundle.GetDsseEnvelope().GetSignatures()) == 0 { + return nil, fmt.Errorf("invalid attestation bundle: missing DSSE signature") + } return DSSEEnvelopeFromBundle(&attBundle), nil } From 9a30da7ad61f44d7fbc7e391203f5bd5ac7d82e2 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 23:17:17 +0200 Subject: [PATCH 4/6] chore: stop tracking .claude/scheduled_tasks.lock Signed-off-by: Miguel Martinez Trivino --- .claude/scheduled_tasks.lock | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index cf59b2946..000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"1f4e5cbe-487f-4fc6-97dd-c1a681f1294b","pid":1224094,"acquiredAt":1776443905793} \ No newline at end of file From a277c386734e0817d344f52938070a3d005439d4 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 23:23:26 +0200 Subject: [PATCH 5/6] refactor(controlplane): persist attestation digest and bundle in a single transaction Collapse the separate `SaveAttestation` and `SaveBundle` repo calls into a single `SaveAttestationBundle` that updates the workflow run and inserts the bundle row inside one ent transaction. The previous two-call sequence could leave the workflow run with an attestation digest set but no bundle row (or vice versa) if the second call failed. Signed-off-by: Miguel Martinez Trivino --- .../pkg/biz/mocks/WorkflowRunRepo.go | 95 ++++--------------- app/controlplane/pkg/biz/workflowrun.go | 13 +-- app/controlplane/pkg/data/workflowrun.go | 45 ++++----- 3 files changed, 45 insertions(+), 108 deletions(-) diff --git a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go index be01a87ac..7ca0b57da 100644 --- a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go +++ b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go @@ -673,37 +673,38 @@ func (_c *WorkflowRunRepo_MarkAsFinished_Call) RunAndReturn(run func(ctx context return _c } -// SaveAttestation provides a mock function for the type WorkflowRunRepo -func (_mock *WorkflowRunRepo) SaveAttestation(ctx context.Context, ID uuid.UUID, digest string) error { - ret := _mock.Called(ctx, ID, digest) +// SaveAttestationBundle provides a mock function for the type WorkflowRunRepo +func (_mock *WorkflowRunRepo) SaveAttestationBundle(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error { + ret := _mock.Called(ctx, ID, digest, bundle) if len(ret) == 0 { - panic("no return value specified for SaveAttestation") + panic("no return value specified for SaveAttestationBundle") } var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, string) error); ok { - r0 = returnFunc(ctx, ID, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, string, []byte) error); ok { + r0 = returnFunc(ctx, ID, digest, bundle) } else { r0 = ret.Error(0) } return r0 } -// WorkflowRunRepo_SaveAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveAttestation' -type WorkflowRunRepo_SaveAttestation_Call struct { +// WorkflowRunRepo_SaveAttestationBundle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveAttestationBundle' +type WorkflowRunRepo_SaveAttestationBundle_Call struct { *mock.Call } -// SaveAttestation is a helper method to define mock.On call +// SaveAttestationBundle is a helper method to define mock.On call // - ctx context.Context // - ID uuid.UUID // - digest string -func (_e *WorkflowRunRepo_Expecter) SaveAttestation(ctx interface{}, ID interface{}, digest interface{}) *WorkflowRunRepo_SaveAttestation_Call { - return &WorkflowRunRepo_SaveAttestation_Call{Call: _e.mock.On("SaveAttestation", ctx, ID, digest)} +// - bundle []byte +func (_e *WorkflowRunRepo_Expecter) SaveAttestationBundle(ctx interface{}, ID interface{}, digest interface{}, bundle interface{}) *WorkflowRunRepo_SaveAttestationBundle_Call { + return &WorkflowRunRepo_SaveAttestationBundle_Call{Call: _e.mock.On("SaveAttestationBundle", ctx, ID, digest, bundle)} } -func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context, ID uuid.UUID, digest string)) *WorkflowRunRepo_SaveAttestation_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) Run(run func(ctx context.Context, ID uuid.UUID, digest string, bundle []byte)) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -717,84 +718,26 @@ func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context if args[2] != nil { arg2 = args[2].(string) } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *WorkflowRunRepo_SaveAttestation_Call) Return(err error) *WorkflowRunRepo_SaveAttestation_Call { - _c.Call.Return(err) - return _c -} - -func (_c *WorkflowRunRepo_SaveAttestation_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, digest string) error) *WorkflowRunRepo_SaveAttestation_Call { - _c.Call.Return(run) - return _c -} - -// SaveBundle provides a mock function for the type WorkflowRunRepo -func (_mock *WorkflowRunRepo) SaveBundle(ctx context.Context, ID uuid.UUID, bundle []byte) error { - ret := _mock.Called(ctx, ID, bundle) - - if len(ret) == 0 { - panic("no return value specified for SaveBundle") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, []byte) error); ok { - r0 = returnFunc(ctx, ID, bundle) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// WorkflowRunRepo_SaveBundle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBundle' -type WorkflowRunRepo_SaveBundle_Call struct { - *mock.Call -} - -// SaveBundle is a helper method to define mock.On call -// - ctx context.Context -// - ID uuid.UUID -// - bundle []byte -func (_e *WorkflowRunRepo_Expecter) SaveBundle(ctx interface{}, ID interface{}, bundle interface{}) *WorkflowRunRepo_SaveBundle_Call { - return &WorkflowRunRepo_SaveBundle_Call{Call: _e.mock.On("SaveBundle", ctx, ID, bundle)} -} - -func (_c *WorkflowRunRepo_SaveBundle_Call) Run(run func(ctx context.Context, ID uuid.UUID, bundle []byte)) *WorkflowRunRepo_SaveBundle_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 uuid.UUID - if args[1] != nil { - arg1 = args[1].(uuid.UUID) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) + var arg3 []byte + if args[3] != nil { + arg3 = args[3].([]byte) } run( arg0, arg1, arg2, + arg3, ) }) return _c } -func (_c *WorkflowRunRepo_SaveBundle_Call) Return(err error) *WorkflowRunRepo_SaveBundle_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) Return(err error) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Return(err) return _c } -func (_c *WorkflowRunRepo_SaveBundle_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, bundle []byte) error) *WorkflowRunRepo_SaveBundle_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Return(run) return _c } diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 13131f060..8eab3c953 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -93,8 +93,7 @@ type WorkflowRunRepo interface { FindByAttestationDigest(ctx context.Context, digest string) (*WorkflowRun, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*WorkflowRun, error) MarkAsFinished(ctx context.Context, ID uuid.UUID, status WorkflowRunStatus, reason string) error - SaveAttestation(ctx context.Context, ID uuid.UUID, digest string) error - SaveBundle(ctx context.Context, ID uuid.UUID, bundle []byte) error + SaveAttestationBundle(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error GetBundle(ctx context.Context, wrID uuid.UUID) ([]byte, error) UpdatePolicyViolationsStatus(ctx context.Context, ID uuid.UUID, hasPolicyViolations bool) error List(ctx context.Context, orgID uuid.UUID, f *RunListFilters, p *pagination.CursorOptions) ([]*WorkflowRun, string, error) @@ -379,14 +378,8 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, bu } } - // Update the workflow run first so a missing runID surfaces as NotFound before the bundle insert, - // which would otherwise fail with a foreign-key violation. - if err := uc.wfRunRepo.SaveAttestation(ctx, runID, digest.String()); err != nil { - return nil, fmt.Errorf("saving attestation: %w", err) - } - - if err = uc.wfRunRepo.SaveBundle(ctx, runID, bundle); err != nil { - return nil, fmt.Errorf("saving bundle: %w", err) + if err := uc.wfRunRepo.SaveAttestationBundle(ctx, runID, digest.String(), bundle); err != nil { + return nil, fmt.Errorf("saving attestation bundle: %w", err) } // Extract and save policy violations status from the predicate diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index d3eea996c..34adf1fe7 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -203,29 +203,30 @@ func (r *WorkflowRunRepo) FindByIDInOrg(ctx context.Context, orgID, id uuid.UUID return entWrToBizWr(ctx, run) } -// SaveAttestation Saves the attestation for a workflow run in the database -func (r *WorkflowRunRepo) SaveAttestation(ctx context.Context, id uuid.UUID, digest string) error { - run, err := r.data.DB.WorkflowRun.UpdateOneID(id). - SetAttestationDigest(digest). - Save(ctx) - if err != nil && !ent.IsNotFound(err) { - return err - } else if run == nil { - return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) - } - - return nil -} - -// SaveBundle Save the bundle for a workflow run in the database -func (r *WorkflowRunRepo) SaveBundle(ctx context.Context, wrID uuid.UUID, bundle []byte) error { - if err := r.data.DB.Attestation.Create(). - SetBundle(bundle).SetWorkflowrunID(wrID). - Exec(ctx); err != nil { - return fmt.Errorf("saving bundle: %w", err) - } +// SaveAttestationBundle persists the attestation digest on the workflow run and the bundle bytes +// in the linked attestation row within a single transaction. Missing workflow runs surface as NotFound. +func (r *WorkflowRunRepo) SaveAttestationBundle(ctx context.Context, id uuid.UUID, digest string, bundle []byte) error { + return WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { + run, err := tx.WorkflowRun.UpdateOneID(id). + SetAttestationDigest(digest). + Save(ctx) + if err != nil { + if ent.IsNotFound(err) { + return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) + } + return err + } + if run == nil { + return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) + } - return nil + if err := tx.Attestation.Create(). + SetBundle(bundle).SetWorkflowrunID(id). + Exec(ctx); err != nil { + return fmt.Errorf("saving bundle: %w", err) + } + return nil + }) } // UpdatePolicyViolationsStatus updates the policy violations status for a workflow run From 2a2743f81ca6e624d5e0534943b998137dedbb61 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 17 Apr 2026 23:26:36 +0200 Subject: [PATCH 6/6] refactor(data): drop unreachable nil branch in SaveAttestationBundle ent's UpdateOneID returns a NotFoundError (never a nil run alongside a nil error) when the row is missing, so the extra `run == nil` check was dead code carried over from the previous SaveAttestation implementation. Switch to Exec since the returned row was unused. Signed-off-by: Miguel Martinez Trivino --- app/controlplane/pkg/data/workflowrun.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 34adf1fe7..03f9de520 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -204,25 +204,17 @@ func (r *WorkflowRunRepo) FindByIDInOrg(ctx context.Context, orgID, id uuid.UUID } // SaveAttestationBundle persists the attestation digest on the workflow run and the bundle bytes -// in the linked attestation row within a single transaction. Missing workflow runs surface as NotFound. +// in the linked attestation row within a single transaction. func (r *WorkflowRunRepo) SaveAttestationBundle(ctx context.Context, id uuid.UUID, digest string, bundle []byte) error { return WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { - run, err := tx.WorkflowRun.UpdateOneID(id). - SetAttestationDigest(digest). - Save(ctx) - if err != nil { + if err := tx.WorkflowRun.UpdateOneID(id).SetAttestationDigest(digest).Exec(ctx); err != nil { if ent.IsNotFound(err) { return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) } return err } - if run == nil { - return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) - } - if err := tx.Attestation.Create(). - SetBundle(bundle).SetWorkflowrunID(id). - Exec(ctx); err != nil { + if err := tx.Attestation.Create().SetBundle(bundle).SetWorkflowrunID(id).Exec(ctx); err != nil { return fmt.Errorf("saving bundle: %w", err) } return nil