AI-Driven Development with Tekton and Claude Code on OpenShift

AI-Driven Development with Tekton and Claude Code on OpenShift

In this article, we will learn how to integrate Claude Code with Tekton Pipelines on OpenShift to automatically implement changes requested through GitHub issues. We will use GitHub webhooks to trigger a Tekton pipeline whenever someone adds a comment starting with @tekton. The pipeline will then clone the repository, communicate with the Claude Sonnet model, implement the requested changes, and finally create a pull request with the generated code.

Actually, we can look at this example from a slightly broader perspective. Our goal is to automate the entire development workflow with AI, from receiving a development request to preparing the changes for review. At the same time, we want to run everything in a controlled OpenShift environment, an enterprise Kubernetes platform. It lets us manage container permissions, credentials, resource limits, and workload isolation using standard Kubernetes mechanisms. Of course, we still need to configure these mechanisms properly, especially when running an AI agent that can modify source code.

We have already explored a similar approach in my previous articles about OpenCode on GitHub with Kubernetes and Claude Code on OpenShift with vLLM and Dev Spaces. This time, however, we will take a slightly different approach. Instead of relying on GitHub Actions or running Claude Code inside a development environment, we will use Tekton to orchestrate the entire process directly on OpenShift.

Actually, this is one of the things I like about Tekton. We can use it for much more than building applications or deploying containers. Since every task runs in a container, we can run almost any tool, including AI coding agents. In our case, Claude Code will act as a developer that receives instructions from GitHub, modifies the application, and prepares the changes for review.

Prerequisites

Before we start, we need an OpenShift cluster with Red Hat OpenShift Pipelines installed. This operator provides Tekton Pipelines and Tekton Triggers, which we will use to handle GitHub events and execute our workflow.

We also need a GitHub repository containing the application we want to modify. In this example, I will use a Java application built with Maven. However, the solution doesn’t depend on Java or Maven. Claude Code can work with different programming languages and project structures.

Finally, we need access to the Anthropic API and a GitHub token with permission to push branches and create pull requests. We will store both credentials in an OpenShift Secret rather than putting them directly into the pipeline definition.

Here are the main components of our solution:

  • GitHub — hosts the source code, issues, and pull requests.
  • Tekton Triggers — receives GitHub webhook events and starts the pipeline.
  • Tekton Pipelines — orchestrates the execution of our tasks on OpenShift.
  • Claude Code — analyzes the repository and implements the requested changes using Claude Sonnet.
  • GitHub CLI — creates a pull request after the pipeline pushes the generated changes.

Build the Claude Code Container Image

Let’s start by preparing a container image for our AI coding agent. First, we need Claude Code CLI to communicate with the Anthropic API and GitHub CLI to create pull requests. We also need Git to clone the repository, commit changes, and push a new branch.

Finally, we will use Red Hat Universal Base Image with Node.js and install the required tools. Claude Code is available through the @anthropic-ai/claude-code npm package. We don’t need an interactive environment because Tekton will execute Claude Code in non-interactive mode. Here is the Dockerfile.

FROM registry.access.redhat.com/ubi9/nodejs-22

USER 0

RUN dnf install -y git jq tar gzip \
    && dnf clean all

RUN npm install -g @anthropic-ai/claude-code

# GitHub CLI z oficjalnego archiwum
ARG GH_VERSION=2.83.2
RUN curl -fsSL \
    "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
    -o /tmp/gh.tar.gz \
    && tar -xzf /tmp/gh.tar.gz -C /tmp \
    && cp /tmp/gh_${GH_VERSION}_linux_amd64/bin/gh /usr/local/bin/gh \
    && rm -rf /tmp/gh*

USER 1001
Dockerfile

Now, let’s build the image and push it to Quay.io.

$ podman build -t quay.io/pminkows/claude-code:latest .
$ podman push quay.io/pminkows/claude-code:latest
ShellSession

That’s all we need for our custom image. We will reference it later in the Tekton Pipeline.

Configure Tekton Triggers on OpenShift

The next step is to connect GitHub with our OpenShift cluster. We will use Tekton Triggers to receive webhook events and create PipelineRun. We need three resources: TriggerBinding, TriggerTemplate, and EventListener.

