@foundation0/git 1.2.3 → 1.2.4
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.
- package/README.md +9 -3
- package/mcp/README.md +5 -0
- package/package.json +1 -1
- package/src/actions-api.ts +616 -0
- package/src/git-service-api.ts +116 -35
- package/src/index.ts +1 -0
- package/src/platform/gitea-rules.ts +3 -1
package/README.md
CHANGED
|
@@ -80,12 +80,18 @@ type GitServiceApiExecutionResult = {
|
|
|
80
80
|
|
|
81
81
|
## Core request shapes
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Write methods accept request fields directly (recommended), or you can set the raw request body with `data` / `json` / `payload` / `requestBody`.
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
await api.repo.issue.create({
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
title: 'Bug report',
|
|
88
|
+
body: 'Describe issue details',
|
|
89
|
+
labels: ['bug'],
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// equivalent explicit-body form
|
|
93
|
+
await api.repo.issue.create({
|
|
94
|
+
data: { title: 'Bug report', body: 'Describe issue details', labels: ['bug'] },
|
|
89
95
|
})
|
|
90
96
|
```
|
|
91
97
|
|
package/mcp/README.md
CHANGED
|
@@ -209,3 +209,8 @@ Tips:
|
|
|
209
209
|
- `normalizeToolCallNameForServer(prefix, toolName)`
|
|
210
210
|
- Includes label-management tools exposed from the API object:
|
|
211
211
|
`repo.label.listManaged`, `repo.label.getByName`, `repo.label.upsert`, and `repo.label.deleteByName`
|
|
212
|
+
- Includes Gitea Actions convenience tools exposed from the API object (best-effort helpers):
|
|
213
|
+
`repo.actions.tasks.list`, `repo.actions.jobs.logs`, `repo.actions.jobs.logsTail`, `repo.actions.jobs.logsForRunTail`,
|
|
214
|
+
and `repo.actions.artifacts.downloadZipUrl`
|
|
215
|
+
- Includes a discovery helper:
|
|
216
|
+
`help.actionsLogs` (also available under `repo.help.actionsLogs`)
|
package/package.json
CHANGED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import type { GitServiceApi, GitServiceApiExecutionResult, GitServiceApiMethod } from './git-service-api'
|
|
2
|
+
import type { GitApiFeatureMapping } from './platform/gitea-adapter'
|
|
3
|
+
import type { GitPlatformConfig } from './platform/config'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LOG_TAIL_LINES = 200
|
|
6
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 60_000
|
|
7
|
+
|
|
8
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
9
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
|
+
|
|
11
|
+
const toTrimmedString = (value: unknown): string | null => {
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
const trimmed = value.trim()
|
|
16
|
+
return trimmed.length > 0 ? trimmed : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const toPositiveInteger = (value: unknown): number | null => {
|
|
20
|
+
const candidate = Number(value)
|
|
21
|
+
if (!Number.isFinite(candidate) || candidate <= 0 || Math.floor(candidate) !== candidate) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
return candidate
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const buildApiBase = (host: string): string => {
|
|
28
|
+
const sanitized = host.replace(/\/$/, '')
|
|
29
|
+
if (sanitized.endsWith('/api/v1')) {
|
|
30
|
+
return sanitized
|
|
31
|
+
}
|
|
32
|
+
return `${sanitized}/api/v1`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toQueryArray = (query: Record<string, string | number | boolean>): string[] =>
|
|
36
|
+
Object.entries(query).map(([key, value]) => `${key}=${String(value)}`)
|
|
37
|
+
|
|
38
|
+
const toHeaderRecord = (headers: string[]): Record<string, string> =>
|
|
39
|
+
Object.fromEntries(
|
|
40
|
+
headers
|
|
41
|
+
.map((entry) => {
|
|
42
|
+
const separatorIndex = entry.indexOf(':')
|
|
43
|
+
if (separatorIndex < 0) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
const name = entry.slice(0, separatorIndex).trim()
|
|
47
|
+
const value = entry.slice(separatorIndex + 1).trim()
|
|
48
|
+
return [name, value]
|
|
49
|
+
})
|
|
50
|
+
.filter((entry): entry is [string, string] => Boolean(entry)),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
type RawCall = {
|
|
54
|
+
args: string[]
|
|
55
|
+
options: Record<string, unknown>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const splitArgsAndOptions = (rawArgs: unknown[]): RawCall => {
|
|
59
|
+
if (rawArgs.length === 0 || !isRecord(rawArgs[rawArgs.length - 1])) {
|
|
60
|
+
return {
|
|
61
|
+
args: rawArgs.map((value) => String(value)),
|
|
62
|
+
options: {},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const last = rawArgs[rawArgs.length - 1] as Record<string, unknown>
|
|
67
|
+
return {
|
|
68
|
+
args: rawArgs.slice(0, -1).map((value) => String(value)),
|
|
69
|
+
options: { ...last },
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type Scope = {
|
|
74
|
+
owner?: string
|
|
75
|
+
repo?: string
|
|
76
|
+
options: Record<string, unknown>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GitActionsApiDefaults {
|
|
80
|
+
defaultOwner?: string
|
|
81
|
+
defaultRepo?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface GitActionsApiContext {
|
|
85
|
+
config: Pick<GitPlatformConfig, 'giteaHost' | 'giteaToken' | 'platform'>
|
|
86
|
+
defaults?: GitActionsApiDefaults
|
|
87
|
+
requestTimeoutMs?: number
|
|
88
|
+
log?: (message: string) => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalizeScopeArgs = (ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown): Scope => {
|
|
92
|
+
const options: Record<string, unknown> = {}
|
|
93
|
+
let owner: string | undefined
|
|
94
|
+
let repo: string | undefined
|
|
95
|
+
|
|
96
|
+
const absorb = (value: unknown): string | undefined => {
|
|
97
|
+
if (value === undefined || value === null) {
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isRecord(value)) {
|
|
102
|
+
Object.assign(options, value)
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const asText = String(value).trim()
|
|
107
|
+
return asText.length > 0 ? asText : undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
owner = absorb(ownerOrOptions)
|
|
111
|
+
repo = absorb(repoOrOptions)
|
|
112
|
+
|
|
113
|
+
if (isRecord(maybeOptions)) {
|
|
114
|
+
Object.assign(options, maybeOptions)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...(owner ? { owner } : {}),
|
|
119
|
+
...(repo ? { repo } : {}),
|
|
120
|
+
options,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resolveOwnerRepo = (
|
|
125
|
+
scope: Pick<Scope, 'owner' | 'repo'>,
|
|
126
|
+
defaults: GitActionsApiDefaults,
|
|
127
|
+
): { owner: string; repo: string } => {
|
|
128
|
+
const owner = scope.owner ?? defaults.defaultOwner
|
|
129
|
+
const repo = scope.repo ?? defaults.defaultRepo
|
|
130
|
+
|
|
131
|
+
if (!owner || !repo) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'Owner/repo is required. Pass owner and repo args or set defaultOwner/defaultRepo when creating the API.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { owner, repo }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const resolveOwnerRepoFromArgs = (
|
|
141
|
+
args: string[],
|
|
142
|
+
scope: Pick<Scope, 'owner' | 'repo'>,
|
|
143
|
+
defaults: GitActionsApiDefaults,
|
|
144
|
+
): { owner: string; repo: string; rest: string[] } => {
|
|
145
|
+
if (scope.owner || scope.repo) {
|
|
146
|
+
const resolved = resolveOwnerRepo(scope, defaults)
|
|
147
|
+
return { ...resolved, rest: args }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (args.length >= 2) {
|
|
151
|
+
return { owner: args[0], repo: args[1], rest: args.slice(2) }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (args.length === 1) {
|
|
155
|
+
const split = args[0].split('/')
|
|
156
|
+
if (split.length === 2 && split[0] && split[1]) {
|
|
157
|
+
return { owner: split[0], repo: split[1], rest: [] }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolved = resolveOwnerRepo({}, defaults)
|
|
162
|
+
return { ...resolved, rest: args }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const resolveOwnerRepoAndId = (
|
|
166
|
+
rawArgs: unknown[],
|
|
167
|
+
defaults: GitActionsApiDefaults,
|
|
168
|
+
): { owner: string; repo: string; id: string; options: Record<string, unknown> } => {
|
|
169
|
+
const parsed = splitArgsAndOptions(rawArgs)
|
|
170
|
+
const args = parsed.args.map((value) => value.trim()).filter((value) => value.length > 0)
|
|
171
|
+
|
|
172
|
+
if (args.length === 0) {
|
|
173
|
+
throw new Error('ID is required.')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (args.length === 1) {
|
|
177
|
+
const { owner, repo } = resolveOwnerRepo({}, defaults)
|
|
178
|
+
return { owner, repo, id: args[0], options: parsed.options }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (args.length === 2) {
|
|
182
|
+
if (args[0].includes('/')) {
|
|
183
|
+
const split = args[0].split('/')
|
|
184
|
+
if (split.length === 2 && split[0] && split[1]) {
|
|
185
|
+
return { owner: split[0], repo: split[1], id: args[1], options: parsed.options }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
'Ambiguous arguments. Pass <id> (and rely on defaults), or pass <owner> <repo> <id>, or pass <owner/repo> <id>.',
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { owner: args[0], repo: args[1], id: args[2], options: parsed.options }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const resolveTimeoutMs = (value: unknown, fallback: number): number => {
|
|
198
|
+
const parsed = toPositiveInteger(value)
|
|
199
|
+
return parsed ?? fallback
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const readTextBody = async (response: Response): Promise<string> => {
|
|
203
|
+
try {
|
|
204
|
+
return await response.text()
|
|
205
|
+
} catch {
|
|
206
|
+
return ''
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const readBody = async (response: Response): Promise<unknown> => {
|
|
211
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
212
|
+
const text = await readTextBody(response)
|
|
213
|
+
if (contentType.toLowerCase().includes('application/json')) {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(text)
|
|
216
|
+
} catch {
|
|
217
|
+
return text
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return text
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const requestGitea = async (
|
|
224
|
+
ctx: GitActionsApiContext,
|
|
225
|
+
featurePath: string[],
|
|
226
|
+
owner: string,
|
|
227
|
+
repo: string,
|
|
228
|
+
method: string,
|
|
229
|
+
pathTail: string[],
|
|
230
|
+
options: Record<string, unknown> = {},
|
|
231
|
+
requestInitOverrides: Partial<RequestInit> = {},
|
|
232
|
+
): Promise<GitServiceApiExecutionResult<unknown>> => {
|
|
233
|
+
if (ctx.config.platform !== 'GITEA') {
|
|
234
|
+
throw new Error(`Actions API only supports GITEA platform, got ${ctx.config.platform}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const apiBase = buildApiBase(ctx.config.giteaHost)
|
|
238
|
+
const mappedPath = ['repos', owner, repo, 'actions', ...pathTail]
|
|
239
|
+
|
|
240
|
+
const query = isRecord(options.query) ? (options.query as Record<string, unknown>) : {}
|
|
241
|
+
const queryRecord: Record<string, string | number | boolean> = {}
|
|
242
|
+
for (const [key, value] of Object.entries(query)) {
|
|
243
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
244
|
+
queryRecord[key] = value
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const url = new URL(`${apiBase}/${mappedPath.join('/')}`)
|
|
249
|
+
for (const [key, value] of Object.entries(queryRecord)) {
|
|
250
|
+
url.searchParams.set(key, String(value))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const headersList: string[] = []
|
|
254
|
+
headersList.push('Accept: */*')
|
|
255
|
+
if (ctx.config.giteaToken) {
|
|
256
|
+
headersList.push(`Authorization: token ${ctx.config.giteaToken}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const headers = toHeaderRecord(headersList)
|
|
260
|
+
|
|
261
|
+
const requestTimeoutMs = resolveTimeoutMs(options.requestTimeoutMs ?? ctx.requestTimeoutMs, DEFAULT_HTTP_TIMEOUT_MS)
|
|
262
|
+
const timeoutSignal =
|
|
263
|
+
requestTimeoutMs > 0 && typeof (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout === 'function'
|
|
264
|
+
? (AbortSignal as unknown as { timeout: (ms: number) => AbortSignal }).timeout(requestTimeoutMs)
|
|
265
|
+
: null
|
|
266
|
+
const controller = !timeoutSignal && requestTimeoutMs > 0 ? new AbortController() : null
|
|
267
|
+
const timeoutId = controller ? setTimeout(() => controller.abort(), requestTimeoutMs) : null
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
ctx.log?.(`actions:http:request ${method.toUpperCase()} ${url.toString()}`)
|
|
271
|
+
const response = await fetch(url.toString(), {
|
|
272
|
+
method: method.toUpperCase(),
|
|
273
|
+
headers,
|
|
274
|
+
...(timeoutSignal ? { signal: timeoutSignal } : {}),
|
|
275
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
276
|
+
...requestInitOverrides,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const body = await readBody(response)
|
|
280
|
+
const responseHeaders: Record<string, string> = {}
|
|
281
|
+
try {
|
|
282
|
+
response.headers.forEach((value, key) => {
|
|
283
|
+
responseHeaders[key.toLowerCase()] = value
|
|
284
|
+
})
|
|
285
|
+
} catch {
|
|
286
|
+
// best effort
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const mapping: GitApiFeatureMapping = {
|
|
290
|
+
platform: 'GITEA',
|
|
291
|
+
featurePath,
|
|
292
|
+
mappedPath,
|
|
293
|
+
method: method.toUpperCase(),
|
|
294
|
+
query: toQueryArray(queryRecord),
|
|
295
|
+
headers: headersList,
|
|
296
|
+
apiBase,
|
|
297
|
+
swaggerPath: `/${mappedPath.join('/')}`,
|
|
298
|
+
mapped: true,
|
|
299
|
+
reason: 'Direct Gitea Actions API call',
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
mapping,
|
|
304
|
+
request: {
|
|
305
|
+
url: url.toString(),
|
|
306
|
+
method: method.toUpperCase(),
|
|
307
|
+
headers,
|
|
308
|
+
query: toQueryArray(queryRecord),
|
|
309
|
+
},
|
|
310
|
+
response: {
|
|
311
|
+
headers: responseHeaders,
|
|
312
|
+
},
|
|
313
|
+
status: response.status,
|
|
314
|
+
ok: response.ok,
|
|
315
|
+
body,
|
|
316
|
+
}
|
|
317
|
+
} finally {
|
|
318
|
+
if (timeoutId) {
|
|
319
|
+
clearTimeout(timeoutId)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const tailText = (input: string, options: { contains?: string; maxLines?: number; maxBytes?: number }): string => {
|
|
325
|
+
const contains = toTrimmedString(options.contains ?? null) ?? null
|
|
326
|
+
const maxLines = toPositiveInteger(options.maxLines) ?? DEFAULT_LOG_TAIL_LINES
|
|
327
|
+
const maxBytes = toPositiveInteger(options.maxBytes)
|
|
328
|
+
|
|
329
|
+
let text = input
|
|
330
|
+
|
|
331
|
+
if (contains) {
|
|
332
|
+
text = text
|
|
333
|
+
.split(/\r?\n/g)
|
|
334
|
+
.filter((line) => line.includes(contains))
|
|
335
|
+
.join('\n')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (maxLines > 0) {
|
|
339
|
+
const lines = text.split(/\r?\n/g)
|
|
340
|
+
text = lines.slice(Math.max(0, lines.length - maxLines)).join('\n')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (maxBytes && maxBytes > 0) {
|
|
344
|
+
const buf = Buffer.from(text, 'utf8')
|
|
345
|
+
if (buf.length > maxBytes) {
|
|
346
|
+
text = buf.subarray(buf.length - maxBytes).toString('utf8')
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return text
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export type GitActionsHelpBody = {
|
|
354
|
+
summary: string
|
|
355
|
+
suggestedCalls: Array<{
|
|
356
|
+
tool: string
|
|
357
|
+
args: string[]
|
|
358
|
+
options?: Record<string, unknown>
|
|
359
|
+
notes?: string
|
|
360
|
+
}>
|
|
361
|
+
assumptions: string[]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export const createGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContext): Record<string, GitServiceApiMethod> => {
|
|
365
|
+
const defaults = ctx.defaults ?? {}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
helpActionsLogs: async (): Promise<GitServiceApiExecutionResult<GitActionsHelpBody>> => {
|
|
369
|
+
const body: GitActionsHelpBody = {
|
|
370
|
+
summary:
|
|
371
|
+
'Use actions.tasks.list to locate the failing run, then actions.jobs.logsTail to pull the last N lines of job logs.',
|
|
372
|
+
suggestedCalls: [
|
|
373
|
+
{
|
|
374
|
+
tool: 'repo.actions.tasks.list',
|
|
375
|
+
args: ['<owner>', '<repo>'],
|
|
376
|
+
options: { query: { limit: 50 } },
|
|
377
|
+
notes: 'Filter client-side by head_sha and run_number from the response.',
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
tool: 'repo.actions.jobs.logsTail',
|
|
381
|
+
args: ['<job_id>'],
|
|
382
|
+
options: { maxLines: 250 },
|
|
383
|
+
notes: 'If you have default owner/repo configured, you can pass only <job_id>.',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
tool: 'repo.actions.jobs.logsForRunTail',
|
|
387
|
+
args: ['<head_sha>', '<run_number>'],
|
|
388
|
+
options: { maxLines: 250 },
|
|
389
|
+
notes: 'Convenience helper: resolves a run from tasks and then fetches logs (assumes run.id is job_id).',
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
assumptions: [
|
|
393
|
+
'Gitea exposes job logs at GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs.',
|
|
394
|
+
'This helper assumes ActionTask.id can be used as job_id for the logs endpoint. If that is false in your Gitea version, use the UI to find the job_id.',
|
|
395
|
+
],
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const mapping: GitApiFeatureMapping = {
|
|
399
|
+
platform: 'GITEA',
|
|
400
|
+
featurePath: ['help', 'actionsLogs'],
|
|
401
|
+
mappedPath: ['help', 'actionsLogs'],
|
|
402
|
+
method: 'GET',
|
|
403
|
+
query: [],
|
|
404
|
+
headers: ['Accept: application/json'],
|
|
405
|
+
apiBase: buildApiBase(ctx.config.giteaHost),
|
|
406
|
+
swaggerPath: '/help/actionsLogs',
|
|
407
|
+
mapped: true,
|
|
408
|
+
reason: 'Local help content',
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
mapping,
|
|
413
|
+
request: {
|
|
414
|
+
url: 'local://help/actionsLogs',
|
|
415
|
+
method: 'GET',
|
|
416
|
+
headers: {},
|
|
417
|
+
query: [],
|
|
418
|
+
},
|
|
419
|
+
response: {
|
|
420
|
+
headers: {},
|
|
421
|
+
},
|
|
422
|
+
status: 200,
|
|
423
|
+
ok: true,
|
|
424
|
+
body,
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
tasks: async (ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown) => {
|
|
429
|
+
const parsed = normalizeScopeArgs(ownerOrOptions, repoOrOptions, maybeOptions)
|
|
430
|
+
const { owner, repo } = resolveOwnerRepo(parsed, defaults)
|
|
431
|
+
return requestGitea(ctx, ['actions', 'tasks', 'list'], owner, repo, 'GET', ['tasks'], parsed.options)
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
jobsLogs: async (...rawArgs: unknown[]) => {
|
|
435
|
+
const resolved = resolveOwnerRepoAndId(rawArgs, defaults)
|
|
436
|
+
return requestGitea(
|
|
437
|
+
ctx,
|
|
438
|
+
['actions', 'jobs', 'logs'],
|
|
439
|
+
resolved.owner,
|
|
440
|
+
resolved.repo,
|
|
441
|
+
'GET',
|
|
442
|
+
['jobs', resolved.id, 'logs'],
|
|
443
|
+
resolved.options,
|
|
444
|
+
)
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
jobsLogsTail: async (...rawArgs: unknown[]) => {
|
|
448
|
+
const resolved = resolveOwnerRepoAndId(rawArgs, defaults)
|
|
449
|
+
const result = await requestGitea(
|
|
450
|
+
ctx,
|
|
451
|
+
['actions', 'jobs', 'logs'],
|
|
452
|
+
resolved.owner,
|
|
453
|
+
resolved.repo,
|
|
454
|
+
'GET',
|
|
455
|
+
['jobs', resolved.id, 'logs'],
|
|
456
|
+
resolved.options,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
const asText = typeof result.body === 'string' ? result.body : JSON.stringify(result.body, null, 2)
|
|
460
|
+
const tailed = tailText(asText, {
|
|
461
|
+
contains: resolved.options.contains,
|
|
462
|
+
maxLines: resolved.options.maxLines,
|
|
463
|
+
maxBytes: resolved.options.maxBytes,
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
...result,
|
|
468
|
+
body: tailed,
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
jobsLogsForRunTail: async (headSha: unknown, runNumber: unknown, maybeOptions?: unknown) => {
|
|
473
|
+
const sha = toTrimmedString(headSha)
|
|
474
|
+
const run = toPositiveInteger(runNumber)
|
|
475
|
+
const options = isRecord(maybeOptions) ? (maybeOptions as Record<string, unknown>) : {}
|
|
476
|
+
|
|
477
|
+
if (!sha || !run) {
|
|
478
|
+
throw new Error('headSha (string) and runNumber (positive integer) are required.')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const scope = normalizeScopeArgs(options.owner, options.repo, options)
|
|
482
|
+
const { owner, repo } = resolveOwnerRepo(scope, defaults)
|
|
483
|
+
const tasks = await requestGitea(ctx, ['actions', 'tasks', 'list'], owner, repo, 'GET', ['tasks'], scope.options)
|
|
484
|
+
|
|
485
|
+
const entries = isRecord(tasks.body) && Array.isArray(tasks.body.workflow_runs)
|
|
486
|
+
? (tasks.body.workflow_runs as unknown[])
|
|
487
|
+
: []
|
|
488
|
+
|
|
489
|
+
const match = entries.find((entry) => {
|
|
490
|
+
if (!isRecord(entry)) return false
|
|
491
|
+
return String(entry.head_sha ?? '') === sha && Number(entry.run_number ?? NaN) === run
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
if (!match || !isRecord(match)) {
|
|
495
|
+
throw new Error(`No matching run found in actions/tasks for head_sha=${sha} run_number=${run}.`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const jobId = match.id
|
|
499
|
+
const jobIdText = toTrimmedString(jobId)
|
|
500
|
+
if (!jobIdText) {
|
|
501
|
+
throw new Error('Matched run entry does not expose an id usable as job_id.')
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const logs = await requestGitea(
|
|
505
|
+
ctx,
|
|
506
|
+
['actions', 'jobs', 'logs'],
|
|
507
|
+
owner,
|
|
508
|
+
repo,
|
|
509
|
+
'GET',
|
|
510
|
+
['jobs', jobIdText, 'logs'],
|
|
511
|
+
scope.options,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
const asText = typeof logs.body === 'string' ? logs.body : JSON.stringify(logs.body, null, 2)
|
|
515
|
+
const tailed = tailText(asText, {
|
|
516
|
+
contains: scope.options.contains,
|
|
517
|
+
maxLines: scope.options.maxLines,
|
|
518
|
+
maxBytes: scope.options.maxBytes,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
...logs,
|
|
523
|
+
body: tailed,
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
artifacts: async (ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown) => {
|
|
528
|
+
const parsed = normalizeScopeArgs(ownerOrOptions, repoOrOptions, maybeOptions)
|
|
529
|
+
const { owner, repo } = resolveOwnerRepo(parsed, defaults)
|
|
530
|
+
return requestGitea(ctx, ['actions', 'artifacts', 'list'], owner, repo, 'GET', ['artifacts'], parsed.options)
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
artifactsByRun: async (runId: unknown, ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown) => {
|
|
534
|
+
const id = toTrimmedString(runId)
|
|
535
|
+
if (!id) {
|
|
536
|
+
throw new Error('runId is required.')
|
|
537
|
+
}
|
|
538
|
+
const parsed = normalizeScopeArgs(ownerOrOptions, repoOrOptions, maybeOptions)
|
|
539
|
+
const { owner, repo } = resolveOwnerRepo(parsed, defaults)
|
|
540
|
+
return requestGitea(ctx, ['actions', 'runs', 'artifacts'], owner, repo, 'GET', ['runs', id, 'artifacts'], parsed.options)
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
artifactZipUrl: async (...rawArgs: unknown[]) => {
|
|
544
|
+
const resolved = resolveOwnerRepoAndId(rawArgs, defaults)
|
|
545
|
+
|
|
546
|
+
const result = await requestGitea(
|
|
547
|
+
ctx,
|
|
548
|
+
['actions', 'artifacts', 'downloadZipUrl'],
|
|
549
|
+
resolved.owner,
|
|
550
|
+
resolved.repo,
|
|
551
|
+
'GET',
|
|
552
|
+
['artifacts', resolved.id, 'zip'],
|
|
553
|
+
resolved.options,
|
|
554
|
+
{ redirect: 'manual' },
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const location =
|
|
558
|
+
typeof (result.response.headers.location) === 'string'
|
|
559
|
+
? result.response.headers.location
|
|
560
|
+
: typeof (result.response.headers['location']) === 'string'
|
|
561
|
+
? result.response.headers['location']
|
|
562
|
+
: null
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
...result,
|
|
566
|
+
body: {
|
|
567
|
+
status: result.status,
|
|
568
|
+
location,
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const ensureNamespace = (root: GitServiceApi, path: string[]): GitServiceApi => {
|
|
576
|
+
let cursor: GitServiceApi = root
|
|
577
|
+
for (const segment of path) {
|
|
578
|
+
const current = cursor[segment]
|
|
579
|
+
if (!isRecord(current)) {
|
|
580
|
+
cursor[segment] = {}
|
|
581
|
+
}
|
|
582
|
+
cursor = cursor[segment] as GitServiceApi
|
|
583
|
+
}
|
|
584
|
+
return cursor
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export const attachGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContext): void => {
|
|
588
|
+
const methods = createGitActionsApi(api, ctx)
|
|
589
|
+
const targets = [
|
|
590
|
+
ensureNamespace(api, ['actions']),
|
|
591
|
+
ensureNamespace(api, ['repo', 'actions']),
|
|
592
|
+
ensureNamespace(api, ['help']),
|
|
593
|
+
ensureNamespace(api, ['repo', 'help']),
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
const mapping: Record<string, { to: string; description: string }> = {
|
|
597
|
+
helpActionsLogs: { to: 'actionsLogs', description: 'Help for fetching Gitea Actions job logs' },
|
|
598
|
+
tasks: { to: 'tasks.list', description: 'List repository action tasks (workflow runs)' },
|
|
599
|
+
jobsLogs: { to: 'jobs.logs', description: 'Download job logs for a workflow run job_id' },
|
|
600
|
+
jobsLogsTail: { to: 'jobs.logsTail', description: 'Download and tail job logs (bounded for LLM)' },
|
|
601
|
+
jobsLogsForRunTail: { to: 'jobs.logsForRunTail', description: 'Resolve run by sha+run_number and tail logs' },
|
|
602
|
+
artifacts: { to: 'artifacts.list', description: 'List repository artifacts' },
|
|
603
|
+
artifactsByRun: { to: 'runs.artifacts', description: 'List artifacts for a run' },
|
|
604
|
+
artifactZipUrl: { to: 'artifacts.downloadZipUrl', description: 'Return redirect URL for artifact zip download' },
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
for (const target of targets) {
|
|
608
|
+
for (const [name, method] of Object.entries(methods)) {
|
|
609
|
+
const exportName = mapping[name]?.to ?? name
|
|
610
|
+
if (!(exportName in target)) {
|
|
611
|
+
target[exportName] = method
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
package/src/git-service-api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getGitPlatformConfig, type GitPlatformConfig } from './platform/config'
|
|
|
3
3
|
import { type GitServiceFlag, type GitServiceFeature, gitServiceFeatureSpec } from './git-service-feature-spec.generated'
|
|
4
4
|
import { type GitApiFeatureMapping } from './platform/gitea-adapter'
|
|
5
5
|
import { attachGitLabelManagementApi } from './label-management'
|
|
6
|
+
import { attachGitActionsApi } from './actions-api'
|
|
6
7
|
import { spawn } from 'node:child_process'
|
|
7
8
|
import crypto from 'node:crypto'
|
|
8
9
|
|
|
@@ -41,7 +42,10 @@ export type GitServiceApi = {
|
|
|
41
42
|
|
|
42
43
|
type ApiCallOptions = {
|
|
43
44
|
method?: string
|
|
44
|
-
|
|
45
|
+
requestBody?: unknown
|
|
46
|
+
requestJson?: unknown
|
|
47
|
+
requestData?: unknown
|
|
48
|
+
requestPayload?: unknown
|
|
45
49
|
json?: unknown
|
|
46
50
|
data?: unknown
|
|
47
51
|
payload?: unknown
|
|
@@ -134,7 +138,18 @@ const mapFlagValues = (
|
|
|
134
138
|
alias.set(canonical, flag.name)
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
const reserved = new Set([
|
|
141
|
+
const reserved = new Set([
|
|
142
|
+
'json',
|
|
143
|
+
'data',
|
|
144
|
+
'payload',
|
|
145
|
+
'headers',
|
|
146
|
+
'query',
|
|
147
|
+
'method',
|
|
148
|
+
'requestbody',
|
|
149
|
+
'requestjson',
|
|
150
|
+
'requestdata',
|
|
151
|
+
'requestpayload',
|
|
152
|
+
])
|
|
138
153
|
|
|
139
154
|
for (const [key, value] of Object.entries(options)) {
|
|
140
155
|
const normalizedKey = normalizeFlagLookup(key)
|
|
@@ -164,30 +179,28 @@ const mapFlagValues = (
|
|
|
164
179
|
const buildRequestBody = (
|
|
165
180
|
method: string,
|
|
166
181
|
options: Record<string, unknown>,
|
|
167
|
-
|
|
182
|
+
defaultBody: Record<string, unknown>,
|
|
168
183
|
): unknown | undefined => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return options.json
|
|
179
|
-
}
|
|
184
|
+
const normalizedMethod = method.toUpperCase()
|
|
185
|
+
const explicit =
|
|
186
|
+
options.requestBody ??
|
|
187
|
+
options.requestJson ??
|
|
188
|
+
options.requestData ??
|
|
189
|
+
options.requestPayload ??
|
|
190
|
+
options.json ??
|
|
191
|
+
options.data ??
|
|
192
|
+
options.payload
|
|
180
193
|
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
194
|
+
if (explicit !== undefined) {
|
|
195
|
+
return explicit
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
198
|
+
if (!['POST', 'PUT', 'PATCH'].includes(normalizedMethod)) {
|
|
199
|
+
return undefined
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
if (Object.keys(
|
|
190
|
-
return
|
|
202
|
+
if (Object.keys(defaultBody).length > 0) {
|
|
203
|
+
return defaultBody
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
return undefined
|
|
@@ -240,6 +253,48 @@ const canUseAbortSignalTimeout = (): boolean =>
|
|
|
240
253
|
const isTestRuntime = (): boolean =>
|
|
241
254
|
Boolean(process.env.VITEST) || process.env.NODE_ENV === 'test'
|
|
242
255
|
|
|
256
|
+
const toQueryRecord = (raw: Record<string, unknown>): Record<string, string | number | boolean> => {
|
|
257
|
+
const query: Record<string, string | number | boolean> = {}
|
|
258
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
259
|
+
if (typeof value === 'string') {
|
|
260
|
+
query[key] = value
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
264
|
+
query[key] = value
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
if (typeof value === 'boolean') {
|
|
268
|
+
query[key] = value
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return query
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const buildDefaultWriteBody = (options: Record<string, unknown>): Record<string, unknown> => {
|
|
275
|
+
const reserved = new Set([
|
|
276
|
+
'headers',
|
|
277
|
+
'query',
|
|
278
|
+
'method',
|
|
279
|
+
'requestBody',
|
|
280
|
+
'requestJson',
|
|
281
|
+
'requestData',
|
|
282
|
+
'requestPayload',
|
|
283
|
+
'json',
|
|
284
|
+
'data',
|
|
285
|
+
'payload',
|
|
286
|
+
])
|
|
287
|
+
|
|
288
|
+
const body: Record<string, unknown> = {}
|
|
289
|
+
for (const [key, value] of Object.entries(options)) {
|
|
290
|
+
if (reserved.has(key)) continue
|
|
291
|
+
if (value !== undefined) {
|
|
292
|
+
body[key] = value
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return body
|
|
296
|
+
}
|
|
297
|
+
|
|
243
298
|
const resolveHttpTransport = (requested?: string): 'fetch' | 'curl' => {
|
|
244
299
|
const normalized = (requested ?? '').trim().toLowerCase()
|
|
245
300
|
if (normalized === 'fetch' || normalized === 'curl') {
|
|
@@ -364,17 +419,33 @@ const createMethod = (
|
|
|
364
419
|
): GitServiceApiMethod => {
|
|
365
420
|
return async (...rawArgs: unknown[]) => {
|
|
366
421
|
const { args, options } = splitArgsAndOptions(rawArgs)
|
|
367
|
-
const { flags, unhandled } = mapFlagValues(feature, options)
|
|
368
422
|
const { query: additionalQuery, method: methodOverride, ...bodyOptions } = (options as ApiCallOptions)
|
|
369
423
|
|
|
370
|
-
const
|
|
424
|
+
const baseMapping = await adapter.mapFeature({
|
|
371
425
|
feature,
|
|
372
426
|
args,
|
|
373
|
-
flagValues:
|
|
427
|
+
flagValues: {},
|
|
374
428
|
method: methodOverride,
|
|
375
429
|
})
|
|
430
|
+
const normalizedMethod = baseMapping.method.toUpperCase()
|
|
431
|
+
|
|
432
|
+
const { mapping, extraQuery } = normalizedMethod === 'GET'
|
|
433
|
+
? (() => {
|
|
434
|
+
const { flags, unhandled } = mapFlagValues(feature, options)
|
|
435
|
+
return {
|
|
436
|
+
mapping: adapter.mapFeature({ feature, args, flagValues: flags, method: methodOverride }),
|
|
437
|
+
extraQuery: toQueryRecord(unhandled),
|
|
438
|
+
}
|
|
439
|
+
})()
|
|
440
|
+
: { mapping: Promise.resolve(baseMapping), extraQuery: {} }
|
|
441
|
+
|
|
442
|
+
const resolvedMapping = await mapping
|
|
443
|
+
const mergedQuery = {
|
|
444
|
+
...(additionalQuery ?? {}),
|
|
445
|
+
...(normalizedMethod === 'GET' ? extraQuery : {}),
|
|
446
|
+
}
|
|
376
447
|
|
|
377
|
-
const hydratedPath =
|
|
448
|
+
const hydratedPath = resolvedMapping.mappedPath.map((segment) => {
|
|
378
449
|
if (segment === '{owner}' && defaults.defaultOwner) {
|
|
379
450
|
return defaults.defaultOwner
|
|
380
451
|
}
|
|
@@ -387,9 +458,13 @@ const createMethod = (
|
|
|
387
458
|
})
|
|
388
459
|
assertResolvedMappedPath(hydratedPath, feature.path)
|
|
389
460
|
|
|
390
|
-
const requestBody = buildRequestBody(
|
|
461
|
+
const requestBody = buildRequestBody(
|
|
462
|
+
resolvedMapping.method,
|
|
463
|
+
bodyOptions,
|
|
464
|
+
normalizedMethod === 'GET' ? {} : buildDefaultWriteBody(options),
|
|
465
|
+
)
|
|
391
466
|
const headers = {
|
|
392
|
-
...toHeaderRecord(
|
|
467
|
+
...toHeaderRecord(resolvedMapping.headers),
|
|
393
468
|
...(options.headers ?? {}),
|
|
394
469
|
}
|
|
395
470
|
|
|
@@ -397,15 +472,15 @@ const createMethod = (
|
|
|
397
472
|
headers['Content-Type'] = 'application/json'
|
|
398
473
|
}
|
|
399
474
|
|
|
400
|
-
const requestUrl = buildUrl(
|
|
475
|
+
const requestUrl = buildUrl(resolvedMapping.apiBase, hydratedPath, resolvedMapping.query, mergedQuery)
|
|
401
476
|
const requestInit: RequestInit = {
|
|
402
|
-
method:
|
|
477
|
+
method: resolvedMapping.method,
|
|
403
478
|
headers,
|
|
404
479
|
body: requestBody !== undefined ? JSON.stringify(requestBody) : undefined,
|
|
405
480
|
}
|
|
406
481
|
|
|
407
482
|
const startedAt = Date.now()
|
|
408
|
-
log?.(`http:request ${
|
|
483
|
+
log?.(`http:request ${resolvedMapping.method} ${requestUrl}`)
|
|
409
484
|
try {
|
|
410
485
|
const responseHeaders: Record<string, string> = {}
|
|
411
486
|
let status = 0
|
|
@@ -416,7 +491,7 @@ const createMethod = (
|
|
|
416
491
|
const curlResult = await callCurl(
|
|
417
492
|
requestUrl,
|
|
418
493
|
{
|
|
419
|
-
method:
|
|
494
|
+
method: resolvedMapping.method,
|
|
420
495
|
headers,
|
|
421
496
|
...(requestInit.body !== undefined ? { body: String(requestInit.body) } : {}),
|
|
422
497
|
},
|
|
@@ -465,7 +540,7 @@ const createMethod = (
|
|
|
465
540
|
}
|
|
466
541
|
} catch (error) {
|
|
467
542
|
if ((timeoutSignal && timeoutSignal.aborted) || controller?.signal.aborted) {
|
|
468
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms: ${
|
|
543
|
+
throw new Error(`Request timed out after ${requestTimeoutMs}ms: ${resolvedMapping.method} ${requestUrl}`)
|
|
469
544
|
}
|
|
470
545
|
throw error
|
|
471
546
|
} finally {
|
|
@@ -475,17 +550,17 @@ const createMethod = (
|
|
|
475
550
|
}
|
|
476
551
|
}
|
|
477
552
|
|
|
478
|
-
log?.(`http:response ${
|
|
553
|
+
log?.(`http:response ${resolvedMapping.method} ${requestUrl} -> ${status} (${Date.now() - startedAt}ms)`)
|
|
479
554
|
return {
|
|
480
555
|
mapping: {
|
|
481
|
-
...
|
|
556
|
+
...resolvedMapping,
|
|
482
557
|
mappedPath: hydratedPath,
|
|
483
558
|
},
|
|
484
559
|
request: {
|
|
485
560
|
url: requestUrl,
|
|
486
|
-
method:
|
|
561
|
+
method: resolvedMapping.method,
|
|
487
562
|
headers,
|
|
488
|
-
query: [...
|
|
563
|
+
query: [...resolvedMapping.query],
|
|
489
564
|
body: requestBody,
|
|
490
565
|
},
|
|
491
566
|
response: {
|
|
@@ -575,6 +650,12 @@ export const createGitServiceApi = (options: GitServiceApiFactoryOptions = {}):
|
|
|
575
650
|
}
|
|
576
651
|
|
|
577
652
|
attachGitLabelManagementApi(root, defaults)
|
|
653
|
+
attachGitActionsApi(root, {
|
|
654
|
+
config,
|
|
655
|
+
defaults,
|
|
656
|
+
requestTimeoutMs,
|
|
657
|
+
log,
|
|
658
|
+
})
|
|
578
659
|
|
|
579
660
|
return root
|
|
580
661
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export {
|
|
|
44
44
|
type GitLabelManagementDefaults,
|
|
45
45
|
type GitRepositoryLabel,
|
|
46
46
|
} from './label-management'
|
|
47
|
+
export { attachGitActionsApi, createGitActionsApi, type GitActionsApiContext, type GitActionsApiDefaults, type GitActionsHelpBody } from './actions-api'
|
|
47
48
|
export { resolveProjectRepoIdentity, type GitRepositoryIdentity } from './repository'
|
|
48
49
|
export type {
|
|
49
50
|
GitServiceApi,
|
|
@@ -82,6 +82,9 @@ export const EXACT_GITEA_RULES: GiteaRouteRule[] = [
|
|
|
82
82
|
rule(['workflow', 'run'], 'POST', '/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches'),
|
|
83
83
|
rule(['workflow', 'view'], 'GET', '/repos/{owner}/{repo}/actions/workflows/{workflow_id}'),
|
|
84
84
|
|
|
85
|
+
// Run commands (Gitea Actions tasks list is the closest analogue)
|
|
86
|
+
rule(['run', 'list'], 'GET', '/repos/{owner}/{repo}/actions/tasks', 'maps run.list to action task list'),
|
|
87
|
+
|
|
85
88
|
// Secret and variable commands
|
|
86
89
|
rule(['secret', 'delete'], 'DELETE', '/repos/{owner}/{repo}/actions/secrets/{secretname}'),
|
|
87
90
|
rule(['secret', 'list'], 'GET', '/repos/{owner}/{repo}/actions/secrets'),
|
|
@@ -112,7 +115,6 @@ export const DEFAULT_GITEA_FIRST_TOKEN_RULES = new Map<string, GiteaRouteRule>([
|
|
|
112
115
|
['pr', rule(['pr'], 'GET', '/repos/{owner}/{repo}/pulls', 'fallback to pull request list')],
|
|
113
116
|
['repo', rule(['repo'], 'GET', '/repos/{owner}/{repo}', 'fallback to repository read')],
|
|
114
117
|
['release', rule(['release'], 'GET', '/repos/{owner}/{repo}/releases', 'fallback to release list')],
|
|
115
|
-
['run', rule(['run'], 'GET', '/repos/{owner}/{repo}/actions/tasks', 'fallback to workflow task list')],
|
|
116
118
|
['workflow', rule(['workflow'], 'GET', '/repos/{owner}/{repo}/actions/workflows', 'fallback to workflow list')],
|
|
117
119
|
['search', rule(['search'], 'GET', '/repos/search', 'fallback to global repo search')],
|
|
118
120
|
['secret', rule(['secret'], 'GET', '/repos/{owner}/{repo}/actions/secrets', 'fallback to repository secret list')],
|