- 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>
14 KiB
name, description, license, version, gitea_version
| name | description | license | version | gitea_version |
|---|---|---|---|---|
| gitea | 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. | Apache-2.0 | 2.0 | 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:
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
# 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 readat runtime
Verify Authentication
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.
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:
./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
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
gitea_api GET "/repos/$GITEA_OWNER/$GITEA_REPO/milestones?state=open" | \
jq '.[] | {id, title, open_issues, closed_issues}'
Get Project Progress
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
gitea_api PATCH "/repos/$GITEA_OWNER/$GITEA_REPO/milestones/{id}" '{"state": "closed"}'
Task Management (Issues)
Create Task
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
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
# 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
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
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)
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
gitea_ready() {
local index="$1" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}"
gitea_swap_workflow "$index" "workflow:ready" "$owner" "$repo"
}
Claim Task
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
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
# 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
# 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
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
# 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
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
# 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
# Create org
gitea_api POST "/orgs" '{"username":"myorg","full_name":"My Org","visibility":"private"}'
# List org repos
gitea_api GET "/orgs/{org}/repos"
Repositories
# 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
# 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
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 readfor 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
IssueMetaformat:{owner, repo, index} - Scoped labels require the
exclusive: trueflag on creation