First, let’s create a TriggerBinding. It extracts four values from the GitHub webhook payload: repository clone URL, full repository name, issue number, and comment body.

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: issue-comment
  namespace: ci
spec:
  params:
    - name: repo-url
      value: $(body.repository.name)
    - name: repo-full-name
      value: $(body.repository.full_name)
    - name: issue
      value: $(body.issue.number)
    - name: comment
      value: $(body.comment.body)
YAML

Next, we need a TriggerTemplate. It creates a PipelineRun for our my-pipeline pipeline and passes the values extracted by TriggerBinding.

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: issue-comment
  namespace: ci
spec:
  params:
    - name: repo-url
    - name: repo-full-name
    - name: issue
    - name: comment
  resourcetemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: issue-comment-
      spec:
        params:
          - name: repo-url
            value: $(tt.params.repo-url)
          - name: repo-full-name
            value: $(tt.params.repo-full-name)
          - name: issue
            value: $(tt.params.issue)
          - name: comment
            value: $(tt.params.comment)
        pipelineRef:
          name: my-pipeline
YAML

After that, let’s create an EventListener that connects our TriggerBinding and TriggerTemplate. We will also configure a CEL interceptor to filter incoming requests. We only want to accept GitHub issue_comment events with the created action and a comment body starting with @tekton. This means that regular comments won’t start the pipeline. We can use @tekton followed by any instruction we want to send to Claude Code.

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: issue-comment
  namespace: ci
spec:
  serviceAccountName: pipeline
  triggers:
    - bindings:
        - kind: TriggerBinding
          ref: issue-comment
      interceptors:
        - params:
            - name: filter
              value: 'header.match(''X-GitHub-Event'', ''issue_comment'') && body.action == ''created'' && body.comment.body.startsWith(''@tekton'')'
          ref:
            kind: ClusterInterceptor
            name: cel
      name: issue-comment
      template:
        ref: issue-comment
YAML

Once we deploy the EventListener, Tekton creates a Kubernetes Service that receives incoming webhook requests.

$ oc get svc
NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
el-issue-comment   ClusterIP   172.30.181.196   <none>        8080/TCP,9000/TCP   7h34m
ShellSession

Finally, we have to expose the service outside the cluster as Route using the following command:

oc expose svc el-issue-comment
ShellSession

For production environments, we should also validate GitHub webhook signatures and verify the comment author’s permissions. The @tekton prefix is useful for filtering commands, but it doesn’t provide authentication. For now, let’s copy the Route to use it later when configuring the webhook in GitHub.

oc get route el-issue-comment
ShellSession

Create the Tekton Pipeline

Now we can define the pipeline that executes our AI agent. Before running it, we need to configure two credentials: ANTHROPIC_API_KEY for Claude Code and GH_TOKEN for GitHub operations. We will store them in an OpenShift Secret named claude-github-secrets.

oc create secret generic claude-github-secrets \
  --from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
  --from-literal=GH_TOKEN="$GH_TOKEN"
ShellSession

Our pipeline name is my-pipeline. It accepts the repository URL, full repository name, issue number, comment body, and target branch. The default target branch is master. For simplicity, we will use two tasks. The first Task, issue-info, uses an Alpine Linux container to print the repository URL, issue number, and comment body. It doesn’t do anything complicated – only helps to verify that Tekton correctly receives the information from GitHub.

The second Task, implement-and-create-pr, runs our custom Claude Code image. It clones the repository, creates a new Git branch, and prepares a prompt using the GitHub comment. Since the comment starts with @tekton, we remove that prefix and the whitespace following it before passing the instruction to Claude Code. Then, the task checks whether the comment contains an actual request and executes Claude Code in non-interactive mode using the Sonnet model.

