feat(gitea): add domain model classes, helpers, and refactor GiteaClient internals; expand README with usage and docs
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/gitea',
|
||||
version: '1.4.0',
|
||||
version: '1.5.0',
|
||||
description: 'A TypeScript client for the Gitea API, providing easy access to repositories, organizations, secrets, and action runs.'
|
||||
}
|
||||
|
||||
124
ts/gitea.classes.actionrun.ts
Normal file
124
ts/gitea.classes.actionrun.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
import type { IGiteaActionRun } from './gitea.interfaces.js';
|
||||
import { GiteaActionRunJob } from './gitea.classes.actionrunjob.js';
|
||||
import {
|
||||
computeDuration,
|
||||
resolveGiteaStatus,
|
||||
extractRefFromPath,
|
||||
extractWorkflowIdFromPath,
|
||||
} from './gitea.helpers.js';
|
||||
|
||||
export class GiteaActionRun {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly runNumber: number;
|
||||
public readonly runAttempt: number;
|
||||
public readonly name: string;
|
||||
public readonly displayTitle: string;
|
||||
public readonly status: string;
|
||||
public readonly conclusion: string;
|
||||
public readonly headBranch: string;
|
||||
public readonly headSha: string;
|
||||
public readonly htmlUrl: string;
|
||||
public readonly event: string;
|
||||
public readonly path: string;
|
||||
public readonly startedAt: string;
|
||||
public readonly completedAt: string;
|
||||
public readonly actorLogin: string;
|
||||
public readonly triggerActorLogin: string;
|
||||
|
||||
// Computed
|
||||
public readonly resolvedStatus: string;
|
||||
public readonly duration: number;
|
||||
public readonly ref: string;
|
||||
public readonly workflowId: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GiteaClient,
|
||||
private ownerRepo: string,
|
||||
raw: IGiteaActionRun,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.runNumber = raw.run_number || 0;
|
||||
this.runAttempt = raw.run_attempt || 1;
|
||||
this.name = raw.name || '';
|
||||
this.displayTitle = raw.display_title || this.name;
|
||||
this.status = raw.status || '';
|
||||
this.conclusion = raw.conclusion || '';
|
||||
this.headBranch = raw.head_branch || '';
|
||||
this.headSha = raw.head_sha || '';
|
||||
this.htmlUrl = raw.html_url || '';
|
||||
this.event = raw.event || '';
|
||||
this.path = raw.path || '';
|
||||
this.startedAt = raw.started_at || '';
|
||||
this.completedAt = raw.completed_at || '';
|
||||
this.actorLogin = raw.actor?.login || '';
|
||||
this.triggerActorLogin = raw.trigger_actor?.login || '';
|
||||
|
||||
// Computed properties
|
||||
this.resolvedStatus = resolveGiteaStatus(this.status, this.conclusion);
|
||||
this.duration = computeDuration(this.startedAt, this.completedAt);
|
||||
this.ref = this.headBranch || extractRefFromPath(this.path);
|
||||
this.workflowId = extractWorkflowIdFromPath(this.path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jobs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getJobs(): Promise<GiteaActionRunJob[]> {
|
||||
const jobs = await this.client.requestGetActionRunJobs(this.ownerRepo, this.id);
|
||||
return jobs.map(j => new GiteaActionRunJob(this.client, this.ownerRepo, j));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Re-dispatch this workflow on the same ref.
|
||||
* Gitea 1.25 has no rerun endpoint, so this dispatches the workflow again.
|
||||
*/
|
||||
async rerun(inputs?: Record<string, string>): Promise<void> {
|
||||
const wfId = this.workflowId;
|
||||
if (!wfId) {
|
||||
throw new Error(`Cannot rerun: no workflow ID found in path "${this.path}"`);
|
||||
}
|
||||
await this.client.requestDispatchWorkflow(this.ownerRepo, wfId, this.ref, inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this action run.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this.client.requestDeleteActionRun(this.ownerRepo, this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGiteaActionRun {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
workflow_id: this.workflowId,
|
||||
status: this.status,
|
||||
conclusion: this.conclusion,
|
||||
head_branch: this.headBranch,
|
||||
head_sha: this.headSha,
|
||||
html_url: this.htmlUrl,
|
||||
event: this.event,
|
||||
path: this.path,
|
||||
display_title: this.displayTitle,
|
||||
run_number: this.runNumber,
|
||||
run_attempt: this.runAttempt,
|
||||
started_at: this.startedAt,
|
||||
completed_at: this.completedAt,
|
||||
actor: { id: 0, login: this.actorLogin, login_name: '', source_id: 0, full_name: '', email: '', avatar_url: '' },
|
||||
trigger_actor: { id: 0, login: this.triggerActorLogin, login_name: '', source_id: 0, full_name: '', email: '', avatar_url: '' },
|
||||
repository: { id: 0, name: '', full_name: this.ownerRepo, html_url: '' },
|
||||
};
|
||||
}
|
||||
}
|
||||
118
ts/gitea.classes.actionrunjob.ts
Normal file
118
ts/gitea.classes.actionrunjob.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
import type { IGiteaActionRunJob, IGiteaActionRunJobStep } from './gitea.interfaces.js';
|
||||
import { computeDuration, resolveGiteaStatus } from './gitea.helpers.js';
|
||||
|
||||
export class GiteaActionRunJobStep {
|
||||
public readonly name: string;
|
||||
public readonly number: number;
|
||||
public readonly status: string;
|
||||
public readonly conclusion: string;
|
||||
public readonly resolvedStatus: string;
|
||||
public readonly startedAt: string;
|
||||
public readonly completedAt: string;
|
||||
public readonly duration: number;
|
||||
|
||||
constructor(raw: IGiteaActionRunJobStep) {
|
||||
this.name = raw.name || '';
|
||||
this.number = raw.number || 0;
|
||||
this.status = raw.status || '';
|
||||
this.conclusion = raw.conclusion || '';
|
||||
this.resolvedStatus = resolveGiteaStatus(this.status, this.conclusion);
|
||||
this.startedAt = raw.started_at || '';
|
||||
this.completedAt = raw.completed_at || '';
|
||||
this.duration = computeDuration(this.startedAt, this.completedAt);
|
||||
}
|
||||
|
||||
toJSON(): IGiteaActionRunJobStep {
|
||||
return {
|
||||
name: this.name,
|
||||
number: this.number,
|
||||
status: this.status,
|
||||
conclusion: this.conclusion,
|
||||
started_at: this.startedAt,
|
||||
completed_at: this.completedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GiteaActionRunJob {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly runId: number;
|
||||
public readonly name: string;
|
||||
public readonly workflowName: string;
|
||||
public readonly headBranch: string;
|
||||
public readonly headSha: string;
|
||||
public readonly status: string;
|
||||
public readonly conclusion: string;
|
||||
public readonly htmlUrl: string;
|
||||
public readonly startedAt: string;
|
||||
public readonly completedAt: string;
|
||||
public readonly labels: string[];
|
||||
public readonly runnerId: number;
|
||||
public readonly runnerName: string;
|
||||
public readonly steps: GiteaActionRunJobStep[];
|
||||
|
||||
// Computed
|
||||
public readonly resolvedStatus: string;
|
||||
public readonly duration: number;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GiteaClient,
|
||||
private ownerRepo: string,
|
||||
raw: IGiteaActionRunJob,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.runId = raw.run_id || 0;
|
||||
this.name = raw.name || '';
|
||||
this.workflowName = raw.workflow_name || '';
|
||||
this.headBranch = raw.head_branch || '';
|
||||
this.headSha = raw.head_sha || '';
|
||||
this.status = raw.status || '';
|
||||
this.conclusion = raw.conclusion || '';
|
||||
this.htmlUrl = raw.html_url || '';
|
||||
this.startedAt = raw.started_at || '';
|
||||
this.completedAt = raw.completed_at || '';
|
||||
this.labels = raw.labels || [];
|
||||
this.runnerId = raw.runner_id || 0;
|
||||
this.runnerName = raw.runner_name || '';
|
||||
this.steps = (raw.steps || []).map(s => new GiteaActionRunJobStep(s));
|
||||
|
||||
// Computed
|
||||
this.resolvedStatus = resolveGiteaStatus(this.status, this.conclusion);
|
||||
this.duration = computeDuration(this.startedAt, this.completedAt);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Log
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getLog(): Promise<string> {
|
||||
return this.client.requestGetJobLog(this.ownerRepo, this.id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGiteaActionRunJob {
|
||||
return {
|
||||
id: this.id,
|
||||
run_id: this.runId,
|
||||
name: this.name,
|
||||
workflow_name: this.workflowName,
|
||||
head_branch: this.headBranch,
|
||||
head_sha: this.headSha,
|
||||
status: this.status,
|
||||
conclusion: this.conclusion,
|
||||
html_url: this.htmlUrl,
|
||||
started_at: this.startedAt,
|
||||
completed_at: this.completedAt,
|
||||
steps: this.steps.map(s => s.toJSON()),
|
||||
labels: this.labels,
|
||||
runner_id: this.runnerId,
|
||||
runner_name: this.runnerName,
|
||||
};
|
||||
}
|
||||
}
|
||||
18
ts/gitea.classes.branch.ts
Normal file
18
ts/gitea.classes.branch.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IGiteaBranch } from './gitea.interfaces.js';
|
||||
|
||||
export class GiteaBranch {
|
||||
public readonly name: string;
|
||||
public readonly commitSha: string;
|
||||
|
||||
constructor(raw: IGiteaBranch) {
|
||||
this.name = raw.name || '';
|
||||
this.commitSha = raw.commit?.id || '';
|
||||
}
|
||||
|
||||
toJSON(): IGiteaBranch {
|
||||
return {
|
||||
name: this.name,
|
||||
commit: { id: this.commitSha },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as plugins from './gitea.plugins.js';
|
||||
import { logger } from './gitea.logging.js';
|
||||
import type {
|
||||
IGiteaUser,
|
||||
IGiteaRepository,
|
||||
@@ -13,23 +12,26 @@ import type {
|
||||
IListOptions,
|
||||
IActionRunListOptions,
|
||||
} from './gitea.interfaces.js';
|
||||
import { GiteaOrganization } from './gitea.classes.organization.js';
|
||||
import { GiteaRepository } from './gitea.classes.repository.js';
|
||||
import { autoPaginate, toGiteaApiStatus } from './gitea.helpers.js';
|
||||
|
||||
export class GiteaClient {
|
||||
private baseUrl: string;
|
||||
private token: string;
|
||||
|
||||
constructor(baseUrl: string, token: string) {
|
||||
// Remove trailing slash if present
|
||||
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
// ===========================================================================
|
||||
// HTTP helpers (internal)
|
||||
// ===========================================================================
|
||||
|
||||
private async request<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
/** @internal */
|
||||
async request<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||
path: string,
|
||||
data?: any,
|
||||
customHeaders?: Record<string, string>,
|
||||
@@ -62,6 +64,9 @@ export class GiteaClient {
|
||||
case 'PUT':
|
||||
response = await builder.put();
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await builder.patch();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await builder.delete();
|
||||
break;
|
||||
@@ -79,7 +84,8 @@ export class GiteaClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async requestText(
|
||||
/** @internal */
|
||||
async requestText(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
@@ -114,9 +120,22 @@ export class GiteaClient {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal — fetch binary data (e.g. avatar images) */
|
||||
async requestBinary(path: string): Promise<Uint8Array> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Authorization': `token ${this.token}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${path}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const buf = await response.arrayBuffer();
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public API — Connection
|
||||
// ===========================================================================
|
||||
|
||||
public async testConnection(): Promise<ITestConnectionResult> {
|
||||
try {
|
||||
@@ -127,11 +146,81 @@ export class GiteaClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repositories
|
||||
// ---------------------------------------------------------------------------
|
||||
// ===========================================================================
|
||||
// Public API — Organizations (returns rich objects)
|
||||
// ===========================================================================
|
||||
|
||||
public async getRepos(opts?: IListOptions): Promise<IGiteaRepository[]> {
|
||||
/**
|
||||
* Get all organizations (auto-paginated).
|
||||
*/
|
||||
public async getOrgs(opts?: IListOptions): Promise<GiteaOrganization[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.requestGetOrgs({ ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(orgs => orgs.map(o => new GiteaOrganization(this, o)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single organization by name.
|
||||
*/
|
||||
public async getOrg(orgName: string): Promise<GiteaOrganization> {
|
||||
const raw = await this.requestGetOrg(orgName);
|
||||
return new GiteaOrganization(this, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new organization.
|
||||
*/
|
||||
public async createOrg(name: string, opts?: {
|
||||
fullName?: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
}): Promise<GiteaOrganization> {
|
||||
const raw = await this.requestCreateOrg(name, opts);
|
||||
return new GiteaOrganization(this, raw);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public API — Repositories (returns rich objects)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Search/list all repositories (auto-paginated).
|
||||
*/
|
||||
public async getRepos(opts?: IListOptions): Promise<GiteaRepository[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.requestGetRepos({ ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(repos => repos.map(r => new GiteaRepository(this, r)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single repository by owner/repo.
|
||||
*/
|
||||
public async getRepo(ownerRepo: string): Promise<GiteaRepository> {
|
||||
const raw = await this.requestGetRepo(ownerRepo);
|
||||
return new GiteaRepository(this, raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a repository within an organization.
|
||||
*/
|
||||
public async createOrgRepo(orgName: string, name: string, opts?: {
|
||||
description?: string;
|
||||
private?: boolean;
|
||||
}): Promise<GiteaRepository> {
|
||||
const raw = await this.requestCreateOrgRepo(orgName, name, opts);
|
||||
return new GiteaRepository(this, raw);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Internal request methods — called by domain classes
|
||||
// ===========================================================================
|
||||
|
||||
// --- Repos ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepos(opts?: IListOptions): Promise<IGiteaRepository[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
let url = `/api/v1/repos/search?page=${page}&limit=${limit}&sort=updated`;
|
||||
@@ -142,40 +231,104 @@ export class GiteaClient {
|
||||
return body.data || body;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Organizations
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal */
|
||||
async requestGetRepo(ownerRepo: string): Promise<IGiteaRepository> {
|
||||
return this.request<IGiteaRepository>('GET', `/api/v1/repos/${ownerRepo}`);
|
||||
}
|
||||
|
||||
public async getOrgs(opts?: IListOptions): Promise<IGiteaOrganization[]> {
|
||||
/** @internal */
|
||||
async requestCreateOrgRepo(orgName: string, name: string, opts?: {
|
||||
description?: string;
|
||||
private?: boolean;
|
||||
}): Promise<IGiteaRepository> {
|
||||
return this.request<IGiteaRepository>('POST', `/api/v1/orgs/${encodeURIComponent(orgName)}/repos`, {
|
||||
name,
|
||||
description: opts?.description || '',
|
||||
private: opts?.private ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestPatchRepo(ownerRepo: string, data: Record<string, any>): Promise<void> {
|
||||
await this.request('PATCH', `/api/v1/repos/${ownerRepo}`, data);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestSetRepoTopics(ownerRepo: string, topics: string[]): Promise<void> {
|
||||
await this.request('PUT', `/api/v1/repos/${ownerRepo}/topics`, { topics });
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestPostRepoAvatar(ownerRepo: string, imageBase64: string): Promise<void> {
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/avatar`, { image: imageBase64 });
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestDeleteRepoAvatar(ownerRepo: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/repos/${ownerRepo}/avatar`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestTransferRepo(ownerRepo: string, newOwner: string, teamIds?: number[]): Promise<void> {
|
||||
const body: any = { new_owner: newOwner };
|
||||
if (teamIds?.length) body.team_ids = teamIds;
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/transfer`, body);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestDeleteRepo(owner: string, repo: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
|
||||
}
|
||||
|
||||
// --- Repo Branches & Tags ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepoBranches(ownerRepo: string, opts?: IListOptions): Promise<IGiteaBranch[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
return this.request<IGiteaBranch[]>('GET', `/api/v1/repos/${ownerRepo}/branches?page=${page}&limit=${limit}`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepoTags(ownerRepo: string, opts?: IListOptions): Promise<IGiteaTag[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
return this.request<IGiteaTag[]>('GET', `/api/v1/repos/${ownerRepo}/tags?page=${page}&limit=${limit}`);
|
||||
}
|
||||
|
||||
// --- Repo Secrets ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetRepoSecrets(ownerRepo: string): Promise<IGiteaSecret[]> {
|
||||
return this.request<IGiteaSecret[]>('GET', `/api/v1/repos/${ownerRepo}/actions/secrets`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestSetRepoSecret(ownerRepo: string, key: string, value: string): Promise<void> {
|
||||
await this.request('PUT', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`, { data: value });
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestDeleteRepoSecret(ownerRepo: string, key: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`);
|
||||
}
|
||||
|
||||
// --- Organizations ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetOrgs(opts?: IListOptions): Promise<IGiteaOrganization[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
return this.request<IGiteaOrganization[]>('GET', `/api/v1/orgs?page=${page}&limit=${limit}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single organization by name
|
||||
*/
|
||||
public async getOrg(orgName: string): Promise<IGiteaOrganization> {
|
||||
/** @internal */
|
||||
async requestGetOrg(orgName: string): Promise<IGiteaOrganization> {
|
||||
return this.request<IGiteaOrganization>('GET', `/api/v1/orgs/${encodeURIComponent(orgName)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List repositories within an organization
|
||||
*/
|
||||
public async getOrgRepos(orgName: string, opts?: IListOptions): Promise<IGiteaRepository[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
let url = `/api/v1/orgs/${encodeURIComponent(orgName)}/repos?page=${page}&limit=${limit}&sort=updated`;
|
||||
if (opts?.search) {
|
||||
url += `&q=${encodeURIComponent(opts.search)}`;
|
||||
}
|
||||
return this.request<IGiteaRepository[]>('GET', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new organization
|
||||
*/
|
||||
public async createOrg(name: string, opts?: {
|
||||
/** @internal */
|
||||
async requestCreateOrg(name: string, opts?: {
|
||||
fullName?: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
@@ -188,87 +341,66 @@ export class GiteaClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a repository within an organization
|
||||
*/
|
||||
public async createOrgRepo(orgName: string, name: string, opts?: {
|
||||
description?: string;
|
||||
private?: boolean;
|
||||
}): Promise<IGiteaRepository> {
|
||||
return this.request<IGiteaRepository>('POST', `/api/v1/orgs/${encodeURIComponent(orgName)}/repos`, {
|
||||
name,
|
||||
description: opts?.description || '',
|
||||
private: opts?.private ?? true,
|
||||
});
|
||||
/** @internal */
|
||||
async requestPatchOrg(orgName: string, data: Record<string, any>): Promise<void> {
|
||||
await this.request('PATCH', `/api/v1/orgs/${encodeURIComponent(orgName)}`, data);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repository Branches & Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
/** @internal */
|
||||
async requestPostOrgAvatar(orgName: string, imageBase64: string): Promise<void> {
|
||||
await this.request('POST', `/api/v1/orgs/${encodeURIComponent(orgName)}/avatar`, { image: imageBase64 });
|
||||
}
|
||||
|
||||
public async getRepoBranches(ownerRepo: string, opts?: IListOptions): Promise<IGiteaBranch[]> {
|
||||
/** @internal */
|
||||
async requestDeleteOrgAvatar(orgName: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/orgs/${encodeURIComponent(orgName)}/avatar`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async requestDeleteOrg(orgName: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/orgs/${encodeURIComponent(orgName)}`);
|
||||
}
|
||||
|
||||
// --- Org repos ---
|
||||
|
||||
/** @internal */
|
||||
async requestGetOrgRepos(orgName: string, opts?: IListOptions): Promise<IGiteaRepository[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
return this.request<IGiteaBranch[]>(
|
||||
'GET',
|
||||
`/api/v1/repos/${ownerRepo}/branches?page=${page}&limit=${limit}`,
|
||||
);
|
||||
let url = `/api/v1/orgs/${encodeURIComponent(orgName)}/repos?page=${page}&limit=${limit}&sort=updated`;
|
||||
if (opts?.search) {
|
||||
url += `&q=${encodeURIComponent(opts.search)}`;
|
||||
}
|
||||
return this.request<IGiteaRepository[]>('GET', url);
|
||||
}
|
||||
|
||||
public async getRepoTags(ownerRepo: string, opts?: IListOptions): Promise<IGiteaTag[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 50;
|
||||
return this.request<IGiteaTag[]>(
|
||||
'GET',
|
||||
`/api/v1/repos/${ownerRepo}/tags?page=${page}&limit=${limit}`,
|
||||
);
|
||||
}
|
||||
// --- Org Secrets ---
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repository Secrets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async getRepoSecrets(ownerRepo: string): Promise<IGiteaSecret[]> {
|
||||
return this.request<IGiteaSecret[]>('GET', `/api/v1/repos/${ownerRepo}/actions/secrets`);
|
||||
}
|
||||
|
||||
public async setRepoSecret(ownerRepo: string, key: string, value: string): Promise<void> {
|
||||
await this.request('PUT', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`, { data: value });
|
||||
}
|
||||
|
||||
public async deleteRepoSecret(ownerRepo: string, key: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/repos/${ownerRepo}/actions/secrets/${key}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Organization Secrets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async getOrgSecrets(orgName: string): Promise<IGiteaSecret[]> {
|
||||
/** @internal */
|
||||
async requestGetOrgSecrets(orgName: string): Promise<IGiteaSecret[]> {
|
||||
return this.request<IGiteaSecret[]>('GET', `/api/v1/orgs/${orgName}/actions/secrets`);
|
||||
}
|
||||
|
||||
public async setOrgSecret(orgName: string, key: string, value: string): Promise<void> {
|
||||
/** @internal */
|
||||
async requestSetOrgSecret(orgName: string, key: string, value: string): Promise<void> {
|
||||
await this.request('PUT', `/api/v1/orgs/${orgName}/actions/secrets/${key}`, { data: value });
|
||||
}
|
||||
|
||||
public async deleteOrgSecret(orgName: string, key: string): Promise<void> {
|
||||
/** @internal */
|
||||
async requestDeleteOrgSecret(orgName: string, key: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/orgs/${orgName}/actions/secrets/${key}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action Runs
|
||||
// ---------------------------------------------------------------------------
|
||||
// --- Action Runs ---
|
||||
|
||||
/**
|
||||
* List action runs for a repository with optional filters.
|
||||
* Supports status, branch, event, actor filtering.
|
||||
*/
|
||||
public async getActionRuns(ownerRepo: string, opts?: IActionRunListOptions): Promise<IGiteaActionRun[]> {
|
||||
/** @internal */
|
||||
async requestGetActionRuns(ownerRepo: string, opts?: IActionRunListOptions): Promise<IGiteaActionRun[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 30;
|
||||
let url = `/api/v1/repos/${ownerRepo}/actions/runs?page=${page}&limit=${limit}`;
|
||||
if (opts?.status) url += `&status=${encodeURIComponent(opts.status)}`;
|
||||
// Translate user-friendly status names to Gitea API values
|
||||
const apiStatus = toGiteaApiStatus(opts?.status);
|
||||
if (apiStatus) url += `&status=${encodeURIComponent(apiStatus)}`;
|
||||
if (opts?.branch) url += `&branch=${encodeURIComponent(opts.branch)}`;
|
||||
if (opts?.event) url += `&event=${encodeURIComponent(opts.event)}`;
|
||||
if (opts?.actor) url += `&actor=${encodeURIComponent(opts.actor)}`;
|
||||
@@ -276,49 +408,24 @@ export class GiteaClient {
|
||||
return body.workflow_runs || body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single action run's full details.
|
||||
*/
|
||||
public async getActionRun(ownerRepo: string, runId: number): Promise<IGiteaActionRun> {
|
||||
return this.request<IGiteaActionRun>(
|
||||
'GET',
|
||||
`/api/v1/repos/${ownerRepo}/actions/runs/${runId}`,
|
||||
);
|
||||
/** @internal */
|
||||
async requestGetActionRun(ownerRepo: string, runId: number): Promise<IGiteaActionRun> {
|
||||
return this.request<IGiteaActionRun>('GET', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List jobs for an action run.
|
||||
*/
|
||||
public async getActionRunJobs(ownerRepo: string, runId: number): Promise<IGiteaActionRunJob[]> {
|
||||
/** @internal */
|
||||
async requestGetActionRunJobs(ownerRepo: string, runId: number): Promise<IGiteaActionRunJob[]> {
|
||||
const body = await this.request<any>('GET', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/jobs`);
|
||||
return body.jobs || body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a job's raw log output.
|
||||
*/
|
||||
public async getJobLog(ownerRepo: string, jobId: number): Promise<string> {
|
||||
/** @internal */
|
||||
async requestGetJobLog(ownerRepo: string, jobId: number): Promise<string> {
|
||||
return this.requestText('GET', `/api/v1/repos/${ownerRepo}/actions/jobs/${jobId}/logs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-run an action run.
|
||||
*/
|
||||
public async rerunAction(ownerRepo: string, runId: number): Promise<void> {
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/rerun`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running action run.
|
||||
*/
|
||||
public async cancelAction(ownerRepo: string, runId: number): Promise<void> {
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/cancel`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a workflow (trigger manually).
|
||||
*/
|
||||
public async dispatchWorkflow(
|
||||
/** @internal */
|
||||
async requestDispatchWorkflow(
|
||||
ownerRepo: string,
|
||||
workflowId: string,
|
||||
ref: string,
|
||||
@@ -331,14 +438,8 @@ export class GiteaClient {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repository Deletion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async deleteRepo(owner: string, repo: string): Promise<void> {
|
||||
await this.request(
|
||||
'DELETE',
|
||||
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
|
||||
);
|
||||
/** @internal */
|
||||
async requestDeleteActionRun(ownerRepo: string, runId: number): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}`);
|
||||
}
|
||||
}
|
||||
|
||||
115
ts/gitea.classes.organization.ts
Normal file
115
ts/gitea.classes.organization.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
import type { IGiteaOrganization, IGiteaSecret, IListOptions } from './gitea.interfaces.js';
|
||||
import { GiteaRepository } from './gitea.classes.repository.js';
|
||||
import { GiteaSecret } from './gitea.classes.secret.js';
|
||||
import { autoPaginate } from './gitea.helpers.js';
|
||||
|
||||
export class GiteaOrganization {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly fullName: string;
|
||||
public readonly description: string;
|
||||
public readonly visibility: string;
|
||||
public readonly repoCount: number;
|
||||
public readonly avatarUrl: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GiteaClient,
|
||||
raw: IGiteaOrganization,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.fullName = raw.full_name || this.name;
|
||||
this.description = raw.description || '';
|
||||
this.visibility = raw.visibility || 'public';
|
||||
this.repoCount = raw.repo_count || 0;
|
||||
this.avatarUrl = raw.avatar_url || '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getRepos(opts?: IListOptions): Promise<GiteaRepository[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetOrgRepos(this.name, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(repos => repos.map(r => new GiteaRepository(this.client, r)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secrets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getSecrets(): Promise<GiteaSecret[]> {
|
||||
const secrets = await this.client.requestGetOrgSecrets(this.name);
|
||||
return secrets.map(s => new GiteaSecret(s));
|
||||
}
|
||||
|
||||
async setSecret(key: string, value: string): Promise<void> {
|
||||
await this.client.requestSetOrgSecret(this.name, key, value);
|
||||
}
|
||||
|
||||
async deleteSecret(key: string): Promise<void> {
|
||||
await this.client.requestDeleteOrgSecret(this.name, key);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update organization properties (description, visibility, etc.)
|
||||
*/
|
||||
async update(data: {
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
fullName?: string;
|
||||
}): Promise<void> {
|
||||
await this.client.requestPatchOrg(this.name, {
|
||||
description: data.description,
|
||||
visibility: data.visibility,
|
||||
full_name: data.fullName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an avatar image for this organization.
|
||||
* @param imageBase64 - Base64-encoded image data
|
||||
*/
|
||||
async setAvatar(imageBase64: string): Promise<void> {
|
||||
await this.client.requestPostOrgAvatar(this.name, imageBase64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the organization's avatar.
|
||||
*/
|
||||
async deleteAvatar(): Promise<void> {
|
||||
await this.client.requestDeleteOrgAvatar(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this organization.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
await this.client.requestDeleteOrg(this.name);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGiteaOrganization {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
full_name: this.fullName,
|
||||
description: this.description,
|
||||
visibility: this.visibility,
|
||||
repo_count: this.repoCount,
|
||||
avatar_url: this.avatarUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
171
ts/gitea.classes.repository.ts
Normal file
171
ts/gitea.classes.repository.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
import type { IGiteaRepository, IGiteaSecret, IListOptions, IActionRunListOptions } from './gitea.interfaces.js';
|
||||
import { GiteaBranch } from './gitea.classes.branch.js';
|
||||
import { GiteaTag } from './gitea.classes.tag.js';
|
||||
import { GiteaSecret } from './gitea.classes.secret.js';
|
||||
import { GiteaActionRun } from './gitea.classes.actionrun.js';
|
||||
import { autoPaginate } from './gitea.helpers.js';
|
||||
|
||||
export class GiteaRepository {
|
||||
// Raw data
|
||||
public readonly id: number;
|
||||
public readonly name: string;
|
||||
public readonly fullName: string;
|
||||
public readonly description: string;
|
||||
public readonly defaultBranch: string;
|
||||
public readonly htmlUrl: string;
|
||||
public readonly isPrivate: boolean;
|
||||
public readonly topics: string[];
|
||||
public readonly updatedAt: string;
|
||||
public readonly ownerId: number;
|
||||
public readonly ownerLogin: string;
|
||||
public readonly ownerAvatarUrl: string;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
private client: GiteaClient,
|
||||
raw: IGiteaRepository,
|
||||
) {
|
||||
this.id = raw.id;
|
||||
this.name = raw.name || '';
|
||||
this.fullName = raw.full_name || '';
|
||||
this.description = raw.description || '';
|
||||
this.defaultBranch = raw.default_branch || 'main';
|
||||
this.htmlUrl = raw.html_url || '';
|
||||
this.isPrivate = raw.private ?? true;
|
||||
this.topics = raw.topics || [];
|
||||
this.updatedAt = raw.updated_at || '';
|
||||
this.ownerId = raw.owner?.id || 0;
|
||||
this.ownerLogin = raw.owner?.login || '';
|
||||
this.ownerAvatarUrl = raw.owner?.avatar_url || '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Branches & Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getBranches(opts?: IListOptions): Promise<GiteaBranch[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetRepoBranches(this.fullName, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(branches => branches.map(b => new GiteaBranch(b)));
|
||||
}
|
||||
|
||||
async getTags(opts?: IListOptions): Promise<GiteaTag[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetRepoTags(this.fullName, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(tags => tags.map(t => new GiteaTag(t)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Secrets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getSecrets(): Promise<GiteaSecret[]> {
|
||||
const secrets = await this.client.requestGetRepoSecrets(this.fullName);
|
||||
return secrets.map(s => new GiteaSecret(s));
|
||||
}
|
||||
|
||||
async setSecret(key: string, value: string): Promise<void> {
|
||||
await this.client.requestSetRepoSecret(this.fullName, key, value);
|
||||
}
|
||||
|
||||
async deleteSecret(key: string): Promise<void> {
|
||||
await this.client.requestDeleteRepoSecret(this.fullName, key);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action Runs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getActionRuns(opts?: IActionRunListOptions): Promise<GiteaActionRun[]> {
|
||||
return autoPaginate(
|
||||
(page, perPage) => this.client.requestGetActionRuns(this.fullName, { ...opts, page, perPage }),
|
||||
opts,
|
||||
).then(runs => runs.map(r => new GiteaActionRun(this.client, this.fullName, r)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mutation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update repository properties.
|
||||
*/
|
||||
async update(data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
defaultBranch?: string;
|
||||
private?: boolean;
|
||||
archived?: boolean;
|
||||
}): Promise<void> {
|
||||
await this.client.requestPatchRepo(this.fullName, {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
default_branch: data.defaultBranch,
|
||||
private: data.private,
|
||||
archived: data.archived,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set topics for this repository (replaces all existing topics).
|
||||
*/
|
||||
async setTopics(topics: string[]): Promise<void> {
|
||||
await this.client.requestSetRepoTopics(this.fullName, topics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an avatar image for this repository.
|
||||
* @param imageBase64 - Base64-encoded image data
|
||||
*/
|
||||
async setAvatar(imageBase64: string): Promise<void> {
|
||||
await this.client.requestPostRepoAvatar(this.fullName, imageBase64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the repository's avatar.
|
||||
*/
|
||||
async deleteAvatar(): Promise<void> {
|
||||
await this.client.requestDeleteRepoAvatar(this.fullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer this repository to a different owner (org or user).
|
||||
*/
|
||||
async transfer(newOwner: string, teamIds?: number[]): Promise<void> {
|
||||
await this.client.requestTransferRepo(this.fullName, newOwner, teamIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this repository.
|
||||
*/
|
||||
async delete(): Promise<void> {
|
||||
const [owner, repo] = this.fullName.split('/');
|
||||
await this.client.requestDeleteRepo(owner, repo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toJSON(): IGiteaRepository {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
full_name: this.fullName,
|
||||
description: this.description,
|
||||
default_branch: this.defaultBranch,
|
||||
html_url: this.htmlUrl,
|
||||
private: this.isPrivate,
|
||||
topics: this.topics,
|
||||
updated_at: this.updatedAt,
|
||||
owner: {
|
||||
id: this.ownerId,
|
||||
login: this.ownerLogin,
|
||||
avatar_url: this.ownerAvatarUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
18
ts/gitea.classes.secret.ts
Normal file
18
ts/gitea.classes.secret.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IGiteaSecret } from './gitea.interfaces.js';
|
||||
|
||||
export class GiteaSecret {
|
||||
public readonly name: string;
|
||||
public readonly createdAt: string;
|
||||
|
||||
constructor(raw: IGiteaSecret) {
|
||||
this.name = raw.name || '';
|
||||
this.createdAt = raw.created_at || '';
|
||||
}
|
||||
|
||||
toJSON(): IGiteaSecret {
|
||||
return {
|
||||
name: this.name,
|
||||
created_at: this.createdAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
19
ts/gitea.classes.tag.ts
Normal file
19
ts/gitea.classes.tag.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { IGiteaTag } from './gitea.interfaces.js';
|
||||
|
||||
export class GiteaTag {
|
||||
public readonly name: string;
|
||||
public readonly commitSha: string;
|
||||
|
||||
constructor(raw: IGiteaTag) {
|
||||
this.name = raw.name || '';
|
||||
this.commitSha = raw.commit?.sha || '';
|
||||
}
|
||||
|
||||
toJSON(): IGiteaTag {
|
||||
return {
|
||||
name: this.name,
|
||||
id: this.name,
|
||||
commit: { sha: this.commitSha },
|
||||
};
|
||||
}
|
||||
}
|
||||
87
ts/gitea.helpers.ts
Normal file
87
ts/gitea.helpers.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Auto-paginate a list endpoint.
|
||||
* If opts includes a specific page, returns just that page (no auto-pagination).
|
||||
*/
|
||||
export async function autoPaginate<T>(
|
||||
fetchPage: (page: number, perPage: number) => Promise<T[]>,
|
||||
opts?: { page?: number; perPage?: number },
|
||||
): Promise<T[]> {
|
||||
const perPage = opts?.perPage || 50;
|
||||
|
||||
// If caller requests a specific page, return just that page
|
||||
if (opts?.page) {
|
||||
return fetchPage(opts.page, perPage);
|
||||
}
|
||||
|
||||
// Otherwise auto-paginate through all pages
|
||||
const all: T[] = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const items = await fetchPage(page, perPage);
|
||||
all.push(...items);
|
||||
if (items.length < perPage) break;
|
||||
page++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute duration in seconds from two ISO timestamps.
|
||||
*/
|
||||
export function computeDuration(startedAt?: string, completedAt?: string): number {
|
||||
if (!startedAt || !completedAt) return 0;
|
||||
const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime();
|
||||
return ms > 0 ? Math.round(ms / 1000) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gitea uses `status` for run state (running, waiting, completed)
|
||||
* and `conclusion` for the actual result (success, failure, cancelled, skipped).
|
||||
* When status is "completed", the conclusion carries the meaningful status.
|
||||
*/
|
||||
export function resolveGiteaStatus(status: string, conclusion: string): string {
|
||||
if (status === 'completed' && conclusion) {
|
||||
return conclusion;
|
||||
}
|
||||
return status || conclusion || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a human-readable ref from the Gitea `path` field.
|
||||
* path format: "workflow.yaml@refs/tags/v1.0.0" or "workflow.yaml@refs/heads/main"
|
||||
*/
|
||||
export function extractRefFromPath(path?: string): string {
|
||||
if (!path) return '';
|
||||
const atIdx = path.indexOf('@');
|
||||
if (atIdx < 0) return '';
|
||||
const ref = path.substring(atIdx + 1);
|
||||
return ref.replace(/^refs\/tags\//, '').replace(/^refs\/heads\//, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the workflow filename from the Gitea `path` field.
|
||||
* path format: "workflow.yaml@refs/tags/v1.0.0" → "workflow.yaml"
|
||||
*/
|
||||
export function extractWorkflowIdFromPath(path?: string): string {
|
||||
if (!path) return '';
|
||||
const atIdx = path.indexOf('@');
|
||||
return atIdx >= 0 ? path.substring(0, atIdx) : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate normalized status names to Gitea API-native query parameter values.
|
||||
* Gitea accepts: pending, queued, in_progress, failure, success, skipped
|
||||
*/
|
||||
export function toGiteaApiStatus(status?: string): string | undefined {
|
||||
if (!status) return undefined;
|
||||
const map: Record<string, string> = {
|
||||
running: 'in_progress',
|
||||
failed: 'failure',
|
||||
canceled: 'cancelled',
|
||||
pending: 'pending',
|
||||
success: 'success',
|
||||
skipped: 'skipped',
|
||||
waiting: 'queued',
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export interface IListOptions {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IActionRunListOptions extends IListOptions {
|
||||
/** Filter by run status (waiting, running, success, failure, cancelled) */
|
||||
/** Filter by run status. Accepts normalized names (running, failed, etc.) — auto-translated to Gitea API values. */
|
||||
status?: string;
|
||||
/** Filter by head branch */
|
||||
branch?: string;
|
||||
@@ -35,13 +35,15 @@ export interface IActionRunListOptions extends IListOptions {
|
||||
export interface IGiteaUser {
|
||||
id: number;
|
||||
login: string;
|
||||
login_name: string;
|
||||
source_id: number;
|
||||
full_name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repositories
|
||||
// Repositories (raw API response)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IGiteaRepository {
|
||||
@@ -62,7 +64,7 @@ export interface IGiteaRepository {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Organizations
|
||||
// Organizations (raw API response)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IGiteaOrganization {
|
||||
@@ -72,6 +74,7 @@ export interface IGiteaOrganization {
|
||||
description: string;
|
||||
visibility: string;
|
||||
repo_count: number;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -84,25 +87,25 @@ export interface IGiteaSecret {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action Runs
|
||||
// Action Runs (raw API response — aligned with Gitea 1.25 swagger)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IGiteaActionRun {
|
||||
id: number;
|
||||
name: string;
|
||||
workflow_id: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
status: string; // run state: running, waiting, completed
|
||||
conclusion: string; // result: success, failure, cancelled, skipped
|
||||
head_branch: string;
|
||||
head_sha: string;
|
||||
html_url: string;
|
||||
event: string;
|
||||
path: string; // e.g. "workflow.yaml@refs/tags/v1.0"
|
||||
display_title: string;
|
||||
run_number: number;
|
||||
run_attempt: number;
|
||||
run_duration: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
actor: IGiteaUser;
|
||||
trigger_actor: IGiteaUser;
|
||||
repository: {
|
||||
@@ -111,15 +114,10 @@ export interface IGiteaActionRun {
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
};
|
||||
head_commit: {
|
||||
id: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action Run Jobs
|
||||
// Action Run Jobs (raw API response)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface IGiteaActionRunJob {
|
||||
@@ -132,7 +130,6 @@ export interface IGiteaActionRunJob {
|
||||
status: string;
|
||||
conclusion: string;
|
||||
html_url: string;
|
||||
run_duration: number;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
steps: IGiteaActionRunJobStep[];
|
||||
|
||||
24
ts/index.ts
24
ts/index.ts
@@ -1,4 +1,26 @@
|
||||
// Main client
|
||||
export { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
|
||||
// Domain classes
|
||||
export { GiteaOrganization } from './gitea.classes.organization.js';
|
||||
export { GiteaRepository } from './gitea.classes.repository.js';
|
||||
export { GiteaActionRun } from './gitea.classes.actionrun.js';
|
||||
export { GiteaActionRunJob, GiteaActionRunJobStep } from './gitea.classes.actionrunjob.js';
|
||||
export { GiteaBranch } from './gitea.classes.branch.js';
|
||||
export { GiteaTag } from './gitea.classes.tag.js';
|
||||
export { GiteaSecret } from './gitea.classes.secret.js';
|
||||
|
||||
// Helpers
|
||||
export {
|
||||
autoPaginate,
|
||||
computeDuration,
|
||||
resolveGiteaStatus,
|
||||
extractRefFromPath,
|
||||
extractWorkflowIdFromPath,
|
||||
toGiteaApiStatus,
|
||||
} from './gitea.helpers.js';
|
||||
|
||||
// Interfaces (raw API types)
|
||||
export type {
|
||||
IGiteaUser,
|
||||
IGiteaRepository,
|
||||
@@ -13,4 +35,6 @@ export type {
|
||||
IListOptions,
|
||||
IActionRunListOptions,
|
||||
} from './gitea.interfaces.js';
|
||||
|
||||
// Commit info
|
||||
export { commitinfo } from './00_commitinfo_data.js';
|
||||
|
||||
Reference in New Issue
Block a user