Module 4: Pipelines as Code

Coolstuff Store’s pipeline is working, but the team has spotted a friction point. Every time a developer pushes code, someone has to manually run oc apply to create a new PipelineRun. Webhooks are configured by hand, pipeline definitions live only in the cluster, and there’s no way to see pipeline status directly on a pull request.

Pipelines as Code (PAC) solves all of this. It stores your pipeline definitions in the .tekton/ directory of your application repository, triggers pipeline runs automatically on Git events such as push and pull request, and reports status back to the Git provider so developers see CI results without leaving their browser.

In this module, you’ll connect your Gitea repository to Pipelines as Code, store a PipelineRun template alongside your application code, and trigger your first automated pipeline run by pushing a commit.

Learning objectives

By the end of this module, you’ll be able to:

  • Explain how Pipelines as Code differs from manually managed Tekton pipelines

  • Create a Repository custom resource to connect a Git repository to OpenShift Pipelines

  • Store a PipelineRun template in the .tekton/ directory with the correct event annotations

  • Trigger pipeline runs automatically by pushing to a Git branch

  • Use GitOps PR comments (/retest, /cancel) to control pipeline execution

How Pipelines as Code works

Pipelines as code logo

With a single pane of glass from your SCM provider (GitHub, GitLab, Bitbucket, or Gitea), pipeline runs are automatically triggered for Pull Requests and Push events.

According to the Pipelines as Code project, PAC follows a GitOps model for CI/CD:

  • Pipeline definitions live in Git: PipelineRun templates are stored in a .tekton/ directory in the application repository. They are versioned, reviewed, and merged alongside application code.

  • Events trigger runs automatically: PAC listens for Git events (push, pull request) via a webhook. When a matching event arrives, PAC creates a PipelineRun in the cluster automatically.

  • Status is reported back to Git: PAC updates the pull request or commit status in the Git provider so developers see pass/fail results without checking OpenShift directly.

  • GitOps commands control execution: Authorized users can comment /retest, /test, or /cancel on a pull request to control pipeline runs without touching the cluster.

The connection between OpenShift and the Git repository is managed by the Repository custom resource. One Repository per Git repository tells PAC: which namespace to create PipelineRuns in, how to authenticate with the Git provider, and which webhook secret to use.

Pipelines as Code is included with Red Hat OpenShift Pipelines 1.21 and is installed automatically by the operator. No separate installation is needed.

Exercise 1: Set up the Pipelines as Code repository

In this exercise, you’ll create a personal access token in Gitea, store it in a Kubernetes Secret, and create a Repository custom resource that connects your coolstuff-app Gitea repository to OpenShift Pipelines.

Verify Pipelines as Code is running

  1. In the Terminal tab, confirm you are in the correct project:

    oc project tekton-workshop-%OPENSHIFT_USERNAME%
  2. Confirm the PAC controller is installed and ready to use.

    oc api-resources | grep pipelinesascode

    Expected output:

    NAME                              SHORTNAME   APIVERSION                          NAMESPACED   KIND
    openshiftpipelinesascodes         opac,pac    operator.tekton.dev/v1alpha1        false        OpenShiftPipelinesAsCode
    repositories                      repo        pipelinesascode.tekton.dev/v1alpha1 true         Repository
  3. The PAC controller’s public route URL is needed for this excersise, Gitea will use this to send webhook events to OpenShift:

    https://pipelines-as-code-controller-openshift-pipelines.%APPS_HOSTNAME_SUFFIX%

    Copy this URL. You will need it when configuring the webhook in Gitea.

Create a Gitea personal access token

  1. Open the Gitea tab again and log in with your credentials if you have been logged out.

    If you have closed the Gitea tab - Click on the 'Developer Repository' button below

    Then, click on 'Sign In' and login via OpenShift as user %OPENSHIFT_USERNAME%/%OPENSHIFT_PASSWORD%

    Gitea start page
    Figure 1. Gitea’s start page with sign in
  2. Click your profile avatar in the top right corner, then select Settings.

  3. In the top menubar, click Applications.

    Gitea settings
    Figure 2. Gitea’s settings page
  4. Under Manage access tokens, fill in:

    • Token name: pac-token

    • Permissions:

      • Select Issues: Read/Write

      • And Repository: Read/Write

  5. Click Generate token and copy the generated token immediately. It is shown only once.

