Repository: openshift-pipelines/pipelines-as-code
Pull request #58 (2021-05-18 13:14:27)

Remote task installation via annotation
Remove the mechanism to do task remote install from tekton.yaml and
instead do it in annotations.

Format looks like this :

```yaml
pipelinesascode.tekton.dev/task: [buildah, git-clone]
```

you can have multiple lines

```yaml
pipelinesascode.tekton.dev/task: [buildah]
pipelinesascode.tekton.dev/task-1: [git-clone]
```

This installs from tekton hub but you can have remote install from
http(s)?:// or from a file inside the repository.

TODO: This deprecate the forceNamespace mechanism that was setin inside the tekton.yaml, 
we would need to be able to do that on annotation which can be done in a following PR.

chmouel: > For "remote task installation" I think we do not support yet tekton bundle right ? We should at some point (tektoncd/community@main/teps/0060-remote-resource-resolution.md should help there).

We already support tekton bundle, user just need to specify them directly in their pipeline and tekton pipeline controller will do its magic 🙃

Altho, the resolver may need to be updated to don't bug on them which should be trivial..

I think down the line we can migrate to whatever tekton has to offer, this probably would not conflict...

Patch:
diff --git a/.tekton/run.yaml b/.tekton/run.yaml
index 37c77fa7..992c6cdb 100644
--- a/.tekton/run.yaml
+++ b/.tekton/run.yaml
@@ -5,6 +5,7 @@ metadata:
   name: pipelines-as-code-test-run
   pipelinesascode.tekton.dev/on-target-branch: "[main]"
   pipelinesascode.tekton.dev/on-event: "[pull_request]"
+  pipelinesascode.tekton.dev/task: "[git-clone, codecov]"
 spec:
   pipelineRef:
     name: pipelines-as-code-test
diff --git a/.tekton/tekton.yaml b/.tekton/tekton.yaml
deleted file mode 100644
index 2cd8bd7d..00000000
--- a/.tekton/tekton.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-tasks:
-  - git-clone
-  - codecov
diff --git a/README.md b/README.md
index 3b6b1379..0d6fabfd 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,14 @@
 
 # Pipelines as Code
 
-Pipelines as code, an opinionated CI based on OpenShift Pipelines / Tekton.
+Pipelines as Code, an opinionated CI based on OpenShift Pipelines / Tekton.
 
 ## Introduction
 
-Pipelines as code let you use the pipelines as code flow directly with OpenShift
+Pipelines as Code let you use the pipelines as Code flow directly with OpenShift
 Pipelines.
 
