feat(core): initial implementation of @apiclient.xyz/gitea TypeScript client
Provides GiteaClient class with methods for repos, orgs, secrets, and action runs.
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
.nogit/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
#------# custom
|
||||
21
license.md
Normal file
21
license.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Lossless GmbH (https://lossless.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
26
npmextra.json
Normal file
26
npmextra.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "apiclient.xyz",
|
||||
"gitrepo": "gitea",
|
||||
"description": "A TypeScript client for the Gitea API, providing easy access to repositories, organizations, secrets, and action runs.",
|
||||
"npmPackagename": "@apiclient.xyz/gitea",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gitea",
|
||||
"api client",
|
||||
"TypeScript",
|
||||
"git hosting",
|
||||
"CI/CD",
|
||||
"actions",
|
||||
"secrets management"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
55
package.json
Normal file
55
package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@apiclient.xyz/gitea",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"description": "A TypeScript client for the Gitea API, providing easy access to repositories, organizations, secrets, and action runs.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --timeout 600)",
|
||||
"build": "(tsbuild --web --allowimplicitany)"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/apiclient.xyz/gitea.git"
|
||||
},
|
||||
"keywords": [
|
||||
"gitea",
|
||||
"api client",
|
||||
"TypeScript",
|
||||
"git hosting",
|
||||
"CI/CD",
|
||||
"actions",
|
||||
"secrets management"
|
||||
],
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartrequest": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^3.1.0",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^2.8.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/tapbundle": "^5.6.0",
|
||||
"@types/node": "^22.15.3"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
}
|
||||
9914
pnpm-lock.yaml
generated
Normal file
9914
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
readme.md
Normal file
30
readme.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# @apiclient.xyz/gitea
|
||||
|
||||
A TypeScript client for the Gitea API, providing easy access to repositories, organizations, secrets, and action runs.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @apiclient.xyz/gitea
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { GiteaClient } from '@apiclient.xyz/gitea';
|
||||
|
||||
const client = new GiteaClient('https://gitea.example.com', 'your-api-token');
|
||||
|
||||
// Test connection
|
||||
const result = await client.testConnection();
|
||||
|
||||
// List repositories
|
||||
const repos = await client.getRepos();
|
||||
|
||||
// Manage secrets
|
||||
await client.setRepoSecret('owner/repo', 'SECRET_KEY', 'secret-value');
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
34
test/test.node.ts
Normal file
34
test/test.node.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import { GiteaClient } from '../ts/index.js';
|
||||
|
||||
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||
|
||||
let giteaClient: GiteaClient;
|
||||
|
||||
tap.test('should create a GiteaClient instance', async () => {
|
||||
const baseUrl = testQenv.getEnvVarOnDemand('GITEA_BASE_URL') || 'https://gitea.lossless.digital';
|
||||
const token = testQenv.getEnvVarOnDemand('GITEA_TOKEN') || '';
|
||||
giteaClient = new GiteaClient(baseUrl, token);
|
||||
expect(giteaClient).toBeInstanceOf(GiteaClient);
|
||||
});
|
||||
|
||||
tap.test('should test connection', async () => {
|
||||
const result = await giteaClient.testConnection();
|
||||
expect(result).toHaveProperty('ok');
|
||||
console.log('Connection test:', result);
|
||||
});
|
||||
|
||||
tap.test('should get repos', async () => {
|
||||
const repos = await giteaClient.getRepos({ perPage: 5 });
|
||||
expect(repos).toBeArray();
|
||||
console.log(`Found ${repos.length} repos`);
|
||||
});
|
||||
|
||||
tap.test('should get orgs', async () => {
|
||||
const orgs = await giteaClient.getOrgs({ perPage: 5 });
|
||||
expect(orgs).toBeArray();
|
||||
console.log(`Found ${orgs.length} orgs`);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiclient.xyz/gitea',
|
||||
version: '1.0.0',
|
||||
description: 'A TypeScript client for the Gitea API, providing easy access to repositories, organizations, secrets, and action runs.',
|
||||
};
|
||||
211
ts/gitea.classes.giteaclient.ts
Normal file
211
ts/gitea.classes.giteaclient.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import * as plugins from './gitea.plugins.js';
|
||||
import { logger } from './gitea.logging.js';
|
||||
import type {
|
||||
IGiteaUser,
|
||||
IGiteaRepository,
|
||||
IGiteaOrganization,
|
||||
IGiteaSecret,
|
||||
IGiteaActionRun,
|
||||
IGiteaActionRunJob,
|
||||
ITestConnectionResult,
|
||||
IListOptions,
|
||||
} from './gitea.interfaces.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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async request<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
data?: any,
|
||||
customHeaders?: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
|
||||
let builder = plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.header('Authorization', `token ${this.token}`)
|
||||
.header('Content-Type', 'application/json');
|
||||
|
||||
if (customHeaders) {
|
||||
for (const [k, v] of Object.entries(customHeaders)) {
|
||||
builder = builder.header(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
builder = builder.json(data);
|
||||
}
|
||||
|
||||
let response: Awaited<ReturnType<typeof builder.get>>;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await builder.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await builder.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await builder.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await builder.delete();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json() as T;
|
||||
} catch {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
private async requestText(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
|
||||
let builder = plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.header('Authorization', `token ${this.token}`)
|
||||
.header('Accept', 'text/plain');
|
||||
|
||||
let response: Awaited<ReturnType<typeof builder.get>>;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await builder.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await builder.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await builder.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await builder.delete();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`${method} ${path}: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async testConnection(): Promise<ITestConnectionResult> {
|
||||
try {
|
||||
await this.request<IGiteaUser>('GET', '/api/v1/user');
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repositories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async getRepos(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`;
|
||||
if (opts?.search) {
|
||||
url += `&q=${encodeURIComponent(opts.search)}`;
|
||||
}
|
||||
const body = await this.request<any>('GET', url);
|
||||
return body.data || body;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Organizations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async getOrgs(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}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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[]> {
|
||||
return this.request<IGiteaSecret[]>('GET', `/api/v1/orgs/${orgName}/actions/secrets`);
|
||||
}
|
||||
|
||||
public async setOrgSecret(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> {
|
||||
await this.request('DELETE', `/api/v1/orgs/${orgName}/actions/secrets/${key}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action Runs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public async getActionRuns(ownerRepo: string, opts?: IListOptions): Promise<IGiteaActionRun[]> {
|
||||
const page = opts?.page || 1;
|
||||
const limit = opts?.perPage || 30;
|
||||
const body = await this.request<any>('GET', `/api/v1/repos/${ownerRepo}/actions/runs?page=${page}&limit=${limit}`);
|
||||
return body.workflow_runs || body;
|
||||
}
|
||||
|
||||
public async getActionRunJobs(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;
|
||||
}
|
||||
|
||||
public async getJobLog(ownerRepo: string, jobId: number): Promise<string> {
|
||||
return this.requestText('GET', `/api/v1/repos/${ownerRepo}/actions/jobs/${jobId}/logs`);
|
||||
}
|
||||
|
||||
public async rerunAction(ownerRepo: string, runId: number): Promise<void> {
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/rerun`);
|
||||
}
|
||||
|
||||
public async cancelAction(ownerRepo: string, runId: number): Promise<void> {
|
||||
await this.request('POST', `/api/v1/repos/${ownerRepo}/actions/runs/${runId}/cancel`);
|
||||
}
|
||||
}
|
||||
70
ts/gitea.interfaces.ts
Normal file
70
ts/gitea.interfaces.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export interface IGiteaUser {
|
||||
id: number;
|
||||
login: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export interface IGiteaRepository {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
default_branch: string;
|
||||
html_url: string;
|
||||
private: boolean;
|
||||
topics: string[];
|
||||
updated_at: string;
|
||||
owner: {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGiteaOrganization {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
visibility: string;
|
||||
repo_count: number;
|
||||
}
|
||||
|
||||
export interface IGiteaSecret {
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface IGiteaActionRun {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
head_branch: string;
|
||||
head_sha: string;
|
||||
html_url: string;
|
||||
event: string;
|
||||
run_duration: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface IGiteaActionRunJob {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
run_duration: number;
|
||||
}
|
||||
|
||||
export interface ITestConnectionResult {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IListOptions {
|
||||
search?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
3
ts/gitea.logging.ts
Normal file
3
ts/gitea.logging.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as plugins from './gitea.plugins.js';
|
||||
|
||||
export const logger = new plugins.smartlog.ConsoleLog();
|
||||
4
ts/gitea.plugins.ts
Normal file
4
ts/gitea.plugins.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
|
||||
export { smartlog, smartrequest };
|
||||
12
ts/index.ts
Normal file
12
ts/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { GiteaClient } from './gitea.classes.giteaclient.js';
|
||||
export type {
|
||||
IGiteaUser,
|
||||
IGiteaRepository,
|
||||
IGiteaOrganization,
|
||||
IGiteaSecret,
|
||||
IGiteaActionRun,
|
||||
IGiteaActionRunJob,
|
||||
ITestConnectionResult,
|
||||
IListOptions,
|
||||
} from './gitea.interfaces.js';
|
||||
export { commitinfo } from './00_commitinfo_data.js';
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user