Create the webhook secret and Kubernetes Secret

  1. In the terminal, generate a random webhook secret:

    WEBHOOK_SECRET=$(openssl rand -hex 20)
    echo "Webhook secret: $WEBHOOK_SECRET"
  2. Copy the value shown. You will need it when creating the Gitea webhook.

  3. Create a Kubernetes Secret that stores both the Gitea token and webhook secret. Replace <your-gitea-token> with the token you copied from Gitea, and <your-webhook-secret> with the value from the previous step:

    cat > gitea-pac-secret.yaml << 'EOF'
    apiVersion: v1
    kind: Secret
    metadata:
      name: gitea-pac-secret
      namespace: tekton-workshop-%OPENSHIFT_USERNAME%
    type: Opaque
    stringData:
      provider.token: "<your-gitea-token>"
      webhook.secret: "<your-webhook-secret>"
    EOF
  4. Edit the file(using vim, nano etc) and replace the placeholder values, then apply it:

    oc apply -f gitea-pac-secret.yaml

    Expected output:

    secret/gitea-pac-secret created

Create the Repository custom resource

  1. Create the Repository resource that links your Gitea repository to OpenShift Pipelines:

    cat > coolstuff-repository.yaml << 'EOF'
    apiVersion: pipelinesascode.tekton.dev/v1alpha1
    kind: Repository
    metadata:
      name: coolstuff-app
      namespace: tekton-workshop-%OPENSHIFT_USERNAME%
    spec:
      url: "https://gitea-workshop-gitea.%APPS_HOSTNAME_SUFFIX%/%OPENSHIFT_USERNAME%/coolstuff-app"
      git_provider:
        url: "https://gitea-workshop-gitea.%APPS_HOSTNAME_SUFFIX%"
        secret:
          name: "gitea-pac-secret"
          key: "provider.token"
        webhook_secret:
          name: "gitea-pac-secret"
          key: "webhook.secret"
    EOF
    Make sure your User-id is correct in the first Gitea-url before applying.
  2. Apply the Repository:

    oc apply -f coolstuff-repository.yaml

    Expected output:

    repository.pipelinesascode.tekton.dev/coolstuff-app created
  3. Confirm the Repository was created:

    oc get repository coolstuff-app

    Expected output:

    NAME             URL
    coolstuff-app    https://gitea-workshop.../userXX/coolstuff-app
  4. Verify that the repository is visible in the Pipelines UI as well. In the OpenShift console, fold out Pipelines and click in Pipelines.

    1. Click on the Repositories tab in the center view. The coolstuff-app repository should be visible.

pipelines-as-code repository view

Configure the Gitea webhook

  1. In Gitea, navigate to your coolstuff-app repository and click Settings, then Webhooks.

  2. Click Add webhook, then select Gitea.

    Add webhook in Gitea
  3. Fill in the webhook details as below, leave the other values as default:

    • Target URL: https://pipelines-as-code-controller-openshift-pipelines.%APPS_HOSTNAME_SUFFIX%

    • Secret: paste the webhook secret value you generated ($WEBHOOK_SECRET)

    • Trigger on: select Custom Events

      • Then mark Push, Pull Request Comment and Pull Request

  4. Click Add webhook.

  5. In the webhook screen, verify that the webhook has been created.

    Added webhook in Gitea
  6. Click on the pencil on the newly created webhook

  7. Click Test delivery to confirm Gitea can reach the PAC controller.

    1. A 202 response confirms the connection is working.

      Gitea webhook settings page showing the PAC controller URL filled in and a successful 200 test delivery response
      Figure 3. Gitea webhook configured to send events to Pipelines as Code
  8. Verify at the bottom of the screen that all looks good(green checkmark on the webhook).

    1. Click the randomly generated string to fold the Request/Response details up.

      Webhook verification in Gitea