-Pipelines as code technique can be described in this web page
+Pipelines as Code technique can be described in this web page
 [https://www.thoughtworks.com/radar/techniques/pipelines-as-code](https://www.thoughtworks.com/radar/techniques/pipelines-as-code) it allows you
 to have your pipelines "sits and live" inside the same repository where your
 code is.
@@ -20,13 +20,13 @@ as Code runs and reports the pipeline status on Pull Request or branch push.
 
 ## Components
 
-Pipelines as code leverage on this technologies :
+Pipelines as Code leverage on this technologies :
 
 - [Tekton Triggers](github.com/tektoncd/triggers): A Tekton Triggers
   EventListenner is spun up in a central namespace (`pipelines-as-code`). The
   EventListenner listen for webhook events and acts upon it.
 
-- Repository CRD: A new CRD introduced with Pipelines as code, It allows the
+- Repository CRD: A new CRD introduced with Pipelines as Code, It allows the
   user to specify which repo is started in which namespace they want.
 
 - Web VCS support. When iterating over a Pull Request, status and control is
@@ -79,7 +79,7 @@ into the same namespace are where we want to execute them.
 
 ### Write Tekton pipeline in `.tekton/` directory
 
-- Pipelines as code tries to be as close to the tekton template as possible.
+- Pipelines as Code tries to be as close to the tekton template as possible.
   Usually you write your template and save them with a ".yaml" extension  and
   Pipelines as Code will run them.
 
@@ -97,8 +97,10 @@ into the same namespace are where we want to execute them.
   `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`:
+#### Event matching to a Pipeline
+
+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:
@@ -117,46 +119,81 @@ into the same namespace are where we want to execute them.
 
   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
+  matching annotations are currently mandated or `Pipelines asCode` will not
   match your `PiplineRun`.
 
-- If `Pipelines as Code` sees multiple documents, it tries to *resolves* it as a
+#### Pipelines asCode resolver
+
+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 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.
+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.
+
+#### Remote Task support
+
+`Pipelines asCode` support fetching remote tasks from remote location via  annotation on PipelineRun.
+
+If the resolver sees a PipelineRun referencing a remote task via its name in a  Pipeline or a PipelineSpec it will automatically inlines it.
+
+An annotation to a remote task looks like this :
+
+  ```yaml
+  pipelinesascode.tekton.dev/task: "[git-clone]"
+  ```
+
+this installs the [git-clone](https://github.com/tektoncd/catalog/tree/main/task/git-clone) task from the [tekton hub](https://hub.tekton.dev) repository via its API.
+
+You can have multiple tasks in there if you separate  them by a comma `,`:
+
+```yaml
+pipelinesascode.tekton.dev/task: "[git-clone, golang-test, tkn]"
+```
+
+You can have multiple lines if you add a `-NUMBER` prefix to the annotation, for example :
+
+```yaml
+  pipelinesascode.tekton.dev/task: "[git-clone]"
+  pipelinesascode.tekton.dev/task-1: "[golang-test]"
+  pipelinesascode.tekton.dev/task-2: "[tkn]"
+```
+
+By default `Pipelines as Code` will interpret the string as the `latest` task to grab from [tekton hub](https://hub.tekton.dev).
+
+If instead you want to have a specific task, you can add a colon `:` to the string and a version number, like in this example :
+
+```yaml
+  pipelinesascode.tekton.dev/task: "[git-clone:0.1]" # will install git-clone 0.1 from tekton.hub
+  ```
+
+If you have a string starting with http:// or https://, `Pipelines asCode`
+will fetch the task directly from that remote url instead of going via the
+`tekton hub` :
+
+```yaml
+  pipelinesascode.tekton.dev/task: "[https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.3/git-clone.yaml]"
+```
 
-- Optionally `Pipelines as Code` supports a configuration file called
-  `tekton.yaml` which allows you to integrate *remote* tasks directly on your
-  pipeline. For example if you have this :
+You can as well a reference to a task from a yaml file inside your repo if you specify the relative path to it, for example :
 
   ```yaml
-  tasks:
-    - https://task.com/task1.yaml
-    - task/dir/task2.yaml
-    - git-clone
-    - buildah:0.2
+  pipelinesascode.tekton.dev/task: "[.tekton/tasks/git-clone.yaml]"
   ```
 
-  - Everything that starts with https:// will be retrieved remotely.
-  - Everything that has a slash (/) into it will be retrieved from inside the repository.
-  - Everything else is a remote task from [the hub](https://hub.tekton.dev/). It
-    will grab the latest from Tekton hub unless there is `:VERSION_NUMBER` which
-    means we want that specific `version_number` to be grabbed for that task.
+will grab the `.tekton/tasks/git-clone.yaml` from the current repository on the `SHA` where the event come from (i.e: the current pull request or the current branch push).
 
-  All those tasks will be resolved inside your Pipeline, so if you ref to it
-  from your pipeline they will be embedded directly into your `PipelineRun`
-  after being retrieved.
+If there is any error fetching those resources, `Pipelines as Code` will error out and not process the pipeline.
+If the object fetched cannot be parsed as a Tekton `Task` it will error out.
 
 ### Running the Pipeline
 
-User create a Pull Request.
+A 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 not the owner of the repository or not a public member of the organization where the repository belong to, `Pipelines 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 :
 
diff --git a/pkg/cmd/resolve/resolve.go b/pkg/cmd/resolve/resolve.go
index 55791e7e..d1d7e3d6 100644
--- a/pkg/cmd/resolve/resolve.go
+++ b/pkg/cmd/resolve/resolve.go
@@ -1,6 +1,7 @@
 package resolve
 
 import (
+	"context"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -9,6 +10,7 @@ import (
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/flags"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/pipelineascode"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/resolve"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
 	"github.com/spf13/cobra"
 	"sigs.k8s.io/yaml"
 )
@@ -29,7 +31,9 @@ func Command(p cli.Params) *cobra.Command {
 				"revision": "SHA",
 				"repo_url": "url",
 			})
-			prun, err := resolve.Resolve(cs, allTemplates, true)
+			ctx := context.Background()
+			runinfo := &webvcs.RunInfo{}
+			prun, err := resolve.Resolve(ctx, cs, runinfo, allTemplates, true)
 			if err != nil {
 				return err
 			}
diff --git a/pkg/config/annotation_matcher.go b/pkg/config/annotation_matcher.go
index 200ff6c0..790dda1c 100644
--- a/pkg/config/annotation_matcher.go
+++ b/pkg/config/annotation_matcher.go
@@ -17,7 +17,8 @@ const (
 	reValidateTag            = `^\[(.*)\]$`
 )
 
-func getAnnotationList(annotation string) ([]string, error) {
+// TODO: move to another file since it's common to all annotations_* files
+func getAnnotationValues(annotation string) ([]string, error) {
 	re := regexp.MustCompile(reValidateTag)
 	match := re.Match([]byte(annotation))
 	if !match {
@@ -68,7 +69,7 @@ func MatchPipelinerunByAnnotation(pruns []*v1beta1.PipelineRun, cs *cli.Clients,
 }
 
 func matchOnAnnotation(targetBranchAnnotation string, match string) (bool, error) {
-	targets, err := getAnnotationList(targetBranchAnnotation)
+	targets, err := getAnnotationValues(targetBranchAnnotation)
 	if err != nil {
 		return false, err
 	}
diff --git a/pkg/config/annotation_tasks_install.go b/pkg/config/annotation_tasks_install.go
new file mode 100644
index 00000000..89a925e7
--- /dev/null
+++ b/pkg/config/annotation_tasks_install.go
@@ -0,0 +1,100 @@
+package config
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"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/hub/api/pkg/cli/hub"
+	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	k8scheme "k8s.io/client-go/kubernetes/scheme"
+)
+
+const (
+	tektonCatalogHubName  = "tekton"
+	taskAnnotationsRegexp = `task(-[0-9]+)?$`
+)
+
+type RemoteTasks struct {
+	Clients *cli.Clients
+	Runinfo *webvcs.RunInfo
+}
+
+func (rt RemoteTasks) convertTotask(data string) (*tektonv1beta1.Task, error) {
+	decoder := k8scheme.Codecs.UniversalDeserializer()
+	obj, _, err := decoder.Decode([]byte(data), nil, nil)
+	if err != nil {
+		// TODO: Better debug information
+		return nil, errors.New("we have a task that is not looking like a kubernetes resource")
+	}
+
+	return obj.(*tektonv1beta1.Task), nil
+}
+
+func (rt RemoteTasks) getTask(ctx context.Context, task string) (*tektonv1beta1.Task, error) {
+	var ret *tektonv1beta1.Task
+	switch {
+	case strings.HasPrefix(task, "https://"), strings.HasPrefix(task, "http://"):
+		res, err := rt.Clients.HTTPClient.Get(task)
+		if err != nil {
+			return ret, err
+		}
+		data, _ := ioutil.ReadAll(res.Body)
+		defer res.Body.Close()
+		return rt.convertTotask(string(data))
+	case strings.Contains(task, "/"):
+		data, err := rt.Clients.GithubClient.GetFileInsideRepo(ctx, task, false, rt.Runinfo)
+		if err != nil {
+			return ret, err
+		}
+		return rt.convertTotask(string(data))
+	default:
+		var version string
+		taskName := task
+		if strings.Contains(task, ":") {
+			split := strings.Split(task, ":")
+			version = split[len(split)-1]
+			taskName = split[0]
+		}
+		resource := rt.Clients.Hub.GetResource(hub.ResourceOption{
+			Name:    taskName,
+			Catalog: tektonCatalogHubName,
+			Kind:    "task",
+			Version: version,
+		})
+		data, err := resource.Manifest()
+		if err != nil {
+			return ret, err
+		}
+		return rt.convertTotask(string(data))
+	}
+}
+
+// GetTaskFromAnnotations Get task remotely if they are on Annotations
+func (rt RemoteTasks) GetTaskFromAnnotations(ctx context.Context, annotations map[string]string) ([]*tektonv1beta1.Task, error) {
+	var ret []*tektonv1beta1.Task
+	rtareg := regexp.MustCompile(fmt.Sprintf("%s/%s", pipelinesascode.GroupName, taskAnnotationsRegexp))
+	for annotationK, annotationV := range annotations {
+		if !rtareg.Match([]byte(annotationK)) {
+			continue
+		}
+		tasks, err := getAnnotationValues(annotationV)
+		if err != nil {
+			return ret, err
+		}
+		for _, v := range tasks {
+			task, err := rt.getTask(ctx, v)
+			if err != nil {
+				return ret, err
+			}
+			ret = append(ret, task)
+		}
+	}
+	return ret, nil
+}
diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go
index e91d8444..f72b7e1c 100644
--- a/pkg/pipelineascode/pipelineascode.go
+++ b/pkg/pipelineascode/pipelineascode.go
@@ -3,7 +3,6 @@ package pipelineascode
 import (
 	"context"
 	"fmt"
-	"path/filepath"
 
 	apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
@@ -15,7 +14,6 @@ import (
 
 const (
 	tektonDir               = ".tekton"
-	tektonConfigurationFile = "tekton.yaml"
 	maxPipelineRunStatusRun = 5
 )
 
@@ -58,7 +56,6 @@ func getRepoByCR(ctx context.Context, cs *cli.Clients, url, branch, forceNamespa
 // Run over the main loop
 func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, runinfo *webvcs.RunInfo) error {
 	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)
@@ -81,18 +78,9 @@ 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)
-		if err != nil {
-			return err
-		}
-	}
-
-	// Match the Event to a Repository Resource
-	repo, err := getRepoByCR(ctx, cs, runinfo.URL, runinfo.BaseBranch, maintekton.Namespace)
+	// Match the Event to a Repository Resource,
+	// TODO: we need to be able to force a Namespace from the configuration as annotation as an extra layer of security for // hijacking
+	repo, err := getRepoByCR(ctx, cs, runinfo.URL, runinfo.BaseBranch, "")
 	if err != nil {
 		return err
 	}
@@ -132,24 +120,6 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		return err
 	}
 
-	// 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)
-			if err != nil {
-				return err
-			}
-
-			yamlConfig, err = processTektonYaml(ctx, cs, runinfo, string(data))
-			if err != nil {
-				return err
-			}
-			break
-		}
-	}
-
 	// Concat all yaml files as one multi document yaml string
 	allTemplates, err := cs.GithubClient.ConcatAllYamlFiles(ctx, objects, runinfo)
 	if err != nil {
@@ -162,13 +132,8 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		"repo_url": runinfo.URL,
 	})
 
-	// Append the remote task to the big template, we don't do replacement on those, maybe we should?
-	if yamlConfig.RemoteTasks != "" {
-		allTemplates += yamlConfig.RemoteTasks
-	}
-
 	// Merge everything (i.e: tasks/pipeline etc..) as a single pipelinerun
-	pipelineRuns, err := resolve.Resolve(cs, allTemplates, true)
+	pipelineRuns, err := resolve.Resolve(ctx, cs, runinfo, allTemplates, true)
 	if err != nil {
 		return err
 	}
@@ -195,7 +160,7 @@ func Run(ctx context.Context, cs *cli.Clients, k8int cli.KubeInteractionIntf, ru
 		return err
 	}
 
-	// Get the UI/webconsole URL for this pipeline to watch the log (only openshift console suppported atm)
+	// Get the UI/webconsole URL for this pipeline to watch the log (only openshift console supported 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
diff --git a/pkg/pipelineascode/processconfig.go b/pkg/pipelineascode/processconfig.go
deleted file mode 100644
index 68104537..00000000
--- a/pkg/pipelineascode/processconfig.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package pipelineascode
-
-import (
-	"context"
-	"io/ioutil"
-	"strings"
-
-	"github.com/ghodss/yaml"
-	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
-	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
-	"github.com/tektoncd/hub/api/pkg/cli/hub"
-)
-
-type tektonYaml struct {
-	Tasks     []string `yaml:"metadata,omitempty"`
-	Namespace string   `yaml:"namespace,omitempty"`
-}
-
-type TektonYamlConfig struct {
-	RemoteTasks string
-	Namespace   string
-}
-
-const (
-	tektonCatalogHubName = "tekton"
-)
-
-func addTaskYamlDocuments(data string) string {
-	return "\n---\n" + strings.TrimSpace(data) + "\n"
-}
-
-func processTektonYaml(ctx context.Context, cs *cli.Clients, runinfo *webvcs.RunInfo, data string) (TektonYamlConfig, error) {
-	tyConfig := TektonYamlConfig{}
-	ty := tektonYaml{}
-	err := yaml.Unmarshal([]byte(data), &ty)
-	if err != nil {
-		return tyConfig, err
-	}
-
-	tyConfig.Namespace = ty.Namespace
-
-	// parse tasks syntax:
-	// - http(s)?://foo -> url
-	// - foo/bar -> internal file inside repo
-	// - word -> latest task from catalog via hub
-	// - word:version -> task specific version from catalog via hub
-	for _, task := range ty.Tasks {
-		switch {
-		case strings.HasPrefix(task, "http://"):
-		case strings.HasPrefix(task, "https://"):
-			res, err := cs.HTTPClient.Get(task)
-			if err != nil {
-				return tyConfig, err
-			}
-			data, _ := ioutil.ReadAll(res.Body)
-			defer res.Body.Close()
-			tyConfig.RemoteTasks += addTaskYamlDocuments(string(data))
-		case strings.Contains(task, "/"):
-			data, err := cs.GithubClient.GetFileInsideRepo(ctx, task, false, runinfo)
-			if err != nil {
-				return tyConfig, err
-			}
-			tyConfig.RemoteTasks += addTaskYamlDocuments(string(data))
-		default:
-			var version string
-			taskName := task
-			if strings.Contains(task, ":") {
-				split := strings.Split(task, ":")
-				version = split[len(split)-1]
-				taskName = split[0]
-			}
-			resource := cs.Hub.GetResource(hub.ResourceOption{
-				Name:    taskName,
-				Catalog: tektonCatalogHubName,
-				Kind:    "task",
-				Version: version,
-			})
-			data, err := resource.Manifest()
-			if err != nil {
-				return tyConfig, err
-			}
-			tyConfig.RemoteTasks += addTaskYamlDocuments(string(data))
-		}
-	}
-
-	return tyConfig, nil
-}
diff --git a/pkg/resolve/resolve.go b/pkg/resolve/resolve.go
index 571b6889..b33b8a86 100644
--- a/pkg/resolve/resolve.go
+++ b/pkg/resolve/resolve.go
@@ -1,11 +1,14 @@
 package resolve
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
 
 	"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/webvcs"
 	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	k8scheme "k8s.io/client-go/kubernetes/scheme"
 )
@@ -17,11 +20,11 @@ type Types struct {
 	Tasks        []*tektonv1beta1.Task
 }
 
-func readTypes(cs *cli.Clients, data []byte) Types {
+func readTypes(cs *cli.Clients, data string) Types {
 	types := Types{}
 	decoder := k8scheme.Codecs.UniversalDeserializer()
 
-	for _, doc := range strings.Split(strings.Trim(string(data), "-"), "---") {
+	for _, doc := range strings.Split(strings.Trim(data, "-"), "---") {
 		if strings.TrimSpace(doc) == "" {
 			continue
 		}
@@ -64,12 +67,37 @@ func getPipelineByName(name string, tasks []*tektonv1beta1.Pipeline) (*tektonv1b
 	return &tektonv1beta1.Pipeline{}, fmt.Errorf("cannot find pipeline %s in input", name)
 }
 
-func resolve(cs *cli.Clients, data []byte, generateName bool) ([]*tektonv1beta1.PipelineRun, error) {
+// Resolve gets a large string which is a yaml multi documents containing
+// Pipeline/PipelineRuns/Tasks and resolve them inline as a single PipelineRun
+// generateName can be set as True to set the name as a generateName + "-" for
+// unique pipelinerun
+func Resolve(ctx context.Context, cs *cli.Clients, runinfo *webvcs.RunInfo, data string, generateName bool) ([]*tektonv1beta1.PipelineRun, error) {
+	s := k8scheme.Scheme
+	if err := tektonv1beta1.AddToScheme(s); err != nil {
+		return []*tektonv1beta1.PipelineRun{}, err
+	}
+
 	types := readTypes(cs, data)
 	if len(types.PipelineRuns) == 0 {
 		return []*tektonv1beta1.PipelineRun{}, errors.New("we need at least one pipelinerun to start with")
 	}
 
+	// First resolve Annotations Tasks
+	for _, pipelinerun := range types.PipelineRuns {
+		if pipelinerun.GetObjectMeta().GetAnnotations() != nil {
+			rt := config.RemoteTasks{
+				Clients: cs,
+				Runinfo: runinfo,
+			}
+			remoteTasks, err := rt.GetTaskFromAnnotations(ctx, pipelinerun.GetObjectMeta().GetAnnotations())
+			if err != nil {
+				return []*tektonv1beta1.PipelineRun{}, err
+			}
+			// Merge remote tasks with local tasks
+			types.Tasks = append(types.Tasks, remoteTasks...)
+		}
+	}
+
 	// Resolve TaskRef inside Pipeline
 	for _, pipeline := range types.Pipelines {
 		var pipelineTasks []tektonv1beta1.PipelineTask
@@ -103,8 +131,8 @@ func resolve(cs *cli.Clients, data []byte, generateName bool) ([]*tektonv1beta1.
 				pipelineTasksResolve = append(pipelineTasksResolve, task)
 			}
 			pipelinerun.Spec.PipelineSpec.Tasks = pipelineTasksResolve
-
 		}
+
 		// Resolve PipelineRef inside PipelineRef
 		if pipelinerun.Spec.PipelineRef != nil {
 			pipelineResolved, err := getPipelineByName(pipelinerun.Spec.PipelineRef.Name, types.Pipelines)
@@ -123,15 +151,3 @@ func resolve(cs *cli.Clients, data []byte, generateName bool) ([]*tektonv1beta1.
 	}
 	return types.PipelineRuns, nil
 }
-
-// Resolve gets a large string which is a yaml multi documents containing
-// Pipeline/PipelineRuns/Tasks and resolve them inline as a single PipelineRun
-// generateName can be set as True to set the name as a generateName + "-" for
-// unique pipelinerun
-func Resolve(cs *cli.Clients, allTemplates string, generateName bool) ([]*tektonv1beta1.PipelineRun, error) {
-	s := k8scheme.Scheme
-	if err := tektonv1beta1.AddToScheme(s); err != nil {
-		return []*tektonv1beta1.PipelineRun{}, err
-	}
-	return resolve(cs, []byte(allTemplates), generateName)
-}


Test patch:
diff --git a/pkg/config/annotation_tasks_install_test.go b/pkg/config/annotation_tasks_install_test.go
new file mode 100644
index 00000000..39e99aca
--- /dev/null
+++ b/pkg/config/annotation_tasks_install_test.go
@@ -0,0 +1,165 @@
+package config
+
+import (
+	"encoding/base64"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"testing"
+
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	ghtesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/test/github"
+	httptesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/test/http"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
+	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	"gotest.tools/v3/assert"
+	k8scheme "k8s.io/client-go/kubernetes/scheme"
+	rtesting "knative.dev/pkg/reconciler/testing"
+)
+
+func TestMain(m *testing.M) {
+	s := k8scheme.Scheme
+	if err := tektonv1beta1.AddToScheme(s); err != nil {
+		log.Fatalf("Unable to add route scheme: (%v)", err)
+	}
+	ret := m.Run()
+	os.Exit(ret)
+}
+
+func TestRemoteTasksGetTaskFromAnnotations(t *testing.T) {
+	simpletask := `---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: task
+spec:
+  steps:
+    - name: task-step
+      image: image
+      script: |
+       echo hello`
+	httpTestClient := httptesthelper.MakeHTTPTestClient(t, 200, "HELLO")
+
+	fakeGHclient, mux, _, ghTeardown := ghtesthelper.SetupGH()
+	defer ghTeardown()
+
+	mux.HandleFunc("/repos/contents/be/healthy", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprint(w, `{
+  "name": "healthy",
+  "path": "be/healthy",
+  "sha": "takepicture"}`)
+	})
+	mux.HandleFunc("/repos/git/blobs/takepicture", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `{
+  "name": "healthy",
+  "path": "be/healthy",
+  "sha": "takepicture",
+  "size": 68,
+  "content": "%s\n",
+  "encoding": "base64"}`,
+			base64.StdEncoding.EncodeToString([]byte(simpletask)))
+	})
+	mux.HandleFunc("/repos/contents/pas/la", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	cs := &cli.Clients{
+		HTTPClient: *httpTestClient,
+		GithubClient: webvcs.GithubVCS{
+			Client: fakeGHclient,
+		},
+	}
+	ctx, _ := rtesting.SetupFakeContext(t)
+
+	tests := []struct {
+		name        string
+		runinfo     *webvcs.RunInfo
+		annotations map[string]string
+		wantErr     string
+		httpReturn  string
+		gotTaskName string
+	}{
+		{
+			name: "test-annotations-error-remote-http-not-k8",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[http://remote.task]",
+			},
+			wantErr: "not looking like a kubernetes resource",
+		},
+		{
+			name: "test-annotations-remote-http",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[http://remote.task]",
+			},
+			gotTaskName: "task",
+			httpReturn:  simpletask,
+		},
+		{
+			name: "test-annotations-remote-https",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[https://remote.task]",
+			},
+			gotTaskName: "task",
+			httpReturn:  simpletask,
+		},
+		{
+			name: "test-annotations-inside-repo",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[be/healthy]",
+			},
+			gotTaskName: "task",
+			httpReturn:  simpletask,
+			runinfo:     &webvcs.RunInfo{},
+		},
+		{
+			name: "test-annotations-remote-http-skipping-notmatching",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task":  "[http://remote.task]",
+				pipelinesascode.GroupName + "/taskA": "[http://other.task]", // That's wrong this would be skipped
+			},
+			gotTaskName: "task",
+			httpReturn:  simpletask,
+		},
+		{
+			name: "test-annotations-remote-http-bad-annotation",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[http://remote.task",
+			},
+			wantErr: "annotations in pipeline are in wrong format",
+		},
+		{
+			name: "test-annotations-remote-inside-file-not-found",
+			annotations: map[string]string{
+				pipelinesascode.GroupName + "/task": "[pas/la]",
+			},
+			runinfo: &webvcs.RunInfo{},
+			wantErr: "404",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.httpReturn != "" {
+				cs.HTTPClient = *httptesthelper.MakeHTTPTestClient(t, 200, tt.httpReturn)
+			}
+
+			rt := RemoteTasks{
+				Clients: cs,
+				Runinfo: tt.runinfo,
+			}
+			got, err := rt.GetTaskFromAnnotations(ctx, tt.annotations)
+			if tt.wantErr != "" {
+				assert.ErrorContains(t, err, tt.wantErr, "We should have get an error with %v but we didn't", tt.wantErr)
+				return
+			}
+
+			assert.NilError(t, err, "GetTaskFromAnnotations() error = %v, wantErr %v", err, tt.wantErr)
+			assert.Assert(t, len(got) > 0, "GetTaskFromAnnotations() error no tasks has been processed")
+
+			if tt.gotTaskName != "" {
+				assert.Equal(t, tt.gotTaskName, got[0].GetName())
+			}
+		})
+	}
+}
diff --git a/pkg/pipelineascode/pipelineascode_test.go b/pkg/pipelineascode/pipelineascode_test.go
index b412e413..aa156c6d 100644
--- a/pkg/pipelineascode/pipelineascode_test.go
+++ b/pkg/pipelineascode/pipelineascode_test.go
@@ -162,6 +162,7 @@ func TestFilterForceNamespace(t *testing.T) {
 }
 
 func TestRunDeniedFromForcedNamespace(t *testing.T) {
+	t.Skip("TODO: Disabled until we reimplement this as annotation")
 	ctx, _ := rtesting.SetupFakeContext(t)
 	fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
 	defer teardown()
diff --git a/pkg/pipelineascode/processconfig_test.go b/pkg/pipelineascode/processconfig_test.go
deleted file mode 100644
index a56cf179..00000000
--- a/pkg/pipelineascode/processconfig_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package pipelineascode
-
-import (
-	"encoding/base64"
-	"fmt"
-	"net/http"
-	"testing"
-
-	ghtesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/test/github"
-	httptesthelper "github.com/openshift-pipelines/pipelines-as-code/pkg/test/http"
-	rtesting "knative.dev/pkg/reconciler/testing"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
-	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
-
-	// hubtest "github.com/tektoncd/hub/api/pkg/cli/test"
-	"gotest.tools/v3/assert"
-)
-
-func TestProcessTektonYamlNamespace(t *testing.T) {
-	ctx, _ := rtesting.SetupFakeContext(t)
-	namespace := "liguedestalents"
-	data := "namespace: " + namespace
-	cs := cli.Clients{}
-	ret, err := processTektonYaml(ctx, &cs, &webvcs.RunInfo{}, data)
-	assert.NilError(t, err)
-	assert.Equal(t, ret.Namespace, namespace)
-}
-
-func TestProcessTektonYamlRemoteTask(t *testing.T) {
-	ctx, _ := rtesting.SetupFakeContext(t)
-	data := `tasks:
-- https://foo.bar
-- https://hello.moto
-`
-	httpTestClient := httptesthelper.MakeHTTPTestClient(t, 200, "HELLO")
-	cs := cli.Clients{HTTPClient: *httpTestClient}
-	expected := `
----
-HELLO
-
----
-HELLO
-`
-
-	ret, err := processTektonYaml(ctx, &cs, &webvcs.RunInfo{}, data)
-	assert.NilError(t, err)
-	if d := cmp.Diff(ret.RemoteTasks, expected); d != "" {
-		t.Fatalf("-got, +want: %v", d)
-	}
-}
-
-func TestProcessTektonYamlRefInternal(t *testing.T) {
-	ctx, _ := rtesting.SetupFakeContext(t)
-	expected := "EAT YOUR VEGGIES"
-	data := `tasks:
-- be/healthy
-`
-	fakeclient, mux, _, teardown := ghtesthelper.SetupGH()
-	defer teardown()
-
-	mux.HandleFunc("/repos/contents/be/healthy", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, `{
-  "name": "healthy",
-  "path": "be/healthy",
-  "sha": "takepicture"}`)
-	})
-	mux.HandleFunc("/repos/git/blobs/takepicture", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, `{
-  "name": "healthy",
-  "path": "be/healthy",
-  "sha": "takepicture",
-  "size": 68,
-  "content": "%s\n",
-  "encoding": "base64"}`,
-			base64.StdEncoding.EncodeToString([]byte(expected)))
-	})
-	mux.HandleFunc("/repos/pas/la/contents/.tekton", func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusNotFound)
-	})
-
-	gcvs := webvcs.GithubVCS{
-		Client: fakeclient,
-	}
-	cs := &cli.Clients{GithubClient: gcvs}
-
-	ret, err := processTektonYaml(ctx, cs, &webvcs.RunInfo{}, data)
-	assert.NilError(t, err)
-
-	if d := cmp.Diff(ret.RemoteTasks, "\n---\n"+expected+"\n"); d != "" {
-		t.Fatalf("-got, +want: %v", d)
-	}
-}
diff --git a/pkg/resolve/resolve_test.go b/pkg/resolve/resolve_test.go
index 8a03d53c..43c74eb6 100644
--- a/pkg/resolve/resolve_test.go
+++ b/pkg/resolve/resolve_test.go
@@ -8,11 +8,13 @@ import (
 	"testing"
 
 	"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"
 	k8scheme "k8s.io/client-go/kubernetes/scheme"
+	rtesting "knative.dev/pkg/reconciler/testing"
 )
 
 func TestMain(m *testing.M) {
@@ -29,7 +31,8 @@ func setup() {
 }
 
 // Not sure how to get testParams fixtures working
-func readTDfile(testname string, generateName bool) (*tektonv1beta1.PipelineRun, *zapobserver.ObservedLogs, error) {
+func readTDfile(t *testing.T, testname string, generateName bool) (*tektonv1beta1.PipelineRun, *zapobserver.ObservedLogs, error) {
+	ctx, _ := rtesting.SetupFakeContext(t)
 	data, err := ioutil.ReadFile("testdata/" + testname + ".yaml")
 	if err != nil {
 		return &tektonv1beta1.PipelineRun{}, nil, err
@@ -39,7 +42,8 @@ func readTDfile(testname string, generateName bool) (*tektonv1beta1.PipelineRun,
 	cs := &cli.Clients{
 		Log: logger,
 	}
-	resolved, err := resolve(cs, data, generateName)
+	runinfo := &webvcs.RunInfo{}
+	resolved, err := Resolve(ctx, cs, runinfo, string(data), generateName)
 	if err != nil {
 		return &tektonv1beta1.PipelineRun{}, nil, err
 	}
@@ -47,7 +51,7 @@ func readTDfile(testname string, generateName bool) (*tektonv1beta1.PipelineRun,
 }
 
 func TestPipelineRunPipelineTask(t *testing.T) {
-	resolved, _, err := readTDfile("pipelinerun-pipeline-task", false)
+	resolved, _, err := readTDfile(t, "pipelinerun-pipeline-task", false)
 	assert.NilError(t, err)
 	assert.Equal(t, resolved.Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0].Name, "first-step")
 
@@ -56,29 +60,43 @@ func TestPipelineRunPipelineTask(t *testing.T) {
 }
 
 func TestGenerateName(t *testing.T) {
-	resolved, _, err := readTDfile("pipelinerun-pipeline-task", true)
+	resolved, _, err := readTDfile(t, "pipelinerun-pipeline-task", true)
 	assert.NilError(t, err)
 	assert.Assert(t, resolved.ObjectMeta.GenerateName != "")
 
-	resolved, _, err = readTDfile("with-generatename", true)
+	resolved, _, err = readTDfile(t, "with-generatename", true)
 	assert.NilError(t, err)
 	assert.Assert(t, resolved.ObjectMeta.GenerateName != "")
 }
 
 func TestPipelineRunPipelineSpecTaskSpec(t *testing.T) {
-	resolved, _, err := readTDfile("pipelinerun-pipelinespec-taskspec", false)
+	resolved, _, err := readTDfile(t, "pipelinerun-pipelinespec-taskspec", false)
 	assert.NilError(t, err)
 	assert.Equal(t, resolved.Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0].Name, "hello-moto")
 }
 
 func TestPipelineRunPipelineSpecTaskRef(t *testing.T) {
-	resolved, _, err := readTDfile("pipelinerun-pipelinespec-taskref", false)
+	resolved, _, err := readTDfile(t, "pipelinerun-pipelinespec-taskref", false)
 	assert.NilError(t, err)
 	assert.Equal(t, resolved.Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0].Name, "task1")
 }
 
+func TestPipelineRunRemoteTaskNotPacAnnotations(t *testing.T) {
+	resolved, _, err := readTDfile(t, "pipelinerun-pipeline-task-annotations-not-pac", false)
+	assert.NilError(t, err)
+
+	if _, ok := resolved.GetObjectMeta().GetAnnotations()["anno"]; !ok {
+		t.Error("Did not get back the annotations")
+	}
+}
+
+func TestPipelineRunRemoteTaskBadPacAnnotations(t *testing.T) {
+	_, _, err := readTDfile(t, "pipelinerun-pipeline-task-bad-pac-annotation", false)
+	assert.ErrorContains(t, err, "annotations in pipeline are in wrong format")
+}
+
 func TestNotTektonDocumentIgnore(t *testing.T) {
-	resolved, log, err := readTDfile("not-a-tekton-document", false)
+	resolved, log, err := readTDfile(t, "not-a-tekton-document", false)
 	assert.NilError(t, err)
 	logs := log.TakeAll()
 	assert.Assert(t, len(logs) > 0)
@@ -87,7 +105,7 @@ func TestNotTektonDocumentIgnore(t *testing.T) {
 }
 
 func TestNotKubernetesDocumentIgnore(t *testing.T) {
-	resolved, log, err := readTDfile("not-a-kubernetes-yaml", false)
+	resolved, log, err := readTDfile(t, "not-a-kubernetes-yaml", false)
 	logs := log.TakeAll()
 	assert.Assert(t, len(logs) > 0)
 	assert.Assert(t, strings.HasPrefix(logs[0].Message, "Skipping"))
@@ -97,21 +115,21 @@ func TestNotKubernetesDocumentIgnore(t *testing.T) {
 }
 
 func TestNoPipelineRuns(t *testing.T) {
-	_, _, err := readTDfile("no-pipelinerun", false)
+	_, _, err := readTDfile(t, "no-pipelinerun", false)
 	assert.Error(t, err, "we need at least one pipelinerun to start with")
 }
 
 func TestReferencedTaskNotInRepo(t *testing.T) {
-	_, _, err := readTDfile("referenced-task-not-in-repo", false)
+	_, _, err := readTDfile(t, "referenced-task-not-in-repo", false)
 	assert.Error(t, err, "cannot find task nothere in input")
 }
 
 func TestReferencedPipelineNotInRepo(t *testing.T) {
-	_, _, err := readTDfile("referenced-pipeline-not-in-repo", false)
+	_, _, err := readTDfile(t, "referenced-pipeline-not-in-repo", false)
 	assert.Error(t, err, "cannot find pipeline pipeline-test1 in input")
 }
 
 func TestIgnoreDocSpace(t *testing.T) {
-	_, _, err := readTDfile("empty-spaces", false)
+	_, _, err := readTDfile(t, "empty-spaces", false)
 	assert.NilError(t, err)
 }
diff --git a/pkg/resolve/testdata/pipelinerun-pipeline-task-annotations-not-pac.yaml b/pkg/resolve/testdata/pipelinerun-pipeline-task-annotations-not-pac.yaml
new file mode 100644
index 00000000..75afb31e
--- /dev/null
+++ b/pkg/resolve/testdata/pipelinerun-pipeline-task-annotations-not-pac.yaml
@@ -0,0 +1,55 @@
+---
+apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+  annotations:
+    anno: tation
+  name: pr
+spec:
+  pipelineRef:
+    name: pipeline-test1
+  params:
+    - name: key
+      value: "{{value}}"
+---
+apiVersion: tekton.dev/v1beta1
+kind: Pipeline
+metadata:
+  name: pipeline-test1
+spec:
+  params:
+    - name: repo_url
+    - name: revision
+  tasks:
+    - name: task-of-pipeline-test1
+      taskSpec:
+        steps:
+          - name: first-step
+            image: image1
+    - name: task-of-pipeline-test2
+      taskRef:
+        name: task-test2
+    - name: task-of-pipeline-test3
+      taskRef:
+        name: task-test3
+  steps:
+    - name: first-step
+      image: image
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: task-test2
+spec:
+  steps:
+    - name: second-step
+      image: image
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: task-test3
+spec:
+  steps:
+    - name: second-step
+      image: image
diff --git a/pkg/resolve/testdata/pipelinerun-pipeline-task-bad-pac-annotation.yaml b/pkg/resolve/testdata/pipelinerun-pipeline-task-bad-pac-annotation.yaml
new file mode 100644
index 00000000..ce521911
--- /dev/null
+++ b/pkg/resolve/testdata/pipelinerun-pipeline-task-bad-pac-annotation.yaml
@@ -0,0 +1,55 @@
+---
+apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+  annotations:
+    pipelinesascode.tekton.dev/task: "[foo"
+  name: pr
+spec:
+  pipelineRef:
+    name: pipeline-test1
+  params:
+    - name: key
+      value: "{{value}}"
+---
+apiVersion: tekton.dev/v1beta1
+kind: Pipeline
+metadata:
+  name: pipeline-test1
+spec:
+  params:
+    - name: repo_url
+    - name: revision
+  tasks:
+    - name: task-of-pipeline-test1
+      taskSpec:
+        steps:
+          - name: first-step
+            image: image1
+    - name: task-of-pipeline-test2
+      taskRef:
+        name: task-test2
+    - name: task-of-pipeline-test3
+      taskRef:
+        name: task-test3
+  steps:
+    - name: first-step
+      image: image
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: task-test2
+spec:
+  steps:
+    - name: second-step
+      image: image
+---
+apiVersion: tekton.dev/v1beta1
+kind: Task
+metadata:
+  name: task-test3
+spec:
+  steps:
+    - name: second-step
+      image: image