Files
gitea-infrastructure/.agents/skills/gitea/SKILL.md
Alex Mikhalev 654e03dc71 feat(gitea-skill): Add integration tests and fix workflow label swap
- 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>
2026-02-18 12:45:18 +01:00

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 read at 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 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