Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -41147,6 +41147,12 @@ const docTemplate = `{
"due_date": {
"type": "string"
},
"execution_stream_uuid": {
"description": "Computed field (not persisted)",
"type": "string",
"format": "uuid",
"readOnly": true
},
"failed-at": {
"type": "string"
},
Expand Down
6 changes: 6 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -41141,6 +41141,12 @@
"due_date": {
"type": "string"
},
"execution_stream_uuid": {
"description": "Computed field (not persisted)",
"type": "string",
"format": "uuid",
"readOnly": true
},
"failed-at": {
"type": "string"
},
Expand Down
5 changes: 5 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9307,6 +9307,11 @@ definitions:
$ref: '#/definitions/gorm.DeletedAt'
due_date:
type: string
execution_stream_uuid:
description: Computed field (not persisted)
format: uuid
readOnly: true
type: string
Comment thread
coderabbitai[bot] marked this conversation as resolved.
failed-at:
type: string
failure_reason:
Expand Down
24 changes: 24 additions & 0 deletions internal/service/relational/workflows/stream_uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package workflows

import (
"crypto/sha256"
"encoding/hex"
"fmt"

"github.com/google/uuid"
)

// ComputeExecutionStreamUUID derives the deterministic evidence stream UUID for a workflow
// execution. The same seed algorithm is used by EvidenceIntegration so both sides always
// agree on the UUID without a database round-trip.
func ComputeExecutionStreamUUID(definitionID, instanceID, executionID uuid.UUID) (uuid.UUID, error) {
seed := fmt.Sprintf("execution:%s:%s:%s:%s",
definitionID.String(),
instanceID.String(),
executionID.String(),
"v1",
)
hash := sha256.Sum256([]byte(seed))
h := hex.EncodeToString(hash[:16])
return uuid.Parse(h[:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32])
}
3 changes: 3 additions & 0 deletions internal/service/relational/workflows/workflow_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type WorkflowExecution struct {
// Relationships
WorkflowInstance *WorkflowInstance `gorm:"foreignKey:WorkflowInstanceID" json:"workflow_instance,omitempty"`
StepExecutions []StepExecution `gorm:"foreignKey:WorkflowExecutionID;constraint:OnDelete:CASCADE" json:"step_executions,omitempty"`

// Computed field (not persisted)
ExecutionStreamUUID *uuid.UUID `gorm:"-" json:"execution_stream_uuid,omitempty" format:"uuid" readonly:"true"`
}

// TableName specifies the table name for WorkflowExecution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ func (s *WorkflowExecutionService) GetByID(id *uuid.UUID) (*WorkflowExecution, e
if err != nil {
return nil, err
}
if execution.WorkflowInstance != nil && execution.WorkflowInstance.WorkflowDefinition != nil &&
execution.WorkflowInstance.WorkflowDefinition.ID != nil && execution.WorkflowInstanceID != nil && execution.ID != nil {
streamUUID, err := ComputeExecutionStreamUUID(
*execution.WorkflowInstance.WorkflowDefinition.ID,
*execution.WorkflowInstanceID,
*execution.ID,
)
if err != nil {
return nil, fmt.Errorf("failed to compute execution stream UUID: %w", err)
}
execution.ExecutionStreamUUID = &streamUUID
}
return &execution, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,39 @@ func TestWorkflowExecutionService_GetByID(t *testing.T) {
assert.Contains(t, err.Error(), "not found")
}

// TestWorkflowExecutionService_GetByID_PopulatesExecutionStreamUUID tests that GetByID computes
// and populates ExecutionStreamUUID so callers can link to the evidence stream without extra queries.
// BCH-1155: terminal evidence is created in a stream but there was no navigable link to it.
func TestWorkflowExecutionService_GetByID_PopulatesExecutionStreamUUID(t *testing.T) {
db := setupTestDB(t)
service := NewWorkflowExecutionService(db)

workflowDef := createTestWorkflowDefinition()
require.NoError(t, db.Create(workflowDef).Error)

instance := createTestWorkflowInstance(workflowDef.ID)
require.NoError(t, db.Create(instance).Error)

execution := createTestWorkflowExecution(instance.ID)
require.NoError(t, db.Create(execution).Error)

retrieved, err := service.GetByID(execution.ID)
require.NoError(t, err)

// ExecutionStreamUUID must equal the value produced by the canonical algorithm
// for the same definition/instance/execution IDs.
expectedUUID, err := ComputeExecutionStreamUUID(*workflowDef.ID, *instance.ID, *execution.ID)
require.NoError(t, err)
require.NotNil(t, retrieved.ExecutionStreamUUID)
assert.Equal(t, expectedUUID, *retrieved.ExecutionStreamUUID)

// Calling again must produce the same UUID (determinism check).
retrieved2, err := service.GetByID(execution.ID)
require.NoError(t, err)
require.NotNil(t, retrieved2.ExecutionStreamUUID)
assert.Equal(t, expectedUUID, *retrieved2.ExecutionStreamUUID)
}

// TestWorkflowExecutionService_GetByWorkflowInstanceID tests the GetByWorkflowInstanceID method
func TestWorkflowExecutionService_GetByWorkflowInstanceID(t *testing.T) {
db := setupTestDB(t)
Expand Down
Loading