fix: align Gitea client with API 1.21+ specification

- Update Label interface with id, exclusive, is_archived fields
- Add IssueLabelsOption type for proper label handling
- Fix addLabels/removeLabel to support both IDs and names
- Add getIssueLabels, replaceLabels, clearLabels methods
- Add getRepoLabels, getLabel, createLabel for label management
- Add updateIssue for partial issue updates
- Add deleteComment for comment removal
- Add getIssueTimeline for issue events
- setStatus now properly removes old status labels before adding new
- All endpoints now match Gitea API 1.21+ specification
This commit is contained in:
swp
2026-04-04 01:14:49 +01:00
parent 35bbdcb08f
commit 35f94e0b8f

View File

@@ -1,5 +1,5 @@
// kilocode_change - integrated module
// Gitea API client for logging agent performance
// Gitea API 1.21+ client for logging agent performance
const GITEA_API_URL = process.env.GITEA_API_URL || "https://git.softuniq.eu/api/v1"
const GITEA_TOKEN = process.env.GITEA_TOKEN || ""
@@ -11,28 +11,51 @@ export interface GiteaConfig {
repo: string
}
// Label structure per Gitea API 1.21+
export interface Label {
id: number
name: string
color: string
description?: string
exclusive?: boolean
is_archived?: boolean
url?: string
}
export interface IssueComment {
id?: number
body: string
created_at?: string
updated_at?: string
user?: { login: string; id: number }
}
export interface Issue {
id?: number
number: number
title: string
body: string
state: "open" | "closed"
labels: Array<{ name: string; color: string }>
assignees: Array<{ login: string }>
labels: Label[]
assignees?: Array<{ id: number; login: string; full_name?: string }>
comments: number
created_at: string
updated_at: string
html_url?: string
}
export interface CreateIssueOptions {
title: string
body: string
labels?: string[]
labels?: number[] | string[]
assignees?: string[]
}
export interface UpdateIssueOptions {
title?: string
body?: string
state?: "open" | "closed"
labels?: number[]
assignees?: string[]
}
@@ -40,6 +63,11 @@ export interface CreateCommentOptions {
body: string
}
// IssueLabelsOption supports both IDs and names
export interface IssueLabelsOption {
labels: number[] | string[]
}
export class GiteaClient {
private baseUrl: string
private token: string
@@ -85,9 +113,16 @@ export class GiteaClient {
throw new Error(`Gitea API error: ${response.status} - ${error}`)
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T
}
return response.json()
}
// ==================== Issues ====================
async getIssue(issueNumber: number): Promise<Issue> {
return this.request(
"GET",
@@ -103,11 +138,76 @@ export class GiteaClient {
)
}
async addLabel(issueNumber: number, labelId: number): Promise<void> {
async updateIssue(issueNumber: number, options: UpdateIssueOptions): Promise<Issue> {
return this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
options
)
}
async closeIssue(issueNumber: number): Promise<Issue> {
return this.updateIssue(issueNumber, { state: "closed" })
}
async reopenIssue(issueNumber: number): Promise<Issue> {
return this.updateIssue(issueNumber, { state: "open" })
}
// ==================== Issue Comments ====================
async getComments(issueNumber: number): Promise<IssueComment[]> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`
)
}
async createComment(issueNumber: number, options: CreateCommentOptions): Promise<IssueComment> {
return this.request(
"POST",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`,
options
)
}
async updateComment(commentId: number, options: CreateCommentOptions): Promise<IssueComment> {
return this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`,
options
)
}
async deleteComment(commentId: number): Promise<void> {
await this.request(
"DELETE",
`/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`
)
}
// ==================== Issue Labels ====================
async getIssueLabels(issueNumber: number): Promise<Label[]> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`
)
}
async addLabels(issueNumber: number, labels: number[] | string[]): Promise<Label[]> {
return this.request(
"POST",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`,
{ labels: [labelId] }
{ labels }
)
}
async replaceLabels(issueNumber: number, labels: number[] | string[]): Promise<Label[]> {
return this.request(
"PUT",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`,
{ labels }
)
}
@@ -118,76 +218,87 @@ export class GiteaClient {
)
}
async createComment(issueNumber: number, options: CreateCommentOptions): Promise<{ id: number; body: string }> {
return this.request(
"POST",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`,
options
)
}
async getComments(issueNumber: number): Promise<IssueComment[]> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`
)
}
async updateComment(issueNumber: number, commentId: number, body: string): Promise<void> {
async clearLabels(issueNumber: number): Promise<void> {
await this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`,
{ body }
"DELETE",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`
)
}
async closeIssue(issueNumber: number): Promise<Issue> {
// ==================== Repository Labels ====================
async getRepoLabels(): Promise<Label[]> {
return this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
{ state: "closed" }
)
}
async reopenIssue(issueNumber: number): Promise<Issue> {
return this.request(
"PATCH",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
{ state: "open" }
)
}
async getStatusLabels(issueNumber: number): Promise<string[]> {
const issue = await this.getIssue(issueNumber)
return issue.labels
.filter(l => l.name.startsWith("status:"))
.map(l => l.name)
}
async setStatus(issueNumber: number, status: string): Promise<void> {
const statusLabel = `status: ${status}`
const allLabels = await this.request<Array<{ id: number; name: string }>>(
"GET",
`/repos/${this.owner}/${this.repo}/labels`
)
}
async getLabel(labelId: number): Promise<Label> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/labels/${labelId}`
)
}
async createLabel(label: { name: string; color: string; description?: string }): Promise<Label> {
return this.request(
"POST",
`/repos/${this.owner}/${this.repo}/labels`,
label
)
}
// ==================== Status Management ====================
async getStatusLabels(issueNumber: number): Promise<Label[]> {
const labels = await this.getIssueLabels(issueNumber)
return labels.filter(l => l.name.startsWith("status:"))
}
async setStatus(issueNumber: number, status: string): Promise<Label[]> {
const statusLabel = `status: ${status}`
const statusLabels = allLabels.filter(l => l.name.startsWith("status:"))
for (const label of statusLabels) {
try {
await this.removeLabel(issueNumber, label.id)
} catch {
// Label might not be on issue
}
}
// Get all repository labels
const allLabels = await this.getRepoLabels()
// Find the target label
const targetLabel = allLabels.find(l => l.name === statusLabel)
if (targetLabel) {
await this.addLabel(issueNumber, targetLabel.id)
if (!targetLabel) {
console.warn(`Label "${statusLabel}" not found in repository`)
// Optionally create it
// await this.createLabel({ name: statusLabel, color: "0052cc" })
}
// Get current issue labels
const currentLabels = await this.getIssueLabels(issueNumber)
// Remove existing status labels
const statusLabels = currentLabels.filter(l => l.name.startsWith("status:"))
for (const label of statusLabels) {
await this.removeLabel(issueNumber, label.id)
}
// Add new status label
if (targetLabel) {
return this.addLabels(issueNumber, [targetLabel.id])
}
return this.getIssueLabels(issueNumber)
}
// ==================== Issue Timeline ====================
async getIssueTimeline(issueNumber: number): Promise<Array<{ id: number; event: string; created_at: string }>> {
return this.request(
"GET",
`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/timeline`
)
}
}
// ==================== Helper Functions ====================
export async function logAgentPerformance(
client: GiteaClient,
issueNumber: number,
@@ -229,14 +340,14 @@ export async function detectRepository(): Promise<{ owner: string; repo: string
const { spawn } = await import("child_process")
return new Promise((resolve) => {
const process = spawn("git", ["remote", "get-url", "origin"], { cwd: process.cwd() })
const child = spawn("git", ["remote", "get-url", "origin"], { cwd: process.cwd() })
let stdout = ""
process.stdout.on("data", (data) => {
child.stdout.on("data", (data) => {
stdout += data.toString()
})
process.on("close", () => {
child.on("close", () => {
// Parse URLs like:
// - git@git.softuniq.eu:UniqueSoft/APAW.git
// - https://git.softuniq.eu/UniqueSoft/APAW.git
@@ -249,7 +360,7 @@ export async function detectRepository(): Promise<{ owner: string; repo: string
}
})
process.on("error", () => {
child.on("error", () => {
resolve({ owner: "", repo: "" })
})
})