Skip to content

Commit a36c209

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 9fc27b5 commit a36c209

File tree

3 files changed

+357
-2
lines changed

3 files changed

+357
-2
lines changed

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: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strconv"
1313
"strings"
1414

15+
"github.com/openshift-pipelines/pipelines-as-code/pkg/action"
16+
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
1517
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1618
"github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
1719
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
@@ -354,22 +356,42 @@ func (v *Provider) CreateStatus(ctx context.Context, event *info.Event, statusOp
354356
Context: gitlab.Ptr(contextName),
355357
}
356358

359+
// Read a previously stored pipeline ID from PipelineRun annotations so
360+
// that all commit statuses land in the same GitLab pipeline instead of
361+
// GitLab potentially auto-creating a new "external" pipeline mid-stream.
362+
if statusOpts.PipelineRun != nil {
363+
if id, ok := statusOpts.PipelineRun.GetAnnotations()[keys.GitLabPipelineID]; ok {
364+
pid, err := strconv.ParseInt(id, 10, 64)
365+
if err == nil {
366+
opt.PipelineID = gitlab.Ptr(pid)
367+
}
368+
}
369+
}
370+
357371
// In case we have access, set the status. Typically, on a Merge Request (MR)
358372
// from a fork in an upstream repository, the token needs to have write access
359373
// to the fork repository in order to create a status. However, the token set on the
360374
// Repository CR usually doesn't have such broad access, preventing from creating
361375
// a status comment on it.
362376
// This would work on a push or an MR from a branch within the same repo.
363377
// Ignoring errors because of the write access issues,
364-
_, _, err := v.Client().Commits.SetCommitStatus(event.SourceProjectID, event.SHA, opt)
378+
commitStatus, _, err := v.Client().Commits.SetCommitStatus(event.SourceProjectID, event.SHA, opt)
365379
if err != nil {
366380
v.Logger.Debugf("cannot set status with the GitLab token on the source project: %v", err)
367381
} else {
382+
v.patchPipelineIDAnnotation(ctx, statusOpts, commitStatus)
368383
// we managed to set the status on the source repo, all good we are done
369384
v.Logger.Debugf("created commit status on source project ID %d", event.TargetProjectID)
370385
return nil
371386
}
372-
if _, _, err = v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt); err == nil {
387+
// Clear pipeline ID when falling back to the target project — the cached
388+
// ID belongs to the source project's pipeline namespace and is invalid on
389+
// a different project (fork MR scenario).
390+
if event.SourceProjectID != event.TargetProjectID {
391+
opt.PipelineID = nil
392+
}
393+
if commitStatus, _, err = v.Client().Commits.SetCommitStatus(event.TargetProjectID, event.SHA, opt); err == nil {
394+
v.patchPipelineIDAnnotation(ctx, statusOpts, commitStatus)
373395
v.Logger.Debugf("created commit status on target project ID %d", event.TargetProjectID)
374396
// we managed to set the status on the target repo, all good we are done
375397
return nil
@@ -860,3 +882,32 @@ func (v *Provider) formatPipelineComment(sha string, status providerstatus.Statu
860882
return fmt.Sprintf("%s **%s: %s/%s for %s**\n\n%s\n\n<small>Full log available [here](%s)</small>",
861883
emoji, status.Title, v.pacInfo.ApplicationName, status.OriginalPipelineRunName, sha, status.Text, status.DetailsURL)
862884
}
885+
886+
// patchPipelineIDAnnotation stores the GitLab pipeline ID from a successful
887+
// SetCommitStatus response as a PipelineRun annotation. This allows the
888+
// reconciler (which creates a new Provider instance) to read it back and
889+
// pass it on subsequent status updates, keeping all statuses in the same
890+
// GitLab pipeline.
891+
func (v *Provider) patchPipelineIDAnnotation(ctx context.Context, statusOpts providerstatus.StatusOpts, cs *gitlab.CommitStatus) {
892+
if cs == nil || cs.PipelineID == 0 {
893+
return
894+
}
895+
pr := statusOpts.PipelineRun
896+
if pr == nil || (pr.GetName() == "" && pr.GetGenerateName() == "") {
897+
return
898+
}
899+
// Skip if annotation is already set — avoid unnecessary patches.
900+
if _, ok := pr.GetAnnotations()[keys.GitLabPipelineID]; ok {
901+
return
902+
}
903+
mergePatch := map[string]any{
904+
"metadata": map[string]any{
905+
"annotations": map[string]string{
906+
keys.GitLabPipelineID: strconv.FormatInt(cs.PipelineID, 10),
907+
},
908+
},
909+
}
910+
if _, err := action.PatchPipelineRun(ctx, v.Logger, "gitlabPipelineID", v.run.Clients.Tekton, pr, mergePatch); err != nil {
911+
v.Logger.Debugf("failed to patch pipelinerun with gitlab pipeline ID: %v", err)
912+
}
913+
}

0 commit comments

Comments
 (0)