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:
@@ -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: "" })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user