From a3e29808a76f4214542cab313e4a151a4929c08a Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Wed, 24 Jun 2026 03:28:25 +0200 Subject: [PATCH] fix(controlplane): exclude soft-deleted rows when finding a project version by name FindByProjectAndVersion did not filter out soft-deleted project version rows. When a version name was deleted and recreated, the lookup matched both the soft-deleted and the active row, returning a "not singular" error and crashing the workflow run list when filtering by that version. Closes #3236 Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: ecd1a33f-90f2-459f-9510-a5267ef123bd Signed-off-by: Miguel Martinez Trivino --- .../biz/projectversion_integration_test.go | 29 +++++++++++++++++++ app/controlplane/pkg/data/projectversion.go | 3 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/controlplane/pkg/biz/projectversion_integration_test.go b/app/controlplane/pkg/biz/projectversion_integration_test.go index 927a95ab5..6a9fcec77 100644 --- a/app/controlplane/pkg/biz/projectversion_integration_test.go +++ b/app/controlplane/pkg/biz/projectversion_integration_test.go @@ -198,6 +198,35 @@ func (s *ProjectVersionIntegrationTestSuite) TestMarkAsLatestInvalidUUID() { require.Error(t, err) } +// Regression test for PFM-6470: a version that was deleted and recreated with the +// same name leaves a soft-deleted row alongside the active one. Looking the version +// up by name must return the active row instead of crashing because the query matched +// more than one row. +func (s *ProjectVersionIntegrationTestSuite) TestFindByProjectAndVersionIgnoresSoftDeleted() { + t := s.T() + ctx := context.Background() + + // Create a version and soft-delete it, simulating a delete from the UI/API. + deleted, err := s.ProjectVersion.Create(ctx, s.project.ID.String(), "v17", true) + require.NoError(t, err) + + _, err = s.Data.DB.ProjectVersion.UpdateOneID(deleted.ID).SetDeletedAt(time.Now()).Save(ctx) + require.NoError(t, err) + + // Recreate a version with the same name; now two rows share version "v17" + // (one soft-deleted, one active). + recreated, err := s.ProjectVersion.Create(ctx, s.project.ID.String(), "v17", true) + require.NoError(t, err) + require.NotEqual(t, deleted.ID, recreated.ID) + + // Looking up "v17" must return the active row, not error out because the query + // matched both the soft-deleted and the active row. + found, err := s.ProjectVersion.FindByProjectAndVersion(ctx, s.project.ID.String(), "v17") + require.NoError(t, err) + require.NotNil(t, found) + require.Equal(t, recreated.ID, found.ID) +} + func TestProjectVersionUseCase(t *testing.T) { suite.Run(t, new(ProjectVersionIntegrationTestSuite)) } diff --git a/app/controlplane/pkg/data/projectversion.go b/app/controlplane/pkg/data/projectversion.go index eb4908ad3..0e10ee0f5 100644 --- a/app/controlplane/pkg/data/projectversion.go +++ b/app/controlplane/pkg/data/projectversion.go @@ -21,7 +21,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/pkg/otelx" "github.com/go-kratos/kratos/v2/log" @@ -46,7 +45,7 @@ func (r *ProjectVersionRepo) FindByProjectAndVersion(ctx context.Context, projec ctx, span := otelx.Start(ctx, projectVersionRepoTracer, "ProjectVersionRepo.FindByProjectAndVersion") defer span.End() - pv, err := r.data.DB.ProjectVersion.Query().Where(projectversion.HasProjectWith(project.ID(projectID)), projectversion.VersionEQ(version)).Only(ctx) + pv, err := findProjectVersionWithClient(ctx, r.data.DB, projectID, version) if err != nil && !ent.IsNotFound(err) { return nil, err } else if pv == nil {