@foundation0/git 1.2.3 → 1.2.5

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.
@@ -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
- body?: unknown
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(['body', 'json', 'data', 'payload', 'headers', 'query', 'method'])
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
- unmappedOptions: Record<string, unknown>,
182
+ defaultBody: Record<string, unknown>,
168
183
  ): unknown | undefined => {
169
- if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase())) {
170
- return undefined
171
- }
172
-
173
- if (options.body !== undefined) {
174
- return options.body
175
- }
176
-
177
- if (options.json !== undefined) {
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 (options.data !== undefined) {
182
- return options.data
194
+ if (explicit !== undefined) {
195
+ return explicit
183
196
  }
184
197
 
185
- if (options.payload !== undefined) {
186
- return options.payload
198
+ if (!['POST', 'PUT', 'PATCH'].includes(normalizedMethod)) {
199
+ return undefined
187
200
  }
188
201
 
189
- if (Object.keys(unmappedOptions).length > 0) {
190
- return unmappedOptions
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 mapping = await adapter.mapFeature({
424
+ const baseMapping = await adapter.mapFeature({
371
425
  feature,
372
426
  args,
373
- flagValues: flags,
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 = mapping.mappedPath.map((segment) => {
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(mapping.method, bodyOptions, unhandled)
461
+ const requestBody = buildRequestBody(
462
+ resolvedMapping.method,
463
+ bodyOptions,
464
+ normalizedMethod === 'GET' ? {} : buildDefaultWriteBody(options),
465
+ )
391
466
  const headers = {
392
- ...toHeaderRecord(mapping.headers),
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(mapping.apiBase, hydratedPath, mapping.query, additionalQuery ?? {})
475
+ const requestUrl = buildUrl(resolvedMapping.apiBase, hydratedPath, resolvedMapping.query, mergedQuery)
401
476
  const requestInit: RequestInit = {
402
- method: mapping.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 ${mapping.method} ${requestUrl}`)
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: mapping.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: ${mapping.method} ${requestUrl}`)
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 ${mapping.method} ${requestUrl} -> ${status} (${Date.now() - startedAt}ms)`)
553
+ log?.(`http:response ${resolvedMapping.method} ${requestUrl} -> ${status} (${Date.now() - startedAt}ms)`)
479
554
  return {
480
555
  mapping: {
481
- ...mapping,
556
+ ...resolvedMapping,
482
557
  mappedPath: hydratedPath,
483
558
  },
484
559
  request: {
485
560
  url: requestUrl,
486
- method: mapping.method,
561
+ method: resolvedMapping.method,
487
562
  headers,
488
- query: [...mapping.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')],