From Pull Request to Production: Tekton Pipelines as Code and Argo CD GitOps Promoter

From Pull Request to Production: Tekton Pipelines as Code and Argo CD GitOps Promoter

In this article, we will learn how to build an end-to-end delivery workflow that takes an application from a pull request to production by combining Tekton Pipelines as Code with Argo CD GitOps Promoter. Tekton Pipelines as Code integrates closely with GitHub. It allows you to trigger CI pipelines from repository events, inspect pipeline runs, and view logs directly from pull requests. By running builds, tests, and validation tasks on Kubernetes, you can automate quality checks and provide developers with fast feedback during code reviews.

On the other hand, Argo CD GitOps Promoter brings a dedicated, Git-native approach to promoting application changes across environments. Its direct GitHub integration lets you automate promotions through pull requests. As a result, it offers a clear, auditable way to review and control changes as they move from development through staging to production.

By combining Tekton Pipelines as Code and GitOps Promoter, we will build a workflow that automatically validates code changes, builds and versions container images, and promotes approved releases across environments. GitHub pull requests will drive the entire process, from CI execution to production promotion.

If you’re interested in other approaches to GitOps-based promotion, check out my previous article about continuous promotion on Kubernetes with Argo CD and Kargo.

Source Code

If you want to try this exercise, check out my source code. This time the implementation is spread across several repositories. One repository contains the source code for a sample application, and another is a global repository of configurations for various applications running on Argo CD. Additionally, the argocd-showcase repository also contains manifests for Argo CD to configure GitOps Promoter. Go to the promoter directory to find the required YAMLs. Then follow my next instructions.

Install and Configure Tekton and Argo CD

Required Kubernetes Operators

We’ll start today’s exercise by installing operators on the OpenShift cluster. We need two operators. The first, Red Hat OpenShift GitOps, runs the Argo CD instance. The second, Red Hat OpenShift Pipelines, installs Tekton.

Integrate Tekton Pipelines with GitHub

Let’s start by looking at OpenShift Pipelines. The great thing is we can set everything up from scratch directly in OpenShift. Simply go to the Pipelines tab in the menu and select the Setup GitHub App option. The wizard will then guide us through the process step by step.

gitops-promoter-github-app

Basically, you just need to choose a name for your application. In my case, the name is piomin-openshift-pipelines-as-code.

Then you will be redirected to the GitHub portal to confirm the application’s creation.

OpenShift will automatically populate everything in our application, including cluster settings and Tekton Pipelines. Here, too, we don’t need to worry about a thing. Below is my application, piomin-openshift-pipelines-as-code.

We need to install the application we’ve created and make it available to selected repositories. At this point, please pay attention to the permissions. To ensure the process works correctly, I added a few permissions. Your list should include at least the permissions shown in the screenshot below.

gitops-promoter-github-permissions

Once you create a new application on GitHub, OpenShift Pipelines will also create the corresponding Secret on the cluster, which contains the application’s key data. This happens in the background. I’m sharing this information to help you better understand how OpenShift Pipelines as Code integrates with GitHub.

$ oc describe secret pipelines-as-code-secret -n openshift-pipelines
Name:         pipelines-as-code-secret
Namespace:    openshift-pipelines
Labels:       <none>
Annotations:  appName: piomin-openshift-pipelines-as-code
              appUrl: https://github.com/apps/piomin-openshift-pipelines-as-code

Type:  Opaque

Data
====
github-application-id:  7 bytes
github-private-key:     1679 bytes
webhook.secret:         40 bytes
ShellSession

Enable GitOps Promoter for Argo CD

Once the OpenShift GitOps operator has been installed, it automatically creates a global Argo CD instance using the ArgoCD CR. However, to activate the GitOps Promoter on the cluster, we need to modify this object. It is located in the openshift-gitops namespace, and its name is also openshift-gitops.

