ci: weekly infrastructure update-audit agent #34
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user