@@ -20,6 +20,7 @@ import (
2020 "io"
2121 "time"
2222
23+ "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events"
2324 "github.com/chainloop-dev/chainloop/pkg/otelx"
2425 "github.com/chainloop-dev/chainloop/pkg/servicelogger"
2526 "github.com/go-kratos/kratos/v2/log"
@@ -51,24 +52,72 @@ type ProjectVersion struct {
5152 ProjectID uuid.UUID
5253}
5354
55+ // ProjectVersionPromotion is the outcome of promoting a project version to be
56+ // the latest one.
57+ type ProjectVersionPromotion struct {
58+ // Promoted reports whether the promotion changed which version is the
59+ // latest one. It is false when the version already was the latest.
60+ Promoted bool
61+ // Version and Project describe the promotion for auditing purposes. Both are
62+ // populated whenever the promotion succeeded, so that a committed promotion
63+ // can always be reported.
64+ Version * ProjectVersion
65+ Project * Project
66+ }
67+
68+ // dispatchProjectVersionPromoted reports that a project version became the
69+ // latest one for its project. Every path that promotes a version funnels
70+ // through here: downstream consumers reconcile off this event, so a promotion
71+ // must be as loud as a creation, otherwise anything tracking the latest version
72+ // of the project silently falls behind.
73+ //
74+ // A promotion deliberately reuses ProjectVersionUpdated rather than declaring
75+ // its own action type, and that choice is load-bearing. Subjects are published
76+ // as "audit.<target_type>.<action_type>", and consumers subscribe to a fixed
77+ // list of them, so a new action type lands on a subject nobody is listening to
78+ // and every promotion is dropped — the very failure this event exists to
79+ // prevent, but silent. If this action type ever changes, the consumers must
80+ // subscribe to the new subject and be released FIRST; MarkedAsLatest is what
81+ // lets a consumer tell a promotion from a rename in the meantime.
82+ func dispatchProjectVersionPromoted (ctx context.Context , auditorUC * AuditorUseCase , project * Project , version * ProjectVersion ) {
83+ if auditorUC == nil || project == nil || version == nil {
84+ return
85+ }
86+
87+ auditorUC .Dispatch (ctx , & events.ProjectVersionUpdated {
88+ ProjectBase : & events.ProjectBase {
89+ ProjectID : & project .ID ,
90+ ProjectName : project .Name ,
91+ },
92+ VersionID : & version .ID ,
93+ Version : version .Version ,
94+ MarkedAsLatest : true ,
95+ }, & project .OrgID )
96+ }
97+
5498type ProjectVersionRepo interface {
5599 FindByProjectAndVersion (ctx context.Context , projectID uuid.UUID , version string ) (* ProjectVersion , error )
56100 Update (ctx context.Context , versionID uuid.UUID , updates * ProjectVersionUpdateOpts ) (* ProjectVersion , error )
57101 Create (ctx context.Context , projectID uuid.UUID , version string , prerelease bool ) (* ProjectVersion , error )
58- MarkAsLatest (ctx context.Context , projectID , versionID uuid.UUID ) error
102+ MarkAsLatest (ctx context.Context , projectID , versionID uuid.UUID ) ( * ProjectVersionPromotion , error )
59103}
60104
61105type ProjectVersionUseCase struct {
62106 projectRepo ProjectVersionRepo
107+ auditorUC * AuditorUseCase
63108 logger * log.Helper
64109}
65110
66- func NewProjectVersionUseCase (repo ProjectVersionRepo , l log.Logger ) * ProjectVersionUseCase {
111+ func NewProjectVersionUseCase (repo ProjectVersionRepo , auditorUC * AuditorUseCase , l log.Logger ) * ProjectVersionUseCase {
67112 if l == nil {
68113 l = log .NewStdLogger (io .Discard )
69114 }
70115
71- return & ProjectVersionUseCase {projectRepo : repo , logger : servicelogger .ScopedHelper (l , "biz/project-version" )}
116+ return & ProjectVersionUseCase {
117+ projectRepo : repo ,
118+ auditorUC : auditorUC ,
119+ logger : servicelogger .ScopedHelper (l , "biz/project-version" ),
120+ }
72121}
73122
74123func (uc * ProjectVersionUseCase ) FindByProjectAndVersion (ctx context.Context , projectID string , version string ) (* ProjectVersion , error ) {
@@ -100,8 +149,13 @@ func (uc *ProjectVersionUseCase) UpdateReleaseStatus(ctx context.Context, versio
100149 return uc .projectRepo .Update (ctx , versionUUID , & ProjectVersionUpdateOpts {Prerelease : & preReleaseValue })
101150}
102151
103- // MarkAsLatest promotes a pre-release version to latest. The platform repo builds the
104- // "project version mark-latest" CLI command and service endpoint on top of this method.
152+ // MarkAsLatest promotes a pre-release version to latest.
153+ //
154+ // Nothing calls this today: no service in this repository uses it, and the
155+ // platform implements its own mark-latest over its own repositories rather than
156+ // this use case. It is kept as the biz-layer entry point for the operation, and
157+ // it dispatches the promotion event so that a future caller cannot reintroduce
158+ // the silent-transition gap this method used to have.
105159func (uc * ProjectVersionUseCase ) MarkAsLatest (ctx context.Context , projectID , versionID string ) error {
106160 ctx , span := otelx .Start (ctx , projectVersionTracer , "ProjectVersionUseCase.MarkAsLatest" )
107161 defer span .End ()
@@ -116,7 +170,16 @@ func (uc *ProjectVersionUseCase) MarkAsLatest(ctx context.Context, projectID, ve
116170 return NewErrInvalidUUID (err )
117171 }
118172
119- return uc .projectRepo .MarkAsLatest (ctx , projectUUID , versionUUID )
173+ promotion , err := uc .projectRepo .MarkAsLatest (ctx , projectUUID , versionUUID )
174+ if err != nil {
175+ return err
176+ }
177+
178+ if promotion .Promoted {
179+ dispatchProjectVersionPromoted (ctx , uc .auditorUC , promotion .Project , promotion .Version )
180+ }
181+
182+ return nil
120183}
121184
122185func (uc * ProjectVersionUseCase ) Create (ctx context.Context , projectID , version string , prerelease bool ) (* ProjectVersion , error ) {
0 commit comments