We also need to enable a feature known as the source hydrator, which the Promoter relies on. This is a relatively new Argo CD feature, described in the documentation here. The source hydrator uses the rendered manifest pattern, which allows us to push hydrated manifests to Git before Argo CD syncs them to the cluster. This lets you review the actual Kubernetes manifests Argo CD will apply before the changes reach the cluster. Below is the section that needs to be added to the ArgoCD openshift-gitops object.

  sourceHydrator:
    enabled: true
  promoter:
    apiserver:
      enabled: true
      tls:
        caSecretKey: tls.key
        caSecretName: openshift-gitops-promoter-apiserver-tls
        certSecretName: openshift-gitops-promoter-apiserver-tls
    argoCDUIExtensionEnabled: true
    enabled: true
    webhook:
      enabled: true
YAML

Once you have made these changes in the ArgoCD project, you should see two new pods in the openshift-gitops namespace.

gitops-promoter-argo-cd

I made one more change to ensure everything worked properly. This may be because GitOps Promoter is still in the technology preview phase on OpenShift, and this will likely be fixed soon. I had to manually add the patch permission for commitstatuses/status to the ClusterRole of the openshift-gitops-openshift-gitops-promoter-controller-manager object. Below is the relevant section I modified.

  - verbs:
      - get
      - patch
      - update
    apiGroups:
      - promoter.argoproj.io
    resources:
      - argocdcommitstatuses/status
      - changetransferpolicies/status
      - clusterscmproviders/status
      - commitstatuses/status
      - gitcommitstatuses/status
      - gitrepositories/status
      - promotionstrategies/status
      - pullrequests/status
      - scheduledcommitstatuses/status
      - scmproviders/status
      - timedcommitstatuses/status
      - webrequestcommitstatuses/status
YAML

By default, the OpenShift GitOps operator overwrites the contents of this ClusterRole. To prevent this, add the DISABLE_CLUSTER_TLS_PROFILE environment variable to the ClusterServiceVersion object for our operator.

Integrate GitOps Promoter with GitHub

Finally, it’s time for the final step in installing GitOps Promoter. We need to create another application on GitHub. However, this won’t be as straightforward as it was with OpenShift Pipelines. We’ll have to create the application manually. So, in your GitHub profile, go to the Settings -> Developer Settings -> GitHub Apps section. Then, choose the New GitHub App button. I called my app GitOps Promoter Piomin.

Creating the application isn’t enough. We need to configure the OpenShift webhook address and generate a private key. Before configuring the webhook, we must expose the GitOps Promoter service outside of OpenShift. You should be able to see this service once you have enabled GitOps Promoter from within the ArgoCD object.

$ oc get svc openshift-gitops-promoter-controller-manager -n openshift-gitops
NAME                                           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
openshift-gitops-promoter-controller-manager   ClusterIP   172.30.80.94   <none>        3333/TCP   14d
ShellSession

Run the following command to expose your service as a Route:

oc expose svc openshift-gitops-promoter-controller-manager
ShellSession

Then, copy the URL of the Route from the following command’s result:

oc get route openshift-gitops-promoter-controller-manager -n openshift-gitops
ShellSession

Paste the URL in the Webhook section in your GitHub app:

gitops-promoter-webhook

Then, go to the Credentials -> Key pairs section. Generate a new private key by clicking the New key button.

Copy the private key you have created and save it as a Kubernetes Secret. You can name it whatever you like, but you will need to specify this name later in the CR object provided by the GitOps Promoter.

kind: Secret
apiVersion: v1
metadata:
  name: gitops-promoter
  namespace: openshift-gitops
stringData:
  githubAppPrivateKey: <YOUR_PRIVATE_KEY>
YAML

Here’s my GitOps Promoter Piomin app with its permissions.

The GitOps Promoter Piomin application itself has permissions to manage pull requests and commits. However, we still need to integrate it with Argo CD. So, create a Secret containing the GitHub app ID and private key as follows. The Secret must be labeled with argocd.argoproj.io/secret-type: repository-write.

kind: Secret
apiVersion: v1
metadata:
  name: my-push-secret
  namespace: openshift-gitops
  labels:
    argocd.argoproj.io/secret-type: repository-write
stringData:
  githubAppID: 5236551
  githubAppPrivateKey: <MY_PRIVATE_KEY>
  type: git
  url: https://github.com/piomin/gitops-apps-config.git
type: Opaque
YAML

Right, that’s basically it for the first part. It took quite a while, but I wanted to show you exactly what you need to do first to get the whole mechanism working. Now, let’s get to the point 🙂

Create and Run Tekton Pipelines As Code

