Skip to content

Commit ddbe9f2

Browse files
committed
fix(gitlab): pin commit statuses to same pipeline
When PAC posts multiple commit statuses for the same SHA, GitLab's auto-assignment logic can route them to different pipelines, leaving the MR pipeline permanently stuck with stale intermediate statuses. Cache the pipeline_id returned by the first SetCommitStatus response and pass it on subsequent calls for the same (project, SHA) pair so all statuses land in the same GitLab pipeline. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Abhishek Ghosh <abghosh@redhat.com>
1 parent 532790b commit ddbe9f2

3 files changed

Lines changed: 566 additions & 2 deletions

File tree

pkg/apis/pipelinesascode/keys/keys.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const (
4848
OriginalPRName = pipelinesascode.GroupName + "/original-prname"
4949
GitAuthSecret = pipelinesascode.GroupName + "/git-auth-secret"
5050
CheckRunID = pipelinesascode.GroupName + "/check-run-id"
51+
GitLabPipelineID = pipelinesascode.GroupName + "/gitlab-pipeline-id"
5152
OnEvent = pipelinesascode.GroupName + "/on-event"
5253
OnComment = pipelinesascode.GroupName + "/on-comment"
5354
OnTargetBranch = pipelinesascode.GroupName + "/on-target-branch"

pkg/provider/gitlab/gitlab.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

7481
var defaultGitlabListOptions = gitlab.ListOptions{
@@ -89,6 +96,9 @@ func (v *Provider) Client() *gitlab.Client {
8996

9097
func (v *Provider) SetGitLabClient(client *gitlab.Client) {
9198
v.gitlabClient = client
99+
if v.pipelineIDMu == nil {
100+
v.pipelineIDMu = &sync.Mutex{}
101+
}
92102
}
93103

94104
func (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

Comments
 (0)