@foundation0/git 1.2.5 → 1.3.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.
- package/mcp/README.md +17 -1
- package/mcp/src/server.ts +227 -7
- package/package.json +1 -1
- package/src/actions-api.ts +150 -4
- package/src/ci-api.ts +544 -0
- package/src/git-service-api.ts +87 -16
- package/src/index.ts +1 -0
- package/src/issue-dependencies.ts +80 -16
- package/src/platform/gitea-adapter.ts +16 -4
package/mcp/README.md
CHANGED
|
@@ -231,7 +231,7 @@ Tips:
|
|
|
231
231
|
`repo.label.listManaged`, `repo.label.getByName`, `repo.label.upsert`, and `repo.label.deleteByName`
|
|
232
232
|
- Includes Gitea Actions convenience tools exposed from the API object (best-effort helpers):
|
|
233
233
|
`repo.actions.tasks.list`, `repo.actions.jobs.logs`, `repo.actions.jobs.logsTail`, `repo.actions.jobs.logsForRunTail`,
|
|
234
|
-
and `repo.actions.artifacts.downloadZipUrl`
|
|
234
|
+
`repo.actions.diagnoseLatestFailure`, and `repo.actions.artifacts.downloadZipUrl`
|
|
235
235
|
- Includes a discovery helper:
|
|
236
236
|
`help.actionsLogs` (also available under `repo.help.actionsLogs`)
|
|
237
237
|
|
|
@@ -248,3 +248,19 @@ Tips:
|
|
|
248
248
|
"maxLines": 250
|
|
249
249
|
}
|
|
250
250
|
```
|
|
251
|
+
|
|
252
|
+
## Notes: actions.diagnoseLatestFailure
|
|
253
|
+
|
|
254
|
+
`repo.actions.diagnoseLatestFailure` is a convenience tool that finds the most recent failing Actions task and returns a log tail.
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{ "owner": "F0", "repo": "adl", "workflowName": "TypeScript", "maxLines": 250 }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Notes: actions.runs.artifacts
|
|
261
|
+
|
|
262
|
+
`repo.actions.runs.artifacts` accepts a named form to avoid legacy positional ordering:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{ "owner": "F0", "repo": "adl", "runId": 11 }
|
|
266
|
+
```
|
package/mcp/src/server.ts
CHANGED
|
@@ -363,6 +363,9 @@ const buildToolName = (tool: ToolDefinition, prefix?: string): string =>
|
|
|
363
363
|
prefix ? `${prefix}.${tool.name}` : tool.name
|
|
364
364
|
|
|
365
365
|
const isLogsForRunTailTool = (toolName: string): boolean => toolName.endsWith('jobs.logsForRunTail')
|
|
366
|
+
const isArtifactsByRunTool = (toolName: string): boolean => toolName.endsWith('runs.artifacts')
|
|
367
|
+
const isDiagnoseLatestFailureTool = (toolName: string): boolean =>
|
|
368
|
+
toolName.endsWith('diagnoseLatestFailure') && toolName.includes('.actions.')
|
|
366
369
|
|
|
367
370
|
const buildGenericToolSchema = (): Record<string, unknown> => ({
|
|
368
371
|
type: 'object',
|
|
@@ -386,6 +389,49 @@ const buildGenericToolSchema = (): Record<string, unknown> => ({
|
|
|
386
389
|
},
|
|
387
390
|
})
|
|
388
391
|
|
|
392
|
+
const buildArtifactsByRunSchema = (): Record<string, unknown> => ({
|
|
393
|
+
type: 'object',
|
|
394
|
+
additionalProperties: true,
|
|
395
|
+
properties: {
|
|
396
|
+
owner: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
description: 'Repository owner/org. Optional if the server was started with a default owner.',
|
|
399
|
+
},
|
|
400
|
+
repo: {
|
|
401
|
+
type: 'string',
|
|
402
|
+
description: 'Repository name. Optional if the server was started with a default repo.',
|
|
403
|
+
},
|
|
404
|
+
runId: {
|
|
405
|
+
description: 'Workflow run id (alias: run_id).',
|
|
406
|
+
anyOf: [{ type: 'integer', minimum: 1 }, { type: 'string' }],
|
|
407
|
+
},
|
|
408
|
+
run_id: {
|
|
409
|
+
description: 'Alias for runId.',
|
|
410
|
+
anyOf: [{ type: 'integer', minimum: 1 }, { type: 'string' }],
|
|
411
|
+
},
|
|
412
|
+
format: {
|
|
413
|
+
type: 'string',
|
|
414
|
+
enum: ['terse', 'debug'],
|
|
415
|
+
description: 'Output format. Default: "terse". Use "debug" to include request/mapping/headers.',
|
|
416
|
+
},
|
|
417
|
+
// Legacy positional forms:
|
|
418
|
+
// - Preferred by humans/LLMs: [owner, repo, runId]
|
|
419
|
+
// - Back-compat with the underlying helper signature: [runId, owner, repo]
|
|
420
|
+
args: {
|
|
421
|
+
type: 'array',
|
|
422
|
+
items: {},
|
|
423
|
+
description:
|
|
424
|
+
'Legacy positional form. Prefer named params. If used, pass [owner, repo, runId] (recommended) or the legacy [runId, owner, repo]. You can also pass only [runId] if defaults are configured.',
|
|
425
|
+
},
|
|
426
|
+
options: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
additionalProperties: true,
|
|
429
|
+
description: 'Options object (query/headers/body payload). Extra top-level keys are merged into options.',
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
anyOf: [{ required: ['runId'] }, { required: ['run_id'] }, { required: ['args'] }],
|
|
433
|
+
})
|
|
434
|
+
|
|
389
435
|
const buildLogsForRunTailSchema = (): Record<string, unknown> => ({
|
|
390
436
|
type: 'object',
|
|
391
437
|
additionalProperties: true,
|
|
@@ -459,8 +505,67 @@ const buildLogsForRunTailSchema = (): Record<string, unknown> => ({
|
|
|
459
505
|
],
|
|
460
506
|
})
|
|
461
507
|
|
|
508
|
+
const buildDiagnoseLatestFailureSchema = (): Record<string, unknown> => ({
|
|
509
|
+
type: 'object',
|
|
510
|
+
additionalProperties: true,
|
|
511
|
+
properties: {
|
|
512
|
+
owner: {
|
|
513
|
+
type: 'string',
|
|
514
|
+
description: 'Repository owner/org. Optional if the server was started with a default owner.',
|
|
515
|
+
},
|
|
516
|
+
repo: {
|
|
517
|
+
type: 'string',
|
|
518
|
+
description: 'Repository name. Optional if the server was started with a default repo.',
|
|
519
|
+
},
|
|
520
|
+
workflowName: {
|
|
521
|
+
type: 'string',
|
|
522
|
+
description: 'Optional filter: match failing runs by name/display_title (case-insensitive substring).',
|
|
523
|
+
},
|
|
524
|
+
limit: {
|
|
525
|
+
type: 'integer',
|
|
526
|
+
minimum: 1,
|
|
527
|
+
description: 'How many tasks/runs to fetch before filtering (default: 50).',
|
|
528
|
+
},
|
|
529
|
+
maxLines: {
|
|
530
|
+
type: 'integer',
|
|
531
|
+
minimum: 1,
|
|
532
|
+
description: 'Max lines to return from the end of the logs (default: 200).',
|
|
533
|
+
},
|
|
534
|
+
maxBytes: {
|
|
535
|
+
type: 'integer',
|
|
536
|
+
minimum: 1,
|
|
537
|
+
description: 'Max bytes to return from the end of the logs.',
|
|
538
|
+
},
|
|
539
|
+
contains: {
|
|
540
|
+
type: 'string',
|
|
541
|
+
description: 'If set, only return log lines containing this substring.',
|
|
542
|
+
},
|
|
543
|
+
format: {
|
|
544
|
+
type: 'string',
|
|
545
|
+
enum: ['terse', 'debug'],
|
|
546
|
+
description: 'Output format. Default: "terse". Use "debug" to include request/mapping/headers.',
|
|
547
|
+
},
|
|
548
|
+
args: {
|
|
549
|
+
type: 'array',
|
|
550
|
+
items: { type: 'string' },
|
|
551
|
+
description: 'Legacy positional form: [owner, repo] plus options.workflowName/maxLines/etc.',
|
|
552
|
+
},
|
|
553
|
+
options: {
|
|
554
|
+
type: 'object',
|
|
555
|
+
additionalProperties: true,
|
|
556
|
+
description: 'Options object. Extra top-level keys are merged into options.',
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
})
|
|
560
|
+
|
|
462
561
|
const buildToolInputSchema = (tool: ToolDefinition): Record<string, unknown> =>
|
|
463
|
-
isLogsForRunTailTool(tool.name)
|
|
562
|
+
isLogsForRunTailTool(tool.name)
|
|
563
|
+
? buildLogsForRunTailSchema()
|
|
564
|
+
: isArtifactsByRunTool(tool.name)
|
|
565
|
+
? buildArtifactsByRunSchema()
|
|
566
|
+
: isDiagnoseLatestFailureTool(tool.name)
|
|
567
|
+
? buildDiagnoseLatestFailureSchema()
|
|
568
|
+
: buildGenericToolSchema()
|
|
464
569
|
|
|
465
570
|
const buildToolList = (tools: ToolDefinition[], batchToolName: string, prefix?: string) => {
|
|
466
571
|
const toolNames = tools.map((tool) => ({
|
|
@@ -544,15 +649,67 @@ const invokeTool = async (
|
|
|
544
649
|
return tool.method(...invocationArgs)
|
|
545
650
|
}
|
|
546
651
|
|
|
652
|
+
const pickArgsFromNormalizedPayload = (normalized: unknown, record: Record<string, unknown>): unknown[] => {
|
|
653
|
+
if (Array.isArray(normalized)) {
|
|
654
|
+
return normalized
|
|
655
|
+
}
|
|
656
|
+
return Array.isArray(record.args) ? record.args : []
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const pickNestedRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? (value as Record<string, unknown>) : {})
|
|
660
|
+
|
|
661
|
+
const normalizeQueryRecord = (query: unknown): Record<string, unknown> => {
|
|
662
|
+
if (isRecord(query)) {
|
|
663
|
+
return query
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!Array.isArray(query)) {
|
|
667
|
+
return {}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const merged: Record<string, unknown> = {}
|
|
671
|
+
|
|
672
|
+
for (const entry of query) {
|
|
673
|
+
if (!isRecord(entry)) continue
|
|
674
|
+
|
|
675
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : ''
|
|
676
|
+
if (name) {
|
|
677
|
+
merged[name] = entry.value
|
|
678
|
+
continue
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Support [{ headSha: "..." }, { runNumber: 11 }] style arrays.
|
|
682
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
683
|
+
if (value !== undefined) merged[key] = value
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return merged
|
|
688
|
+
}
|
|
689
|
+
|
|
547
690
|
const invokeLogsForRunTailTool = async (tool: ToolDefinition, payload: unknown): Promise<GitServiceApiExecutionResult> => {
|
|
548
691
|
const normalized = normalizeArgumentPayload(payload)
|
|
549
692
|
const record = isRecord(normalized) ? normalized : {}
|
|
550
|
-
const args =
|
|
693
|
+
const args = pickArgsFromNormalizedPayload(normalized, record)
|
|
551
694
|
const options = pickRecord(record.options)
|
|
552
|
-
|
|
553
|
-
const
|
|
695
|
+
const query = normalizeQueryRecord(options.query)
|
|
696
|
+
const data = pickNestedRecord(options.data)
|
|
697
|
+
|
|
698
|
+
const headShaNamed = toTrimmedString(
|
|
699
|
+
pickFirst(
|
|
700
|
+
record.headSha,
|
|
701
|
+
record.head_sha,
|
|
702
|
+
options.headSha,
|
|
703
|
+
options.head_sha,
|
|
704
|
+
query.headSha,
|
|
705
|
+
query.head_sha,
|
|
706
|
+
data.headSha,
|
|
707
|
+
data.head_sha,
|
|
708
|
+
),
|
|
709
|
+
)
|
|
554
710
|
const runNumberNamed = pickFirst(
|
|
555
711
|
toPositiveInteger(pickFirst(record.runNumber, record.run_number, options.runNumber, options.run_number)),
|
|
712
|
+
toPositiveInteger(pickFirst(query.runNumber, query.run_number, data.runNumber, data.run_number)),
|
|
556
713
|
null,
|
|
557
714
|
)
|
|
558
715
|
|
|
@@ -570,20 +727,28 @@ const invokeLogsForRunTailTool = async (tool: ToolDefinition, payload: unknown):
|
|
|
570
727
|
const ownerFromArgs = shouldTreatArgsAsOwnerRepo ? toTrimmedString(args[0]) : ''
|
|
571
728
|
const repoFromArgs = shouldTreatArgsAsOwnerRepo ? toTrimmedString(args[1]) : ''
|
|
572
729
|
|
|
573
|
-
const owner =
|
|
574
|
-
|
|
730
|
+
const owner =
|
|
731
|
+
toTrimmedString(pickFirst(record.owner, options.owner, query.owner, data.owner, ownerFromArgs)) || undefined
|
|
732
|
+
const repo = toTrimmedString(pickFirst(record.repo, options.repo, query.repo, data.repo, repoFromArgs)) || undefined
|
|
575
733
|
|
|
576
734
|
const sha = toTrimmedString(pickFirst(headShaNamed || null, headShaPositional || null)) || ''
|
|
577
735
|
const run = pickFirst(runNumberNamed, runNumberPositional) ?? null
|
|
578
736
|
|
|
579
737
|
if (!sha || !run) {
|
|
580
738
|
throw new Error(
|
|
581
|
-
'headSha (string) and runNumber (positive integer) are required. Preferred invocation: { owner, repo, headSha, runNumber, maxLines }.',
|
|
739
|
+
'headSha (string) and runNumber (positive integer) are required. Preferred invocation: { owner, repo, headSha, runNumber, maxLines }. (Compatibility: options.query.{headSha,runNumber} is also accepted.)',
|
|
582
740
|
)
|
|
583
741
|
}
|
|
584
742
|
|
|
743
|
+
const containsFromQuery = typeof query.contains === 'string' ? query.contains : undefined
|
|
744
|
+
const maxLinesFromQuery = toPositiveInteger(query.maxLines)
|
|
745
|
+
const maxBytesFromQuery = toPositiveInteger(query.maxBytes)
|
|
746
|
+
|
|
585
747
|
const cleanedOptions = stripMcpOnlyOptions({
|
|
586
748
|
...options,
|
|
749
|
+
...(containsFromQuery ? { contains: containsFromQuery } : {}),
|
|
750
|
+
...(maxLinesFromQuery ? { maxLines: maxLinesFromQuery } : {}),
|
|
751
|
+
...(maxBytesFromQuery ? { maxBytes: maxBytesFromQuery } : {}),
|
|
587
752
|
...(owner ? { owner } : {}),
|
|
588
753
|
...(repo ? { repo } : {}),
|
|
589
754
|
})
|
|
@@ -591,6 +756,57 @@ const invokeLogsForRunTailTool = async (tool: ToolDefinition, payload: unknown):
|
|
|
591
756
|
return tool.method(sha, run, cleanedOptions)
|
|
592
757
|
}
|
|
593
758
|
|
|
759
|
+
const invokeArtifactsByRunTool = async (
|
|
760
|
+
tool: ToolDefinition,
|
|
761
|
+
payload: unknown,
|
|
762
|
+
): Promise<GitServiceApiExecutionResult> => {
|
|
763
|
+
const normalized = normalizeArgumentPayload(payload)
|
|
764
|
+
const record = isRecord(normalized) ? normalized : {}
|
|
765
|
+
const args = pickArgsFromNormalizedPayload(normalized, record)
|
|
766
|
+
const options = pickRecord(record.options)
|
|
767
|
+
|
|
768
|
+
const runIdNamed = toTrimmedString(pickFirst(record.runId, record.run_id, options.runId, options.run_id)) || ''
|
|
769
|
+
|
|
770
|
+
const a0 = args.length >= 1 ? toTrimmedString(args[0]) : ''
|
|
771
|
+
const a1 = args.length >= 2 ? toTrimmedString(args[1]) : ''
|
|
772
|
+
const a2 = args.length >= 3 ? toTrimmedString(args[2]) : ''
|
|
773
|
+
|
|
774
|
+
// Heuristic:
|
|
775
|
+
// - If named runId is set, use it.
|
|
776
|
+
// - Else if args look like [owner, repo, runId], use that.
|
|
777
|
+
// - Else treat args as legacy [runId, owner, repo] (or [runId] with defaults).
|
|
778
|
+
const shouldTreatAsOwnerRepoRunId = Boolean(a0) && Boolean(a1) && Boolean(a2) && !runIdNamed
|
|
779
|
+
|
|
780
|
+
const owner =
|
|
781
|
+
toTrimmedString(pickFirst(record.owner, options.owner, shouldTreatAsOwnerRepoRunId ? a0 : a1)) || undefined
|
|
782
|
+
const repo =
|
|
783
|
+
toTrimmedString(pickFirst(record.repo, options.repo, shouldTreatAsOwnerRepoRunId ? a1 : a2)) || undefined
|
|
784
|
+
const runId = toTrimmedString(pickFirst(runIdNamed || null, shouldTreatAsOwnerRepoRunId ? a2 : a0)) || ''
|
|
785
|
+
|
|
786
|
+
if (!runId) {
|
|
787
|
+
throw new Error('runId is required. Preferred invocation: { owner, repo, runId }.')
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const cleanedOptions = stripMcpOnlyOptions({
|
|
791
|
+
...options,
|
|
792
|
+
...(owner ? { owner } : {}),
|
|
793
|
+
...(repo ? { repo } : {}),
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
// Underlying helper signature is (runId, owner?, repo?, options?).
|
|
797
|
+
// We always pass runId first, and owner/repo if we have them.
|
|
798
|
+
if (owner && repo) {
|
|
799
|
+
return tool.method(runId, owner, repo, cleanedOptions)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (owner && !repo) {
|
|
803
|
+
// Unusual: allow passing only owner explicitly.
|
|
804
|
+
return tool.method(runId, owner, cleanedOptions)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return tool.method(runId, cleanedOptions)
|
|
808
|
+
}
|
|
809
|
+
|
|
594
810
|
export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpServerInstance => {
|
|
595
811
|
const api = createGitServiceApi(options)
|
|
596
812
|
const tools = collectGitTools(api)
|
|
@@ -650,6 +866,8 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
650
866
|
|
|
651
867
|
const data = await (isLogsForRunTailTool(toolDefinition.name)
|
|
652
868
|
? invokeLogsForRunTailTool(toolDefinition, mergedPayload)
|
|
869
|
+
: isArtifactsByRunTool(toolDefinition.name)
|
|
870
|
+
? invokeArtifactsByRunTool(toolDefinition, mergedPayload)
|
|
653
871
|
: invokeTool(toolDefinition, mergedPayload))
|
|
654
872
|
|
|
655
873
|
const { isError, envelope } = toMcpEnvelope(data, effectiveFormat)
|
|
@@ -705,6 +923,8 @@ export const createGitMcpServer = (options: GitMcpServerOptions = {}): GitMcpSer
|
|
|
705
923
|
const controls = extractOutputControls(request.params.arguments)
|
|
706
924
|
const result = await (isLogsForRunTailTool(tool.name)
|
|
707
925
|
? invokeLogsForRunTailTool(tool, request.params.arguments)
|
|
926
|
+
: isArtifactsByRunTool(tool.name)
|
|
927
|
+
? invokeArtifactsByRunTool(tool, request.params.arguments)
|
|
708
928
|
: invokeTool(tool, request.params.arguments))
|
|
709
929
|
|
|
710
930
|
const { isError, envelope } = toMcpEnvelope(result, controls.format ?? 'terse')
|
package/package.json
CHANGED
package/src/actions-api.ts
CHANGED
|
@@ -9,9 +9,25 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
9
9
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
10
|
|
|
11
11
|
const toTrimmedString = (value: unknown): string | null => {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
if (!Number.isFinite(value)) return null
|
|
18
|
+
const asText = String(Math.trunc(value)).trim()
|
|
19
|
+
return asText.length > 0 ? asText : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof value === 'bigint') {
|
|
23
|
+
const asText = value.toString().trim()
|
|
24
|
+
return asText.length > 0 ? asText : null
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
if (typeof value !== 'string') {
|
|
13
28
|
return null
|
|
14
29
|
}
|
|
30
|
+
|
|
15
31
|
const trimmed = value.trim()
|
|
16
32
|
return trimmed.length > 0 ? trimmed : null
|
|
17
33
|
}
|
|
@@ -162,6 +178,19 @@ const resolveOwnerRepoFromArgs = (
|
|
|
162
178
|
return { ...resolved, rest: args }
|
|
163
179
|
}
|
|
164
180
|
|
|
181
|
+
type NormalizedActionsState = 'success' | 'failure' | 'pending'
|
|
182
|
+
|
|
183
|
+
const normalizeActionsState = (raw: unknown): NormalizedActionsState => {
|
|
184
|
+
const value = toTrimmedString(raw).toLowerCase()
|
|
185
|
+
if (value === 'success') return 'success'
|
|
186
|
+
if (value === 'failure' || value === 'failed' || value === 'error') return 'failure'
|
|
187
|
+
if (value === 'cancelled' || value === 'canceled') return 'failure'
|
|
188
|
+
if (value === 'skipped') return 'success'
|
|
189
|
+
if (value === 'running' || value === 'in_progress' || value === 'queued' || value === 'waiting') return 'pending'
|
|
190
|
+
if (value === 'pending') return 'pending'
|
|
191
|
+
return 'pending'
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
const resolveOwnerRepoAndId = (
|
|
166
195
|
rawArgs: unknown[],
|
|
167
196
|
defaults: GitActionsApiDefaults,
|
|
@@ -495,10 +524,24 @@ export const createGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContex
|
|
|
495
524
|
throw new Error(`No matching run found in actions/tasks for head_sha=${sha} run_number=${run}.`)
|
|
496
525
|
}
|
|
497
526
|
|
|
498
|
-
const
|
|
499
|
-
|
|
527
|
+
const jobIdCandidate =
|
|
528
|
+
(match as Record<string, unknown>).job_id ??
|
|
529
|
+
(match as Record<string, unknown>).jobId ??
|
|
530
|
+
(match as Record<string, unknown>).task_id ??
|
|
531
|
+
(match as Record<string, unknown>).taskId ??
|
|
532
|
+
(match as Record<string, unknown>).id
|
|
533
|
+
|
|
534
|
+
const jobIdText = toTrimmedString(jobIdCandidate)
|
|
500
535
|
if (!jobIdText) {
|
|
501
|
-
|
|
536
|
+
const keys = Object.keys(match).slice(0, 32).join(',')
|
|
537
|
+
const preview = (() => {
|
|
538
|
+
try {
|
|
539
|
+
return JSON.stringify(match).slice(0, 500)
|
|
540
|
+
} catch {
|
|
541
|
+
return ''
|
|
542
|
+
}
|
|
543
|
+
})()
|
|
544
|
+
throw new Error(`Matched run entry does not expose an id usable as job_id. keys=[${keys}] preview=${preview}`)
|
|
502
545
|
}
|
|
503
546
|
|
|
504
547
|
const logs = await requestGitea(
|
|
@@ -524,6 +567,109 @@ export const createGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContex
|
|
|
524
567
|
}
|
|
525
568
|
},
|
|
526
569
|
|
|
570
|
+
diagnoseLatestFailure: async (ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown) => {
|
|
571
|
+
const parsed = normalizeScopeArgs(ownerOrOptions, repoOrOptions, maybeOptions)
|
|
572
|
+
// Allow calling with a single object payload: { owner, repo, workflowName, ... }.
|
|
573
|
+
const scope = normalizeScopeArgs(parsed.options.owner, parsed.options.repo, parsed.options)
|
|
574
|
+
const { owner, repo } = resolveOwnerRepo(scope, defaults)
|
|
575
|
+
|
|
576
|
+
const workflowNameRaw = typeof scope.options.workflowName === 'string' ? scope.options.workflowName.trim() : ''
|
|
577
|
+
const workflowName = workflowNameRaw.length > 0 ? workflowNameRaw : null
|
|
578
|
+
const maxLines = toPositiveInteger(scope.options.maxLines) ?? DEFAULT_LOG_TAIL_LINES
|
|
579
|
+
const maxBytes = toPositiveInteger(scope.options.maxBytes)
|
|
580
|
+
const contains = typeof scope.options.contains === 'string' ? scope.options.contains : undefined
|
|
581
|
+
const limit = toPositiveInteger(scope.options.limit) ?? 50
|
|
582
|
+
|
|
583
|
+
const tasks = await requestGitea(
|
|
584
|
+
ctx,
|
|
585
|
+
['actions', 'tasks', 'list'],
|
|
586
|
+
owner,
|
|
587
|
+
repo,
|
|
588
|
+
'GET',
|
|
589
|
+
['tasks'],
|
|
590
|
+
{ ...scope.options, query: { ...(isRecord(scope.options.query) ? scope.options.query : {}), limit } },
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
const entries = isRecord(tasks.body) && Array.isArray(tasks.body.workflow_runs)
|
|
594
|
+
? (tasks.body.workflow_runs as unknown[])
|
|
595
|
+
: []
|
|
596
|
+
|
|
597
|
+
const normalizedWorkflowName = workflowName ? workflowName.toLowerCase() : null
|
|
598
|
+
const failing = entries
|
|
599
|
+
.filter((entry) => isRecord(entry))
|
|
600
|
+
.filter((entry) => normalizeActionsState((entry as Record<string, unknown>).status) === 'failure')
|
|
601
|
+
.filter((entry) => {
|
|
602
|
+
if (!normalizedWorkflowName) return true
|
|
603
|
+
const name = String((entry as Record<string, unknown>).name ?? '').toLowerCase()
|
|
604
|
+
const displayTitle = String((entry as Record<string, unknown>).display_title ?? '').toLowerCase()
|
|
605
|
+
return name.includes(normalizedWorkflowName) || displayTitle.includes(normalizedWorkflowName)
|
|
606
|
+
})
|
|
607
|
+
.sort((a, b) => {
|
|
608
|
+
const aRecord = a as Record<string, unknown>
|
|
609
|
+
const bRecord = b as Record<string, unknown>
|
|
610
|
+
const aUpdated = Date.parse(String(aRecord.updated_at ?? '')) || 0
|
|
611
|
+
const bUpdated = Date.parse(String(bRecord.updated_at ?? '')) || 0
|
|
612
|
+
return bUpdated - aUpdated
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
const match = failing[0]
|
|
616
|
+
if (!match || !isRecord(match)) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
workflowName
|
|
619
|
+
? `No failing workflow run found matching workflowName=${workflowName}.`
|
|
620
|
+
: 'No failing workflow run found.',
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const sha = String((match as Record<string, unknown>).head_sha ?? '').trim()
|
|
625
|
+
const run = toPositiveInteger((match as Record<string, unknown>).run_number)
|
|
626
|
+
if (!sha || !run) {
|
|
627
|
+
throw new Error('Matched run entry is missing head_sha or run_number.')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const jobIdCandidate =
|
|
631
|
+
(match as Record<string, unknown>).job_id ??
|
|
632
|
+
(match as Record<string, unknown>).jobId ??
|
|
633
|
+
(match as Record<string, unknown>).task_id ??
|
|
634
|
+
(match as Record<string, unknown>).taskId ??
|
|
635
|
+
(match as Record<string, unknown>).id
|
|
636
|
+
|
|
637
|
+
const jobIdText = toTrimmedString(jobIdCandidate)
|
|
638
|
+
if (!jobIdText) {
|
|
639
|
+
throw new Error('Matched run entry does not expose an id usable as job_id.')
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const logs = await requestGitea(
|
|
643
|
+
ctx,
|
|
644
|
+
['actions', 'jobs', 'logs'],
|
|
645
|
+
owner,
|
|
646
|
+
repo,
|
|
647
|
+
'GET',
|
|
648
|
+
['jobs', jobIdText, 'logs'],
|
|
649
|
+
scope.options,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
const asText = typeof logs.body === 'string' ? logs.body : JSON.stringify(logs.body, null, 2)
|
|
653
|
+
const tailed = tailText(asText, {
|
|
654
|
+
contains,
|
|
655
|
+
maxLines,
|
|
656
|
+
...(maxBytes ? { maxBytes } : {}),
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
...logs,
|
|
661
|
+
body: {
|
|
662
|
+
run: match,
|
|
663
|
+
logsTail: tailed,
|
|
664
|
+
owner,
|
|
665
|
+
repo,
|
|
666
|
+
headSha: sha,
|
|
667
|
+
runNumber: run,
|
|
668
|
+
jobId: jobIdText,
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
|
|
527
673
|
artifacts: async (ownerOrOptions?: unknown, repoOrOptions?: unknown, maybeOptions?: unknown) => {
|
|
528
674
|
const parsed = normalizeScopeArgs(ownerOrOptions, repoOrOptions, maybeOptions)
|
|
529
675
|
const { owner, repo } = resolveOwnerRepo(parsed, defaults)
|
|
@@ -599,6 +745,7 @@ export const attachGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContex
|
|
|
599
745
|
jobsLogs: { to: 'jobs.logs', description: 'Download job logs for a workflow run job_id' },
|
|
600
746
|
jobsLogsTail: { to: 'jobs.logsTail', description: 'Download and tail job logs (bounded for LLM)' },
|
|
601
747
|
jobsLogsForRunTail: { to: 'jobs.logsForRunTail', description: 'Resolve run by sha+run_number and tail logs' },
|
|
748
|
+
diagnoseLatestFailure: { to: 'diagnoseLatestFailure', description: 'Find latest failing run and return log tail' },
|
|
602
749
|
artifacts: { to: 'artifacts.list', description: 'List repository artifacts' },
|
|
603
750
|
artifactsByRun: { to: 'runs.artifacts', description: 'List artifacts for a run' },
|
|
604
751
|
artifactZipUrl: { to: 'artifacts.downloadZipUrl', description: 'Return redirect URL for artifact zip download' },
|
|
@@ -613,4 +760,3 @@ export const attachGitActionsApi = (api: GitServiceApi, ctx: GitActionsApiContex
|
|
|
613
760
|
}
|
|
614
761
|
}
|
|
615
762
|
}
|
|
616
|
-
|