Skip to content

Onboarding a New Bot Instance

How to add a new bot runner instance to the shared OpenShift cluster. Each instance gets its own Jira label, repo set, and personas, but shares the memory server, database, and Vault secrets deployed by the primary instance (platform-frontend-ai-dev). Instances share the proxy by default, but can optionally deploy their own proxy for custom Jira credentials.

For the system architecture, see ARCHITECTURE.md.


Prerequisites

Before starting, you need:

  • [ ] A GitHub repo for your instance (e.g. RedHatInsights/my-bot-instance)
  • [ ] A Jira label for your team's tickets (e.g. hcc-ai-myteam)
  • [ ] Access to the app-interface repo (gitlab.cee.redhat.com/service/app-interface)
  • [ ] The primary instance (platform-frontend-ai-dev) already deployed in the target namespace — it provides the shared proxy, memory server, and secrets

Step 1: Create the Runner Repo

The runner repo uses dev-bot as a git submodule. It contains only instance-specific config — no bot code.

mkdir my-bot-instance && cd my-bot-instance
git init
git submodule add https://github.com/RedHatInsights/platform-frontend-ai-dev.git dev-bot

Create the required files:

setup.sh

Runs as root during the Docker build. Install instance-specific packages here.

#!/bin/bash
set -e

echo "my-bot-instance" > /home/botuser/app/.instance-id

# Instance-specific packages go here:
# dnf install -y --nodocs <package>
# pip3.12 install <package>
# npm install -g <package>

echo "Instance setup complete: my-bot-instance"

instance/ directory

Create your instance config. This entire directory gets COPYed into the image at /home/botuser/app/instance/. The bot loads it at startup via BOT_CONFIG_PATH.

instance/my-config/
└── agent/
    ├── instance.yaml         # preset selection (workflow, env presets, CLAUDE.md strategy)
    ├── CLAUDE.md             # instance-specific instructions (optional, strategy-dependent)
    ├── project-repos.json    # repos this instance works on
    ├── mcp.json              # MCP server overrides (usually just Jira)
    ├── personas/             # domain-specific guidelines
    │   ├── frontend/
    │   │   └── prompt.md
    │   └── ...
    ├── extra-hosts           # custom /etc/hosts entries for browser preset (optional)
    ├── preflight/            # instance-specific preflight scripts (optional)
    │   └── 01-check-something.py
    └── workflows/            # custom workflows (optional, if not using a built-in)
        └── my-workflow/
            ├── CLAUDE.md
            ├── manifest.yaml
            └── preflight/
                └── 01-check.py

Instance-level preflight/ scripts run alongside the workflow's preflight scripts — use them for checks specific to your instance. Custom workflows/ are referenced via workflow: ./workflows/<name> in instance.yaml. See Creating Custom Workflows and Writing Custom Preflight Scripts for details.

instance.yaml

Declares which workflow and env presets your instance uses. Required for all new instances.

workflow: jira-sprint
source: jira
envs:
  - browser
  - slack
  - container-scan
Field Type Default Description
workflow string jira-sprint Workflow preset name (built-in) or ./path (custom). See below.
source string jira Ticket source. jira = Jira sprint polling. scheduled = time-based.
envs list or null null (all) Env presets to activate. null/omitted = all available. [] = none.
claude_md.strategy string ignore How to handle instance CLAUDE.md: ignore, append, replace.

Workflows: The built-in jira-sprint workflow handles the full autonomous development loop (triage → implement → PR → maintain). For specialized use cases — monitoring, review-only, scheduled tasks — you can create custom workflows in your instance config repo using workflow: ./workflows/<name>. See Creating Custom Workflows for the full guide.

Env presets add tools and runtimes to the bot image. List only what your instance needs — unused presets waste build time and image size.

