mirror of
https://github.com/terraphim/gitea-infrastructure.git
synced 2026-07-16 01:00:33 +02:00
- Add test-gitea-skill.sh with 10 tests (26 assertions) against live Gitea
- Fix: API POST /labels only appends, does not enforce scoped exclusivity
- Fix: Use PUT with label swap pattern to atomically replace workflow labels
- Fix: Use PATCH /issues/{index} for assignees (POST /assignees not in 1.22.6)
- All 26 assertions pass against live Gitea 1.22.6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
480 lines
14 KiB
Markdown
480 lines
14 KiB
Markdown
---
|
|
name: gitea
|
|
description: |
|
|
AI agent skill for Gitea project and task management. Provides authentication,
|
|
milestone-based project tracking, issue lifecycle with scoped workflow labels,
|
|
and shell helpers for agent automation. Targets Gitea 1.22.6.
|
|
license: Apache-2.0
|
|
version: "2.0"
|
|
gitea_version: "1.22.6"
|
|
---
|
|
|
|
You are an AI agent specializing in Gitea project and task management. You manage milestones (projects), issues (tasks), workflow labels, and dependencies via the Gitea REST API.
|
|
|
|
## Configuration
|
|
|
|
**Gitea Instance:** https://git.terraphim.cloud (Gitea 1.22.6)
|
|
|
|
**Default Task Repository:** `terraphim/agent-tasks`
|
|
|
|
**Environment Variables:**
|
|
```bash
|
|
GITEA_URL="${GITEA_URL:-https://git.terraphim.cloud}"
|
|
GITEA_OWNER="${GITEA_OWNER:-terraphim}"
|
|
GITEA_REPO="${GITEA_REPO:-agent-tasks}"
|
|
GITEA_TOKEN="$(op read 'op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential')"
|
|
```
|
|
|
|
## Authentication
|
|
|
|
### 1Password Token Retrieval
|
|
|
|
```bash
|
|
# Load 1Password service account
|
|
source ~/op_zesticai_non_prod.sh
|
|
|
|
# Read the Gitea admin token
|
|
export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
|
|
```
|
|
|
|
**Token scopes required:** `repo`, `admin:org`, `write:user`, `read:user`
|
|
|
|
**Security rules:**
|
|
- Never echo or log `$GITEA_TOKEN`
|
|
- Never hardcode tokens in scripts or commit them
|
|
- Always use `op read` at runtime
|
|
|
|
### Verify Authentication
|
|
|
|
```bash
|
|
curl -s -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/user" | jq '{login, is_admin}'
|
|
```
|
|
|
|
## Base API Helper
|
|
|
|
All operations use this helper. It outputs JSON to stdout and errors to stderr.
|
|
|
|
```bash
|
|
gitea_api() {
|
|
local method="$1" endpoint="$2" data="${3:-}"
|
|
local args=(-s -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json")
|
|
if [ -n "$data" ]; then
|
|
args+=(-X "$method" -d "$data")
|
|
else
|
|
args+=(-X "$method")
|
|
fi
|
|
curl "${args[@]}" "$GITEA_URL/api/v1$endpoint"
|
|
}
|
|
```
|
|
|
|
## Initial Setup
|
|
|
|
Run once per repository to create labels:
|
|
|
|
```bash
|
|
./setup-labels.sh # Uses defaults: terraphim/agent-tasks
|
|
./setup-labels.sh myorg myrepo # Custom repo
|
|
GITEA_OWNER=zestic-ai GITEA_REPO=tasks ./setup-labels.sh # Via env vars
|
|
```
|
|
|
|
This creates the repo (if missing) and 12 scoped labels (workflow, priority, type).
|
|
|
|
## Project Management (Milestones)
|
|
|
|
Gitea 1.22.6 has no project board API. Milestones serve as project containers with built-in progress tracking.
|
|
|
|
### Create Project
|
|
|
|
```bash
|
|
gitea_create_project() {
|
|
local title="$1" desc="${2:-}" due="${3:-}"
|
|
local owner="${4:-$GITEA_OWNER}" repo="${5:-$GITEA_REPO}"
|
|
local data
|
|
data=$(jq -n --arg t "$title" --arg d "$desc" --arg due "$due" \
|
|
'{title: $t, description: $d} + (if $due != "" then {due_on: $due} else {} end)')
|
|
gitea_api POST "/repos/$owner/$repo/milestones" "$data"
|
|
}
|
|
|
|
# Example:
|
|
gitea_create_project "Sprint 2026-W08" "Weekly sprint" "2026-02-28T00:00:00Z"
|
|
```
|
|
|
|
### List Projects
|
|
|
|
```bash
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/milestones?state=open" | \
|
|
jq '.[] | {id, title, open_issues, closed_issues}'
|
|
```
|
|
|
|
### Get Project Progress
|
|
|
|
```bash
|
|
gitea_progress() {
|
|
local milestone="${1:-}" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}"
|
|
if [ -n "$milestone" ]; then
|
|
gitea_api GET "/repos/$owner/$repo/milestones/$milestone" | jq '{
|
|
milestone: .title,
|
|
open: .open_issues,
|
|
closed: .closed_issues,
|
|
total: (.open_issues + .closed_issues),
|
|
percent_complete: (if (.open_issues + .closed_issues) > 0
|
|
then (.closed_issues * 100 / (.open_issues + .closed_issues))
|
|
else 0 end),
|
|
due_on: .due_on,
|
|
state: .state
|
|
}'
|
|
else
|
|
gitea_api GET "/repos/$owner/$repo/milestones?state=open" | \
|
|
jq '[.[] | {id, title, open: .open_issues, closed: .closed_issues,
|
|
pct: (if (.open_issues + .closed_issues) > 0
|
|
then (.closed_issues * 100 / (.open_issues + .closed_issues))
|
|
else 0 end)}]'
|
|
fi
|
|
}
|
|
```
|
|
|
|
### Close Project
|
|
|
|
```bash
|
|
gitea_api PATCH "/repos/$GITEA_OWNER/$GITEA_REPO/milestones/{id}" '{"state": "closed"}'
|
|
```
|
|
|
|
## Task Management (Issues)
|
|
|
|
### Create Task
|
|
|
|
```bash
|
|
gitea_create_task() {
|
|
local title="$1" body="$2" milestone="$3"
|
|
local labels="${4:-}" owner="${5:-$GITEA_OWNER}" repo="${6:-$GITEA_REPO}"
|
|
local data
|
|
data=$(jq -n --arg t "$title" --arg b "$body" --argjson m "$milestone" \
|
|
'{title: $t, body: $b, milestone: $m}')
|
|
if [ -n "$labels" ]; then
|
|
data=$(echo "$data" | jq --argjson l "[$labels]" '. + {labels: $l}')
|
|
fi
|
|
gitea_api POST "/repos/$owner/$repo/issues" "$data"
|
|
}
|
|
|
|
# Example -- create task in milestone 1 with "workflow:ready" (id=4) and "type:task" (id=14):
|
|
gitea_create_task "Implement auth module" "Design and implement OAuth2 flow" 1 "4,14"
|
|
```
|
|
|
|
### List Tasks by Milestone
|
|
|
|
```bash
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues?milestones=Sprint+2026-W08&state=all" | \
|
|
jq '[.[] | {number, title, state, labels: [.labels[].name]}]'
|
|
```
|
|
|
|
### List Tasks by Workflow State
|
|
|
|
```bash
|
|
# Ready tasks (available for agents to claim)
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues?labels=workflow:ready&state=open" | \
|
|
jq '[.[] | {number, title, labels: [.labels[].name]}]'
|
|
|
|
# In-progress tasks
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues?labels=workflow:in-progress&state=open" | \
|
|
jq '[.[] | {number, title, assignees: [.assignees[].login]}]'
|
|
|
|
# Blocked tasks
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues?labels=workflow:blocked&state=open" | \
|
|
jq '[.[] | {number, title}]'
|
|
```
|
|
|
|
### Update Task Labels
|
|
|
|
```bash
|
|
gitea_set_labels() {
|
|
local index="$1" labels="$2"
|
|
local owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_api PUT "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": [$labels]}"
|
|
}
|
|
```
|
|
|
|
### Add Comment
|
|
|
|
```bash
|
|
gitea_api POST "/repos/$GITEA_OWNER/$GITEA_REPO/issues/{index}/comments" \
|
|
"$(jq -n --arg b "Progress update: completed auth module" '{body: $b}')"
|
|
```
|
|
|
|
## Agent Workflow
|
|
|
|
Inspired by the br (beads_rust) pattern: ready -> claim -> implement -> close.
|
|
|
|
```
|
|
create_task
|
|
|
|
|
v
|
|
+---------------+
|
|
| workflow:ready |<--------+
|
|
+-------+-------+ |
|
|
| |
|
|
gitea_claim (unblock)
|
|
| |
|
|
v |
|
|
+--------------------+ +-----------------+
|
|
| workflow:in-progress|->| workflow:blocked |
|
|
+---------+----------+ +-----------------+
|
|
|
|
|
gitea_close_task
|
|
|
|
|
v
|
|
+---------------+
|
|
| workflow:done |
|
|
| (issue closed) |
|
|
+---------------+
|
|
```
|
|
|
|
Scoped labels are visually exclusive in the Gitea UI, but the API POST endpoint only appends labels. The helpers below use a swap pattern: get current labels, remove old workflow labels, add the new one, then PUT the full set.
|
|
|
|
### Swap Workflow Label (internal helper)
|
|
|
|
```bash
|
|
gitea_swap_workflow() {
|
|
local index="$1" new_label="$2" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
local repo_labels issue_labels new_id kept_ids all_ids
|
|
|
|
# Get the ID of the new workflow label
|
|
repo_labels=$(gitea_api GET "/repos/$owner/$repo/labels")
|
|
new_id=$(echo "$repo_labels" | jq -r --arg n "$new_label" '.[] | select(.name==$n) | .id')
|
|
|
|
# Get current issue label IDs, filtering out all workflow:* labels
|
|
issue_labels=$(gitea_api GET "/repos/$owner/$repo/issues/$index")
|
|
kept_ids=$(echo "$issue_labels" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]')
|
|
|
|
# Combine kept labels + new workflow label
|
|
all_ids=$(echo "$kept_ids" | jq --argjson nid "$new_id" '. + [$nid]')
|
|
|
|
# Replace all labels atomically
|
|
gitea_api PUT "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": $all_ids}"
|
|
}
|
|
```
|
|
|
|
### Mark Ready
|
|
|
|
```bash
|
|
gitea_ready() {
|
|
local index="$1" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:ready" "$owner" "$repo"
|
|
}
|
|
```
|
|
|
|
### Claim Task
|
|
|
|
```bash
|
|
gitea_claim() {
|
|
local index="$1" user="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:in-progress" "$owner" "$repo"
|
|
if [ -n "$user" ]; then
|
|
gitea_api PATCH "/repos/$owner/$repo/issues/$index" "{\"assignees\": [\"$user\"]}"
|
|
fi
|
|
}
|
|
```
|
|
|
|
### Close Task
|
|
|
|
```bash
|
|
gitea_close_task() {
|
|
local index="$1" comment="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}"
|
|
gitea_swap_workflow "$index" "workflow:done" "$owner" "$repo"
|
|
gitea_api PATCH "/repos/$owner/$repo/issues/$index" '{"state": "closed"}'
|
|
if [ -n "$comment" ]; then
|
|
gitea_api POST "/repos/$owner/$repo/issues/$index/comments" \
|
|
"$(jq -n --arg b "$comment" '{body: $b}')"
|
|
fi
|
|
}
|
|
```
|
|
|
|
### Workflow Command Reference
|
|
|
|
| br Command | Gitea Equivalent | Description |
|
|
|------------|------------------|-------------|
|
|
| `br ready` | `gitea_ready 1` | Mark task #1 ready |
|
|
| `br update ID --status in_progress` | `gitea_claim 1 root` | Claim task #1 |
|
|
| `br close ID` | `gitea_close_task 1 "Done"` | Complete task #1 |
|
|
| `br list --status ready` | List issues `?labels=workflow:ready` | Ready tasks |
|
|
| `br status` | `gitea_progress 1` | Milestone progress |
|
|
|
|
## Issue Dependencies
|
|
|
|
Dependencies express task ordering. Gitea uses `IssueMeta` format: `{owner, repo, index}`.
|
|
|
|
### Add Dependency
|
|
|
|
```bash
|
|
# Make issue #3 depend on issue #2 (issue #2 blocks issue #3)
|
|
gitea_api POST "/repos/$GITEA_OWNER/$GITEA_REPO/issues/3/dependencies" \
|
|
"{\"owner\": \"$GITEA_OWNER\", \"repo\": \"$GITEA_REPO\", \"index\": 2}"
|
|
```
|
|
|
|
### List Dependencies
|
|
|
|
```bash
|
|
# What blocks issue #3?
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues/3/dependencies" | \
|
|
jq '[.[] | {number, title, state}]'
|
|
|
|
# What does issue #2 block?
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues/2/blocks" | \
|
|
jq '[.[] | {number, title}]'
|
|
```
|
|
|
|
### Remove Dependency
|
|
|
|
```bash
|
|
gitea_api DELETE "/repos/$GITEA_OWNER/$GITEA_REPO/issues/3/dependencies" \
|
|
"{\"owner\": \"$GITEA_OWNER\", \"repo\": \"$GITEA_REPO\", \"index\": 2}"
|
|
```
|
|
|
|
## Label Scheme
|
|
|
|
All labels use scoped naming (`scope:name`) with `exclusive: true` for mutual exclusivity within each scope.
|
|
|
|
### Workflow Labels
|
|
|
|
| Label | Color | Description |
|
|
|-------|-------|-------------|
|
|
| `workflow:ready` | `#0e8a16` green | Ready for an agent to pick up |
|
|
| `workflow:in-progress` | `#fbca04` yellow | Actively being worked on |
|
|
| `workflow:blocked` | `#d93f0b` red | Blocked by a dependency |
|
|
| `workflow:done` | `#6f42c1` purple | Complete |
|
|
|
|
### Priority Labels
|
|
|
|
| Label | Color | Description |
|
|
|-------|-------|-------------|
|
|
| `priority:critical` | `#b60205` dark red | Immediate |
|
|
| `priority:high` | `#d93f0b` red | This cycle |
|
|
| `priority:medium` | `#fbca04` yellow | Normal |
|
|
| `priority:low` | `#0e8a16` green | Nice to have |
|
|
|
|
### Type Labels
|
|
|
|
| Label | Color | Description |
|
|
|-------|-------|-------------|
|
|
| `type:feature` | `#1d76db` blue | New functionality |
|
|
| `type:bug` | `#d93f0b` red | Defect fix |
|
|
| `type:task` | `#5319e7` violet | General task |
|
|
| `type:research` | `#006b75` teal | Investigation |
|
|
|
|
## Time Tracking
|
|
|
|
```bash
|
|
# Start stopwatch
|
|
gitea_api POST "/repos/$GITEA_OWNER/$GITEA_REPO/issues/{index}/stopwatch/start"
|
|
|
|
# Stop stopwatch
|
|
gitea_api POST "/repos/$GITEA_OWNER/$GITEA_REPO/issues/{index}/stopwatch/stop"
|
|
|
|
# Get tracked times
|
|
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues/{index}/times" | jq '.[].time'
|
|
```
|
|
|
|
## MCP Integration (Optional)
|
|
|
|
The official Gitea MCP Server provides ~80 tools for repos, issues, PRs, files, and CI/CD.
|
|
It does NOT cover milestones or workflow labels -- use the shell helpers above for those.
|
|
|
|
### Claude Code Setup
|
|
|
|
```bash
|
|
claude mcp add --transport stdio --scope user gitea \
|
|
--env GITEA_ACCESS_TOKEN="$(op read 'op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential')" \
|
|
--env GITEA_HOST=https://git.terraphim.cloud \
|
|
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
|
```
|
|
|
|
### Available MCP Tools
|
|
|
|
Repos, branches, tags, releases, files, commits, issues (CRUD + comments), PRs (CRUD + reviews + merge), org labels, CI/CD actions (secrets, variables, workflows, runs, jobs), wiki pages.
|
|
|
|
**Not covered by MCP:** milestones, repo labels, issue dependencies, time tracking, workflow labels.
|
|
|
|
## Core API Reference (Gitea 1.22.6)
|
|
|
|
### Users
|
|
|
|
```bash
|
|
# Create admin (CLI inside container)
|
|
docker exec gitea gitea admin user create \
|
|
--username <user> --password <pass> --email <email> --admin --access-token
|
|
|
|
# List users
|
|
gitea_api GET "/admin/users"
|
|
|
|
# Get user
|
|
gitea_api GET "/users/{username}"
|
|
```
|
|
|
|
### Organizations
|
|
|
|
```bash
|
|
# Create org
|
|
gitea_api POST "/orgs" '{"username":"myorg","full_name":"My Org","visibility":"private"}'
|
|
|
|
# List org repos
|
|
gitea_api GET "/orgs/{org}/repos"
|
|
```
|
|
|
|
### Repositories
|
|
|
|
```bash
|
|
# Create user repo
|
|
gitea_api POST "/user/repos" '{"name":"my-repo","auto_init":true,"private":true}'
|
|
|
|
# Create org repo
|
|
gitea_api POST "/orgs/{org}/repos" '{"name":"my-repo","private":false}'
|
|
|
|
# Delete repo
|
|
gitea_api DELETE "/repos/{owner}/{repo}"
|
|
|
|
# List user repos
|
|
gitea_api GET "/user/repos"
|
|
```
|
|
|
|
### Pull Requests
|
|
|
|
```bash
|
|
# List PRs
|
|
gitea_api GET "/repos/{owner}/{repo}/pulls?state=all"
|
|
|
|
# Get PR
|
|
gitea_api GET "/repos/{owner}/{repo}/pulls/{index}"
|
|
|
|
# Merge PR
|
|
gitea_api POST "/repos/{owner}/{repo}/pulls/{index}/merge" '{"do":"merge"}'
|
|
```
|
|
|
|
### Releases
|
|
|
|
```bash
|
|
gitea_api POST "/repos/{owner}/{repo}/releases" \
|
|
'{"tag_name":"v1.0.0","name":"Release v1.0.0","body":"Release notes"}'
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
| HTTP Code | Meaning | Action |
|
|
|-----------|---------|--------|
|
|
| 200 | Success | Parse JSON response |
|
|
| 201 | Created | Resource created successfully |
|
|
| 204 | No Content | Delete succeeded |
|
|
| 401 | Unauthorized | Check GITEA_TOKEN; re-read from 1Password |
|
|
| 403 | Forbidden | Token lacks required scope |
|
|
| 404 | Not Found | Check owner/repo/index; resource may not exist |
|
|
| 409 | Conflict | Resource already exists (labels, repos) |
|
|
| 422 | Validation Error | Check request body format |
|
|
|
|
## Constraints
|
|
|
|
- Always use HTTPS
|
|
- Never commit tokens to version control
|
|
- Use `op read` for secrets at runtime
|
|
- Prefer API over CLI for idempotent operations
|
|
- Project board API does NOT exist in Gitea 1.22.6 -- use milestones
|
|
- Issue dependency body uses `IssueMeta` format: `{owner, repo, index}`
|
|
- Scoped labels require the `exclusive: true` flag on creation
|