Repository: openshift-pipelines/pipelines-as-code
Pull request #56 (2021-05-17 14:35:13)

Match pipelinerun by annotation
Introduce pipeline annotation based event matching.

If we have multiple pipelineruns in the .tekton directory, we go over the
annotations and match the event on the pipeline annotation.

For example :

```yaml
metadata:
 name: pipeline-pr-main
 annotations:
    pipelinesascode.tekton.dev/on-target-branch: [main]
    pipelinesascode.tekton.dev/on-event: [pull_request]
```

will match the pipelinerun pipeline-pr-main if the github event target the main
branch and is coming from a pull_request.

If there is multiple pipeline matching the same event, it will match the first
one.

TODO: We currently enforce annotations to be in PipelineRun, maybe this can be
optional and by default if there isn't annotation the target branch will be the
repository default branch (ie: main) and the event will be pull_request.

/cc @siamaksade


Patch:
diff --git a/.tekton/run.yaml b/.tekton/run.yaml
index e6deb247..37c77fa7 100644
--- a/.tekton/run.yaml
+++ b/.tekton/run.yaml
@@ -3,20 +3,22 @@ apiVersion: tekton.dev/v1beta1
 kind: PipelineRun
 metadata:
   name: pipelines-as-code-test-run
+  pipelinesascode.tekton.dev/on-target-branch: "[main]"
+  pipelinesascode.tekton.dev/on-event: "[pull_request]"
 spec:
   pipelineRef:
     name: pipelines-as-code-test
   params:
     - name: repo_url
-      value: {{repo_url}}
+      value: "{{repo_url}}"
     - name: revision
-      value: {{revision}}
+      value: "{{revision}}"
   workspaces:
-  - name: source
-    volumeClaimTemplate:
-      spec:
-        accessModes:
-          - ReadWriteOnce
-        resources:
-          requests:
-            storage: 1Gi
+    - name: source
+      volumeClaimTemplate:
+        spec:
+          accessModes:
+            - ReadWriteOnce
+          resources:
+            requests:
+              storage: 1Gi
diff --git a/README.md b/README.md
index a8b86653..3b6b1379 100644
--- a/README.md
+++ b/README.md
@@ -94,17 +94,41 @@ into the same namespace are where we want to execute them.
   - `{{revision}}`: The revision of the commit.
 
 - You need at least one `PipelineRun` with a `PipelineSpec` or a separated
-  `Pipeline` object. You can have as many yaml files as you want but only one
-  pipeline by repo is supported. You can have embedded `TaskSpec` inside
+  `Pipeline` object. You can have embedded `TaskSpec` inside
   `Pipeline` or you can have them defined separately as `Task`.
 
+- Each `PipelineRun` can match different vcs events via some special annotations
+  on the `PipelineRun`. For example when you have these metadatas in your `PipelineRun`:
+
+```yaml
+ metadata:
+ name: pipeline-pr-main
+ annotations:
+    pipelinesascode.tekton.dev/on-target-branch: "[main]"
+    pipelinesascode.tekton.dev/on-event: "[pull_request]"
+```
+
+  `Pipelines as Code` will match the piplinerun `pipeline-pr-main` if the VCS
+  events target the branch `main`.
+
+  Multiple event or target branch can be specified separated by comma, i.e:
+
+  `[main, release-nightly]`
+
+  If there is multiple pipeline matching the event, it will match the first one.
+
+  matching annotations are currently mandated or `Pipeline as Code` will not
+  match your `PiplineRun`.
+
 - If `Pipelines as Code` sees multiple documents, it tries to *resolves* it as a
   single PiplineSpec embedded to a `PipelineRun`. It will add a `generateName`
   based on the Pipeline name as well. This allows you to have multiple runs in
   the same namespace without risk of conflicts.
 
-- Everything that runs your pipeline should be self contained inside the
-  `.tekton/` directory including the tasks. If pipelines as code cannot resolve the referenced tasks in the `Pipeline` or `PipelineSpec` it will fails.
+- Everything that runs your pipelinerun should be self contained inside the
+  `.tekton/` directory or from some remote tasks (see below).  If pipelines as
+  code cannot resolve the referenced tasks in the `Pipeline` or `PipelineSpec`
+  it will fails before applying the pipelinerun onto the cluster.
 
 - Optionally `Pipelines as Code` supports a configuration file called
   `tekton.yaml` which allows you to integrate *remote* tasks directly on your
