Files
Gitea-Tools/README.md
T
sysadmin 389382c2bd docs: add LLM-operated Gitea workflow runbooks (#17)
Add docs/llm-workflow-runbooks.md — the final roadmap #10 deliverable:
operational runbooks for LLM-operated Gitea workflows, built on the shipped
canonical profiles + interactive menu + gated review/merge + audit logging.

Covers:
- Principle: the profile is the role, not the LLM (task-scoped, not assigned).
- Canonical config: GITEA_MCP_CONFIG / GITEA_MCP_PROFILE, version, profiles,
  keychain + env auth references, precedence, legacy env-only fallback.
- Interactive menu (python gitea_config.py menu): create author/reviewer
  profiles, generate Claude/Gemini/Codex launcher snippets, validate auth,
  check PR reviewer eligibility.
- Thin-launcher pattern: LLM configs carry only command/args + the two
  GITEA_MCP_* vars — never raw tokens/passwords.
- Migration away from duplicated GITEA_USER_*/GITEA_PASS_*/GITEA_SITE_* blocks;
  secrets referenced by keychain id or env var name only.
- Per-workflow runbooks (create issue/children, implement+PR, review/request-
  changes/approve, merge, close-after-merge, stop-on-blocker) with safe prompts.
- Fail-closed behavior table (unknown identity/profile, self-author, moved head,
  unexpected files, detected secrets, production/deploy) and no self-review/merge.

Docs-only: no implementation code. Safe placeholder examples only (no real
tokens, passwords, usernames, or private config). README links the new runbook.

Closes #17. Refs #10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:24:33 -04:00

372 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gitea Tools
A collection of Python scripts and an MCP server to automate interactions with Gitea instances.
## Supported Instances
| Remote | Host | Org / Repo |
|----------------|----------------------------|-------------------------------------|
| `dadeschools` | `gitea.dadeschools.net` | `Contractor / Timesheet` |
| `prgs` | `gitea.prgs.cc` | `Scaled-Tech-Consulting / Timesheet`|
## Authentication
Authentication is configured via environment variables or a local `.env` file in the repository root (uses `python-dotenv`).
Create a `.env` file in the project root:
```bash
# Option A: Gitea Personal Access Tokens (Recommended)
GITEA_TOKEN_DADESCHOOLS="your_token_here"
GITEA_TOKEN_PRGS="your_token_here"
# Option B: Gitea Username & Password (fallback)
GITEA_USER_DADESCHOOLS="username"
GITEA_PASS_DADESCHOOLS="password"
GITEA_USER_PRGS="username"
GITEA_PASS_PRGS="password"
# Optional: Fallback to macOS Keychain (via git credential fill)
# GITEA_USE_KEYCHAIN=1
```
## MCP Server (Recommended)
The Gitea-Tools MCP server exposes all functionality as structured tool calls.
Any MCP-compatible agent (Antigravity, Claude Code, etc.) can call these tools natively.
### Available Tools
| Tool | Description |
|------|-------------|
| `gitea_create_issue` | Create an issue with title, body, remote |
| `gitea_create_pr` | Open a pull request with title, head, base |
| `gitea_edit_pr` | Edit details of an existing pull request |
| `gitea_list_prs` | List pull requests with state/remote |
| `gitea_view_pr` | Get full details of a single pull request |
| `gitea_merge_pr` | Gated merge: merge/squash/rebase only after identity+profile+eligibility gates pass, explicit `confirmation="MERGE PR <n>"`, optional head-SHA and changed-files pinning (no self-merge, no force) |
| `gitea_review_pr` | Legacy wrapper for `gitea_submit_pr_review` (merging disabled) |
| `gitea_delete_branch` | Delete a remote branch |
| `gitea_close_issue` | Close an issue by number |
| `gitea_list_issues` | List issues with state/label filters |
| `gitea_view_issue` | Get full details of a single issue |
| `gitea_whoami` | Read-only: identify the authenticated Gitea account (safe metadata only) |
| `gitea_get_profile` | Read-only: describe the active runtime execution profile (safe metadata only) |
| `gitea_check_pr_eligibility` | Read-only: check if the current identity/profile may review/approve/request_changes/merge a PR |
| `gitea_submit_pr_review` | Gated review mutation: comment/approve/request_changes, only after identity+profile+eligibility gates pass (no merge, no self-approval) |
| `gitea_mark_issue` | Claim/release an issue (start/done) |
| `gitea_list_labels` | List all available labels in a repository |
| `gitea_create_label` | Create a new label with custom color |
| `gitea_set_issue_labels` | Replace all labels on an issue |
| `gitea_get_file` | Retrieve file content and SHA metadata |
| `gitea_commit_files` | Commit changes to multiple files atomically |
| `gitea_mirror_refs` | Mirror branches + tags between instances |
### Setup
#### 1. Install dependencies
```bash
cd /Users/jasonwalker/Development/Gitea-Tools
python3 -m venv venv # skip if venv already exists
source venv/bin/activate
pip install "mcp[cli]"
```
#### 2. Configure your AI client
The MCP server uses **stdio transport** — each client starts it as a subprocess.
Add the config below to your client, then restart it.
<details>
<summary><strong>Antigravity (Google)</strong></summary>
Add to `~/.gemini/antigravity-ide/mcp_config.json` inside `"mcpServers"`:
```json
"gitea-tools": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {}
}
```
Restart Antigravity to load the server. Tools appear as lazy-loaded MCP tools
(call via `call_mcp_tool` with `ServerName: "gitea-tools"`).
</details>
<details>
<summary><strong>Claude Code (Anthropic)</strong></summary>
Add to `~/.claude.json` (global) or `.mcp.json` in the project root:
```json
{
"mcpServers": {
"gitea-tools": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"]
}
}
}
```
Restart Claude Code. Tools appear as `mcp__gitea-tools__gitea_create_issue`, etc.
</details>
<details>
<summary><strong>Any MCP-compatible client</strong></summary>
The server is a standard MCP stdio server. Point your client at:
- **Command:** `/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3`
- **Args:** `["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"]`
- **Transport:** `stdio`
No environment variables needed — auth is handled via macOS keychain.
</details>
<details>
<summary><strong>Runtime profiles (multiple env-configured entries)</strong></summary>
The same server can run as **separate MCP entries**, each authenticating as its
own Gitea token and carrying its own profile name. This keeps roles task-scoped:
*the profile is the role, not the LLM.* Point each entry at a different
gitignored env file.
```json
{
"mcpServers": {
"gitea-tools-reviewer": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_PROFILE_NAME": "gitea-reviewer",
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"
}
},
"gitea-tools-merger": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_PROFILE_NAME": "gitea-merger",
"GITEA_ALLOWED_OPERATIONS": "read,merge"
}
}
}
}
```
Recognized environment fields (see [`.env.example`](.env.example) for placeholders):
| Variable | Purpose |
|----------|---------|
| `GITEA_TOKEN` | API token for this runtime. Read only by the auth layer; **never** returned, logged, or committed. |
| `GITEA_PROFILE_NAME` | Non-secret label for the running profile (e.g. `gitea-reviewer`). Surfaced by `gitea_whoami`. |
| `GITEA_ALLOWED_OPERATIONS` | Optional, comma-separated operation categories (descriptive metadata only for now). |
| `GITEA_FORBIDDEN_OPERATIONS` | Optional, comma-separated categories this profile must not perform (descriptive). |
| `GITEA_AUDIT_LABEL` | Optional short label for this runtime, for audit purposes. |
| `GITEA_TOKEN_SOURCE` | Optional *name* of the token source (e.g. an env var name). A name only — never the token value. |
| `GITEA_BASE_URL` | Optional informational base URL. |
| `GITEA_AUDIT_LOG` | Optional path to an audit log file. When set, mutating actions append one redacted JSON record each (profile + authenticated user + outcome). Unset ⇒ auditing off (no records, no extra API calls). |
| `GITEA_MCP_CONFIG` | Optional path to a JSON file defining multiple named runtime profiles. Unset ⇒ pure env behaviour. |
| `GITEA_MCP_PROFILE` | Name of the profile (from `GITEA_MCP_CONFIG`) to activate for this runtime. |
Notes:
- This provides **one token + one profile per process**. It does not implement
multi-token switching inside a single runtime, nor any approve/merge/eligibility
gating — those are later roadmap items (#14#18).
- Profile name and allowed operations are **metadata only**; the token value is
never part of any tool output. `gitea_whoami` returns the profile name, and
`gitea_get_profile` returns the full non-secret profile metadata so a workflow
can inspect which runtime it is talking to before deciding to act.
- See [`docs/gitea-execution-profiles.md`](docs/gitea-execution-profiles.md) for
the full profile model, and
[`docs/llm-workflow-runbooks.md`](docs/llm-workflow-runbooks.md) for the
task-scoped, profile-based runbooks (create/review/merge/close, thin
launchers, migration, fail-closed rules).
- **Audit logging (#18):** mutating actions emit a durable, redacted JSON audit
record — timestamp, action, result (`allowed`/`blocked`/`failed`/`succeeded`),
profile name + audit label, authenticated username, target repo/issue/PR,
branch and head SHA where applicable — when `GITEA_AUDIT_LOG` is set. Auditing
is off by default and never adds API calls or breaks the action when off.
See [`gitea_audit.py`](gitea_audit.py).
**Canonical runtime profiles (#19).** Define every Gitea profile **once**, in a
canonical JSON file, and keep each LLM launcher (Claude / Gemini / Codex) a
*thin* pointer at it — no duplicated `GITEA_USER_*` / `GITEA_PASS_*` blocks and
no raw tokens in client configs. See [`gitea-mcp.example.json`](gitea-mcp.example.json),
loaded by [`gitea_config.py`](gitea_config.py).
Canonical profile file (e.g. `~/.config/gitea-tools/profiles.json`):
```json
{
"version": 1,
"profiles": {
"prgs": {
"base_url": "https://gitea.prgs.cc",
"username": "jcwalker3",
"auth": { "type": "keychain", "id": "prgs-gitea-token" },
"default_owner": "Scaled-Tech-Consulting",
"execution_profile": "personal-prgs"
},
"mdcps": {
"base_url": "https://gitea.dadeschools.net",
"username": "913443",
"auth": { "type": "env", "name": "GITEA_TOKEN_MDCPS" },
"execution_profile": "mdcps"
}
}
}
```
Thin LLM launcher (Claude / Gemini / Codex) — only two env vars, no secrets:
```json
"gitea-tools": {
"command": "/Users/jasonwalker/Development/Gitea-Tools/venv/bin/python3",
"args": ["/Users/jasonwalker/Development/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_MCP_CONFIG": "/Users/jasonwalker/.config/gitea-tools/profiles.json",
"GITEA_MCP_PROFILE": "prgs"
}
}
```
- **Secrets by reference only:** a profile's `auth` names *where* the token
lives — `{ "type": "keychain", "id": "..." }` (macOS keychain) or
`{ "type": "env", "name": "..." }` (env var). Inline `token`/`password` keys
are rejected. The value is resolved on demand and never stored in, returned
by, or logged as profile metadata.
- **Precedence:** explicit process env vars (`GITEA_PROFILE_NAME`,
`GITEA_BASE_URL`, `GITEA_TOKEN`, …) **override** the JSON profile; the JSON
profile only fills what the environment leaves unset.
- **Backwards compatible / fail-safe:** with `GITEA_MCP_CONFIG` unset, behaviour
is exactly the legacy env-only mode. A missing file, invalid JSON, unsupported
`version`, unknown/unset selected profile, or unresolvable secret reference
raises a clear startup error that never prints file contents, tokens, or
passwords. Parsing makes no network calls.
**Migrating from duplicated `GITEA_PASS_*` blocks.** Move each instance's
credentials into one canonical profile entry (referencing a keychain id or env
var for the secret), then delete the `GITEA_USER_*` / `GITEA_PASS_*` /
`GITEA_SITE_*` blocks from every LLM `mcp_config.json`, leaving only
`GITEA_MCP_CONFIG` + `GITEA_MCP_PROFILE`. Existing env-only setups keep working
unchanged until migrated.
**Interactive setup — no hand-editing JSON.** Run the menu to create/edit/
validate profiles, store a token in the macOS keychain (never echoed or written
to any config), test a profile's authentication, print the authenticated user,
check reviewer eligibility for a PR, and generate ready-to-paste launcher
snippets for Claude / Gemini / Codex:
```bash
python gitea_config.py menu
```
The generated launcher snippets contain only `command`, `args`,
`GITEA_MCP_CONFIG`, and `GITEA_MCP_PROFILE` — never a token or password.
</details>
<details>
<summary><strong>Codex / non-MCP tools</strong></summary>
OpenAI Codex and other tools that don't support MCP can use the CLI scripts
directly. See the [CLI Scripts](#cli-scripts) section below.
```bash
# Example: Codex can shell out to the scripts
python3 /Users/jasonwalker/Development/Gitea-Tools/create_issue.py \
--remote prgs --title "Bug report" --body "Details here"
```
</details>
## CLI Scripts
The MCP tools can also be used as standalone CLI scripts:
| Script | Description |
|---------------------|--------------------------------------------------------------------|
| `create_issue.py` | Create an issue (`--remote`, `--title`, `--body`, `--body-file`) |
| `create_pr.py` | Open a Pull Request (`--remote`, `--title`, `--head`, `--base`) |
| `edit_pr.py` | Edit a Pull Request (`--title`, `--body`, `--body-file`, etc.) |
| `review_pr.py` | Review/sign-off on a pull request (`--merge` is disabled — fails closed; merge only via gated `gitea_merge_pr`) |
| `close_issue.py` | Close a specific issue |
| `mark_issue.py` | Claim/release an issue via `status:in-progress` label |
| `manage_labels.py` | Create label set and apply label mappings (`--dry` to preview) |
| `mirror_refs.sh` | Mirror branches + tags between dadeschools ⇄ prgs |
### Quick Examples
```bash
# Create an issue
./create_issue.py --title "Fix PDF output" --body "Blank on Safari"
# Create an issue on the prgs instance
./create_issue.py --remote prgs --title "Add tests" --body-file description.md
# Create a PR
./create_pr.py --title "feat: add validation" --head feat/validation --body "Closes #12"
# Edit a PR's description or title
./edit_pr.py 155 --body "Updated description wording"
# Review and approve a PR (review only — CLI merge is disabled; use the
# gated gitea_merge_pr MCP workflow to merge)
./review_pr.py --pr-number 12 --event APPROVE --body "Approved"
# Close issue #5
./close_issue.py 5
# Claim an issue before working on it
./mark_issue.py 10 start
# Release when done
./mark_issue.py 10 done
# Mirror refs (dry-run by default)
./mirror_refs.sh
# Actually push the refs
./mirror_refs.sh --apply
```
Use `--help` on any Python script or shell script for full usage details.
## Architecture
```
gitea_auth.py ← shared auth & API helpers (get_credentials, api_request)
mcp_server.py ← MCP server (FastMCP, stdio transport)
create_issue.py ← CLI: create issues
create_pr.py ← CLI: create PRs
edit_pr.py ← CLI: edit PRs
review_pr.py ← CLI: review PRs
manage_labels.py ← CLI: label management
close_issue.py ← CLI: close issues
mark_issue.py ← CLI: claim/release issues
mirror_refs.sh ← CLI: ref mirroring
```
## Tests
```bash
# Run with the venv (includes MCP SDK)
source venv/bin/activate
python3 -m pytest tests/ -v
```
| Test file | Covers |
|--------------------------|---------------------------------------------------------|
| `test_mcp_server.py` | All 7 MCP tools: create, list, view, close, mark, PR, mirror |
| `test_create_issue.py` | CLI arg parsing, remote resolution, payload, auth, errors |
| `test_create_pr.py` | CLI arg parsing, remote resolution, payload, auth, errors |
| `test_credentials.py` | `get_credentials()`, `get_auth_header()`, `repo_api_url()` |
| `test_manage_labels.py` | Label create/skip, dry run, mapping, constant validation |
| `test_python_cli.py` | `close_issue.py` + `mark_issue.py` CLI validation |
| `test_mirror_refs.py` | Flags, safety defaults, local integration tests |
All tests mock network and keychain access — no real API calls are made.