Pipeline/frontend/src/lib/api-forgejo.ts

604 lines
14 KiB
TypeScript

import { customFetch } from "@/api/mutator";
// Forgejo Connection types
export interface ForgejoConnection {
id: string;
organization_id: string;
name: string;
base_url: string;
token: null; // Always null in responses
active: boolean;
has_token: boolean;
token_last_eight: string | null;
created_at: string;
updated_at: string;
}
export interface ForgejoConnectionCreate {
name: string;
base_url: string;
token: string;
}
export interface ForgejoConnectionUpdate {
name?: string;
base_url?: string;
token?: string;
}
// Forgejo Repository types
export interface ForgejoRepoLabel {
id: number | null;
name: string;
color: string;
description: string;
}
export interface ForgejoRepository {
id: string;
organization_id: string;
connection_id: string;
owner: string;
repo: string;
display_name: string;
default_branch: string;
active: boolean;
has_webhook_secret: boolean;
description: string | null;
open_issues_count: number;
is_archived: boolean;
topics: string[];
labels: ForgejoRepoLabel[];
connection: ForgejoConnection;
last_sync_at: string | null;
last_sync_error: string | null;
created_at: string;
updated_at: string;
}
export interface ForgejoRepositoryCreate {
connection_id: string;
owner: string;
repo: string;
display_name?: string;
default_branch?: string;
}
export interface ForgejoRepositoryUpdate {
display_name?: string;
default_branch?: string;
webhook_secret?: string | null;
}
type ApiResponse<T> = {
data: T;
status: number;
headers: Headers;
};
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
const response = await customFetch<ApiResponse<T>>(path, init ?? {});
return response.data;
}
// Forgejo Connection API
export async function getForgejoConnections(): Promise<ForgejoConnection[]> {
return fetchJson<ForgejoConnection[]>("/api/v1/forgejo/connections");
}
export async function createForgejoConnection(
data: ForgejoConnectionCreate,
): Promise<ForgejoConnection> {
return fetchJson<ForgejoConnection>("/api/v1/forgejo/connections", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function getForgejoConnection(
connectionId: string,
): Promise<ForgejoConnection> {
return fetchJson<ForgejoConnection>(
`/api/v1/forgejo/connections/${connectionId}`,
);
}
export async function updateForgejoConnection(
connectionId: string,
data: ForgejoConnectionUpdate,
): Promise<ForgejoConnection> {
return fetchJson<ForgejoConnection>(
`/api/v1/forgejo/connections/${connectionId}`,
{
method: "PATCH",
body: JSON.stringify(data),
},
);
}
export async function deleteForgejoConnection(
connectionId: string,
): Promise<void> {
await customFetch<ApiResponse<unknown>>(
`/api/v1/forgejo/connections/${connectionId}`,
{
method: "DELETE",
},
);
}
// Forgejo Repository API
export async function getForgejoRepositories(): Promise<ForgejoRepository[]> {
return fetchJson<ForgejoRepository[]>("/api/v1/forgejo/repositories");
}
export async function createForgejoRepository(
data: ForgejoRepositoryCreate,
): Promise<ForgejoRepository> {
return fetchJson<ForgejoRepository>("/api/v1/forgejo/repositories", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function getForgejoRepository(
repositoryId: string,
): Promise<ForgejoRepository> {
return fetchJson<ForgejoRepository>(
`/api/v1/forgejo/repositories/${repositoryId}`,
);
}
export async function updateForgejoRepository(
repositoryId: string,
data: ForgejoRepositoryUpdate,
): Promise<ForgejoRepository> {
return fetchJson<ForgejoRepository>(
`/api/v1/forgejo/repositories/${repositoryId}`,
{
method: "PATCH",
body: JSON.stringify(data),
},
);
}
export async function deleteForgejoRepository(
repositoryId: string,
): Promise<void> {
await customFetch<ApiResponse<unknown>>(
`/api/v1/forgejo/repositories/${repositoryId}`,
{
method: "DELETE",
},
);
}
// Pre-save connection test
export interface ForgejoConnectionTestRepo {
full_name: string;
name: string;
owner: string;
default_branch: string;
private: boolean;
description: string | null;
}
export interface ForgejoConnectionTestResult {
valid: boolean;
user_login: string | null;
user_full_name: string | null;
repo_count: number;
repos: ForgejoConnectionTestRepo[];
error: string | null;
response_time_ms: number;
}
export async function testForgejoConnection(data: {
base_url: string;
token: string;
}): Promise<ForgejoConnectionTestResult> {
return fetchJson<ForgejoConnectionTestResult>(
"/api/v1/forgejo/connections/test",
{
method: "POST",
body: JSON.stringify(data),
},
);
}
// Remote repo discovery
export interface ForgejoRemoteRepo {
full_name: string;
name: string;
owner: string;
default_branch: string;
description: string | null;
private: boolean;
html_url: string;
}
export async function getConnectionRepos(
connectionId: string,
): Promise<ForgejoRemoteRepo[]> {
return fetchJson<ForgejoRemoteRepo[]>(
`/api/v1/forgejo/connections/${connectionId}/repos`,
);
}
// Forgejo Sync & Validation API
export async function syncRepository(repositoryId: string): Promise<{
created: number;
updated: number;
open: number;
closed: number;
total: number;
}> {
return fetchJson<{
created: number;
updated: number;
open: number;
closed: number;
total: number;
}>(`/api/v1/forgejo/repositories/${repositoryId}/sync`, {
method: "POST",
});
}
export interface ForgejoValidationStatus {
ok: boolean;
status: string;
error_message?: string | null;
}
export interface ForgejoConnectionValidationResponse {
connection_id: string;
status: ForgejoValidationStatus;
response_time_ms: number;
validated_at: string;
}
export interface ForgejoRepositoryValidationResponse {
repository_id: string;
status: ForgejoValidationStatus;
response_time_ms: number;
validated_at: string;
repo_exists?: boolean | null;
}
export async function validateConnection(
connectionId: string,
): Promise<ForgejoConnectionValidationResponse> {
return fetchJson<ForgejoConnectionValidationResponse>(
`/api/v1/forgejo/connections/${connectionId}/validate`,
{
method: "POST",
},
);
}
export async function validateRepository(
repositoryId: string,
): Promise<ForgejoRepositoryValidationResponse> {
return fetchJson<ForgejoRepositoryValidationResponse>(
`/api/v1/forgejo/repositories/${repositoryId}/validate`,
{
method: "POST",
},
);
}
// Forgejo Issue types
export interface ForgejoIssueLabel {
id?: number | null;
name: string;
color: string;
description?: string | null;
}
export interface ForgejoIssueMilestone {
id: number | null;
title: string;
state: string;
description: string | null;
due_on: string | null;
closed_at: string | null;
}
export interface ForgejoIssue {
id: string;
organization_id: string;
repository_id: string;
forgejo_issue_number: number;
title: string;
body: string | null;
body_preview: string | null;
state: string;
is_pull_request: boolean;
labels: ForgejoIssueLabel[];
assignees: Record<string, unknown>[];
milestone: ForgejoIssueMilestone | null;
author: string;
html_url: string;
forgejo_created_at: string;
forgejo_updated_at: string;
forgejo_closed_at: string | null;
last_synced_at: string;
created_at: string;
updated_at: string;
}
export interface ForgejoIssueDetail extends ForgejoIssue {
forgejo_payload?: Record<string, unknown> | null;
forgejo_comments_payload?: Record<string, unknown>[];
forgejo_timeline_payload?: Record<string, unknown>[];
forgejo_reactions_payload?: Record<string, unknown>[];
}
export interface ForgejoIssueListResponse {
items: ForgejoIssue[];
total: number;
page: number;
limit: number;
}
// Forgejo Issue API
export async function getForgejoIssues(params?: {
repository_id?: string;
board_id?: string;
state?: string;
search?: string;
page?: number;
limit?: number;
}): Promise<ForgejoIssueListResponse> {
const searchParams = new URLSearchParams();
if (params?.repository_id)
searchParams.set("repository_id", params.repository_id);
if (params?.board_id) searchParams.set("board_id", params.board_id);
if (params?.state) searchParams.set("state", params.state);
if (params?.search) searchParams.set("search", params.search);
if (params?.page) searchParams.set("page", params.page.toString());
if (params?.limit) searchParams.set("limit", params.limit.toString());
const qs = searchParams.toString();
return fetchJson<ForgejoIssueListResponse>(
`/api/v1/forgejo/issues${qs ? `?${qs}` : ""}`,
);
}
export async function getForgejoIssue(
issueId: string,
): Promise<ForgejoIssueDetail> {
return fetchJson<ForgejoIssueDetail>(`/api/v1/forgejo/issues/${issueId}`);
}
export const ISSUE_TEMPLATE = `## Summary
Briefly describe the issue in 1 to 3 sentences.
## Problem
Explain what is wrong, missing, confusing, or broken.
## Affected area
Example:
- UI page:
- Backend service:
- API:
- Database:
- Auth:
- CI/CD:
- Docs:
## Affected files
Known or suspected files:
- \`path/to/file\`
- \`path/to/other-file\`
## Affected routes or endpoints
Known or suspected routes:
- \`GET /example\`
- \`POST /api/example\`
## Steps to reproduce
1.
2.
3.
## Expected behavior
Describe what should happen.
## Actual behavior
Describe what actually happens.
## Error output, logs, or screenshots
Paste relevant logs only. Redact secrets.
\`\`\`text
Paste logs here
\`\`\``;
export interface CreateIssueRequest {
repository_id: string;
title: string;
body: string;
}
export interface CreateIssueResponse {
success: boolean;
issue_id: string;
forgejo_issue_number: number;
html_url: string;
repository_id: string;
}
export async function createForgejoIssue(
data: CreateIssueRequest,
): Promise<CreateIssueResponse> {
return fetchJson<CreateIssueResponse>("/api/v1/forgejo/issues", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function closeForgejoIssue(
issueId: string,
): Promise<ForgejoIssue> {
return fetchJson<ForgejoIssue>(`/api/v1/forgejo/issues/${issueId}/close`, {
method: "POST",
});
}
export interface PostCommentResponse {
success: boolean;
issue_id: string;
comment_id: number | string | null;
body: string;
created_at: string;
}
export async function postForgejoIssueComment(
issueId: string,
body: string,
): Promise<PostCommentResponse> {
return fetchJson<PostCommentResponse>(
`/api/v1/forgejo/issues/${issueId}/comments`,
{
method: "POST",
body: JSON.stringify({ body }),
},
);
}
export interface EditIssueRequest {
title?: string;
body?: string;
state?: string;
}
export interface EditIssueResponse {
success: boolean;
issue_id: string;
forgejo_issue_number: number;
title: string;
body: string | null;
state: string;
forgejo_updated_at: string;
}
export async function editForgejoIssue(
issueId: string,
data: EditIssueRequest,
): Promise<EditIssueResponse> {
return fetchJson<EditIssueResponse>(`/api/v1/forgejo/issues/${issueId}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
// Board Repository Linking API
export interface BoardForgejoRepositoryLink {
id: string;
board_id: string;
repository_id: string;
organization_id: string;
created_at: string;
repository?: ForgejoRepository;
}
export type BoardForgejoRepositoriesResponse =
| BoardForgejoRepositoryLink[]
| { repositories: BoardForgejoRepositoryLink[] };
export async function getBoardForgejoRepositories(
boardId: string,
): Promise<BoardForgejoRepositoriesResponse> {
return fetchJson<BoardForgejoRepositoriesResponse>(
`/api/v1/boards/${boardId}/forgejo/repositories`,
);
}
export async function linkBoardForgejoRepository(
boardId: string,
repositoryId: string,
): Promise<BoardForgejoRepositoryLink | { link?: BoardForgejoRepositoryLink }> {
return fetchJson<
BoardForgejoRepositoryLink | { link?: BoardForgejoRepositoryLink }
>(`/api/v1/boards/${boardId}/forgejo/repositories`, {
method: "POST",
body: JSON.stringify({ repository_id: repositoryId }),
});
}
export async function unlinkBoardForgejoRepository(
boardId: string,
repositoryId: string,
): Promise<void> {
await customFetch<ApiResponse<unknown>>(
`/api/v1/boards/${boardId}/forgejo/repositories/${repositoryId}`,
{
method: "DELETE",
},
);
}
// Forgejo Metrics types
export interface RepositorySyncHealth {
repository_id: string;
owner: string;
repo: string;
display_name: string | null;
last_sync_at: string | null;
last_sync_error: string | null;
has_error: boolean;
}
export interface ForgejoIssueMetrics {
open_issues: number;
closed_issues: number;
closed_last_7_days: number;
closed_last_30_days: number;
stale_open_issues: number;
repositories_synced: number;
last_sync_timestamps: Record<string, string>;
sync_error_counts: Record<string, number>;
}
// Forgejo Heatmap API
export interface ForgejoHeatmapDay {
date: string; // "YYYY-MM-DD"
count: number;
}
export interface ForgejoHeatmapResponse {
days: ForgejoHeatmapDay[];
max_count: number;
}
export async function getForgejoHeatmap(params?: {
organization_id?: string;
}): Promise<ForgejoHeatmapResponse> {
const searchParams = new URLSearchParams();
if (params?.organization_id)
searchParams.set("organization_id", params.organization_id);
const qs = searchParams.toString();
return fetchJson<ForgejoHeatmapResponse>(
`/api/v1/forgejo/heatmap${qs ? `?${qs}` : ""}`,
);
}
// Forgejo Metrics API
export async function getForgejoMetrics(params?: {
organization_id?: string;
board_id?: string;
repository_id?: string;
}): Promise<ForgejoIssueMetrics> {
const searchParams = new URLSearchParams();
if (params?.organization_id)
searchParams.set("organization_id", params.organization_id);
if (params?.board_id) searchParams.set("board_id", params.board_id);
if (params?.repository_id)
searchParams.set("repository_id", params.repository_id);
const qs = searchParams.toString();
return fetchJson<ForgejoIssueMetrics>(
`/api/v1/forgejo/metrics${qs ? `?${qs}` : ""}`,
);
}