Add Gitea integration for agent to push branches and open pull requests
- Implemented `register-gitea-bot.sh` to provision Gitea access token. - Updated `maintainer.py` to support Gitea API for push and PR operations. - Modified `install.sh` to warn if Gitea token is not provisioned. - Enhanced documentation in `README.md`, `CLAUDE.md`, and `architecture.md` to reflect new Gitea functionality. - Added Gitea configuration to `config.example.json`.
This commit is contained in:
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
# register-gitea-bot.sh — provision the Gitea access token the maintenance agent
|
||||
# uses to push branches and open pull requests as the `claude-code` user.
|
||||
#
|
||||
# git.semprini.me (Gitea) authenticates humans via Keycloak OIDC and has the
|
||||
# password sign-in form AND HTTP basic auth disabled. `claude-code` is an
|
||||
# OIDC-linked account that is already a push-capable collaborator on the repo; it
|
||||
# has no password, so — exactly like the Matrix bot's shared-secret registration
|
||||
# — we mint a standalone personal access token via the Gitea admin CLI inside the
|
||||
# running container and store it in config.json.
|
||||
#
|
||||
# git-over-HTTPS still accepts a personal access token as the password even with
|
||||
# basic auth disabled, and the API accepts it via the `Authorization: token`
|
||||
# header — so no SSH exposure or core-stack change is required.
|
||||
#
|
||||
# Idempotent: reuses the token already in config.json when it still validates;
|
||||
# otherwise mints a fresh one (Gitea reveals a token only at creation time, so a
|
||||
# lost token means a new one). Old tokens can be revoked in the Gitea UI.
|
||||
#
|
||||
# Requirements: run on the core-stack host with the gitea container running.
|
||||
#
|
||||
# Usage:
|
||||
# ./register-gitea-bot.sh # uses config.json next to this script
|
||||
# CONFIG=/path/config.json ./register-gitea-bot.sh
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG="${CONFIG:-$DIR/config.json}"
|
||||
GITEA_CONTAINER="${GITEA_CONTAINER:-user-gitea}"
|
||||
# write:repository -> push + open PRs; read:user -> token self-validation.
|
||||
TOKEN_SCOPES="${TOKEN_SCOPES:-write:repository,read:user}"
|
||||
|
||||
[ -f "$CONFIG" ] || {
|
||||
echo "ERROR: $CONFIG not found. Copy config.example.json to config.json first."
|
||||
exit 1; }
|
||||
|
||||
# Read a python expression against the parsed config (c), with safe defaults so a
|
||||
# config.json that predates the gitea block still works.
|
||||
cfg_get() { python3 -c "import json; c=json.load(open('$CONFIG')); print($1)"; }
|
||||
|
||||
API_BASE="$(cfg_get "c.get('gitea',{}).get('api_base','https://git.semprini.me/api/v1')")"
|
||||
GUSER="$(cfg_get "c.get('gitea',{}).get('user','claude-code')")"
|
||||
REPO="$(cfg_get "c.get('gitea',{}).get('repo','paul/semprini-core')")"
|
||||
|
||||
echo "→ Gitea API : $API_BASE"
|
||||
echo "→ User : $GUSER"
|
||||
echo "→ Repo : $REPO"
|
||||
|
||||
token_valid() {
|
||||
local t="$1"
|
||||
[ -n "$t" ] || return 1
|
||||
case "$t" in FILLED_BY_*) return 1;; esac
|
||||
local login
|
||||
login="$(curl -s -m 15 -H "Authorization: token $t" "$API_BASE/user" \
|
||||
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("login",""))' \
|
||||
2>/dev/null || true)"
|
||||
[ "$login" = "$GUSER" ]
|
||||
}
|
||||
|
||||
# 1. Reuse the stored token if it still validates; otherwise mint a fresh one.
|
||||
STORED="$(cfg_get "c.get('gitea',{}).get('token','')")"
|
||||
if token_valid "$STORED"; then
|
||||
echo "→ Existing token in config.json is valid; reusing it."
|
||||
TOKEN="$STORED"
|
||||
else
|
||||
NAME="semprini-maintainer-$(date +%s)"
|
||||
echo "→ Minting a new access token ($NAME; scopes: $TOKEN_SCOPES)…"
|
||||
TOKEN="$(docker exec -u git "$GITEA_CONTAINER" \
|
||||
gitea admin user generate-access-token \
|
||||
-u "$GUSER" -t "$NAME" --scopes "$TOKEN_SCOPES" --raw 2>&1 \
|
||||
| tr -d '[:space:]')"
|
||||
echo "$TOKEN" | grep -qE '^[0-9a-f]{40}$' || {
|
||||
echo "ERROR: failed to mint token: $TOKEN"; exit 1; }
|
||||
fi
|
||||
|
||||
# 2. Verify push access to the target repo (warn only — token is still usable).
|
||||
PUSH="$(curl -s -m 15 -H "Authorization: token $TOKEN" "$API_BASE/repos/$REPO" \
|
||||
| python3 -c 'import sys,json; print("yes" if json.load(sys.stdin).get("permissions",{}).get("push") else "no")' \
|
||||
2>/dev/null || echo no)"
|
||||
if [ "$PUSH" = "yes" ]; then
|
||||
echo "→ $GUSER has push access to $REPO ✓"
|
||||
else
|
||||
echo "WARN: $GUSER cannot push to $REPO. Add it as a collaborator with Write"
|
||||
echo " permission in Gitea, or pushes/PRs will be rejected."
|
||||
fi
|
||||
|
||||
# 3. Write the token back into config.json (creating the gitea block if absent).
|
||||
python3 - "$CONFIG" "$TOKEN" <<'PY'
|
||||
import json, sys
|
||||
cfg_path, token = sys.argv[1], sys.argv[2]
|
||||
cfg = json.load(open(cfg_path))
|
||||
cfg.setdefault("gitea", {})["token"] = token
|
||||
json.dump(cfg, open(cfg_path, "w"), indent=2)
|
||||
print(f"→ Wrote gitea.token to {cfg_path}")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "✓ Done. The maintenance agent can now push branches and open PRs as $GUSER."
|
||||
Reference in New Issue
Block a user