Preparation

The source code for our sample application is located in the sample-ocp-app repository. Before we move on to creating and running a Tekton pipeline within it, we need to add the following CR object to OpenShift:

apiVersion: pipelinesascode.tekton.dev/v1alpha1
kind: Repository
metadata:
  name: sample-ocp-app
  namespace: ci
spec:
  url: 'https://github.com/piomin/sample-ocp-app'
YAML

Run Pipeline on Pull Request

Tekton pipelines in the repository must be located in the .tekton directory. Our sample pipeline is very simple. It integrates with GitHub through Pipelines as Code and runs automatically when a pull request targets the master branch. It acts as a GitHub check, giving developers feedback on their changes before they merge them. The pipeline clones the repository at the specified revision and runs a Maven build using Java 25 to verify that the application builds successfully.

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: sample-ocp-app
  annotations:
    pipelinesascode.tekton.dev/on-event: "[pull_request]"
    pipelinesascode.tekton.dev/on-target-branch: "[master]"
    pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
  params:
    - name: repo_url
      value: "{{ repo_url }}"
    - name: revision
      value: "{{ revision }}"
  pipelineSpec:
    params:
      - name: repo_url
      - name: revision
    workspaces:
      - name: source
    tasks:
      - name: fetch-repository
        taskRef:
          resolver: cluster
          params:
            - name: kind
              value: task
            - name: name
              value: git-clone
            - name: namespace
              value: openshift-pipelines
        workspaces:
          - name: output
            workspace: source
        params:
          - name: URL
            value: $(params.repo_url)
          - name: REVISION
            value: $(params.revision)
          - name: SSL_VERIFY
            value: "false"
      - name: maven-build
        runAfter:
          - fetch-repository
        taskRef:
          resolver: cluster
          params:
            - name: kind
              value: task
            - name: name
              value: maven
            - name: namespace
              value: openshift-pipelines
        workspaces:
          - name: source
            workspace: source
        params:
          - name: JAVA_VERSION
            value: '25'
  workspaces:
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
YAML

Now we just need to update the repo and create a pull request. The pipeline will run automatically. There are a few other checks here, but the red arrow indicates the Tekton pipeline running.

gitops-promoter-app-pull-request

We can click this workflow to view its execution details. As you can see, it consisted of two stages: fetch-repository and maven-build. Both were successful.

The pipeline ran on the OpenShift cluster. You can view its progress and logs in the OpenShift web console.

Release Pipeline on New Tag

Now all that remains is for us to merge the pull request into the main branch. There, another Tekton pipeline handles the release. The release pipeline visible below integrates with GitHub through Pipelines as Code and runs when a Git tag is pushed to the repository. It builds on the previous pipeline by first cloning the repository and running a Maven build with Java 25, then extracting the tag name from the Git reference. Finally, it uses Source-to-Image (S2I) to build a container image, tag it with the release version, and push it to the OpenShift internal image registry.

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: sample-ocp-app-tag
  annotations:
    pipelinesascode.tekton.dev/on-event: "[push]"
    pipelinesascode.tekton.dev/on-target-branch: "[refs/tags/*]"
    pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
  params:
    - name: repo_url
      value: "{{ repo_url }}"
    - name: revision
      value: "{{ revision }}"
    - name: target_branch
      value: "{{ target_branch }}"
  pipelineSpec:
    params:
      - name: repo_url
      - name: revision
      - name: target_branch
    workspaces:
      - name: source
    tasks:
      - name: fetch-repository
        taskRef:
          resolver: cluster
          params:
            - name: kind
              value: task
            - name: name
              value: git-clone
            - name: namespace
              value: openshift-pipelines
        workspaces:
          - name: output
            workspace: source
        params:
          - name: URL
            value: $(params.repo_url)
          - name: REVISION
            value: $(params.revision)
          - name: SSL_VERIFY
            value: "false"
      - name: maven-build
        runAfter:
          - fetch-repository
        taskRef:
          resolver: cluster
          params:
            - name: kind
              value: task
            - name: name
              value: maven
            - name: namespace
              value: openshift-pipelines
        workspaces:
          - name: source
            workspace: source
        params:
          - name: JAVA_VERSION
            value: '25'
      - name: parse-tag
        runAfter:
          - maven-build
        params:
          - name: target_branch
            value: $(params.target_branch)
        taskSpec:
          params:
            - name: target_branch
          results:
            - name: tag-name
          steps:
            - name: parse
              image: registry.access.redhat.com/ubi9/ubi-minimal:latest
              script: |
                #!/bin/sh
                echo -n "$(params.target_branch)" | sed 's#^refs/tags/##' | tee $(results.tag-name.path)
      - name: s2i-java
        runAfter:
          - parse-tag
        taskRef:
          resolver: cluster
          params:
            - name: kind
              value: task
            - name: name
              value: s2i-java
            - name: namespace
              value: openshift-pipelines
        workspaces:
          - name: source
            workspace: source
        params:
          - name: IMAGE
            value: image-registry.openshift-image-registry.svc:5000/ci/sample-ocp-app:$(tasks.parse-tag.results.tag-name)
  workspaces:
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
YAML