The agent to read and modify files. Once Claude Code finishes, the pipeline checks the modified files, creates a commit, and pushes the new branch. Finally, it uses GitHub CLI to create a pull request against the target branch. Here is the complete pipeline definition.

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: my-pipeline
spec:
  params:
    - name: repo-url
      type: string
    - name: repo-full-name
      type: string
    - name: issue
      type: string
    - name: comment
      type: string
    - name: base-branch
      type: string
      default: main

  tasks:
    # ==========================================
    # TASK 1: Print GitHub Issue information
    # ==========================================
    - name: issue-info
      params:
        - name: repo-url
          value: $(params.repo-url)
        - name: issue
          value: $(params.issue)
        - name: comment
          value: $(params.comment)

      taskSpec:
        metadata: {}
        params:
          - name: repo-url
            type: string
          - name: issue
            type: string
          - name: comment
            type: string

        steps:
          - name: print
            image: alpine:3.20
            env:
              - name: REPO
                value: $(params.repo-url)
              - name: ISSUE
                value: $(params.issue)
              - name: COMMENT
                value: $(params.comment)
            script: |
              #!/bin/sh
              echo "===== GitHub Issue Comment ====="
              echo "Repository: $REPO"
              echo "Issue: $ISSUE"
              echo "Comment: $COMMENT"
              echo "================================"

    # ==========================================
    # TASK 2: Claude Code + Create Pull Request
    # ==========================================
    - name: implement-and-create-pr
      runAfter:
        - issue-info

      params:
        - name: repo-url
          value: $(params.repo-url)
        - name: repo-full-name
          value: $(params.repo-full-name)
        - name: issue
          value: $(params.issue)
        - name: comment
          value: $(params.comment)
        - name: base-branch
          value: $(params.base-branch)

      taskSpec:
        params:
          - name: repo-url
            type: string
          - name: repo-full-name
            type: string
          - name: issue
            type: string
          - name: comment
            type: string
          - name: base-branch
            type: string

        volumes:
          - name: work
            emptyDir: {}

        steps:
          - name: implement
            image: image-registry.example.com/ai/claude-code:1.0
            workingDir: /workspace

            volumeMounts:
              - name: work
                mountPath: /workspace

            computeResources:
              requests:
                memory: 1Gi
                cpu: "500m"
              limits:
                memory: 4Gi
                cpu: "2"

            env:
              - name: ANTHROPIC_API_KEY
                valueFrom:
                  secretKeyRef:
                    name: claude-github-secrets
                    key: ANTHROPIC_API_KEY

              - name: GH_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: claude-github-secrets
                    key: GH_TOKEN

              - name: REPO_URL
                value: $(params.repo-url)
              - name: REPO_FULL_NAME
                value: $(params.repo-full-name)
              - name: ISSUE_NUMBER
                value: $(params.issue)
              - name: COMMENT
                value: $(params.comment)
              - name: BASE_BRANCH
                value: $(params.base-branch)
              - name: HOME
                value: /workspace/home

            script: |
              #!/usr/bin/env bash
              set -euo pipefail

              echo "=== Preparing environment ==="

              mkdir -p "$HOME"

              git config --global user.name "tekton-ai"
              git config --global user.email \
                "tekton-ai@users.noreply.github.com"

              # GitHub authentication
              git config --global credential.helper \
                '!f() { echo username=x-access-token; echo password=$GH_TOKEN; }; f'

              echo "=== Cloning repository ==="

              git clone \
                --branch "$BASE_BRANCH" \
                -- "$REPO_URL" /workspace/repo

              cd /workspace/repo

              BRANCH="ai/issue-${ISSUE_NUMBER}-${HOSTNAME}"

              git switch -c "$BRANCH"

              echo "=== Preparing Claude prompt ==="

              printf '%s\n' "$COMMENT" \
                > /workspace/issue-request.txt

              {
                echo "Implement changes in this repository."
                echo
                echo "GitHub Issue: #${ISSUE_NUMBER}"
                echo
                echo "User request:"
                cat /workspace/issue-request.txt
                echo
                echo "Instructions:"
                echo "- Analyze the existing code."
                echo "- Implement the requested changes."
                echo "- Add or update tests where appropriate."
                echo "- Do not push changes."
                echo "- Do not create a pull request."
                echo "- Do not modify CI security settings."
                echo "- Do not access secrets."
              } > /workspace/claude-prompt.txt

              echo "=== Running Claude Sonnet ==="

              claude -p \
                --model sonnet \
                --permission-mode acceptEdits \
                --allowedTools "Read,Edit,Write,Glob,Grep" \
                < /workspace/claude-prompt.txt

              echo "=== Checking changes ==="

              git status --short

              if [ -z "$(git status --porcelain)" ]; then
                echo "No changes generated by Claude"
                exit 1
              fi

              echo "=== Committing changes ==="

              git add -A

              git commit -m \
                "feat: implement issue #${ISSUE_NUMBER}"

              echo "=== Pushing branch ==="

              git push origin "$BRANCH"

              echo "=== Creating Pull Request ==="

              gh pr create \
                --repo "$REPO_FULL_NAME" \
                --base "$BASE_BRANCH" \
                --head "$BRANCH" \
                --title "AI: implement issue #${ISSUE_NUMBER}" \
                --body "Closes #${ISSUE_NUMBER}

              Changes generated by Claude Code.
              Review required before merging."

              echo "=== Pipeline completed ==="