Preset What it provides
node nvm + Node.js 22 LTS + npm/npx
go goenv + Go 1.24/1.25 + golangci-lint
patternfly-mcp PatternFly component guidance MCP server (requires node)
browser Chromium + chrome-devtools MCP for visual verification
container-scan Grype + Buildah for CVE scanning
dev-proxy Caddy reverse proxy for stage UI verification
slack Slack notifications via webhook

For the full preset system reference, see the Presets overview, env presets, and workflow presets.

project-repos.json

List only the repos your instance should work on:

{
  "my-frontend": {
    "url": "https://github.com/your-bot-fork/my-frontend.git",
    "upstream": "https://github.com/YourOrg/my-frontend.git"
  },
  "my-backend": {
    "url": "https://github.com/your-bot-fork/my-backend.git",
    "upstream": "https://github.com/YourOrg/my-backend.git"
  },
  "app-interface": {
    "url": "https://gitlab.cee.redhat.com/your-bot-fork/app-interface.git",
    "upstream": "https://gitlab.cee.redhat.com/service/app-interface.git",
    "host": "gitlab"
  }
}
  • url — bot's fork (where it pushes branches)
  • upstream — original repo (PR/MR target)
  • host — set to "gitlab" for GitLab repos (default: GitHub)
  • readonly — set to true if bot should only read, never push

mcp.json

Typically just points to the shared Jira MCP server:

{
  "mcpServers": {
    "mcp-atlassian": {
      "url": "${JIRA_MCP_URL}"
    }
  }
}

Personas

Copy and adapt from dev-bot/rehor-config/agent/personas/. Each persona is a prompt.md with coding standards, test commands, and conventions for that repo type.

README.md

# my-bot-instance

