381 lines
16 KiB
Markdown
381 lines
16 KiB
Markdown
# 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
|
||
./scripts/gitea-config-menu
|
||
```
|
||
|
||
The generated launcher snippets contain only `command`, `args`,
|
||
`GITEA_MCP_CONFIG`, and `GITEA_MCP_PROFILE` — never a token or password.
|
||
</details>
|
||
|
||
### Portable LLM workflow skill
|
||
|
||
Reusable LLM operating rules are packaged as a portable skill at
|
||
[`skills/llm-project-workflow/SKILL.md`](skills/llm-project-workflow/SKILL.md).
|
||
It documents issue-first work, isolated branch worktrees, no self-review or
|
||
self-merge, profile safety, fail-closed behavior, merge cleanup, and recovery
|
||
patterns. Copy the `skills/llm-project-workflow/` directory into other projects
|
||
that should use the same workflow.
|
||
|
||
<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.
|