I merged the pull request and then pushed a tag with the new version 1.2.2 to the application repository. As you can see below, all GitHub commits succeeded. Let’s review the details.

As you can see, the sample-ocp-app-tag pipeline has been triggered in response to the new tag.

gitops-promoter-release-pipeline

Below is the pipeline flow as seen in OpenShift. In the s2i-java step, the pipeline built the image and pushed it to the internal registry. To run it in the cluster, we must use the following image tag: image-registry.openshift-image-registry.svc:5000/ci/sample-ocp-app:1.2.2.

gitops-promoter-image-build

Using GitOps Promoter with Argo CD

Next Steps

In the previous steps, we built an image with the new application version. We moved from the pull request stage to pushing a new version tag. So now we can move on to the CD phase. We will deploy our image across the three environments: dev → test → prod.

Integrate GitOps Promoter with Configuration Repository

Before we move on to the promotion, we still need to configure the GitOps Promoter for our configuration repository. As a reminder, at the start we created a gitops-promoter Secret containing the private key for our GitHub application. We’ll start by creating an ScmProvider object, which references the aforementioned Secret and our GitHub application.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ScmProvider
metadata:
  name: github-piomin
  namespace: openshift-gitops
spec:
  github:
    appID: 5236551
  secretRef:
    name: gitops-promoter
YAML

Then, we must create the GitRepository object, which refers to the previously created ScmProvider.

apiVersion: promoter.argoproj.io/v1alpha1
kind: GitRepository
metadata:
  name: gitops-apps-config
  namespace: openshift-gitops
spec:
  github:
    name: gitops-apps-config
    owner: piomin
  scmProviderRef:
    kind: ScmProvider
    name: github-piomin
YAML

The next step is to instantiate the PromotionStrategy object. We will promote between the three environments based on the health status of the Argo CD application from the previous environment.

apiVersion: promoter.argoproj.io/v1alpha1
kind: PromotionStrategy
metadata:
  name: sample-ocp-app
  namespace: openshift-gitops
spec:
  activeCommitStatuses:
    - key: argocd-health
  environments:
    - autoMerge: true
      branch: environment/dev
    - autoMerge: false
      branch: environment/test
    - autoMerge: false
      branch: environment/prod
  gitRepositoryRef:
    name: gitops-apps-config
YAML

GitOps Promoter itself offers various promotion criteria. In this article, I’d like to show you one of the simpler forms of promotion based on the ArgoCDCommitStatus object. In this case, it links the PromotionStrategy to the applications in Argo CD responsible for deploying to subsequent environments. Applications are selected based on the appName label.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ArgoCDCommitStatus
metadata:
  name: sample-ocp-app
  namespace: openshift-gitops
spec:
  applicationSelector:
    matchLabels:
      appName: sample-ocp-app
  key: argocd-health
  promotionStrategyRef:
    name: sample-ocp-app
YAML

This ApplicationSet generates Argo CD Applications for dev, test, and prod. The appName: sample-ocp-app label links all environment-specific Applications to the corresponding PromotionStrategy in GitOps Promoter.

Argo CD Source Hydrator automatically renders the Helm chart using environment-specific values and commits the resulting manifests to environment/<env>-next branches. GitOps Promoter can then promote these changes to the corresponding active branches, environment/<env> enabling a Git-based promotion workflow across environments.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: sample-ocp-app
  namespace: openshift-gitops