@@ -134,12 +158,13 @@ User create a Pull Request.
 
 If the user sending the Pull Request is not the owner of the repository or not a public member of the organization where the repository belong to, `Pipeline as Code` will not run.
 
-If the user sending the Pull Request is inside an OWNER file located in the repository root in the main branch (the main branch as defined in the Github configuration for the repo) in the `approvers` or `reviewers` section like this :  
+If the user sending the Pull Request is inside an OWNER file located in the repository root in the main branch (the main branch as defined in the Github configuration for the repo) in the `approvers` or `reviewers` section like this :
 
 ```yaml
 approvers:
   - approved
 ```
+
 then the user `approved` will be allowed.
 
 If the user is allowed, `Pipelines as Code` will start creating the `PipelineRun` in the target user namespace.
diff --git a/pkg/config/annotation_matcher.go b/pkg/config/annotation_matcher.go
new file mode 100644
index 00000000..200ff6c0
--- /dev/null
+++ b/pkg/config/annotation_matcher.go
@@ -0,0 +1,86 @@
+package config
+
+import (
+	"errors"
+	"regexp"
+	"strings"
+
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
+	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+)
+
+const (
+	onEventAnnotation        = "on-event"
+	onTargetBranchAnnotation = "on-target-branch"
+	reValidateTag            = `^\[(.*)\]$`
+)
+
+func getAnnotationList(annotation string) ([]string, error) {
+	re := regexp.MustCompile(reValidateTag)
+	match := re.Match([]byte(annotation))
+	if !match {
+		return nil, errors.New("annotations in pipeline are in wrong format")
+	}
+	splitted := strings.Split(re.FindStringSubmatch(annotation)[1], ",")
+	if splitted[0] == "" {
+		return nil, errors.New("annotations in pipeline are empty")
+	}
+
+	return strings.Split(re.FindStringSubmatch(annotation)[1], ","), nil
+}
+
+func MatchPipelinerunByAnnotation(pruns []*v1beta1.PipelineRun, cs *cli.Clients,
+	runinfo *webvcs.RunInfo) (*v1beta1.PipelineRun, error) {
+	for _, prun := range pruns {
+		if prun.GetObjectMeta().GetAnnotations() == nil {
+			cs.Log.Warnf("PipelineRun %s does not have any annotations", prun.GetName())
+			continue
+		}
+
+		if targetEvent, ok := prun.GetObjectMeta().GetAnnotations()[pipelinesascode.
+			GroupName+"/"+onEventAnnotation]; ok {
+			matched, err := matchOnAnnotation(targetEvent, runinfo.EventType)
+			if err != nil {
+				return nil, err
+			}
+			if !matched {
+				continue
+			}
+		}
+
+		if targetBranch, ok := prun.GetObjectMeta().GetAnnotations()[pipelinesascode.
+			GroupName+"/"+onTargetBranchAnnotation]; ok {
+			matched, err := matchOnAnnotation(targetBranch, runinfo.BaseBranch)
+			if err != nil {
+				return nil, err
+			}
+			if !matched {
+				continue
+			}
+		}
+
+		return prun, nil
+	}
+	// TODO: more descriptive error message
+	return nil, errors.New("cannot match any pipeline")
+}
+
+func matchOnAnnotation(targetBranchAnnotation string, match string) (bool, error) {
+	targets, err := getAnnotationList(targetBranchAnnotation)
+	if err != nil {
+		return false, err
+	}
+
+	var gotit string
+	for _, v := range targets {
+		if v == match {
+			gotit = v
+		}
+	}
+	if gotit == "" {
+		return false, nil
+	}
+	return true, nil
+}
diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go
index b45725c0..e91d8444 100644
--- a/pkg/pipelineascode/pipelineascode.go
+++ b/pkg/pipelineascode/pipelineascode.go
@@ -7,6 +7,7 @@ import (
 
 	apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/config"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/resolve"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -59,17 +60,19 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 	var err error
 	var maintekton TektonYamlConfig
 
+	// Create first check run to let know the user we have started the pipeline
 	checkRun, err := cs.GithubClient.CreateCheckRun(ctx, "in_progress", runinfo)
 	if err != nil {
 		return err
 	}
+	// Set the runId on runInfo so if we have an error we can report it on UI (GH checks UI for GH PR)
 	runinfo.CheckRunID = checkRun.ID
 
+	// Check if submitted is allowed to run this.
 	allowed, err := aclCheck(ctx, cs, runinfo)
 	if err != nil {
 		return err
 	}
-
 	if !allowed {
 		_, _ = cs.GithubClient.CreateStatus(ctx, runinfo, "completed", "skipped",
 			fmt.Sprintf("User %s is not allowed to run CI on this repo.", runinfo.Sender),
@@ -78,6 +81,8 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		return nil
 	}
 
+	// Checkout the tekton.yaml from the main/default branch, we get configuration for there.
+	// TODO: to trash away
 	maintektonyaml, _ := cs.GithubClient.GetFileFromDefaultBranch(ctx, filepath.Join(tektonDir, tektonConfigurationFile), runinfo)
 	if maintektonyaml != "" {
 		maintekton, err = processTektonYaml(ctx, cs, runinfo, maintektonyaml)
@@ -86,11 +91,11 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		}
 	}
 
+	// Match the Event to a Repository Resource
 	repo, err := getRepoByCR(ctx, cs, runinfo.URL, runinfo.BaseBranch, maintekton.Namespace)
 	if err != nil {
 		return err
 	}
-
 	if repo.Spec.Namespace == "" {
 		_, _ = cs.GithubClient.CreateStatus(ctx, runinfo, "completed", "skipped",
 			"Could not find a configuration for this repository", "https://tenor.com/search/sad-cat-gifs")
@@ -98,6 +103,7 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		return nil
 	}
 
+	// Get everything in tekton directory
 	objects, err := cs.GithubClient.GetTektonDir(ctx, tektonDir, runinfo)
 	if err != nil {
 		_, _ = cs.GithubClient.CreateStatus(ctx, runinfo, "completed", "skipped",
@@ -110,18 +116,25 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		"sha", runinfo.SHA,
 		"event_type", "pull_request")
 
+	// Make sure we have the namespace already created or error it.
+	// TODO: this probably can be trashed since repo is only can be created in
+	// Namespace
 	err = k8int.GetNamespace(ctx, repo.Spec.Namespace)
 	if err != nil {
 		return err
 	}
+
+	// Update status in UI
 	_, err = cs.GithubClient.CreateStatus(ctx, runinfo, "in_progress", "",
-		fmt.Sprintf("Creating pipelinerun in namespace <b>%s</b>", repo.Spec.Namespace),
+		fmt.Sprintf("Getting pipelinerun configuration in namespace <b>%s</b>", repo.Spec.Namespace),
 		"https://tenor.com/search/sad-cat-gifs")
 	if err != nil {
 		return err
 	}
 
-	yamlConfig := TektonYamlConfig{}
+	// Process the tekton.yaml, we will get the extra tasks from there
+	// TODO: to trash when we get tasks inside annotations imp
+	var yamlConfig TektonYamlConfig
 	for _, file := range objects {
 		if file.GetName() == tektonConfigurationFile {
 			data, err := cs.GithubClient.GetObject(ctx, file.GetSHA(), runinfo)
@@ -137,30 +150,38 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		}
 	}
 
-	allTemplates, err := cs.GithubClient.GetTektonDirTemplate(ctx, objects, runinfo)
+	// Concat all yaml files as one multi document yaml string
+	allTemplates, err := cs.GithubClient.ConcatAllYamlFiles(ctx, objects, runinfo)
 	if err != nil {
 		return err
 	}
 
+	// Replace those {{var}} placeholders user has in her template to the runinfo variable
 	allTemplates = ReplacePlaceHoldersVariables(allTemplates, map[string]string{
 		"revision": runinfo.SHA,
 		"repo_url": runinfo.URL,
 	})
 
-	// Do not do place holders replacement on remote tasks, who knows maybe not good!
+	// Append the remote task to the big template, we don't do replacement on those, maybe we should?
 	if yamlConfig.RemoteTasks != "" {
 		allTemplates += yamlConfig.RemoteTasks
 	}
 
-	pruns, err := resolve.Resolve(cs, allTemplates, true)
+	// Merge everything (i.e: tasks/pipeline etc..) as a single pipelinerun
+	pipelineRuns, err := resolve.Resolve(cs, allTemplates, true)
 	if err != nil {
 		return err
 	}
 
-	// TODO: Should we support multiples pipeline runs?
-	prun := pruns[0]
+	// Match the pipelinerun with annotation
+	pipelineRun, err := config.MatchPipelinerunByAnnotation(pipelineRuns, cs, runinfo)
+	if err != nil {
+		return err
+	}
 
-	prun.Labels = map[string]string{
+	// Add labels on the soon to be created pipelinerun so UI/CLI can easily
+	// query them.
+	pipelineRun.Labels = map[string]string{
 		"tekton.dev/pipeline-ascode-owner":      runinfo.Owner,
 		"tekton.dev/pipeline-ascode-repository": runinfo.Repository,
 		"tekton.dev/pipeline-ascode-sha":        runinfo.SHA,
@@ -168,17 +189,20 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		"tekton.dev/pipeline-ascode-branch":     runinfo.BaseBranch,
 	}
 
-	pr, err := cs.Tekton.TektonV1beta1().PipelineRuns(repo.Spec.Namespace).Create(ctx, prun, metav1.CreateOptions{})
+	// Create the actual pipeline
+	pr, err := cs.Tekton.TektonV1beta1().PipelineRuns(repo.Spec.Namespace).Create(ctx, pipelineRun, metav1.CreateOptions{})
 	if err != nil {
 		return err
 	}
 
+	// Get the UI/webconsole URL for this pipeline to watch the log (only openshift console suppported atm)
 	consoleURL, err := k8int.GetConsoleUI(ctx, repo.Spec.Namespace, pr.GetName())
 	if err != nil {
 		// Don't bomb out if we can't get the console UI
 		consoleURL = "https://giphy.com/explore/cat-exercise-wheel"
 	}
 
+	// Create status with the log url
 	_, err = cs.GithubClient.CreateStatus(ctx, runinfo, "in_progress", "",
 		fmt.Sprintf(`Starting Pipelinerun <b>%s</b> in namespace <b>%s</b><br><br>You can follow the execution on the command line with : <br><br><code>tkn pr logs -f -n %s %s</code>`,
 			pr.GetName(), repo.Spec.Namespace, repo.Spec.Namespace, pr.GetName()),
@@ -188,11 +212,14 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 	}
 
 	// Use this as a wait holder until the logs is finished, maybe we would do something with the log output.
+	// TODO: to remove and use just a simple wait for deployment
 	_, err = k8int.TektonCliFollowLogs(repo.Spec.Namespace, pr.GetName())
 	if err != nil {
 		return err
 	}
 
+	// Post the final status to GitHub check status with a nice breakdown and
+	// tekton cli describe output.
 	newPr, err := postFinalStatus(ctx, cs, k8int, runinfo, pr.Name, repo.Spec.Namespace)
 	if err != nil {
 		return err
@@ -205,14 +232,15 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		CompletionTime:  newPr.Status.CompletionTime,
 	}
 
-	// TODO: Get another time the repo in case it was updated, there may be a
-	// locking problem we should solve here but we are talking milliseconds race.
+	// Get repo again in case it was updated while we were running the CI
+	// NOTE: there may be a race issue we should maybe solve here, between the Get and
+	// Update but we are talking sub-milliseconds issue here.
 	lastrepo, err := cs.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(repo.Spec.Namespace).Get(ctx, repo.Name, metav1.GetOptions{})
 	if err != nil {
 		return err
 	}
 
-	// TODO: Reversed?
+	// Append pipelinerun status files to the repo status
 	if len(lastrepo.Status) >= maxPipelineRunStatusRun {
 		copy(lastrepo.Status, lastrepo.Status[len(lastrepo.Status)-maxPipelineRunStatusRun+1:])
 		lastrepo.Status = lastrepo.Status[:maxPipelineRunStatusRun-1]
diff --git a/pkg/pipelineascode/templating.go b/pkg/pipelineascode/templating.go
index 8ad6fa62..37144c14 100644
--- a/pkg/pipelineascode/templating.go
+++ b/pkg/pipelineascode/templating.go
@@ -7,6 +7,7 @@ import (
 
 var reTemplate = regexp.MustCompile(`{{([^}]{2,})}}`)
 
+// ReplacePlaceHoldersVariables Replace those {{var}} placeholders to the runinfo variable
 func ReplacePlaceHoldersVariables(template string, dico map[string]string) string {
 	return reTemplate.ReplaceAllStringFunc(template, func(s string) string {
 		parts := reTemplate.FindStringSubmatch(s)
diff --git a/pkg/webvcs/github.go b/pkg/webvcs/github.go
index 1a0a255e..70d631a7 100644
--- a/pkg/webvcs/github.go
+++ b/pkg/webvcs/github.go
@@ -261,8 +261,9 @@ func (v GithubVCS) GetFileFromDefaultBranch(ctx context.Context, path string, ru
 	return tektonyaml, err
 }
 
-// GetTektonDirTemplate Get all templates in tekton directory
-func (v GithubVCS) GetTektonDirTemplate(ctx context.Context, objects []*github.RepositoryContent, runinfo *RunInfo) (string, error) {
+// ConcatAllYamlFiles concat all yaml files from a directory as one big multi document yaml string
+// TODO: trash the tekton.yaml condition when we don't need it anymore
+func (v GithubVCS) ConcatAllYamlFiles(ctx context.Context, objects []*github.RepositoryContent, runinfo *RunInfo) (string, error) {
 	var allTemplates string
 
 	for _, value := range objects {


Test patch:
diff --git a/pkg/config/annotation_matcher_test.go b/pkg/config/annotation_matcher_test.go
new file mode 100644
index 00000000..0672e275
--- /dev/null
+++ b/pkg/config/annotation_matcher_test.go
@@ -0,0 +1,177 @@
+package config
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
+	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	"go.uber.org/zap"
+	zapobserver "go.uber.org/zap/zaptest/observer"
+	"gotest.tools/v3/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestMatchPipelinerunByAnnotation(t *testing.T) {
+	pipelineGood := &tektonv1beta1.PipelineRun{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "pipeline-good",
+			Annotations: map[string]string{
+				pipelinesascode.GroupName + "/" + onEventAnnotation:        "[pull_request]",
+				pipelinesascode.GroupName + "/" + onTargetBranchAnnotation: "[main]",
+			},
+		},
+	}
+
+	pipelineOther := &tektonv1beta1.PipelineRun{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "pipeline-other",
+			Annotations: map[string]string{
+				pipelinesascode.GroupName + "/" + onEventAnnotation:        "[pull_request]",
+				pipelinesascode.GroupName + "/" + onTargetBranchAnnotation: "[main]",
+			},
+		},
+	}
+
+	observer, log := zapobserver.New(zap.InfoLevel)
+	logger := zap.New(observer).Sugar()
+	cs := &cli.Clients{
+		Log: logger,
+	}
+
+	type args struct {
+		pruns   []*tektonv1beta1.PipelineRun
+		runinfo *webvcs.RunInfo
+	}
+	tests := []struct {
+		name       string
+		args       args
+		wantErr    bool
+		wantPrName string
+		wantLog    string
+	}{
+		{
+			name: "good-match-with-only-one",
+			args: args{
+				pruns:   []*tektonv1beta1.PipelineRun{pipelineGood},
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "main"},
+			},
+			wantErr:    false,
+			wantPrName: "pipeline-good",
+		},
+		{
+			name: "first-one-match-with-two-good-ones",
+			args: args{
+				pruns:   []*tektonv1beta1.PipelineRun{pipelineGood, pipelineOther},
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "main"},
+			},
+			wantErr:    false,
+			wantPrName: "pipeline-good",
+		},
+		{
+			name: "no-match-on-event",
+			args: args{
+				pruns:   []*tektonv1beta1.PipelineRun{pipelineGood, pipelineOther},
+				runinfo: &webvcs.RunInfo{EventType: "push", BaseBranch: "main"},
+			},
+			wantErr: true,
+		},
+		{
+			name: "no-match-on-target-branch",
+			args: args{
+				pruns:   []*tektonv1beta1.PipelineRun{pipelineGood, pipelineOther},
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "other"},
+			},
+			wantErr: true,
+		},
+		{
+			name: "no-annotation",
+			args: args{
+				pruns: []*tektonv1beta1.PipelineRun{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "pipeline-other",
+						},
+					},
+				},
+				runinfo: &webvcs.RunInfo{EventType: "push", BaseBranch: "main"},
+			},
+			wantErr: true,
+			wantLog: "does not have any annotations",
+		},
+		{
+			name: "bad-event-annotation",
+			args: args{
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "main"},
+				pruns: []*tektonv1beta1.PipelineRun{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "bad-event-annotation",
+							Annotations: map[string]string{
+								pipelinesascode.GroupName + "/" + onEventAnnotation:        "pull_request",
+								pipelinesascode.GroupName + "/" + onTargetBranchAnnotation: "[main]",
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "bad-target-branch-annotation",
+			args: args{
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "main"},
+				pruns: []*tektonv1beta1.PipelineRun{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "bad-target-branch-annotation",
+							Annotations: map[string]string{
+								pipelinesascode.GroupName + "/" + onEventAnnotation:        "[pull_request]",
+								pipelinesascode.GroupName + "/" + onTargetBranchAnnotation: "main",
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "empty-annotation",
+			args: args{
+				runinfo: &webvcs.RunInfo{EventType: "pull_request", BaseBranch: "main"},
+				pruns: []*tektonv1beta1.PipelineRun{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "bad-target-branch-annotation",
+							Annotations: map[string]string{
+								pipelinesascode.GroupName + "/" + onEventAnnotation:        "[]",
+								pipelinesascode.GroupName + "/" + onTargetBranchAnnotation: "[]",
+							},
+						},
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := MatchPipelinerunByAnnotation(tt.args.pruns, cs, tt.args.runinfo)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("MatchPipelinerunByAnnotation() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if tt.wantPrName != "" {
+				assert.Assert(t, got.GetName() == tt.wantPrName, "Pipelinerun hasn't been matched: "+got.GetName()+"!="+tt.wantPrName)
+			}
+			if tt.wantLog != "" {
+				logmsg := log.TakeAll()
+				assert.Assert(t, len(logmsg) > 0)
+				assert.Assert(t, strings.Contains(logmsg[0].Message, tt.wantLog))
+			}
+		})
+	}
+}
diff --git a/pkg/pipelineascode/pipelineascode_test.go b/pkg/pipelineascode/pipelineascode_test.go
index 38b35012..b412e413 100644
--- a/pkg/pipelineascode/pipelineascode_test.go
+++ b/pkg/pipelineascode/pipelineascode_test.go
@@ -191,7 +191,7 @@ func TestRunDeniedFromForcedNamespace(t *testing.T) {
 			}
 		})
 
-	forcedNamespaceContent := base64.RawStdEncoding.EncodeToString([]byte("namespace: "+forcedNamespace+"\n")) + "="
+	forcedNamespaceContent := base64.StdEncoding.EncodeToString([]byte("namespace: " + forcedNamespace + "\n"))
 	mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/git/blobs/shaofmaintektonyaml", runinfo.Owner, runinfo.Repository),
 		func(w http.ResponseWriter, r *http.Request) {
 			fmt.Fprintf(w, `{"content": "%s"}`, forcedNamespaceContent)
@@ -235,6 +235,7 @@ func TestRun(t *testing.T) {
 		HeadBranch: "press",
 		BaseBranch: "main",
 		Sender:     "fantasio",
+		EventType:  "pull_request",
 	}
 
 	replyString(mux,
@@ -275,16 +276,16 @@ func TestRun(t *testing.T) {
 	assert.NilError(t, err)
 	replyString(mux,
 		fmt.Sprintf("/repos/%s/%s/git/blobs/internaltasksha", runinfo.Owner, runinfo.Repository),
-		fmt.Sprintf(`{"encoding": "base64","content": "%s="}`,
-			base64.RawStdEncoding.EncodeToString(taskB)))
+		fmt.Sprintf(`{"encoding": "base64","content": "%s"}`,
+			base64.StdEncoding.EncodeToString(taskB)))
 
 	// Tekton.yaml
 	tlB, err := ioutil.ReadFile("testdata/tekton.yaml")
 	assert.NilError(t, err)
 	replyString(mux,
 		fmt.Sprintf("/repos/%s/%s/git/blobs/eacad9fa044f3d9039bb04c9452eadf0c43e3195", runinfo.Owner, runinfo.Repository),
-		fmt.Sprintf(`{"encoding": "base64","content": "%s="}`,
-			base64.RawStdEncoding.EncodeToString(tlB)))
+		fmt.Sprintf(`{"encoding": "base64","content": "%s"}`,
+			base64.StdEncoding.EncodeToString(tlB)))
 
 	// Run.yaml
 	prB, err := ioutil.ReadFile("testdata/run.yaml")
@@ -292,15 +293,15 @@ func TestRun(t *testing.T) {
 	replyString(mux,
 		fmt.Sprintf("/repos/%s/%s/git/blobs/9085026cd00516d1db7101191d61a4371933c735", runinfo.Owner, runinfo.Repository),
 		fmt.Sprintf(`{"encoding": "base64","content": "%s"}`,
-			base64.RawStdEncoding.EncodeToString(prB)))
+			base64.StdEncoding.EncodeToString(prB)))
 
 	// Pipeline.yaml
 	pB, err := ioutil.ReadFile("testdata/pipeline.yaml")
 	assert.NilError(t, err)
 	replyString(mux,
 		fmt.Sprintf("/repos/%s/%s/git/blobs/5f44631b24c740288924767c608af932756d6c1a", runinfo.Owner, runinfo.Repository),
-		fmt.Sprintf(`{"encoding": "base64","content": "%s=="}`,
-			base64.RawStdEncoding.EncodeToString(pB)))
+		fmt.Sprintf(`{"encoding": "base64","content": "%s"}`,
+			base64.StdEncoding.EncodeToString(pB)))
 
 	replyString(mux,
 		fmt.Sprintf("/repos/%s/%s/check-runs", runinfo.Owner, runinfo.Repository),
diff --git a/pkg/pipelineascode/testdata/run.yaml b/pkg/pipelineascode/testdata/run.yaml
index bd673dfb..ea6d7c29 100644
--- a/pkg/pipelineascode/testdata/run.yaml
+++ b/pkg/pipelineascode/testdata/run.yaml
@@ -3,6 +3,9 @@ apiVersion: tekton.dev/v1beta1
 kind: PipelineRun
 metadata:
   name: hello-test-pipeline
+  annotations:
+    pipelinesascode.tekton.dev/on-target-branch: "[main]"
+    pipelinesascode.tekton.dev/on-event: "[pull_request]"
 spec:
   pipelineRef:
     name: pipeline1
diff --git a/pkg/webvcs/github_test.go b/pkg/webvcs/github_test.go
index e5e5c28e..03b7ed80 100644
--- a/pkg/webvcs/github_test.go
+++ b/pkg/webvcs/github_test.go
@@ -126,7 +126,7 @@ func TestPayLoadFix(t *testing.T) {
 	logger, _ := getLogger()
 	_, err = gvcs.ParsePayload(ctx, logger, "pull_request", payloadFix(string(b)))
 	// would bomb out on "assertion failed: error is not nil: invalid character
-	// '\n' in string literal" if we don't payloadfix
+	// '\n' in string literal" if we don't fix the payload
 	assert.NilError(t, err)
 }
 
@@ -135,9 +135,9 @@ func TestParsePayloadRerequest(t *testing.T) {
 	prOwner := "openshift"
 	repoName := "pipelines"
 	prNumber := "123"
-	checkrunEvent := fmt.Sprintf(`{"action": "rerequested", 
+	checkrunEvent := fmt.Sprintf(`{"action": "rerequested",
 	"sender": {"login": "%s"},
-	"check_run": {"check_suite": {"pull_requests": [{"number": %s}]}}, 
+	"check_run": {"check_suite": {"pull_requests": [{"number": %s}]}},
 	"repository": {"name": "%s", "owner": {"login": "%s"}}}`,
 		checkrunSender, prNumber, repoName, prOwner)
 	fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
@@ -417,7 +417,7 @@ hello runyaml
 	ghr, err := gcvs.GetTektonDir(ctx, ".tekton", runinfo)
 	assert.NilError(t, err)
 
-	got, err := gcvs.GetTektonDirTemplate(ctx, ghr, runinfo)
+	got, err := gcvs.ConcatAllYamlFiles(ctx, ghr, runinfo)
 	assert.NilError(t, err)
 	if d := cmp.Diff(got, expected); d != "" {
 		t.Fatalf("-got, +want: %v", d)