YAML

For this demo, we keep Claude Code execution and GitHub operations in a single Task. In a production environment, I would separate these operations to avoid exposing the GitHub write token to the AI agent.

Configure the GitHub Webhook

We have already prepared the container image, Tekton Triggers, and the pipeline. The last configuration step is to connect our GitHub repository to the EventListener. Open the GitHub repository settings and navigate to Webhooks. Click Add webhook and paste the HTTPS address of the OpenShift Route into the Payload URL field. Set the content type to application/json. Instead of selecting all events, choose Let me select individual events and enable only Issue comments.

After saving the webhook, GitHub can send issue comment events to our OpenShift cluster. The EventListener will then apply the CEL filter and create a PipelineRun only for matching comments. We are ready to test the whole workflow.

Running the demo

Now let’s see how everything works in practice. For this demonstration, I will use a Java application built with Maven. The repository contains a pom.xml file with the project version and a README.md file with basic information about the application. Our task is quite simple. We want Claude Code to increase the patch version in pom.xml and update the README with the new version. Of course, we could do this manually in a few seconds. However, the goal here is to demonstrate the entire workflow, from a GitHub issue comment to a pull request generated by an AI coding agent.

Step 1: Create a GitHub Issue

First, let’s create a new issue in our GitHub repository. We can give it a title related to updating the application version. Then, we may add the comment starting with the @tekton prefix as shown below.

claude-code-tekton-issue

That’s all we need to do on GitHub. In Wwebhok’s history you should see recent calls.

Step 2: Trigger the Pipeline on OpenShift

Once we submit the comment, GitHub sends an issue_comment webhook event to our EventListener. The CEL interceptor checks the event type, action, and comment prefix. If everything matches, Tekton creates a new PipelineRun.

Let’s open the OpenShift console and navigate to the Pipelines section. Then, we should see a new run of my-pipeline with two tasks: issue-info and implement-and-create-pr.

claude-code-tekton-pipeline

The first task prints the repository URL, issue number, and original comment. We can check its logs to confirm that the webhook integration works correctly.

Step 3: Claude Code Implements the Changes

The task with Claude Code analyzes the Maven project, finds the current version in pom.xml, and increases its patch number. It also updates README.md to reflect the new version. Therefore, we don’t need to specify the exact XML element or README section. The agent can inspect the repository and determine which files need changes. Once Claude Code finishes, the pipeline checks the Git working tree. Since the agent has modified the files, it creates a commit and pushes the branch to GitHub.

claude-code-tekton-ai

Step 4: Review the Generated Pull Request

Finally, the pipeline uses GitHub CLI to create a pull request. Let’s go back to GitHub and open the Issues tab. We should see a new PR associated with our original issue.

The important thing is that we didn’t modify either file manually. We only created an issue and added a comment describing the expected changes. Tekton orchestrated the workflow, Claude Sonnet implemented the request, and GitHub received the generated commit and pull request. Of course, we should still review the code before merging it. If everything looks good and the required checks pass, we can merge the pull request into the master branch.

claude-code-tekton-pull-request

Final Thoughts

In this article, we built an AI-driven development workflow using GitHub, Tekton Triggers, Tekton Pipelines, and Claude Code on OpenShift.

The idea is simple. We use GitHub issue comments to describe development tasks. Tekton receives the webhook and runs Claude Code inside a container. The agent analyzes the repository and implements the requested changes using Claude Sonnet. Finally, the pipeline commits the code and creates a pull request.

What I particularly like about this approach is that we don’t need a dedicated AI agent platform. We can reuse our existing Kubernetes infrastructure and Tekton components to orchestrate AI-powered development tasks.

Our demo only updated a Maven version and README file. However, the same workflow could handle more advanced tasks, including implementing new features, updating dependencies, or refactoring existing code.

Leave a Reply