spec:
  generators:
    - list:
        elements:
          - autoSync: true
            env: dev
          - autoSync: false
            env: test
          - autoSync: false
            env: prod
  goTemplate: true
  template:
    metadata:
      labels:
        appName: sample-ocp-app
      name: 'sample-ocp-app-{{ .env }}'
    spec:
      destination:
        namespace: 'pminkows-{{ .env }}'
        server: 'https://kubernetes.default.svc'
      project: default
      sourceHydrator:
        drySource:
          helm:
            valueFiles:
              - values-{{ .env }}.yaml
            releaseName: sample-ocp-app
          path: apps/sample-ocp-app
          repoURL: 'https://github.com/piomin/gitops-apps-config.git'
          targetRevision: HEAD
        hydrateTo:
          targetBranch: environment/{{ .env }}-next
        syncSource:
          path: apps/sample-ocp-app
          targetBranch: environment/{{ .env }}
  templatePatch: |
    spec:
      destination:
        server: 'https://kubernetes.default.svc'
    {{- if .autoSync }}
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
    {{- end }}
YAML

Finally, the last element before we proceed to the demo phase. We also need an Argo CD application to handle the promotion itself. The key to this is the PromotionStrategy object, which I’ve already shown you earlier in this section. We need to create an Argo CD application that synchronizes all the CRs discussed earlier, provided by the GitOps Promoter, on the cluster. These are located in the argocd-showcase repository, in the promoter directory.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sample-ocp-app-promoter
  namespace: openshift-gitops
spec:
  destination:
    namespace: openshift-gitops
    server: https://kubernetes.default.svc
  project: default
  source:
    path: promoter/config
    repoURL: https://github.com/piomin/argocd-showcase.git
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
YAML

Use GitOps Promoter with Argo CD

Below is the status in Argo CD prior to the deployment of the new version (1.2.2) of our sample-ocp-app.

gitops-promoter-argocd

Let’s take the Argo CD application responsible for production deployments as an example. As you can see, it has the source hydrator enabled. Synchronization takes place with the environment/prod branch, where the resulting manifests created by “hydrating” Helm charts are located.

Let’s see how this works in practice. This is a very important step. Please note: we commit the changes with the new version in all values-{env}.yaml files exclusively to the master branch. The rest happens in the background. This is where the source hydrator comes in, breaking our change down into individual environment/{env} branches.

gitops-promoter-new-version

We will start with the dev environment. The commit was made to the environment/dev-next branch by the GitOps Promoter.

Next, the GitOps Promoter app creates a pull request from the environment/dev-next branch to the environment/dev branch. Because we set the “auto-merge” option for the dev environment, everything happens automatically and doesn’t require our approval.

gitops-promoter-overview

This is what it looks like on GitHub.

The deployment to the dev environment was successful. We can see this on the GitOps Promoter side as the promoter-previous-environment field. Then, we can promote the release to the test environment. To do this, click the pull request icon as shown below.

This is what a pull request to the test environment looks like. The “auto-merge” option is disabled this time, so we’ll need to merge it manually via GitHub.

Finally, we can merge the pull request.

Then manually sync the sample-ocp-app-test Argo CD app. After that, we can only monitor the deployment status to the test environment. Finally, we’ll simply repeat the same process in production.

Final Thoughts

In this article, we’ve built an end-to-end delivery workflow by combining Tekton Pipelines as Code with Argo CD GitOps Promoter on OpenShift. The process covers everything from pull request validation and image building to deployment across multiple environments.

The biggest advantage is GitOps Promoter, which introduces a dedicated promotion mechanism directly into the Argo CD ecosystem. It automates the movement of changes from development through test to production using Git branches and pull requests, with support for automatic or manual approvals.

Another key feature is the native GitHub integration. Tekton Pipelines as Code runs CI pipelines and reports results directly on pull requests, while GitOps Promoter manages promotion pull requests between environments. Finally, Argo CD Source Hydrator provides fully rendered Kubernetes manifests with environment-specific values, committed to Git before deployment. Unlike relying solely on helm template. That way, we can review the exact manifests Argo CD will apply.

Leave a Reply