@@ -11,7 +11,10 @@ import (
1111 "regexp"
1212 "strconv"
1313 "strings"
14+ "sync"
1415
16+ "github.com/openshift-pipelines/pipelines-as-code/pkg/action"
17+ "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
1518 "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1619 "github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
1720 "github.com/openshift-pipelines/pipelines-as-code/pkg/events"
@@ -69,6 +72,10 @@ type Provider struct {
6972 memberCache map [int64 ]bool
7073 cachedChangedFiles * changedfiles.ChangedFiles
7174 pacUserID int64 // user login used by PAC
75+ // pipelineIDMu protects pipelineIDCache from concurrent access when
76+ // multiple PipelineRun goroutines call CreateStatus simultaneously.
77+ pipelineIDMu * sync.Mutex
78+ pipelineIDCache map [string ]int64
7279}
7380
7481var defaultGitlabListOptions = gitlab.ListOptions {
@@ -89,6 +96,9 @@ func (v *Provider) Client() *gitlab.Client {
8996
9097func (v * Provider ) SetGitLabClient (client * gitlab.Client ) {
9198 v .gitlabClient = client
99+ if v .pipelineIDMu == nil {
100+ v .pipelineIDMu = & sync.Mutex {}
101+ }
92102}
93103
94104func (v * Provider ) SetPacInfo (pacInfo * info.PacOpts ) {
@@ -220,6 +230,9 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, runevent *info.
220230 v .eventEmitter = eventsEmitter
221231 v .repo = repo
222232 v .triggerEvent = runevent .EventType
233+ if v .pipelineIDMu == nil {
234+ v .pipelineIDMu = & sync.Mutex {}
235+ }
223236
224237 // Try to detect automatically the API url if url is not coming from public
225238 // gitlab. Unless user has set a spec.provider.url in its repo crd
@@ -354,22 +367,46 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
354367 Context : gitlab .Ptr (contextName ),
355368 }
356369
370+ // Reuse a previously discovered pipeline ID so that all commit statuses
371+ // for the same SHA land in the same GitLab pipeline. Check the in-memory
372+ // cache first (shared across concurrent PipelineRun goroutines within the
373+ // same webhook event), then fall back to the PipelineRun annotation
374+ // (persisted across Provider instances for the reconciler phase).
375+ if pid := v .getPipelineID (event .SHA ); pid != 0 {
376+ opt .PipelineID = gitlab .Ptr (pid )
377+ } else if statusOpts .PipelineRun != nil {
378+ if id , ok := statusOpts .PipelineRun .GetAnnotations ()[keys .GitLabPipelineID ]; ok {
379+ pid , err := strconv .ParseInt (id , 10 , 64 )
380+ if err == nil {
381+ opt .PipelineID = gitlab .Ptr (pid )
382+ }
383+ }
384+ }
385+
357386 // In case we have access, set the status. Typically, on a Merge Request (MR)
358387 // from a fork in an upstream repository, the token needs to have write access
359388 // to the fork repository in order to create a status. However, the token set on the
360389 // Repository CR usually doesn't have such broad access, preventing from creating
361390 // a status comment on it.
362391 // This would work on a push or an MR from a branch within the same repo.
363392 // Ignoring errors because of the write access issues,
364- _ , _ , err := v .Client ().Commits .SetCommitStatus (event .SourceProjectID , event .SHA , opt )
393+ commitStatus , _ , err := v .Client ().Commits .SetCommitStatus (event .SourceProjectID , event .SHA , opt )
365394 if err != nil {
366395 v .Logger .Debugf ("cannot set status with the GitLab token on the source project: %v" , err )
367396 } else {
397+ v .storePipelineID (ctx , event .SHA , statusOpts , commitStatus )
368398 // we managed to set the status on the source repo, all good we are done
369399 v .Logger .Debugf ("created commit status on source project ID %d" , event .TargetProjectID )
370400 return nil
371401 }
372- if _ , _ , err = v .Client ().Commits .SetCommitStatus (event .TargetProjectID , event .SHA , opt ); err == nil {
402+ // Clear pipeline ID when falling back to the target project — the cached
403+ // ID belongs to the source project's pipeline namespace and is invalid on
404+ // a different project (fork MR scenario).
405+ if event .SourceProjectID != event .TargetProjectID {
406+ opt .PipelineID = nil
407+ }
408+ if commitStatus , _ , err = v .Client ().Commits .SetCommitStatus (event .TargetProjectID , event .SHA , opt ); err == nil {
409+ v .storePipelineID (ctx , event .SHA , statusOpts , commitStatus )
373410 v .Logger .Debugf ("created commit status on target project ID %d" , event .TargetProjectID )
374411 // we managed to set the status on the target repo, all good we are done
375412 return nil
@@ -860,3 +897,53 @@ func (v *Provider) formatPipelineComment(sha string, status providerstatus.Statu
860897 return fmt .Sprintf ("%s **%s: %s/%s for %s**\n \n %s\n \n <small>Full log available [here](%s)</small>" ,
861898 emoji , status .Title , v .pacInfo .ApplicationName , status .OriginalPipelineRunName , sha , status .Text , status .DetailsURL )
862899}
900+
901+ // getPipelineID returns a cached pipeline ID for the given SHA, or 0 if none.
902+ func (v * Provider ) getPipelineID (sha string ) int64 {
903+ v .pipelineIDMu .Lock ()
904+ defer v .pipelineIDMu .Unlock ()
905+ if v .pipelineIDCache == nil {
906+ return 0
907+ }
908+ return v .pipelineIDCache [sha ]
909+ }
910+
911+ // storePipelineID caches the pipeline ID from a successful SetCommitStatus
912+ // response in the in-memory map (for concurrent goroutines within the same
913+ // webhook event) and patches it onto the PipelineRun annotation (for the
914+ // reconciler which creates a new Provider instance).
915+ func (v * Provider ) storePipelineID (ctx context.Context , sha string , statusOpts providerstatus.StatusOpts , cs * gitlab.CommitStatus ) {
916+ if cs == nil || cs .PipelineID == 0 {
917+ return
918+ }
919+ v .pipelineIDMu .Lock ()
920+ if v .pipelineIDCache == nil {
921+ v .pipelineIDCache = make (map [string ]int64 )
922+ }
923+ v .pipelineIDCache [sha ] = cs .PipelineID
924+ v .pipelineIDMu .Unlock ()
925+
926+ v .patchPipelineIDAnnotation (ctx , statusOpts , cs )
927+ }
928+
929+ // patchPipelineIDAnnotation stores the GitLab pipeline ID as a PipelineRun
930+ // annotation so the reconciler can read it back across Provider instances.
931+ func (v * Provider ) patchPipelineIDAnnotation (ctx context.Context , statusOpts providerstatus.StatusOpts , cs * gitlab.CommitStatus ) {
932+ pr := statusOpts .PipelineRun
933+ if pr == nil || (pr .GetName () == "" && pr .GetGenerateName () == "" ) {
934+ return
935+ }
936+ if _ , ok := pr .GetAnnotations ()[keys .GitLabPipelineID ]; ok {
937+ return
938+ }
939+ mergePatch := map [string ]any {
940+ "metadata" : map [string ]any {
941+ "annotations" : map [string ]string {
942+ keys .GitLabPipelineID : strconv .FormatInt (cs .PipelineID , 10 ),
943+ },
944+ },
945+ }
946+ if _ , err := action .PatchPipelineRun (ctx , v .Logger , "gitlabPipelineID" , v .run .Clients .Tekton , pr , mergePatch ); err != nil {
947+ v .Logger .Debugf ("failed to patch pipelinerun with gitlab pipeline ID: %v" , err )
948+ }
949+ }
0 commit comments