Files
agent-tools/AGENTS.md
T
unkin-agent 05594113a2
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Fix non-zero exit on error and debounce transient mergeable=false
agentpr/watchpr already propagated command errors to a non-zero exit, but
that behaviour had no regression coverage and the root command was not
constructible outside main(). watchpr also fired a spurious conflict alert
because Gitea computes mergeability asynchronously and can briefly report
mergeable=false right after a push. The docs additionally printed the
AppRole role_id literal UUID.

- Extract newRootCmd() in both cmd/agentpr and cmd/watchpr so main() only
  runs Execute and exits non-zero on error; add tests asserting Execute
  returns an error for a bad PR ref / malformed --repo / no args.
- Debounce mergeability loss in MeaningfulChange: only alert when
  mergeable=false persists across two consecutive polls (both prev and cur
  false, still open); update the table test for one-poll-false (benign),
  false-persisting (alert), and recovered false->true (benign).
- Refer to AGENT_APPROLE_ROLE_ID by env var in README.md/AGENTS.md without
  printing the literal role_id; keep the code default and env override.
2026-08-12 22:17:10 +10:00

4.2 KiB

AGENTS.md

Project Overview

This repo ships two Gitea-automation CLIs in one RPM (agent-tools). Both act as the unkin-agent user by minting a scoped Gitea token from Vault, so actions are attributed to the agent rather than to whoever runs the tool.

  • agentpr — create pull requests and post PR comments as unkin-agent (fixes the "tea posts as Ben" attribution problem). Subcommands: pr create, pr comment, whoami.
  • watchpr — poll one or more PRs and exit when a tracked PR changes meaningfully: it merges/closes, gets a new non-agent comment, its CI fails, or it loses mergeability. Benign transitions (CI pending→success, the agent's own comments) are ignored.

Both tools are separate main packages under cmd/ and share the internal/agent package (Vault AppRole login, Gitea REST client, PR-ref parsing, watch-state comparison).

Structure

cmd/agentpr/main.go      # agentpr CLI (pr create / pr comment / whoami)
cmd/watchpr/main.go      # watchpr CLI (poll + meaningful-change exit)
internal/agent/          # shared plumbing:
  token.go               #   env config + in-process Gitea-token cache
  vault.go               #   AppRole login + read gitea/creds/unkin-agent
  gitea.go               #   Gitea REST client (PR create/get, comments, status, whoami)
  parse.go               #   owner/repo#N and owner/repo parsing
  watch.go               #   PRState snapshot + MeaningfulChange comparison
go.mod                   # module git.unkin.net/unkin/agent-tools
Makefile                 # build / test / lint / completions / rpm / version-bump
packaging/nfpm.yaml      # nfpm spec (envsubst-templated) for the RPM (both binaries)
scripts/build-rpm.sh     # generates completions + packages the RPM with nfpm
.woodpecker/             # CI: build, test, pre-commit (PR) + release (tag)
dist/                    # build output: binaries, completions, RPM (not committed)

Every binary is a separate main package, so make build builds each with its own -o (a single go build ./... can't emit multiple mains to one file).

Token acquisition (shared)

Both tools call agent.GiteaToken(), which (once per process):

  1. AppRole login: POST $VAULT_ADDR/v1/auth/approle/login with role_id only (no secret_id) → client_token.
  2. GET $VAULT_ADDR/v1/gitea/creds/unkin-agent with X-Vault-Token.data.token.

Config via env (all have defaults):

Variable Default Purpose
VAULT_ADDR https://vault.service.consul:8200 Vault/OpenBao address
AGENT_APPROLE_ROLE_ID built-in default AppRole role_id (overridable)
GITEA_URL https://git.unkin.net Gitea base URL
AGENT_LOGIN unkin-agent login whose comments watchpr ignores

Build

make build          # -> dist/agentpr, dist/watchpr (CGO disabled, static)

Requires Go 1.21+. Dependency: github.com/spf13/cobra (CLI).

Packaging (RPM)

make rpm            # build both binaries + package into dist/*.rpm via nfpm

scripts/build-rpm.sh generates bash/zsh/fish completions from the built binaries and bundles them alongside /usr/bin/agentpr and /usr/bin/watchpr. On a v* tag the release pipeline builds the RPM and PUTs it to the artifactapi rpm-internal repo, then cuts a Gitea release.

Shell completions

Cobra provides a completion subcommand for each binary (agentpr completion bash, etc.). The RPM installs them to the standard system paths (/usr/share/bash-completion/completions/, /usr/share/zsh/site-functions/, /usr/share/fish/vendor_completions.d/).

Testing

make test           # go test -v -race ./...

internal/agent covers PR-ref parsing, the MeaningfulChange table (benign vs alerting transitions), request-body construction, and the Vault+Gitea client against httptest servers (fake AppRole login + gitea creds + PR create / comment / whoami / status). No live Vault/Gitea access is required for tests.

Gotchas

  • watchpr exits 0 with no output changes on --once (just prints state).
  • The token cache is process-wide (sync.Once); tests call the unexported fetchGiteaToken to avoid it.
  • CI "combined status" comes from /commits/{sha}/status; an empty head SHA yields an empty state without an API call.