Custom bot runner built on [dev-bot](https://github.com/RedHatInsights/platform-frontend-ai-dev).

## Build

\`\`\`bash
git submodule update --init --recursive
docker build -f dev-bot/Dockerfile.runner -t my-bot-instance:local .
\`\`\`

## Updating dev-bot

\`\`\`bash
cd dev-bot && git pull origin master && cd ..
git add dev-bot
git commit -m "chore: update dev-bot submodule"
\`\`\`

Reference PRs


Step 2: Deploy Template

Create deploy/template.yaml. This is a bot-only template — it does NOT create the memory server (that comes from the primary instance). The proxy is shared by default but can optionally be deployed per-instance for custom Jira credentials.

Copy from hcc-ui-agent-dev/deploy/template.yaml and adjust:

  • metadata.name — your instance name
  • Default BOT_NAME — e.g. devbot-myteam
  • Default BOT_LABEL — your Jira label
  • BOT_IMAGE — your Quay image path

The template creates these resources: 1. Proxy Deployment (optional) — own proxy with custom Jira credentials. Set PROXY_REPLICAS=0 (default) to use the shared proxy, or PROXY_REPLICAS=1 to deploy your own. 2. Proxy Service (optional) — ClusterIP service for the per-instance proxy. 3. Bot Deployment — bot container with env vars pointing to shared infra. 4. NetworkPolicy — egress restricted to proxy + memory-server + DNS only. References ${PROXY_NAME} so it correctly targets either shared or per-instance proxy. 5. ScaledObject (KEDA cron scaler) — auto-scales the bot on a time-based schedule. See Step 2b: Scheduling.

Key environment variables (already wired in the template): - BOT_MEMORY_URL=http://devbot-memory-server:8080/mcp — shared memory server - EXECUTOR_ADDR=${PROXY_NAME}:9090 — executor (shared or per-instance proxy) - HTTP_PROXY=http://${PROXY_NAME}:3128 — Squid proxy - JIRA_MCP_URL=http://${PROXY_NAME}:8444/mcp — Jira MCP

Shared vs Per-Instance Proxy

Mode PROXY_REPLICAS PROXY_NAME JIRA_SECRET_NAME When to use
Shared (default) 0 devbot-proxy devbot-secrets Same Jira identity as primary instance
Per-instance 1 unique name (e.g. devbot-myteam-proxy) your secret name Custom Jira credentials needed (different Jira project access)

Why a separate proxy? Bot pods are network-isolated — the NetworkPolicy only allows egress to the proxy and memory server. The proxy runs mcp-atlassian (Jira MCP server) with the Jira credentials baked in. To use a different Jira account, you need a separate proxy pod with different credentials. A sidecar won't work because pods in the same deployment share the same NetworkPolicy.

NetworkPolicy proxy label — Important

The NetworkPolicy must use app.kubernetes.io/name: devbot-proxy to match the shared proxy pod. Do NOT use proxy — the proxy pod's label is devbot-proxy, not proxy. A wrong label silently blocks all egress and the bot pod will hang waiting for the executor.

# Correct
- to:
  - podSelector:
      matchLabels:
        app.kubernetes.io/name: devbot-proxy    # ← must match the proxy pod label
  ports:
  - port: 3128
    protocol: TCP
  # ...

If using a per-instance proxy (PROXY_REPLICAS=1), use ${PROXY_NAME} instead (it resolves to your custom proxy name).

DNS Egress — Important

OpenShift uses custom DNS on port 5353 in the openshift-dns namespace, not the standard port 53. Your NetworkPolicy must allow:

- to:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: openshift-dns
  ports:
  - port: 5353
    protocol: UDP
  - port: 5353
    protocol: TCP

Using port 53 or k8s-app: kube-dns will cause pods to hang — they can't resolve service names.

Reference PRs


Step 2b: Scheduling (KEDA Cron Scaler)

Every instance must include a KEDA ScaledObject in its deploy template. This controls when the bot runs — without it, you'd need to manually scale replicas up and down.

Add the following to deploy/template.yaml after the NetworkPolicy:

# --- Cron Scaler ---
- apiVersion: keda.sh/v1alpha1
  kind: ScaledObject
  metadata:
    name: ${BOT_NAME}-cron-scaler
    labels:
      app.kubernetes.io/name: ${BOT_NAME}
      app.kubernetes.io/part-of: devbot
  spec:
    scaleTargetRef:
      apiVersion: apps/v1
      kind: Deployment
      name: ${BOT_NAME}
    minReplicaCount: 0
    maxReplicaCount: 1
    triggers:
    - type: cron
      metadata:
        timezone: "Europe/Prague"
        start: "0 9 * * 1-5"
        end: "0 23 * * 1-5"
        desiredReplicas: "1"

Adjust timezone, start, and end to match your team's working hours. The example above runs weekdays 9:00–23:00 Prague time.

For more schedule examples (US hours, weekends, split windows, etc.) and details on how multiple triggers combine, see the full Scheduling guide.

App-interface: Your SaaS file's managedResourceTypes must include ScaledObject.keda.sh — see Step 4.


Step 3: Konflux CI/CD

This step must be completed before Step 4 (app-interface). App-interface references the Quay image built by Konflux — if the image doesn't exist or isn't public, the deployment will fail.

Follow the Konflux onboarding rules to register your repo. The key requirements:

  1. Onboard your repo to Konflux — follow the standard Konflux onboarding process for your tenant namespace
  2. Configure a ReleasePlan — your component needs a ReleasePlan and corresponding ReleasePlanAdmission so that push builds produce a release to your Quay prod repo
  3. Make the image public — configure the ReleasePlanAdmission to set the image repository as public. This is handled within the release config, not manually in Quay

Pipeline config:

# .tekton/my-bot-push.yaml (and pull-request.yaml)
dockerfile: dev-bot/Dockerfile.runner
path-context: .

Konflux auto-generates the .tekton/ pipeline files when you onboard. The important bits: - Dockerfile path points to dev-bot/Dockerfile.runner (the submodule) - Build context is . (the runner repo root) - Push builds go to your Quay prod repo (via the ReleasePlan) - PR builds go to quay.io/redhat-user-workloads/... with 5-day expiry

Reference PRs


Step 4: App-Interface Configuration

The namespace, app, and shared infrastructure (proxy, memory server, secrets) are already deployed by the primary instance. You do not need to do the full app-interface onboarding — just add your instance as a new deployment to the existing namespace.

Add to deploy.yml

Add your instance as a new resourceTemplate in the existing SaaS file:

resourceTemplates:
# ... existing primary instance ...

- name: my-bot-instance
  path: /deploy/template.yaml
  url: https://github.com/YourOrg/my-bot-instance
  targets:
  - namespace:
      $ref: /services/insights/platform-frontend-ai-dev/namespaces/stage.hcmais01ue1.yml
    ref: <git-commit-sha>
    parameters:
      BOT_IMAGE_TAG: <git-commit-sha>
      BOT_IMAGE: quay.io/your-org/my-bot-instance
      BOT_NAME: devbot-myteam
      BOT_LABEL: hcc-ai-myteam
      BOT_REPLICAS: '0'                    # start disabled, enable after verification
      BOT_BOARD_NAME: 'Your Board Name'    # only used by claim-ticket for sprint assignment
      BOT_SPRINT_PREFIX: 'Your Sprint'     # only used by claim-ticket for sprint assignment
      BOT_INCLUDE_BACKLOG: 'true'
      BOT_INSTANCE_ID: 'Your Bot Name'     # human-readable, used in memory server
      GCP_PROJECT_ID: your-gcp-project
      GCP_REGION: global
      VERTEX_ALLOWED_MODELS: claude-sonnet-4-6,claude-opus-4-6,claude-haiku-4-5
      BOT_CONFIG_REPO: https://github.com/YourOrg/my-bot-instance.git
      BOT_CONFIG_PATH: instance/my-config
      SLACK_WEBHOOK_URL: 'https://hooks.slack.com/...'
      # --- Per-instance proxy (optional — only if custom Jira, and other creds needed) ---
      # PROXY_IMAGE: quay.io/redhat-services-prod/hcc-platex-services/platform-frontend-ai-dev-proxy
      # PROXY_IMAGE_TAG: <proxy-image-sha>
      # PROXY_REPLICAS: '1'
      # PROXY_NAME: devbot-myteam-proxy
      # JIRA_SECRET_NAME: myteam-jira-secrets

Add managed resource types

Your SaaS file needs managedResourceTypes to include all resource kinds your template creates:

managedResourceTypes:
- Deployment
- NetworkPolicy
- ScaledObject.keda.sh

Without ScaledObject.keda.sh, app-interface will prune the KEDA cron scaler on every sync.

Add image pattern

Add your Quay image to the imagePatterns list in deploy.yml:

imagePatterns:
- quay.io/your-org/my-bot-instance

Add to app.yml

Register your repo as a code component:

codeComponents:
- name: my-bot-instance
  url: https://github.com/YourOrg/my-bot-instance
  resource: upstream

Namespace

Your instance deploys to the same namespace as the primary instance — that's how it accesses the shared proxy, memory server, and secrets. No new namespace needed.

Vault Secrets

Every instance needs its own Vault secret with Jira credentials — a custom Jira identity means a custom proxy container. The memory server can still be shared.

Add a Vault secret to the namespace YAML:

# In namespaces/stage.hcmais01ue1.yml
openshiftResources:
- provider: vault-secret
  path: your/vault/path/jira-credentials
  name: myteam-jira-secrets
  version: 1
  annotations:
    qontract.recycle: "true"

The secret needs two keys: jira-email and jira-token. Then set JIRA_SECRET_NAME=myteam-jira-secrets and PROXY_REPLICAS=1 in your deploy.yml parameters so the instance gets its own proxy pod with these credentials.

The shared devbot-secrets secret (GitHub/GitLab/GPG/GCP credentials) is still used by all instances — only the Jira identity is per-instance.

Reference: Existing app-interface config

Use the framework instance (hcc-ui-agent-dev) as your reference — it always has the latest configuration. Its app-interface directory (data/services/insights/platform-frontend-ai-dev/) contains:

File Purpose
app.yml App metadata, code components, service owners
deploy.yml SaaS file — image patterns, resource templates, parameters
namespaces/stage.hcmais01ue1.yml Namespace config, Vault secret ref, RDS definition
pipelines/tekton-*.yml Tekton pipeline provider
roles/ RBAC roles for namespace access

Step 5: Jira Setup

  1. Create your label (e.g. hcc-ai-myteam) in the Jira project
  2. Label tickets the bot should pick up with your label + repo:<name> labels
  3. Board name (optional) — only used by claim-ticket to add tickets to the active sprint. The ticket query itself is label-only.

repo: labels support both bare names (repo:my-frontend) and org-prefixed (repo:YourOrg/my-frontend). Both resolve against project-repos.json.


Step 6: Bot Fork Repos

For each repo in project-repos.json, the bot needs a fork to push branches to:

GitHub repos: - Create forks under a bot GitHub user/org - The bot pushes to the fork and opens PRs against the upstream

GitLab repos: - Create forks under a bot GitLab user/group - The bot pushes to the fork and opens MRs against the upstream

The shared devbot-secrets Vault secret provides GitHub (gh-bot-cli-token) and GitLab (gl-bot-cli-token) PATs. These tokens must have push access to your fork repos.


Verification

After deploying, verify in order:

  1. Pod starts: oc get pods -l app.kubernetes.io/name=devbot-myteam
  2. DNS works: oc exec <pod> -- nslookup devbot-proxy — should resolve
  3. Memory server reachable: oc exec <pod> -- curl -s http://devbot-memory-server:8080/health
  4. Executor reachable: check logs for "Connected to executor at devbot-proxy:9090"
  5. Config loaded: check logs for remote config sync from BOT_CONFIG_REPO
  6. Enable via schedule: configure the KEDA cron scaler (see Step 2b) to run during your team's working hours. Don't set BOT_REPLICAS: '1' permanently — use the schedule to avoid unnecessary token consumption on weekends and off-hours

Parameter Reference

Parameter Required Description
BOT_IMAGE yes Quay image path
BOT_IMAGE_TAG yes Git SHA for image tag
BOT_NAME yes Deployment name (e.g. devbot-myteam)
BOT_LABEL yes Jira label to filter tickets
BOT_REPLICAS yes Number of replicas ('0' to disable)
BOT_INSTANCE_ID yes Human-readable name for memory server
BOT_CONFIG_REPO yes Git URL for remote config repo
BOT_CONFIG_PATH yes Path within config repo to agent/ dir
GCP_PROJECT_ID yes GCP project for Vertex AI
GCP_REGION yes GCP region (usually global)
VERTEX_ALLOWED_MODELS yes Comma-separated model allowlist
BOT_BOARD_NAME no Jira board name (for sprint assignment only)
BOT_SPRINT_PREFIX no Sprint name prefix filter (for sprint assignment only)
BOT_INCLUDE_BACKLOG no 'true' to include backlog tickets
SLACK_WEBHOOK_URL no Slack webhook — Incoming (/services/, recommended) or Workflow Builder (/triggers/)
SLACK_NOTIFY_MODE no immediate (default) or daily_digest. In digest mode, individual notifications are suppressed and a daily snapshot of open PRs is sent instead. Requires SLACK_DIGEST_HOUR to be set.
SLACK_DIGEST_HOUR no UTC hour (0-23) when daily digest is sent. Opt-in — digest is disabled unless this is set.
PROXY_IMAGE no Proxy container image (only needed if PROXY_REPLICAS=1)
PROXY_IMAGE_TAG no Proxy image tag (default: latest)
PROXY_REPLICAS no '0' = use shared proxy (default), '1' = deploy own proxy
PROXY_NAME no Proxy service name (default: devbot-proxy). Set to a unique name when deploying own proxy.
JIRA_SECRET_NAME no Vault secret with jira-email + jira-token keys (default: devbot-secrets)

Slack Workspace Configuration

Bot instances connect to a specific Slack workspace via their SLACK_USER_TOKEN. The token determines which workspace the bot operates in — production or sandbox.

Production vs Sandbox

Production Sandbox
Workspace redhat.enterprise.slack.com redhat-sandbox.enterprise.slack.com
Token xoxp-... from production Vault xoxp-... from sandbox workspace
Channel IDs Different per workspace Different per workspace
Custom emoji Workspace-specific Must exist in sandbox too

The hcc-framework-agent-dev instance connects to the production Slack grid. To test Slack features against the sandbox workspace, you need either:

  • Option A: Separate dev instance — deploy a new instance with sandbox-specific Vault secrets (sandbox SLACK_USER_TOKEN, sandbox channel IDs, sandbox emoji names). This is the cleanest approach.
  • Option B: Direct production testing — skip sandbox entirely and test in the existing dev instance against production Slack (requires Slack app approval for production workspace).

Slack Environment Variables

When pointing an instance at a different workspace, update all Slack-related parameters:

Variable Description
SLACK_USER_TOKEN xoxp-... token for the target workspace
SLACK_OPEN_PRS_CHANNEL Channel ID for the Open PRs thread (workspace-specific)
SLACK_EMOJI_APPROVED Emoji name for approved PRs (e.g. lgtm-5363)
SLACK_EMOJI_MERGED Emoji name for merged PRs (e.g. merged2)
SLACK_WEBHOOK_URL Webhook URL (also workspace-specific)

Channel IDs and custom emoji names differ between workspaces — you cannot reuse production values in sandbox.


Gotchas

NetworkPolicy proxy label must be devbot-proxy

The proxy pod's label is app.kubernetes.io/name: devbot-proxynot proxy. Using the wrong label in your NetworkPolicy silently blocks all bot egress. The bot will start but hang forever waiting for the executor connection. This has caused real outages — see kessel-ai-dev#8.

DNS port is 5353, not 53

OpenShift uses a custom DNS server in the openshift-dns namespace on port 5353. Standard port 53 or kube-dns selectors won't work. Symptom: pods hang forever waiting for network connections.

GPG signing doesn't work for GitLab

Commits pushed to GitLab via the proxy are unsigned. GitHub commits are signed. This is a known limitation — GitLab requires a different GPG verification flow. A fix is in progress.

Submodule updates

When dev-bot merges new features, Konflux opens automated PRs to update the submodule in your runner repo. You can also update manually if you don't want to wait for the automation:

cd dev-bot && git pull origin master && cd ..
git add dev-bot && git commit -m "chore: update dev-bot submodule"
Then bump the ref in app-interface after the image builds.

Merge order matters

When changes span multiple repos, merge in this order: 1. app-interface (config/params) — first, so the cluster config is ready 2. dev-bot (core changes) — second 3. runner instance (submodule bump) — last, after dev-bot merges

Shared identities

All bot instances share credentials from devbot-secrets — Jira, GitHub, GitLab, GPG, and GCP. This means all instances push code, open PRs, and comment on Jira as the same user. The bot identifies its own Jira comments by content patterns (structured reports, PR links, tables), not by username.

A different identity (Jira, GitHub, or GitLab) requires a separate proxy instance — credentials are baked into the proxy container, not the bot pod. See Shared vs Per-Instance Proxy in Step 2 and Vault Secrets in Step 4.

BOT_BOARD_NAME is fragile

If someone renames the Jira board, claim-ticket breaks. Consider using BOT_BOARD_ID (stable numeric ID) instead. The ticket query (new-work) doesn't use the board at all — it's label-only with sprint in openSprints().

Memory server is shared

All bot instances share one memory server. Task isolation is via instance_id — always pass it in task tool calls. Memories (learnings) are shared across instances, which is intentional.