Back to Blog

Building Pipeline Agents with the Ankra CLI

13 min read
On this page

Most posts about AI agents stop at the architecture diagram. This one walks through three working agents, built from plain CI jobs and the Ankra CLI, that you can copy into a repo today.

What they do:

  1. PR review agent. When a pull request touches your cluster definition, it validates the change against the live platform and comments on the PR with what will actually change on the cluster and what looks risky.
  2. Deploy watcher. On merge it applies the change, waits for the rollout to finish, verifies health, and posts the result to Slack. When the deploy fails, it posts a root cause instead of a red X.
  3. Scheduled infra watcher. Runs every 30 minutes, independent of CI. It stays silent while things are healthy and posts to Slack only when something degrades.

There is no framework here, no orchestration layer, and no vector database. Each agent is a trigger, a few deterministic CLI calls to gather facts, one LLM call for the judgment step, and a write to somewhere humans actually look: a PR thread or a Slack channel.

You also never wire up a model yourself. ankra chat is a one-shot, scriptable command, and the AI behind it already has server-side access to your cluster context: logs, events, manifests, stack history, operations, and metrics. You ask a question from CI and it answers against live state. The pipeline never holds a model API key or a kubeconfig.


Prerequisites

You need an Ankra-managed cluster, imported or provisioned, with its definition versioned in the repo the pipelines run in. That is the standard GitOps layout: a cluster.yaml plus a stacks/ directory.

Create an API token for the agent. Do this from a machine where you are logged in:

Terminal window
ankra login
ankra tokens create ci-agent

Store the returned token as ANKRA_API_TOKEN in your CI secret store (GitHub Actions secrets, GitLab CI/CD variables). Also add SLACK_WEBHOOK_URL, a plain Slack incoming webhook for the channel you want reports in.

The CLI installs in one line on any runner:

Terminal window
bash <(curl -sL https://github.com/ankraio/ankra-cli/releases/latest/download/install.sh)

One gotcha before you script anything. The CLI resolves its token in this order: the --token flag, then a saved login from ankra login, then ANKRA_API_TOKEN. On a throwaway CI runner the environment variable works fine. On a self-hosted runner where someone once ran ankra login, the saved login silently wins over your CI token. If you run on shared machines, pass --token "$ANKRA_API_TOKEN" explicitly.


Agent 1: PR review comments

The job triggers when a pull request touches the cluster definition. It does three things: server-side validation, an impact question against the live cluster, and a comment on the PR.

.github/workflows/infra-review.yml:

name: infra-review
on:
pull_request:
paths:
- "cluster.yaml"
- "stacks/**"
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install the Ankra CLI
run: bash <(curl -sL https://github.com/ankraio/ankra-cli/releases/latest/download/install.sh)
- name: Validate against the platform
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
ankra cluster validate -f cluster.yaml --strict-secrets 2>&1 | tee validation.txt
- name: Ask what the change does to the live cluster
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
DIFF=$(git diff "origin/${GITHUB_BASE_REF}"...HEAD -- cluster.yaml stacks/ | head -c 12000)
ankra chat --cluster production "A pull request changes our cluster definition.
Review the diff below against the current state of this cluster.
Answer in three short sections:
1. What changes on the cluster (which stacks, addons, manifests).
2. Which workloads will restart or be replaced.
3. Anything risky given live state right now (capacity, dependencies, ordering).
Be specific. If nothing is risky, say so in one line.
${DIFF}" > impact.md
- name: Comment on the pull request
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "## Infra review"
echo
echo "**Validation** (\`ankra cluster validate --strict-secrets\`)"
echo '```'
cat validation.txt
echo '```'
echo
echo "**Impact against live cluster state**"
echo
cat impact.md
} > comment.md
gh pr comment "${{ github.event.pull_request.number }}" --body-file comment.md

Why each piece is there:

  • ankra cluster validate -f runs local structural and dependency checks first, then sends the file to the API for the checks you cannot do offline: whether the referenced charts exist in your connected Helm registries, whether there are plaintext Secrets or unencrypted addon values, and whether parents references resolve against the cluster’s deployed resources. --strict-secrets turns a plaintext secret from a warning into a failed job, which is exactly what you want on a PR.
  • The ankra chat step is the only LLM call. The diff is in the prompt; the cluster state is on the server side. That combination is what makes the review better than a generic LLM looking at YAML. It knows the addon you are reparenting is mid-rollout, or that the node group you are scaling down is already at 85% memory.
  • The comment is plain gh pr comment. No app installation, no webhook receiver. The default GITHUB_TOKEN with pull-requests: write is enough.

A real comment from this job looks like:

Infra review

Validation (ankra cluster validate --strict-secrets)

cluster.yaml: OK
stacks: payments (2 addons, 3 manifests), monitoring (unchanged)
chart payments-api 1.4.2 found in registry harbor-internal
no plaintext secrets detected

Impact against live cluster state

  1. Bumps the payments-api chart from 1.4.1 to 1.4.2 and raises its memory limit from 256Mi to 512Mi.
  2. The payments-api Deployment (3 replicas, namespace payments) rolls. Nothing else restarts.
  3. Worker group default has 1.1Gi allocatable headroom; three new 512Mi pods fit, but only just. If you also merge PR #214 (raises checkout limits), schedule them apart.

That last point, the cross-reference against live headroom, is the check a human reviewer skips at 5pm on a Friday.

The same agent on GitLab

The structure is identical. The one piece of friction is that CI_JOB_TOKEN cannot post notes on merge requests. Create a project access token with the api scope and Reporter role, store it as GITLAB_AGENT_TOKEN, and post to the notes API:

infra-review:
stage: review
image: alpine:3.20
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- cluster.yaml
- stacks/**/*
script:
- apk add --no-cache bash curl git jq
- bash <(curl -sL https://github.com/ankraio/ankra-cli/releases/latest/download/install.sh)
- ankra cluster validate -f cluster.yaml --strict-secrets 2>&1 | tee validation.txt
- |
DIFF=$(git diff "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}"...HEAD -- cluster.yaml stacks/ | head -c 12000)
ankra chat --cluster production "A merge request changes our cluster definition. Review the diff against live state. What changes, what restarts, what is risky right now? ${DIFF}" > impact.md
- |
BODY=$(printf '## Infra review\n\n```\n%s\n```\n\n%s\n' "$(cat validation.txt)" "$(cat impact.md)")
curl -sS --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_AGENT_TOKEN}" \
--data-urlencode "body=${BODY}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"

Agent 2: merge, deploy, verify, report to Slack

On merge to main, this agent applies the cluster definition, blocks until the platform finishes, verifies health, and reports. The interesting part is the failure path: instead of “pipeline failed, go look”, the Slack message contains the failed operation and a root cause pulled from live state.

.github/workflows/deploy-watch.yml:

name: deploy-watch
on:
push:
branches: [main]
paths:
- "cluster.yaml"
- "stacks/**"
permissions:
contents: read
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install the Ankra CLI
run: bash <(curl -sL https://github.com/ankraio/ankra-cli/releases/latest/download/install.sh)
- name: Select the target cluster
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: ankra cluster select production
- name: Apply and wait
id: apply
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
set +e
ankra cluster apply -f cluster.yaml --wait --timeout 15m 2>&1 | tee apply.txt
echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
- name: Verify health after deploy
if: steps.apply.outputs.exit_code == '0'
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
ankra chat health > report.md
ankra cluster metrics query \
'sum(rate(http_requests_total{code=~"5.."}[5m]))' \
-o json > error-rate.json
- name: Investigate failure
if: steps.apply.outputs.exit_code != '0'
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
ankra cluster operations list 2>&1 | head -n 20 > operations.txt
ankra chat --cluster production "The apply for commit ${GITHUB_SHA::12} failed or timed out.
Find the most recent failed operation on this cluster, look at its jobs,
and tell me: the exact resource that failed, the error, and whether a
retry is likely to succeed or the change itself is broken.
Apply output for reference: $(tail -c 4000 apply.txt)" > report.md
- name: Report to Slack
if: always()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
STATUS="DEPLOYED"
if [ "${{ steps.apply.outputs.exit_code }}" != "0" ]; then STATUS="FAILED"; fi
jq -n \
--arg status "$STATUS" \
--arg commit "${GITHUB_SHA::12}" \
--arg url "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" \
--rawfile report report.md \
'{text: "[\($status)] production <\($url)|\($commit)>\n\($report)"}' \
| curl -sS -X POST -H 'Content-type: application/json' --data @- "$SLACK_WEBHOOK_URL"
- name: Comment on the merged PR
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
PR=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" --jq '.[0].number' || true)
if [ -n "$PR" ]; then
gh pr comment "$PR" --body-file report.md
fi

Details worth knowing:

  • ankra cluster apply is asynchronous by default. Without --wait the command exits 0 the moment the platform accepts the change, while the actual rollout continues in the background. For a deploy watcher that exit code is useless on its own. Always pass --wait with a --timeout that matches your slowest stack.
  • ankra cluster select production is run once, by name, non-interactively. Bare ankra cluster select opens an interactive picker and will hang a CI job until the timeout kills it. The same applies to every command with an interactive mode: in CI, always pass names.
  • The failure path does not parse error strings or grep logs. It collects the apply output and the recent operations as raw facts and hands the diagnosis to ankra chat, which can also see the operation’s jobs, pod events, and container logs on the server side. The one thing the prompt insists on is a verdict on “retry or broken change”, because that is the decision the on-call human has to make. Make the agent answer it.
  • The last step closes the loop: the verdict lands back on the PR that caused it. Whoever merged gets the outcome in the same thread as the review, without watching the pipeline.

The Slack message on a bad day reads like this:

[FAILED] production a3f8c91b02d4

Operation op-7c21 failed at job helm-upgrade payments-api. The new pod is in CrashLoopBackOff: the 1.4.2 image reads PAYMENTS_DB_POOL_SIZE at startup and the variable is not set in this environment. Rolled-back replicas from 1.4.1 are still serving; no user impact. A retry will fail the same way because the change itself is broken. Add the variable to the stack, or revert the image bump, before re-merging.

Compare that to what a failed pipeline normally gives you: a red X and twenty minutes of kubectl describe.


Agent 3: a scheduled watcher that only speaks when something is wrong

CI-triggered agents miss everything that breaks between deploys: node pressure, a certificate that expired, an addon that drifted. This one runs on a schedule and posts to Slack only on a degraded verdict.

name: infra-watch
on:
schedule:
- cron: "*/30 * * * *"
jobs:
watch:
runs-on: ubuntu-latest
steps:
- name: Install the Ankra CLI
run: bash <(curl -sL https://github.com/ankraio/ankra-cli/releases/latest/download/install.sh)
- name: Check cluster health
env:
ANKRA_API_TOKEN: ${{ secrets.ANKRA_API_TOKEN }}
run: |
ankra cluster select production
ankra chat --cluster production "Check this cluster now: failed or stuck operations
in the last 30 minutes, pods not Running or Succeeded, nodes not Ready,
and anything alarming in recent events. The FIRST LINE of your answer
must be exactly OK or DEGRADED. Then the details." > verdict.md
- name: Post to Slack only when degraded
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
if ! head -n 1 verdict.md | grep -qx "OK"; then
jq -n --rawfile v verdict.md '{text: "[infra-watch] production\n\($v)"}' \
| curl -sS -X POST -H 'Content-type: application/json' --data @- "$SLACK_WEBHOOK_URL"
fi

The verdict-first-line trick is the load-bearing part. You cannot branch a shell script on prose, so force the model to emit a machine-checkable first line and treat anything that is not exactly OK as degraded. If the model ignores the format, the worst case is a spurious Slack message rather than a missed incident. That is the right direction to fail in.

If this overlaps with alerting you already have, keep the alerts. The watcher’s value is the paragraph under the verdict: which operation, which pod, what the events say. Ankra’s built-in alerts and webhooks cover the push-based version of this with AI analysis attached; the scheduled agent is for the checks you want to phrase yourself.


The rules that keep this safe

The agents are read-only by design. Everything above validates, asks, and reports. The only mutation is ankra cluster apply on merge, which is the deploy your GitOps flow was going to run anyway, gated by the PR review. If you want an agent to propose fixes, use ankra cluster draft -f. It stages every stack in the file as reviewable drafts in Ankra instead of deploying, and a human ships them from the stack builder. Never give a pipeline agent a path to mutate the cluster outside Git.

Treat the model output as untrusted text. It goes into a PR comment or a Slack message and stops there. Never pipe ankra chat output into a shell, an apply, or anything that executes. The diff you feed in came from a PR author; the output inherits that trust level.

Scope the credentials. The CI token is created with ankra tokens create under a real account, so use a dedicated machine user with the minimum org role rather than your own login. The Slack webhook posts to one channel and can do nothing else. The GitHub side runs on the default GITHUB_TOKEN with one extra permission. There is no kubeconfig and no cloud credential anywhere in the pipeline.

Forked PRs do not get secrets. On GitHub, pull_request runs from forks without access to repository secrets, so the review agent simply does not run there. That is the behavior you want, since untrusted diffs should not reach your ankra chat context anyway. Run it on internal branches and have maintainers re-trigger for external contributions.

Control the noise. One comment per PR update, one Slack message per merge, silence from the watcher when healthy. The fastest way to kill an agent like this is to let it chat. If nobody would act on the message, do not send it.


Where this goes next

Everything here flows one way, from pipeline to humans. The natural next step is letting humans answer back: replying in the Slack thread with “scale it down” or “show me the logs” and having an agent act on it. That takes a long-running process rather than a CI job. The OpenClaw integration covers deploying a chat-native agent on your cluster with the Ankra CLI installed as a skill, using the same token model as above.

Start with the pipeline agents, though. They are three YAML files, one token, and an afternoon of work, and they put live-cluster context into the two places your team already looks: the PR thread and the Slack channel.


Get started: Create a free account on Ankra.

Join our community: Slack

Follow us on: LinkedIn | GitHub

Contact us: [email protected]

ShareXLinkedInHN

Get the next post in your inbox

Platform engineering guides and product updates. No spam, unsubscribe anytime.

Related Posts