Exercise 2: Add a PipelineRun template to the repository

With the Repository resource in place, PAC will watch your Gitea repository for events. Now you’ll add a PipelineRun template to the .tekton/ directory.

PAC automatically detects this file, reads its event annotations, and creates a PipelineRun whenever a matching Git event arrives.

Understanding the .tekton directory structure

PAC reads pipeline definitions from the .tekton/ directory at the root of your repository. Each YAML file in that directory can contain a PipelineRun or TaskRun resource with special annotations that control when it runs:

  • pipelinesascode.tekton.dev/on-event: which Git events trigger this run. Values: [push], [pull_request], [push, pull_request].

  • pipelinesascode.tekton.dev/on-target-branch: which branch(es) the event must target. Values: [main], [main, develop], or a glob pattern like [feature/*].

  • pipelinesascode.tekton.dev/max-keep-runs: how many completed PipelineRuns to retain before pruning older ones.

PAC also injects dynamic variables into the template before creating the PipelineRun:

  • {{ revision }}: the commit SHA that triggered the run

  • {{ repo_url }}: the full URL of the repository

  • {{ source_branch }}: the branch the event came from

  • {{ target_branch }}: the target branch (relevant for pull requests)

Create the .tekton directory and PipelineRun template

  1. In the Gitea web UI, navigate to your coolstuff-app repository.

  2. Click Add file button to create a new file.

    Gitea add a new file to the Coolstuff app repository
  3. Set the file path to .tekton/push-pipeline.yaml (include the directory name in the path — Gitea creates the directory automatically).

  4. Click on the first empty row in the file editor in the central view then paste the following PipelineRun template as the file content:

    apiVersion: tekton.dev/v1
    kind: PipelineRun
    metadata:
      name: coolstuff-build-push
      annotations:
        pipelinesascode.tekton.dev/on-event: "[push]"
        pipelinesascode.tekton.dev/on-target-branch: "[main]"
        pipelinesascode.tekton.dev/max-keep-runs: "5"
    spec:
      pipelineSpec:
        tasks:
          - name: build
            taskRef:
              resolver: cluster
              params:
                - name: kind
                  value: task
                - name: name
                  value: build-coolstuff-app
                - name: namespace
                  value: tekton-workshop-%OPENSHIFT_USERNAME%
            params:
              - name: app-name
                value: "coolstuff-store-app"
              - name: app-version
                value: "{{ revision }}"
            workspaces:
              - name: source
                workspace: shared-data
          - name: summarize
            runAfter:
              - build
            taskRef:
              resolver: cluster
              params:
                - name: kind
                  value: task
                - name: name
                  value: summarize-build
                - name: namespace
                  value: tekton-workshop-%OPENSHIFT_USERNAME%
            params:
              - name: app-name
                value: "coolstuff-store-app"
              - name: app-version
                value: "{{ revision }}"
            workspaces:
              - name: source
                workspace: shared-data
        workspaces:
          - name: shared-data
      workspaces:
        - name: shared-data
          volumeClaimTemplate:
            spec:
              accessModes:
                - ReadWriteOnce
              resources:
                requests:
                  storage: 500Mi
    The {{ revision }} placeholder is replaced by PAC with the actual commit SHA at runtime. The Tasks build-coolstuff-app and summarize-build must exist in the cluster. They were created in Modules 2 and 3.
    This template, like the pipelines before uses VolumeClaimTemplate as the workspace backing to store data between tasks.
  5. Scroll down to the bottom of the page andSet the commit message to Add PAC pipeline definition

  6. Click Commit changes to commit directly to main.

Watch the pipeline trigger automatically

  1. Switch back to the Terminal tab and watch for new PipelineRuns:

    oc get pipelinerun -w

    Within a few seconds of the Gitea commit, PAC creates a new PipelineRun automatically:

    NAME                                           SUCCEEDED   REASON    STARTTIME   COMPLETIONTIME
    coolstuff-build-push-<revision>-push-...      Unknown     Running   5s
    coolstuff-build-push-<revision>-push-...      True        Succeeded 35s         36s

    Press Ctrl+C to stop watching.

  2. View the logs of the PAC-triggered run:

    tkn pipelinerun logs --last -f

    Expected output includes:

    [build : validate] === Validating coolstuff-store-app v<commit-sha> ===
    [build : validate] Validation passed.
    [build : test] All tests passed.
    [build : package] Package created: coolstuff-store-app-<commit-sha>.tar.gz
    [summarize : summarize] Artifact    : coolstuff-store-app-<commit-sha>.tar.gz
    [summarize : summarize] Status      : SUCCESS -- ready for deployment
  3. View the pipeline run in the OpenShift console.

  4. Navigate to Pipelines, then PipelineRuns.

    1. The PAC-triggered run will be listed alongside the manually created runs from earlier modules: coolstuff-build-push-RANDOM_ID.

      OpenShift console PipelineRuns list showing a PAC-triggered run with a commit SHA in the name alongside previous manually created runs
      Figure 4. PAC-triggered PipelineRun created automatically on Git push
  5. Click on the PipelineRun to see more details, like commit-id, triggering event and SHA.

    OpenShift console PipelineRuns list showing a PAC-triggered run with a commit SHA in the name alongside previous manually created runs
    Figure 5. Details on the PAC run, with SHA

Verify

List the different pipelineruns and look at status of the runs using the cli.

tkn pipelinerun list | head -5

Look at more details for the pac-run using the terminal:

oc get pipelinerun coolstuff-build-push-lvkbs -o jsonpath='{.metadata.annotations}' | jq

Expected output:

{
  "chains.tekton.dev/signed": "true",
  "pipelinesascode.tekton.dev/branch": "refs/heads/main",
  "pipelinesascode.tekton.dev/controller-info": "{\"name\":\"default\",\"configmap\":\"pipelines-as-code\",\"secret\":\"pipelines-as-code-secret\", \"gRepo\": \"pipelines-as-code\"}",
  "pipelinesascode.tekton.dev/event-type": "push",
  "pipelinesascode.tekton.dev/git-auth-secret": "pac-gitauth-dghdpx",
  "pipelinesascode.tekton.dev/git-provider": "gitea",
  "pipelinesascode.tekton.dev/log-url": "https://console-openshift-console.apps.cluster-twf54.twf54.sandbox2398.opentlc.com/k8s/ns/tekton-workshop-user1/tekton.dev~v1~PipelineRun/coolstuff-build-push-lvkbs",
  "pipelinesascode.tekton.dev/max-keep-runs": "5",
  "pipelinesascode.tekton.dev/on-event": "[push]",
  "pipelinesascode.tekton.dev/on-target-branch": "[main]",
  "pipelinesascode.tekton.dev/original-prname": "coolstuff-build-push",
  "pipelinesascode.tekton.dev/repo-url": "https://gitea-workshop-gitea.apps.cluster-twf54.twf54.sandbox2398.opentlc.com/user1/coolstuff-app",
  "pipelinesascode.tekton.dev/repository": "coolstuff-app",
  "pipelinesascode.tekton.dev/scm-reporting-plr-started": "true",
  "pipelinesascode.tekton.dev/sender": "user1",
  "pipelinesascode.tekton.dev/sha": "87efe0e239cf585d646bc93f6800f482c2d9e5c3",
  "pipelinesascode.tekton.dev/sha-title": "Update .tekton/push-pipeline.yaml\n",
  "pipelinesascode.tekton.dev/sha-url": "https://gitea-workshop-gitea.apps.cluster-twf54.twf54.sandbox2398.opentlc.com/user1/coolstuff-app/commit/87efe0e239cf585d646bc93f6800f482c2d9e5c3",
  "pipelinesascode.tekton.dev/source-branch": "refs/heads/main",
  "pipelinesascode.tekton.dev/source-repo-url": "https://gitea-workshop-gitea.apps.cluster-twf54.twf54.sandbox2398.opentlc.com/user1/coolstuff-app",
  "pipelinesascode.tekton.dev/state": "completed",
  "pipelinesascode.tekton.dev/url-org": "user1",
  "pipelinesascode.tekton.dev/url-repository": "coolstuff-app",
  "results.tekton.dev/record": "tekton-workshop-user1/results/d54963a0-1cc6-48d2-896c-107f1bd7a00d/records/d54963a0-1cc6-48d2-896c-107f1bd7a00d",
  "results.tekton.dev/recordSummaryAnnotations": "{\"repo\":\"coolstuff-app\",\"commit\":\"87efe0e239cf585d646bc93f6800f482c2d9e5c3\",\"eventType\":\"push\"}",
  "results.tekton.dev/result": "tekton-workshop-user1/results/d54963a0-1cc6-48d2-896c-107f1bd7a00d",
  "results.tekton.dev/stored": "true"
}
  • The most recent PipelineRun name includes the commit SHA

  • Status shows Succeeded

  • The run was created automatically without an oc apply command

Exercise 3: Use GitOps commands to control pipeline runs

One of the most useful features of Pipelines as Code is the ability to control pipeline runs with comments on a pull request. In this exercise, you’ll create a pull request in Gitea, observe the automatic pipeline trigger, and use /retest to re-trigger a run without pushing new code.

Create a feature branch and work with pull requests

  1. In the Gitea web ui, navigate to your coolstuff-app repository.

  2. Click Add File then New file.

  3. Name the file change.txt and add a line in the file — for example, add a line ## Module 4 update.

  4. Set the commit message to Add module 4 update note.

  5. Instead of committing directly to main, select Create a new branch for this commit. Name the branch feature/module-4-update.

  6. Click Propose file change to create the branch.

    Gitea new file added and committed
    Figure 6. New file committed, pull request waiting
  7. Click on new Pull Request to open a pull request to merge your change into the main branch.

    1. On the pull request creation page, confirm the base branch is main and click Create Pull Request.

      Gitea create pull request
  8. Verify that your branch has been created.

  9. Using the terminal verify that no new pipelineRun has been triggered.

    1. Since we only have the main branch configured to trigger on push no PipelineRun will be started for feature branches.

      oc get pipelinerun
  10. Click Create merge commit and then Merge your pull request to main. This will trigger a new pipelineRun.

Add triggers on Pull Request events

  1. To fix pipelineRuns on pull request we need to add a trigger to our pipelinerun file in the .tekton folder in Gitea.

    1. Let’s take a look at the .tekton/push-pipeline.yaml file in Gitea. It contains annotations only includes push and main as a trigger events.

  2. We will now update the file to add pull request support as shown below.

    1. To add pull request triggering, edit .tekton/push-pipeline.yaml in Gitea and update the annotation:

      pipelinesascode.tekton.dev/on-event: "[push, pull_request]"

      Commit this change to main .

  3. Return to the Terminal tab and watch for a new PipelineRun triggered by the push event:

    oc get pipelinerun -w
  4. When the run has completed, edit the change.txt file by clickin on the file in the repository and then cliciking on the pencil icon on the right side.

    1. Add a new line below the first and then scroll down to the bottom and choose Create a new branch for this commit and start a pull request

    2. Name the branch feature/module4-newedit then click on Propose file change

  5. Click on New Pull Request then Click on Create Pull request. This will now trigger a pipelineRun in OpenShift.

  6. In the OpenShift console in the left hand menu fold out Pipelines then click Pipelines. In the center view click on Repositories.

    1. In the list you can see the status of the latest run and see the Event is pull_request.

    2. Open up the pipelineRun to see more details.

Gitea pull request triggered

Use GitOps commands in pull request comments

Once a pipeline run has completed for the pull request, you can control re-execution using PR comments.

  1. In Gitea, open up the previous pull request.

    Gitea open pull request
  2. Click Comment and type /retest in the comment body and submit.

    PAC detects the comment from an authorized user and triggers a new PipelineRun for the same commit. This is useful when you want to retry a CI run without pushing new code — for example, after a transient infrastructure failure.

  3. Watch the new run appear:

    tkn pipelinerun list | head -5
  4. To cancel a running PipelineRun, post a /cancel comment in the pull request while a run is in progress. PAC cancels all matching running PipelineRuns for that commit.

Available GitOps commands, as documented in the Pipelines as Code GitOps commands reference:

  • /retest — Re-runs all failed PipelineRuns for the current commit

  • /test — Runs all matching PipelineRuns regardless of current status

  • /retest <pipelinerun-name> — Re-runs a specific PipelineRun by name

  • /cancel — Cancels all running PipelineRuns for the current commit

  • /ok-to-test — Allows repository owners to approve pipeline runs for pull requests from external contributors who are not already authorized

Verify

tkn pipelinerun list -o json | jq '.items[] | {name: .metadata.name, sha: .metadata.labels["pipelinesascode.tekton.dev/sha"], status: .status.conditions[0].reason}'
  • Multiple PipelineRuns exist with the same commit SHA in their names (showing the /retest triggered a new run on the same commit)

  • Status of the different runs

{
  "name": "coolstuff-build-push-49s2d",
  "sha": "4b6a3c4f77745d7e2f742574d5ae7ed2d6ac2dfa",
  "status": "Cancelled"
}
{
  "name": "coolstuff-build-push-xj2hf",
  "sha": "4b6a3c4f77745d7e2f742574d5ae7ed2d6ac2dfa",
  "status": "Succeeded"
}
{
  "name": "coolstuff-build-push-bqr4v",
  "sha": "4b6a3c4f77745d7e2f742574d5ae7ed2d6ac2dfa",
  "status": "Succeeded"
}
{
  "name": "coolstuff-build-push-4wd2p",
  "sha": "783c132b71495c4c5dc8985ff90738867c5c75e6",
  "status": "Succeeded"
}
{
  "name": "coolstuff-build-push-dbrqf",
  "sha": "6bd30dd1a55b8f0c6cd4d739aaef36e27a3d48c9",
  "status": "Succeeded"
}
{
  "name": "coolstuff-build-push-h6hb9",
  "sha": "b3cf23d18e17701ad4b7f7891fbe3b0f3b0c8548",
  "status": "Succeeded"
}

Module summary

Coolstuff Store’s team can now push code and see their pipeline run automatically — no manual PipelineRun creation, no separate webhook scripting, and no need to check OpenShift to see if the build passed.

What you accomplished:

  • Verified Pipelines as Code is installed and running in the cluster

  • Created a Repository custom resource linking the Gitea repository to OpenShift Pipelines

  • Configured a Gitea webhook to send push and pull request events to the PAC controller

  • Stored a PipelineRun template in the .tekton/ directory with event annotations

  • Triggered an automatic pipeline run by pushing a commit to Gitea

  • Used /retest in a pull request comment to re-trigger a run without pushing new code

Key takeaways:

  • Pipelines as Code stores CI/CD definitions in Git alongside application code, making pipelines reviewable and versionable.

  • The Repository CRD is the link between a Git repository and an OpenShift namespace. 1 Repository per Git repository.

  • Annotations on the PipelineRun template control which Git events trigger the run and which branches are included.

  • PAC injects dynamic variables like {{ revision }} and {{ source_branch }} at runtime, removing the need to hardcode commit references.

  • GitOps commands (/retest, /cancel) give developers pipeline control without cluster access.

Learning outcomes

By completing this module, you should now understand:

  • How Pipelines as Code automates pipeline triggering by listening to Git events through a webhook, removing the need for manual PipelineRun creation

  • The role of the Repository custom resource as the connection point between a Git repository and an OpenShift namespace

  • How .tekton/ directory annotations (on-event, on-target-branch) filter which Git events trigger which pipeline definitions

  • How PAC dynamic variables like {{ revision }} and {{ source_branch }} make pipeline templates self-contained and reusable across commits and branches

  • How GitOps PR commands give developers pipeline control directly in their Git workflow without requiring cluster access

Next steps:

Module 5: Monitoring and troubleshooting covers how to investigate failed pipeline runs, read logs at the task and step level, and use the console to diagnose problems quickly.