@remogram/provider-gitea-api 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +293 -0
  2. package/package.json +27 -0
package/index.js ADDED
@@ -0,0 +1,293 @@
1
+ import {
2
+ fetchJson,
3
+ sanitizeField,
4
+ sanitizeUrl,
5
+ assertGitRef,
6
+ assertGitRemote,
7
+ gitRevParse,
8
+ gitCurrentBranch,
9
+ gitAheadBehind,
10
+ ERROR_CODES,
11
+ forgeError,
12
+ } from '@remogram/core';
13
+ const PUBLIC_GITEA_HOST = 'gitea.com';
14
+ const PUBLIC_GITEA_API = 'https://gitea.com/api/v1';
15
+ const AUTH_CAPABILITIES = [
16
+ 'repo_status',
17
+ 'ref_compare',
18
+ 'pr_status',
19
+ 'pr_checks',
20
+ 'merge_plan',
21
+ 'sync_plan',
22
+ ];
23
+
24
+ const STRUCTURED_COMMANDS = AUTH_CAPABILITIES.map((name) => ({ name, implemented: true }));
25
+
26
+ export function giteaToken() {
27
+ return process.env.GITEA_TOKEN || null;
28
+ }
29
+
30
+ export function requireToken() {
31
+ const token = giteaToken();
32
+ if (!token) {
33
+ throw Object.assign(new Error('GITEA_TOKEN not set'), {
34
+ forgeError: forgeError(ERROR_CODES.UNAUTHENTICATED_PROVIDER, 'GITEA_TOKEN not set'),
35
+ });
36
+ }
37
+ return token;
38
+ }
39
+
40
+ function configuredHost(config) {
41
+ if (!config.baseUrl) return null;
42
+ try {
43
+ return new URL(config.baseUrl).host;
44
+ } catch {
45
+ throw Object.assign(new Error('Invalid baseUrl for gitea-api'), {
46
+ forgeError: forgeError(ERROR_CODES.CONFIG_INVALID, 'Invalid baseUrl for gitea-api provider'),
47
+ });
48
+ }
49
+ }
50
+
51
+ function configOrigin(config) {
52
+ if (!config.baseUrl) return null;
53
+ try {
54
+ return new URL(config.baseUrl).origin;
55
+ } catch {
56
+ throw Object.assign(new Error('Invalid baseUrl for gitea-api'), {
57
+ forgeError: forgeError(ERROR_CODES.CONFIG_INVALID, 'Invalid baseUrl for gitea-api provider'),
58
+ });
59
+ }
60
+ }
61
+
62
+ export function apiBase(config, parsed = {}) {
63
+ const remoteHost = parsed.host || configuredHost(config);
64
+ if (!remoteHost) {
65
+ throw Object.assign(new Error('remote host required for gitea-api'), {
66
+ forgeError: forgeError(ERROR_CODES.CONFIG_INVALID, 'remote host required for gitea-api provider'),
67
+ });
68
+ }
69
+
70
+ const host = remoteHost.toLowerCase();
71
+ const configured = configuredHost(config);
72
+ if (host === PUBLIC_GITEA_HOST) {
73
+ if (configured && configured.toLowerCase() !== PUBLIC_GITEA_HOST) {
74
+ const message = 'gitea.com remotes may use only https://gitea.com for API requests';
75
+ throw Object.assign(new Error(message), {
76
+ forgeError: forgeError(ERROR_CODES.UNTRUSTED_BASE_URL, message),
77
+ });
78
+ }
79
+ return PUBLIC_GITEA_API;
80
+ }
81
+
82
+ if (!configured) {
83
+ throw Object.assign(new Error('baseUrl required for gitea-api'), {
84
+ forgeError: forgeError(ERROR_CODES.CONFIG_INVALID, 'baseUrl required for gitea-api provider'),
85
+ });
86
+ }
87
+
88
+ if (configured.toLowerCase() !== host) {
89
+ const message = `Gitea API host must match remote host ${remoteHost}`;
90
+ throw Object.assign(new Error(message), {
91
+ forgeError: forgeError(ERROR_CODES.UNTRUSTED_BASE_URL, message),
92
+ });
93
+ }
94
+
95
+ const origin = configOrigin(config) || `https://${remoteHost}`;
96
+ return `${origin.replace(/\/$/, '')}/api/v1`;
97
+ }
98
+
99
+ export function authHeaders(token) {
100
+ return { Authorization: `token ${token}`, Accept: 'application/json' };
101
+ }
102
+
103
+ export function repoApiPath(config, ...segments) {
104
+ const owner = encodeURIComponent(config.owner);
105
+ const repo = encodeURIComponent(config.repo);
106
+ const base = `/repos/${owner}/${repo}`;
107
+ if (!segments.length) return base;
108
+ return `${base}/${segments.map((s) => encodeURIComponent(String(s))).join('/')}`;
109
+ }
110
+
111
+ export async function giteaFetch(config, parsed, path, options = {}) {
112
+ const token = requireToken();
113
+ const url = `${apiBase(config, parsed)}${path}`;
114
+ return fetchJson(url, {
115
+ ...options,
116
+ headers: { ...authHeaders(token), ...(options.headers || {}) },
117
+ });
118
+ }
119
+
120
+ export async function repoStatus(ctx) {
121
+ const token = giteaToken();
122
+ let defaultBranch = null;
123
+ if (token) {
124
+ const repo = await giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config));
125
+ defaultBranch = sanitizeField(repo.default_branch);
126
+ }
127
+ return {
128
+ auth_present: Boolean(token),
129
+ auth_env: token ? 'GITEA_TOKEN' : null,
130
+ capabilities: token ? AUTH_CAPABILITIES : ['repo_status'],
131
+ default_branch: defaultBranch,
132
+ };
133
+ }
134
+
135
+ export function providerCapabilities() {
136
+ return {
137
+ commands: STRUCTURED_COMMANDS,
138
+ auth_envs: ['GITEA_TOKEN'],
139
+ check_sources: ['commit_statuses'],
140
+ mergeability_confidence: 'direct',
141
+ host_binding: 'verified_remote_host',
142
+ pagination: 'first_page_only',
143
+ write_support: false,
144
+ };
145
+ }
146
+
147
+ export async function refsCompare(ctx, baseRef, headRef) {
148
+ requireToken();
149
+ assertGitRef(baseRef, 'base');
150
+ assertGitRef(headRef, 'head');
151
+ const baseSha = gitRevParse(ctx.cwd, baseRef);
152
+ const headSha = gitRevParse(ctx.cwd, headRef);
153
+ if (!baseSha || !headSha) {
154
+ throw Object.assign(new Error('Missing ref'), {
155
+ forgeError: forgeError(ERROR_CODES.MISSING_REF, 'Could not resolve base or head ref'),
156
+ });
157
+ }
158
+ const counts = gitAheadBehind(ctx.cwd, baseSha, headSha);
159
+ return {
160
+ base_ref: sanitizeField(baseRef),
161
+ base_sha: baseSha,
162
+ head_ref: sanitizeField(headRef),
163
+ head_sha: headSha,
164
+ ...counts,
165
+ };
166
+ }
167
+
168
+ export async function getPull(ctx, { number }) {
169
+ if (number == null) {
170
+ throw Object.assign(new Error('--number required'), {
171
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for PR lookup'),
172
+ });
173
+ }
174
+ return giteaFetch(ctx.config, ctx.parsed, repoApiPath(ctx.config, 'pulls', number));
175
+ }
176
+
177
+ function mergeability(pr) {
178
+ if (pr.mergeable === true) return 'clean';
179
+ if (pr.mergeable === false) return 'conflicted';
180
+ return 'unknown';
181
+ }
182
+
183
+ export async function prView(ctx, opts) {
184
+ const pr = await getPull(ctx, opts);
185
+ return {
186
+ pr_number: pr.number,
187
+ url: sanitizeUrl(pr.html_url ?? pr.url),
188
+ title: sanitizeField(pr.title),
189
+ state: sanitizeField(pr.state),
190
+ base_ref: sanitizeField(pr.base?.ref),
191
+ base_sha: sanitizeField(pr.base?.sha),
192
+ head_ref: sanitizeField(pr.head?.ref),
193
+ head_sha: sanitizeField(pr.head?.sha),
194
+ mergeability: mergeability(pr),
195
+ };
196
+ }
197
+
198
+ export async function prChecks(ctx, opts) {
199
+ requireToken();
200
+ let sha;
201
+ if (opts.ref) {
202
+ assertGitRef(opts.ref, 'ref');
203
+ sha = gitRevParse(ctx.cwd, opts.ref);
204
+ if (!sha) {
205
+ throw Object.assign(new Error('Missing ref'), {
206
+ forgeError: forgeError(ERROR_CODES.MISSING_REF, `Could not resolve ref ${opts.ref}`),
207
+ });
208
+ }
209
+ } else {
210
+ const pr = await getPull(ctx, opts);
211
+ sha = pr.head?.sha;
212
+ }
213
+ if (!sha) {
214
+ throw Object.assign(new Error('No SHA'), {
215
+ forgeError: forgeError(ERROR_CODES.MISSING_REF, 'Could not determine head SHA for checks'),
216
+ });
217
+ }
218
+ const statuses = await giteaFetch(
219
+ ctx.config,
220
+ ctx.parsed,
221
+ repoApiPath(ctx.config, 'commits', sha, 'statuses'),
222
+ );
223
+ const mapped = (statuses || []).map((s) => ({
224
+ context: sanitizeField(s.context),
225
+ state: sanitizeField(s.state),
226
+ description: sanitizeField(s.description),
227
+ }));
228
+ const conclusion = summarizeChecks(mapped);
229
+ return { head_sha: sha, check_conclusion: conclusion, statuses: mapped };
230
+ }
231
+
232
+ function summarizeChecks(statuses) {
233
+ if (!statuses.length) return 'missing';
234
+ if (statuses.some((s) => s.state === 'failure' || s.state === 'error')) return 'failure';
235
+ if (statuses.some((s) => s.state === 'pending')) return 'pending';
236
+ if (statuses.every((s) => s.state === 'success')) return 'success';
237
+ return 'unknown';
238
+ }
239
+
240
+ export async function mergePlan(ctx, opts) {
241
+ const view = await prView(ctx, opts);
242
+ const checks = await prChecks(ctx, { number: view.pr_number });
243
+ const blockers = [];
244
+ if (view.mergeability === 'conflicted') blockers.push('merge_conflict');
245
+ if (view.state !== 'open') blockers.push('pr_not_open');
246
+ if (checks.check_conclusion === 'failure') blockers.push('checks_failed');
247
+ if (checks.check_conclusion === 'missing') blockers.push('checks_missing');
248
+ if (checks.check_conclusion === 'pending') blockers.push('checks_pending');
249
+ return {
250
+ pr_number: view.pr_number,
251
+ mergeability: view.mergeability,
252
+ checks_conclusion: checks.check_conclusion,
253
+ blockers,
254
+ };
255
+ }
256
+
257
+ export async function syncPlan(ctx, remoteName = 'origin') {
258
+ assertGitRemote(remoteName, 'remote');
259
+ const localSha = gitRevParse(ctx.cwd, 'HEAD');
260
+ const branch = gitCurrentBranch(ctx.cwd);
261
+ let remoteSha = null;
262
+ if (branch && branch !== 'HEAD') {
263
+ remoteSha = gitRevParse(ctx.cwd, `${remoteName}/${branch}`);
264
+ }
265
+ const blockers = [];
266
+ let diverged = false;
267
+ if (localSha && remoteSha && localSha !== remoteSha) {
268
+ const { ahead_by, behind_by } = gitAheadBehind(ctx.cwd, remoteSha, localSha);
269
+ if (ahead_by > 0 && behind_by > 0) {
270
+ diverged = true;
271
+ blockers.push('divergent_remotes');
272
+ }
273
+ }
274
+ if (!remoteSha) blockers.push('missing_remote_ref');
275
+ return {
276
+ remote: sanitizeField(remoteName),
277
+ local_sha: localSha,
278
+ remote_sha: remoteSha,
279
+ diverged,
280
+ blockers,
281
+ };
282
+ }
283
+
284
+ export const provider = {
285
+ id: 'gitea-api',
286
+ providerCapabilities,
287
+ repoStatus,
288
+ refsCompare,
289
+ prView,
290
+ prChecks,
291
+ mergePlan,
292
+ syncPlan,
293
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@remogram/provider-gitea-api",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Gitea REST API forge provider for remogram",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/attebury/remogram.git",
10
+ "directory": "packages/provider-gitea-api"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "*.js"
17
+ ],
18
+ "exports": {
19
+ ".": "./index.js"
20
+ },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "dependencies": {
25
+ "@remogram/core": "0.1.0-beta.0"
26
+ }
27
+ }