Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
206 lines
7.7 KiB
Plaintext
206 lines
7.7 KiB
Plaintext
# Git Board — Starlark Backend (v0.2.0)
|
|
#
|
|
# Library consumer — delegates all Gitea API work to gitea-client.
|
|
#
|
|
# Entry points:
|
|
# on_request(req) → surface API routes
|
|
# on_tool_call(tool_name, params) → LLM tool execution
|
|
#
|
|
# Modules:
|
|
# connections — resolve gitea connection (connections.read)
|
|
# lib — load gitea-client library
|
|
# json — encode/decode (universal)
|
|
# settings — user: default_owner, default_repo
|
|
|
|
gitea = lib.require("gitea-client")
|
|
|
|
|
|
def _conn():
|
|
"""Resolve the first available gitea connection."""
|
|
return connections.get("gitea")
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# Surface API routes (/s/git-board/api/*)
|
|
# ═══════════════════════════════════════════════
|
|
|
|
def on_request(req):
|
|
path = req["path"]
|
|
method = req["method"]
|
|
|
|
conn = _conn()
|
|
if conn == None:
|
|
return _resp(400, {"error": "no gitea connection configured — add one in Settings > Connections"})
|
|
|
|
if method == "GET" and path == "/repos":
|
|
return _handle_repos(conn)
|
|
elif method == "GET" and path == "/board":
|
|
return _handle_board(conn, req)
|
|
elif method == "GET" and path.startswith("/issue/"):
|
|
return _handle_get_issue(conn, path[len("/issue/"):])
|
|
elif method == "POST" and path.startswith("/issue/"):
|
|
return _handle_post_issue(conn, path[len("/issue/"):], req)
|
|
elif method == "GET" and path.startswith("/ci/"):
|
|
return _handle_ci(conn, path[len("/ci/"):])
|
|
|
|
return _resp(404, {"error": "not found"})
|
|
|
|
|
|
def _handle_repos(conn):
|
|
"""List repositories accessible via the connection."""
|
|
repos = gitea.get_repos(conn)
|
|
if repos == None:
|
|
return _resp(502, {"error": "gitea request failed"})
|
|
return _resp(200, {"data": repos})
|
|
|
|
|
|
def _handle_board(conn, req):
|
|
"""Combined issues + PRs for kanban board."""
|
|
q = req.get("query", {})
|
|
owner = _str(q.get("owner", "")) or settings.get("default_owner") or ""
|
|
repo = _str(q.get("repo", "")) or settings.get("default_repo") or ""
|
|
if not owner or not repo:
|
|
return _resp(400, {"error": "owner and repo required (query params or user settings)"})
|
|
|
|
issues = gitea.get_issues(conn, owner, repo, "open") or []
|
|
prs = gitea.get_prs(conn, owner, repo, "open") or []
|
|
|
|
board = {"issues": issues, "pull_requests": prs}
|
|
return _resp(200, board)
|
|
|
|
|
|
def _handle_get_issue(conn, issue_path):
|
|
"""Get issue detail: /issue/:owner/:repo/:number"""
|
|
parts = issue_path.split("/", 2)
|
|
if len(parts) < 3:
|
|
return _resp(400, {"error": "path must be /issue/:owner/:repo/:number"})
|
|
owner, repo, num = parts[0], parts[1], int(parts[2])
|
|
data = gitea.get_issue(conn, owner, repo, num)
|
|
if data == None:
|
|
return _resp(404, {"error": "issue not found"})
|
|
return _resp(200, data)
|
|
|
|
|
|
def _handle_post_issue(conn, issue_path, req):
|
|
"""Update issue or add comment: /issue/:owner/:repo/:number[/comment]"""
|
|
parts = issue_path.split("/", 3)
|
|
if len(parts) < 3:
|
|
return _resp(400, {"error": "path must be /issue/:owner/:repo/:number"})
|
|
owner, repo, num = parts[0], parts[1], int(parts[2])
|
|
action = parts[3] if len(parts) > 3 else ""
|
|
|
|
body = json.decode(req.get("body", "{}"))
|
|
|
|
if action == "comment":
|
|
text = body.get("body", "")
|
|
if not text:
|
|
return _resp(400, {"error": "comment body required"})
|
|
result = gitea.add_comment(conn, owner, repo, num, text)
|
|
if result == None:
|
|
return _resp(502, {"error": "failed to add comment"})
|
|
return _resp(201, result)
|
|
|
|
# Default: update issue fields (state, title, body, assignee)
|
|
title = body.get("title", "")
|
|
issue_body = body.get("body", "")
|
|
state = body.get("state", "")
|
|
result = gitea.update_issue(conn, owner, repo, num, title, issue_body, state)
|
|
if result == None:
|
|
return _resp(502, {"error": "failed to update issue"})
|
|
return _resp(200, result)
|
|
|
|
|
|
def _handle_ci(conn, ref_path):
|
|
"""CI status for owner/repo/ref."""
|
|
parts = ref_path.split("/", 2)
|
|
if len(parts) < 3:
|
|
return _resp(400, {"error": "path must be /ci/:owner/:repo/:ref"})
|
|
owner, repo, ref = parts[0], parts[1], parts[2]
|
|
result = gitea.get_ci_status(conn, owner, repo, ref)
|
|
if result == None:
|
|
return _resp(502, {"error": "CI status request failed"})
|
|
return _resp(200, result)
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# LLM Tools (called by AI during chat)
|
|
# ═══════════════════════════════════════════════
|
|
|
|
def on_tool_call(tool_name, params):
|
|
conn = _conn()
|
|
if conn == None:
|
|
return {"error": "no gitea connection configured — add one in Settings > Connections"}
|
|
|
|
owner = params.get("owner", "")
|
|
repo = params.get("repo", "")
|
|
|
|
if tool_name == "list_issues":
|
|
state = params.get("state", "open")
|
|
data = gitea.get_issues(conn, owner, repo, state)
|
|
if data == None:
|
|
return {"error": "failed to list issues"}
|
|
return {"issues": data, "count": len(data)}
|
|
|
|
elif tool_name == "get_issue":
|
|
num = int(params.get("number", 0))
|
|
data = gitea.get_issue(conn, owner, repo, num)
|
|
if data == None:
|
|
return {"error": "issue not found"}
|
|
return data
|
|
|
|
elif tool_name == "create_issue":
|
|
data = gitea.create_issue(conn, owner, repo,
|
|
params.get("title", ""),
|
|
params.get("body", ""),
|
|
params.get("labels", ""))
|
|
if data == None:
|
|
return {"error": "failed to create issue"}
|
|
return data
|
|
|
|
elif tool_name == "update_issue":
|
|
num = int(params.get("number", 0))
|
|
data = gitea.update_issue(conn, owner, repo, num,
|
|
params.get("title", ""),
|
|
params.get("body", ""),
|
|
params.get("state", ""))
|
|
if data == None:
|
|
return {"error": "failed to update issue"}
|
|
return data
|
|
|
|
elif tool_name == "add_comment":
|
|
num = int(params.get("number", 0))
|
|
data = gitea.add_comment(conn, owner, repo, num, params.get("body", ""))
|
|
if data == None:
|
|
return {"error": "failed to add comment"}
|
|
return data
|
|
|
|
elif tool_name == "list_pull_requests":
|
|
state = params.get("state", "open")
|
|
data = gitea.get_prs(conn, owner, repo, state)
|
|
if data == None:
|
|
return {"error": "failed to list PRs"}
|
|
return {"pull_requests": data, "count": len(data)}
|
|
|
|
elif tool_name == "get_ci_status":
|
|
ref = params.get("ref", "")
|
|
data = gitea.get_ci_status(conn, owner, repo, ref)
|
|
if data == None:
|
|
return {"error": "failed to get CI status"}
|
|
return data
|
|
|
|
return {"error": "unknown tool: " + tool_name}
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# Response helpers
|
|
# ═══════════════════════════════════════════════
|
|
|
|
def _resp(status, data):
|
|
return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}}
|
|
|
|
def _str(v):
|
|
"""Coerce Starlark query param to string."""
|
|
if v == None:
|
|
return ""
|
|
return str(v)
|