@@ -512,6 +512,84 @@ func TestRemote(t *testing.T) {
512512 }
513513}
514514
515+ // Verifies that cached remote pipelines are deep-copied before modification.
516+ // Without deep copy, when the first PipelineRun applies its task annotation,
517+ // it would mutate the cached pipeline and leak that task into the second run.
518+ func TestSharedRemotePipelineCacheNotMutated (t * testing.T ) {
519+ taskName := "shared-task"
520+
521+ pipelineTask := tektonv1.TaskSpec {
522+ Steps : []tektonv1.Step {
523+ {Name : "from-pipeline" , Image : "alpine" , Command : []string {"true" }},
524+ },
525+ }
526+ pipelinerunTask := tektonv1.TaskSpec {
527+ Steps : []tektonv1.Step {
528+ {Name : "from-pipelinerun" , Image : "busybox" , Command : []string {"false" }},
529+ },
530+ }
531+
532+ // remote pipeline with a single TaskRef
533+ pipeline := ttkn .MakePipeline ("shared-pipeline" , []tektonv1.PipelineTask {
534+ {Name : taskName , TaskRef : & tektonv1.TaskRef {Name : taskName }},
535+ }, map [string ]string {
536+ apipac .Task : "http://remote/shared-task" ,
537+ })
538+ pipelineB , err := yaml .Marshal (pipeline )
539+ assert .NilError (t , err )
540+
541+ pipelineTaskB , err := ttkn .MakeTaskB (taskName , pipelineTask )
542+ assert .NilError (t , err )
543+ pipelinerunTaskB , err := ttkn .MakeTaskB (taskName , pipelinerunTask )
544+ assert .NilError (t , err )
545+
546+ remoteURLS := map [string ]map [string ]string {
547+ "http://remote/shared-pipeline" : {"body" : string (pipelineB ), "code" : "200" },
548+ "http://remote/shared-task" : {"body" : string (pipelineTaskB ), "code" : "200" },
549+ "http://remote/pr-task" : {"body" : string (pipelinerunTaskB ), "code" : "200" },
550+ }
551+
552+ // first run: overrides the task via pipelinerun annotation
553+ // second run: same pipeline, no override — should get the original task
554+ pipelineruns := []* tektonv1.PipelineRun {
555+ ttkn .MakePR ("first-run" , map [string ]string {
556+ apipac .Pipeline : "http://remote/shared-pipeline" ,
557+ apipac .Task : "http://remote/pr-task" ,
558+ }, tektonv1.PipelineRunSpec {
559+ PipelineRef : & tektonv1.PipelineRef {Name : "shared-pipeline" },
560+ }),
561+ ttkn .MakePR ("second-run" , map [string ]string {
562+ apipac .Pipeline : "http://remote/shared-pipeline" ,
563+ }, tektonv1.PipelineRunSpec {
564+ PipelineRef : & tektonv1.PipelineRef {Name : "shared-pipeline" },
565+ }),
566+ }
567+
568+ observer , _ := zapobserver .New (zap .InfoLevel )
569+ logger := zap .New (observer ).Sugar ()
570+ ctx , _ := rtesting .SetupFakeContext (t )
571+ httpTestClient := httptesthelper .MakeHTTPTestClient (remoteURLS )
572+ rt := & matcher.RemoteTasks {
573+ ProviderInterface : & testprovider.TestProviderImp {},
574+ Logger : logger ,
575+ Run : & params.Run {
576+ Clients : clients.Clients {HTTP : * httpTestClient },
577+ },
578+ }
579+
580+ ret , err := resolveRemoteResources (ctx , rt , TektonTypes {PipelineRuns : pipelineruns }, & Opts {RemoteTasks : true , GenerateName : true })
581+ assert .NilError (t , err )
582+ assert .Equal (t , len (ret ), 2 )
583+
584+ firstStep := ret [0 ].Spec .PipelineSpec .Tasks [0 ].TaskSpec .Steps [0 ]
585+ assert .Equal (t , firstStep .Name , "from-pipelinerun" )
586+ assert .Equal (t , firstStep .Image , "busybox" )
587+
588+ secondStep := ret [1 ].Spec .PipelineSpec .Tasks [0 ].TaskSpec .Steps [0 ]
589+ assert .Equal (t , secondStep .Name , "from-pipeline" )
590+ assert .Equal (t , secondStep .Image , "alpine" )
591+ }
592+
515593func TestAssembleTaskFQDNs (t * testing.T ) {
516594 tests := []struct {
517595 name string
0 commit comments