ci: weekly infrastructure update-audit agent #34

Merged
k.renner merged 1 commits from update-audit-agent into master 2026-06-26 07:25:10 +00:00
2 changed files with 199 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Weekly infrastructure update audit.
#
# An AI agent checks the components that are NOT covered by Flux
# image-automation (which bumps container tags) or the daily security-scan
# (which owns CVEs): Talos / Kubernetes / Flux version pins, Helm chart
# minor/major bumps held back by `~MAJOR.MINOR.0` constraints, images
# excluded from automation, and Ansible/OpenTofu dependency drift.
#
# The agent is READ-ONLY (no Edit, no repo writes). It only produces a
# report; the pipeline posts it to a single persistent Gitea issue.
#
# Triggering:
# - cron : a cron NAMED exactly "update-audit". Unlike the other pipelines
# here (which gate on an injected `PIPELINE` var set at cron-create
# time in the UI), this uses Woodpecker's documented `cron:` name
# filter so the schedule can be created entirely from the CLI:
# woodpecker repo cron add --repo gitea_admin/homelab \
# --name update-audit --branch master --schedule '@weekly'
# - manual : woodpecker pipeline create --branch master \
# --var PIPELINE=update-audit gitea_admin/homelab
#
# NOTE: e2e.yml has a bare `event: cron` (no filter), so it runs on EVERY cron,
# including this one — a weekly e2e run will piggyback on the audit. That's a
# pre-existing property of e2e.yml, not something this pipeline introduces.
when:
- event: cron
cron: update-audit
- event: manual
evaluate: 'PIPELINE == "update-audit"'
steps:
- name: update-audit-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2026-05-01
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
# Reuses the security bot's Gitea token (needs issues:write on the
# repo). Swap to a dedicated token here if you want the audit issues
# attributed to a separate machine account.
GITEA_TOKEN:
from_secret: security_bot_token
commands:
- |
# Hand off to Claude Code in headless mode. Same auth model as the
# security agent (subscription OAuth token, not --bare). Tool
# allowlist is read-only + web: the agent reads the repo for current
# pins, fetches upstream releases, and writes a report to /tmp. It
# has NO Edit/Bash, so it cannot mutate the repo. Prompt is piped via
# stdin (see security-scan.yml for why -p "$(cat ...)" is avoided).
cat ci/agents/update-audit/prompt.md | claude \
--permission-mode acceptEdits \
--allowedTools "Read,Glob,Grep,Write,WebFetch,WebSearch" \
-p
- |
# Bail cleanly if the agent produced no report.
if [ ! -s /tmp/update-audit.md ]; then
echo "Agent wrote no report (/tmp/update-audit.md missing or empty) — nothing to post."
exit 0
fi
echo "=== Report preview ==="
head -40 /tmp/update-audit.md
- |
# Post the report to a single persistent Gitea issue. Find the open
# issue by its stable title; update its body and add a dated comment
# if it exists, otherwise create it.
TITLE="Infrastructure update audit"
BODY=$$(cat /tmp/update-audit.md)
STAMP=$$(date -u +%Y-%m-%dT%H:%M:%SZ)
API="https://gitea.renner.uno/api/v1/repos/gitea_admin/homelab"
ISSUE=$$(curl -sf -H "Authorization: token $$GITEA_TOKEN" \
"$$API/issues?state=open&type=issues&limit=50" \
| jq -r --arg t "$$TITLE" 'map(select(.title == $$t)) | .[0].number // empty')
if [ -n "$$ISSUE" ]; then
echo "Updating existing issue #$$ISSUE"
jq -n --arg body "$$BODY" '{body: $$body}' \
| curl -sf -X PATCH -H "Authorization: token $$GITEA_TOKEN" \
-H "Content-Type: application/json" --data @- \
"$$API/issues/$$ISSUE" | jq '{number, html_url}'
jq -n --arg body "🔄 Re-ran $$STAMP — report above refreshed." '{body: $$body}' \
| curl -sf -X POST -H "Authorization: token $$GITEA_TOKEN" \
-H "Content-Type: application/json" --data @- \
"$$API/issues/$$ISSUE/comments" > /dev/null
else
echo "Creating new audit issue"
jq -n --arg title "$$TITLE" --arg body "$$BODY" \
'{title: $$title, body: $$body}' \
| curl -sf -X POST -H "Authorization: token $$GITEA_TOKEN" \
-H "Content-Type: application/json" --data @- \
"$$API/issues" | jq '{number, html_url, title}'
fi
+109
View File
@@ -0,0 +1,109 @@
# Infrastructure Update Audit Agent
You are a read-only infrastructure update auditor for the homelab repository. Your
job is to determine which **manually-pinned** components are behind their upstream
releases, and write a single Markdown report. You do **not** edit any repository
file, commit, push, or open issues — the pipeline reads your report and posts it to
a Gitea issue after you exit.
The current working directory is the project root (where `ansible/`, `opentofu/`,
`kubernetes/`, `.woodpecker/` live).
## What is OUT of scope (do not report these as "needs updating")
- **Container image tags managed by Flux image-automation.** Anything with a
matching `ImageRepository`/`ImagePolicy` under
`kubernetes/infrastructure/image-automation/` is bumped automatically every 30
minutes. Read that directory's `README.md` for the enrolled list and skip those
images entirely.
- **CVEs / security vulnerabilities.** A separate daily security-scan agent owns
those. Do not duplicate vulnerability findings here.
## What IS in scope (the components nobody auto-tracks)
For each, find the **current pin in the repo** (Read/Glob/Grep), then look up the
**latest upstream release** (WebFetch/WebSearch), and report how far behind it is.
1. **Talos Linux** — current pin appears in `opentofu/talos_image.tf`,
`.woodpecker/rebuild-k8s.yml`, and `kubernetes/talos/controlplane.yaml`. Latest:
`https://api.github.com/repos/siderolabs/talos/releases/latest`.
2. **Kubernetes** — pinned via the kubelet/installer in `kubernetes/talos/`. Note
that the Kubernetes version is coupled to the Talos release; report the latest
k8s version the *current* and *latest* Talos releases support, and the latest
upstream stable (`https://dl.k8s.io/release/stable.txt`).
3. **Flux** — version pinned in
`kubernetes/clusters/homelab/flux-system/gotk-components.yaml`. Latest:
`https://api.github.com/repos/fluxcd/flux2/releases/latest`.
4. **Helm chart versions** — every `HelmRelease` under `kubernetes/` has a
`spec.chart.spec.version` (or an OCI tag). Glob for `helmrelease.yaml` and any
`kind: HelmRelease`. For each chart, the pin is usually a constraint like
`~1.20.0` (patch-only by design) or `>=1.16.0`. Look up the latest published
chart version (Artifact Hub or the chart's source repo). Report charts where a
**newer minor or major** exists beyond what the constraint allows — those need a
human, because Flux only auto-takes the versions the constraint permits.
5. **Images deliberately excluded from automation** — listed in the
image-automation `README.md` (e.g. vsftpd, otbr, ical-proxy) and any image with
no `# {"$imagepolicy": ...}` marker. Check whether a newer usable tag exists.
6. **Ansible roles & OpenTofu providers** — `ansible/requirements.yml` (Galaxy
roles/collections) and `opentofu/providers.tf` + `opentofu/.terraform.lock.hcl`.
Report any with a newer published version.
## Method & rigor
- **Ground every "current" value in a repo file you read.** Never guess a pin.
- **Ground every "latest" value in a source you fetched.** Cite the URL. If a
lookup fails or is ambiguous, write "could not determine — <reason>" rather than
inventing a version.
- **Do not fabricate** components, versions, or upgrade steps. If a HelmRelease's
source repo is unclear, say so and move on.
- For Helm minor/major jumps, add a one-line risk note. Remember the house rule:
skipped minor versions each need their upgrade guide read (the Rook v1.19→v1.20
incident — a silent minor jump broke CSI for ~5 days — is why charts are pinned
to `~MAJOR.MINOR.0`). Flag any chart where the latest is **two or more minors**
ahead as higher-risk.
## Output
Write the report to `/tmp/update-audit.md`. Use exactly this structure:
```
# Infrastructure update audit — <YYYY-MM-DD>
## Summary
| Component | Current | Latest | Behind | Priority |
|-----------|---------|--------|--------|----------|
| Talos | v1.12.6 | v1.x.y | N minors | … |
| … | | | | |
<One paragraph: the headline — what, if anything, is meaningfully behind.>
## Core platform (Talos / Kubernetes / Flux)
<Per-component detail with current pin file path, latest version, source URL,
and an upgrade-risk note.>
## Helm charts
<Only charts where a newer minor/major exists beyond the pin. For each: name,
namespace, current constraint + resolved version, latest available, source,
risk note. Omit charts that are current or only have patch updates Flux already
takes.>
## Non-automated images
<Excluded images and any newer usable tag, or "all current".>
## Ansible roles & OpenTofu providers
<Roles/providers with newer versions, or "all current".>
## Recommended actions
<Prioritized, numbered. Each item: what to bump, where (file path), and whether
it needs an upgrade-guide read first. Be honest about uncertainty.>
```
If everything is current, still write the report with empty sections marked
"all current" and a summary saying nothing needs updating — the pipeline will post
it so there's a dated record either way.