Repository: kubesphere/ks-devops
Pull request #251 (2021-09-23 10:19:42)

Check existing PipelineRun with same run ID
### What this PR dose / Why we need it

- Rename PipelineRun variable to `pipelineRunCopied`
- Check existing PipelineRun with same run ID after triggering Jenkins build

### TODO

- [ ] Identity PipelineRun for multi-branch pipeline.

### Steps to test

Docker image for test: `johnniang/devops-controller:repeated-pipelinerun`
/kind bug

ks-ci-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/251#" title="Author self-approved">JohnNiang</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **linuxsuren** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @linuxsuren` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["linuxsuren"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#251](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (c902e60) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/d8ac1add462a93163b8624cf42989cf5336482bc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (d8ac1ad) will **increase** coverage by `0.04%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/251/graphs/tree.svg?width=650&height=150&src=pr&token=qSK4RBbyNg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff            @@
##           master    #251      +/-   ##
=========================================
+ Coverage    7.38%   7.42%   +0.04%     
=========================================
  Files          84      84              
  Lines       21261   21281      +20     
=========================================
+ Hits         1570    1580      +10     
- Misses      19565   19577      +12     
+ Partials      126     124       -2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `7.42% <0.00%> (+0.04%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [...lers/jenkins/pipelinerun/pipelinerun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/251/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZXJ1bi9waXBlbGluZXJ1bl9jb250cm9sbGVyLmdv) | `14.76% <0.00%> (-1.56%)` | :arrow_down: |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/251/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpcnVuL3MyaXJ1bl9jb250cm9sbGVyLmdv) | `37.22% <0.00%> (+3.64%)` | :arrow_up: |
| [controllers/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/251/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvcGlwZWxpbmUvcGlwZWxpbmVfY29udHJvbGxlci5nbw==) | `41.91% <0.00%> (+3.67%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [d8ac1ad...c902e60](https://codecov.io/gh/kubesphere/ks-devops/pull/251?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).

LinuxSuRen: @JohnNiang Please fix the Conflicting files.
JohnNiang: > @JohnNiang Please fix the Conflicting files.

Fixed, please have a look at the new change.
JohnNiang: I'm working in progress on multi-branch pipeline.
ks-ci-bot: New changes are detected. LGTM label has been removed.
ks-ci-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/251#" title="Author self-approved">JohnNiang</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **linuxsuren** after the PR has been reviewed.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["linuxsuren"]} -->

Patch:
diff --git a/.gitpod.yml b/.gitpod.yml
index 5721abf..0c0caad 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -7,6 +7,6 @@ tasks:
       [[ ! -z "${GITHUB_USER}" && ! -z "${GITHUB_TOKEN}" ]] && docker login ghcr.io/linuxsuren -u${GITHUB_USER} -p${GITHUB_TOKEN}
       git config --global user.name $GIT_AUTHOR_NAME
       git config --global user.email $GIT_COMMITTER_EMAIL
-      gh repo fork --remote
+      echo kubesphere/ks-devops | gh repo fork --remote
       make docker-build-push-controller
       make docker-build-push-apiserver
diff --git a/controllers/jenkins/pipelinerun/pipelinerun_controller.go b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
index 2aaa411..116f084 100644
--- a/controllers/jenkins/pipelinerun/pipelinerun_controller.go
+++ b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
@@ -61,44 +61,44 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	log := r.log.WithValues("PipelineRun", req.NamespacedName)
 
 	// get PipelineRun
-	var pr v1alpha3.PipelineRun
+	pipelineRun := &v1alpha3.PipelineRun{}
 	var err error
-	if err = r.Client.Get(ctx, req.NamespacedName, &pr); err != nil {
+	if err = r.Client.Get(ctx, req.NamespacedName, pipelineRun); err != nil {
 		log.Error(err, "unable to fetch PipelineRun")
 		return ctrl.Result{}, client.IgnoreNotFound(err)
 	}
 
 	// don't modify the cache in other places, like informer cache.
-	pr = *pr.DeepCopy()
+	pipelineRunCopied := pipelineRun.DeepCopy()
 
 	// DeletionTimestamp.IsZero() means copyPipeline has not been deleted.
-	if !pr.ObjectMeta.DeletionTimestamp.IsZero() {
-		if err = r.deleteJenkinsJobHistory(&pr); err != nil {
+	if !pipelineRunCopied.ObjectMeta.DeletionTimestamp.IsZero() {
+		if err = r.deleteJenkinsJobHistory(pipelineRunCopied); err != nil {
 			klog.V(4).Infof("failed to delete Jenkins job history from PipelineRun: %s/%s, error: %v",
-				pr.Namespace, pr.Name, err)
+				pipelineRunCopied.Namespace, pipelineRunCopied.Name, err)
 		} else {
-			pr.ObjectMeta.Finalizers = sliceutil.RemoveString(pr.ObjectMeta.Finalizers, func(item string) bool {
+			pipelineRunCopied.ObjectMeta.Finalizers = sliceutil.RemoveString(pipelineRunCopied.ObjectMeta.Finalizers, func(item string) bool {
 				return item == v1alpha3.PipelineRunFinalizerName
 			})
-			err = r.Update(context.TODO(), &pr)
+			err = r.Update(context.TODO(), pipelineRunCopied)
 		}
 		return ctrl.Result{}, err
 	}
 
 	// the PipelineRun cannot allow building
-	if !pr.Buildable() {
+	if !pipelineRunCopied.Buildable() {
 		return ctrl.Result{}, nil
 	}
 
 	// check PipelineRef
-	if pr.Spec.PipelineRef == nil || pr.Spec.PipelineRef.Name == "" {
+	if pipelineRunCopied.Spec.PipelineRef == nil || pipelineRunCopied.Spec.PipelineRef.Name == "" {
 		// make the PipelineRun as orphan
-		return ctrl.Result{}, r.makePipelineRunOrphan(ctx, &pr)
+		return ctrl.Result{}, r.makePipelineRunOrphan(ctx, pipelineRunCopied)
 	}
 
 	// get pipeline
-	var pipeline v1alpha3.Pipeline
-	if err := r.Client.Get(ctx, client.ObjectKey{Namespace: pr.Namespace, Name: pr.Spec.PipelineRef.Name}, &pipeline); err != nil {
+	pipeline := &v1alpha3.Pipeline{}
+	if err := r.Client.Get(ctx, client.ObjectKey{Namespace: pipelineRunCopied.Namespace, Name: pipelineRunCopied.Spec.PipelineRef.Name}, pipeline); err != nil {
 		log.Error(err, "unable to get pipeline")
 		return ctrl.Result{}, client.IgnoreNotFound(err)
 	}
@@ -107,30 +107,30 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	pipelineName := pipeline.GetName()
 
 	// set Pipeline name and SCM ref name into labels
-	if pr.Labels == nil {
-		pr.Labels = make(map[string]string)
+	if pipelineRunCopied.Labels == nil {
+		pipelineRunCopied.Labels = make(map[string]string)
 	}
-	pr.Labels[v1alpha3.PipelineNameLabelKey] = pipelineName
-	if refName, err := getSCMRefName(&pr.Spec); err == nil && refName != "" {
-		pr.Labels[v1alpha3.SCMRefNameLabelKey] = refName
+	pipelineRunCopied.Labels[v1alpha3.PipelineNameLabelKey] = pipelineName
+	if refName, err := getSCMRefName(&pipelineRunCopied.Spec); err == nil && refName != "" {
+		pipelineRunCopied.Labels[v1alpha3.SCMRefNameLabelKey] = refName
 	}
 
 	log = log.WithValues("namespace", namespaceName, "Pipeline", pipelineName)
 
 	// check PipelineRun status
-	if pr.HasStarted() {
+	if pipelineRunCopied.HasStarted() {
 		log.V(5).Info("pipeline has already started, and we are retrieving run data from Jenkins.")
-		pipelineBuild, err := r.getPipelineRunResult(namespaceName, pipelineName, &pr)
+		pipelineBuild, err := r.getPipelineRunResult(namespaceName, pipelineName, pipelineRunCopied)
 		if err != nil {
 			log.Error(err, "unable get PipelineRun data.")
-			r.recorder.Eventf(&pr, corev1.EventTypeWarning, v1alpha3.RetrieveFailed, "Failed to retrieve running data from Jenkins, and error was %s", err)
+			r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.RetrieveFailed, "Failed to retrieve running data from Jenkins, and error was %s", err)
 			return ctrl.Result{}, err
 		}
 
-		prNodes, err := r.getPipelineNodes(namespaceName, pipelineName, &pr)
+		prNodes, err := r.getPipelineNodes(namespaceName, pipelineName, pipelineRunCopied)
 		if err != nil {
 			log.Error(err, "unable to get PipelineRun nodes detail")
-			r.recorder.Eventf(&pr, corev1.EventTypeWarning, v1alpha3.RetrieveFailed, "Failed to retrieve nodes detail from Jenkins, and error was %s", err)
+			r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.RetrieveFailed, "Failed to retrieve nodes detail from Jenkins, and error was %s", err)
 			return ctrl.Result{}, err
 		}
 
@@ -143,19 +143,19 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 		if err != nil {
 			return ctrl.Result{}, err
 		}
-		if pr.Annotations == nil {
-			pr.Annotations = make(map[string]string)
+		if pipelineRunCopied.Annotations == nil {
+			pipelineRunCopied.Annotations = make(map[string]string)
 		}
-		pr.Annotations[v1alpha3.JenkinsPipelineRunStatusKey] = string(runResultJSON)
-		pr.Annotations[v1alpha3.JenkinsPipelineRunStagesStatusKey] = string(prNodesJSON)
+		pipelineRunCopied.Annotations[v1alpha3.JenkinsPipelineRunStatusKey] = string(runResultJSON)
+		pipelineRunCopied.Annotations[v1alpha3.JenkinsPipelineRunStagesStatusKey] = string(prNodesJSON)
 
 		// update PipelineRun
-		if err := r.updateLabelsAndAnnotations(ctx, &pr); err != nil {
+		if err := r.updateLabelsAndAnnotations(ctx, pipelineRunCopied); err != nil {
 			log.Error(err, "unable to update PipelineRun labels and annotations.")
 			return ctrl.Result{RequeueAfter: time.Second}, err
 		}
 
-		status := pr.Status.DeepCopy()
+		status := pipelineRunCopied.Status.DeepCopy()
 		pbApplier := pipelineBuildApplier{pipelineBuild}
 		pbApplier.apply(status)
 
@@ -165,47 +165,79 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 			log.Error(err, "unable to update PipelineRun status.")
 			return ctrl.Result{RequeueAfter: time.Second}, err
 		}
-		r.recorder.Eventf(&pr, corev1.EventTypeNormal, v1alpha3.Updated, "Updated running data for PipelineRun %s", req.NamespacedName)
+		r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeNormal, v1alpha3.Updated, "Updated running data for PipelineRun %s", req.NamespacedName)
 		// until the status is okay
 		// TODO make the RequeueAfter configurable
 		return ctrl.Result{RequeueAfter: 3 * time.Second}, nil
 	}
 
 	// first run
-	pipelineBuild, err := r.triggerJenkinsJob(namespaceName, pipelineName, &pr.Spec)
+	jobRun, err := r.triggerJenkinsJob(namespaceName, pipelineName, &pipelineRunCopied.Spec)
 	if err != nil {
 		log.Error(err, "unable to run pipeline", "namespace", namespaceName, "pipeline", pipeline.Name)
-		r.recorder.Eventf(&pr, corev1.EventTypeWarning, v1alpha3.TriggerFailed, "Failed to trigger PipelineRun %s, and error was %s", req.NamespacedName, err)
+		r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.TriggerFailed, "Failed to trigger PipelineRun %s, and error was %s", req.NamespacedName, err)
 		return ctrl.Result{}, err
 	}
-	log.Info("Triggered a PipelineRun", "runID", pipelineBuild.ID)
+	// check if there is still a same PipelineRun
+	if exists, err := r.hasSamePipelineRun(jobRun, pipeline); err != nil {
+		return ctrl.Result{}, err
+	} else if exists {
+		// if there still exists the same pending PipelineRun, then give up reconciling
+		if err := r.Delete(ctx, pipelineRunCopied); err != nil {
+			// ignore the not found error here
+			return ctrl.Result{}, client.IgnoreNotFound(err)
+		}
+		log.Info("Skipped this PipelineRun because there was still a pending Pipeline with the same parameter")
+		return ctrl.Result{}, nil
+	}
+
+	log.Info("Triggered a PipelineRun", "runID", jobRun.ID)
 
 	// set Jenkins run ID
-	if pr.Annotations == nil {
-		pr.Annotations = make(map[string]string)
+	if pipelineRunCopied.Annotations == nil {
+		pipelineRunCopied.Annotations = make(map[string]string)
 	}
-	pr.Annotations[v1alpha3.JenkinsPipelineRunIDKey] = pipelineBuild.ID
+	pipelineRunCopied.Annotations[v1alpha3.JenkinsPipelineRunIDKey] = jobRun.ID
 
 	// the Update method only updates fields except subresource: status
-	if err := r.updateLabelsAndAnnotations(ctx, &pr); err != nil {
+	if err := r.updateLabelsAndAnnotations(ctx, pipelineRunCopied); err != nil {
 		log.Error(err, "unable to update PipelineRun labels and annotations.")
 		return ctrl.Result{}, err
 	}
 
-	pr.Status.StartTime = &v1.Time{Time: time.Now()}
-	pr.Status.UpdateTime = &v1.Time{Time: time.Now()}
+	pipelineRunCopied.Status.StartTime = &v1.Time{Time: time.Now()}
+	pipelineRunCopied.Status.UpdateTime = &v1.Time{Time: time.Now()}
 	// due to the status is subresource of PipelineRun, we have to update status separately.
 	// see also: https://book-v1.book.kubebuilder.io/basics/status_subresource.html
 
-	if err := r.updateStatus(ctx, &pr.Status, req.NamespacedName); err != nil {
+	if err := r.updateStatus(ctx, &pipelineRunCopied.Status, req.NamespacedName); err != nil {
 		log.Error(err, "unable to update PipelineRun status.")
 		return ctrl.Result{}, err
 	}
-	r.recorder.Eventf(&pr, corev1.EventTypeNormal, v1alpha3.Started, "Started PipelineRun %s", req.NamespacedName)
+	r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeNormal, v1alpha3.Started, "Started PipelineRun %s", req.NamespacedName)
 	// requeue after 1 second
 	return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
 }
 
+func (r *Reconciler) hasSamePipelineRun(jobRun *job.PipelineRun, pipeline *v1alpha3.Pipeline) (exists bool, err error) {
+	// check if the run ID exists in the PipelineRun
+	pipelineRuns := &v1alpha3.PipelineRunList{}
+	listOptions := []client.ListOption{
+		client.InNamespace(pipeline.Namespace),
+		client.MatchingLabels{v1alpha3.PipelineNameLabelKey: pipeline.Name},
+	}
+	if pipeline.Spec.Type == v1alpha3.MultiBranchPipelineType {
+		// add SCM reference name into list options for multi-branch Pipeline
+		listOptions = append(listOptions, client.MatchingLabels{v1alpha3.SCMRefNameLabelKey: jobRun.Pipeline})
+	}
+	if err = r.Client.List(context.Background(), pipelineRuns, listOptions...); err == nil {
+		isMultiBranch := pipeline.Spec.Type == v1alpha3.MultiBranchPipelineType
+		finder := newPipelineRunFinder(pipelineRuns.Items)
+		_, exists = finder.find(jobRun, isMultiBranch)
+	}
+	return
+}
+
 func (r *Reconciler) deleteJenkinsJobHistory(pipelineRun *v1alpha3.PipelineRun) (err error) {
 	var buildNum int
 	if buildNum = getJenkinsBuildNumber(pipelineRun); buildNum < 0 {


Test patch:
diff --git a/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go b/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
index a0ee258..66af724 100644
--- a/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
+++ b/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
@@ -10,12 +10,22 @@ import (
 
 	"github.com/golang/mock/gomock"
 	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"github.com/jenkins-zh/jenkins-client/pkg/job"
 	"github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/client/clientset/versioned/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	// nolint
+	// The fakeclient will undeprecated starting with v0.7.0
+	// Reference:
+	// - https://github.com/kubernetes-sigs/controller-runtime/issues/768
+	// - https://github.com/kubernetes-sigs/controller-runtime/pull/1101
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
 func Test_getBranch(t *testing.T) {
@@ -291,3 +301,145 @@ var _ = Describe("Test deleteJenkinsJobHistory", func() {
 		ctrl.Finish()
 	})
 })
+
+var _ = Describe("TestReconciler_hasSamePipelineRun", func() {
+	var genernalPipeline *v1alpha3.Pipeline
+	var multiBranchPipeline *v1alpha3.Pipeline
+	var generalPipelineRun *v1alpha3.PipelineRun
+	var multiBranchPipelineRun *v1alpha3.PipelineRun
+	var client client.Client
+
+	BeforeEach(func() {
+		genernalPipeline = &v1alpha3.Pipeline{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "general-pipeline",
+				Namespace: "default",
+			},
+			Spec: v1alpha3.PipelineSpec{
+				Type: v1alpha3.NoScmPipelineType,
+			},
+		}
+		multiBranchPipeline = &v1alpha3.Pipeline{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "multi-branch-pipeline",
+				Namespace: "default",
+			},
+			Spec: v1alpha3.PipelineSpec{
+				Type: v1alpha3.MultiBranchPipelineType,
+			},
+		}
+
+		generalPipelineRun = &v1alpha3.PipelineRun{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "pipeline-run-2",
+				Namespace: "default",
+				Annotations: map[string]string{
+					v1alpha3.JenkinsPipelineRunIDKey: "123",
+				},
+				Labels: map[string]string{
+					v1alpha3.PipelineNameLabelKey: "general-pipeline",
+				},
+			},
+		}
+
+		multiBranchPipelineRun = &v1alpha3.PipelineRun{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "pipeline-run-1",
+				Namespace: "default",
+				Annotations: map[string]string{
+					v1alpha3.JenkinsPipelineRunIDKey: "123",
+				},
+				Labels: map[string]string{
+					v1alpha3.SCMRefNameLabelKey:   "main",
+					v1alpha3.PipelineNameLabelKey: "multi-branch-pipeline",
+				},
+			},
+			Spec: v1alpha3.PipelineRunSpec{
+				SCM: &v1alpha3.SCM{
+					RefName: "main",
+				},
+			},
+		}
+	})
+
+	Context("Multi-branch PipelineRun", func() {
+		BeforeEach(func() {
+			scheme := scheme.Scheme
+			Expect(v1alpha3.AddToScheme(scheme)).To(Succeed())
+			client = fake.NewFakeClientWithScheme(scheme, multiBranchPipelineRun)
+		})
+
+		It("multi-branch PipelineRun has existed", func() {
+			reconciler := &Reconciler{
+				Client: client,
+			}
+			jobRun := &job.PipelineRun{
+				ID:       "123",
+				Pipeline: "main",
+			}
+			exists, err := reconciler.hasSamePipelineRun(jobRun, multiBranchPipeline)
+			Expect(err).To(BeNil())
+			Expect(exists).To(BeTrue())
+		})
+
+		It("Different run ID", func() {
+			reconciler := &Reconciler{
+				Client: client,
+			}
+			jobRun := &job.PipelineRun{
+				ID:       "non-existent-id",
+				Pipeline: "main",
+			}
+			exists, err := reconciler.hasSamePipelineRun(jobRun, multiBranchPipeline)
+			Expect(err).To(BeNil())
+			Expect(exists).To(BeFalse())
+		})
+
+		It("Different SCM reference name", func() {
+			reconciler := &Reconciler{
+				Client: client,
+			}
+			jobRun := &job.PipelineRun{
+				ID:       "123",
+				Pipeline: "non-existent-branch",
+			}
+			exists, err := reconciler.hasSamePipelineRun(jobRun, multiBranchPipeline)
+			Expect(err).To(BeNil())
+			Expect(exists).To(BeFalse())
+		})
+	})
+
+	Context("General PipelineRun", func() {
+		BeforeEach(func() {
+			scheme := scheme.Scheme
+			Expect(v1alpha3.AddToScheme(scheme)).To(Succeed())
+			client = fake.NewFakeClientWithScheme(scheme, generalPipelineRun)
+		})
+
+		It("general PipelineRun has existed", func() {
+			reconciler := &Reconciler{
+				Client: client,
+			}
+			jobRun := &job.PipelineRun{
+				ID:       "123",
+				Pipeline: "general-pipeline",
+			}
+			exists, err := reconciler.hasSamePipelineRun(jobRun, genernalPipeline)
+			Expect(err).To(Succeed())
+			Expect(exists).To(BeTrue())
+		})
+
+		It("Different run ID", func() {
+			reconciler := &Reconciler{
+				Client: client,
+			}
+			jobRun := &job.PipelineRun{
+				ID:       "non-existent-id",
+				Pipeline: "general-pipeline",
+			}
+			exists, err := reconciler.hasSamePipelineRun(jobRun, genernalPipeline)
+			Expect(err).To(Succeed())
+			Expect(exists).To(BeFalse())
+		})
+	})
+})