@kitman-sh/software-engineering 0.10.58 → 0.10.59
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/agents/deployer.md +11 -0
- package/dist/bundle.js +1933 -1836
- package/dist/declarations.json +109 -11
- package/kitman.config.ts +5 -0
- package/package.json +3 -3
- package/src/build.ts +13 -1
- package/src/release.ts +51 -2
- package/src/review.ts +30 -9
- package/src/shepherd-output.ts +5 -1
- package/src/shepherd.ts +40 -2
package/dist/declarations.json
CHANGED
|
@@ -337,6 +337,39 @@
|
|
|
337
337
|
"type": "integer",
|
|
338
338
|
"minimum": -9007199254740991,
|
|
339
339
|
"maximum": 9007199254740991
|
|
340
|
+
},
|
|
341
|
+
"release": {
|
|
342
|
+
"anyOf": [
|
|
343
|
+
{
|
|
344
|
+
"type": "object",
|
|
345
|
+
"properties": {
|
|
346
|
+
"runStatus": {
|
|
347
|
+
"type": "string"
|
|
348
|
+
},
|
|
349
|
+
"reason": {
|
|
350
|
+
"type": "string"
|
|
351
|
+
},
|
|
352
|
+
"tag": {
|
|
353
|
+
"anyOf": [
|
|
354
|
+
{
|
|
355
|
+
"type": "string"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"type": "null"
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
"required": [
|
|
364
|
+
"runStatus",
|
|
365
|
+
"reason"
|
|
366
|
+
],
|
|
367
|
+
"additionalProperties": true
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"type": "null"
|
|
371
|
+
}
|
|
372
|
+
]
|
|
340
373
|
}
|
|
341
374
|
},
|
|
342
375
|
"required": [
|
|
@@ -418,7 +451,9 @@
|
|
|
418
451
|
"call",
|
|
419
452
|
"ask",
|
|
420
453
|
"read_change_request",
|
|
421
|
-
"merge_change_request"
|
|
454
|
+
"merge_change_request",
|
|
455
|
+
"claim",
|
|
456
|
+
"publish_release"
|
|
422
457
|
]
|
|
423
458
|
},
|
|
424
459
|
{
|
|
@@ -542,7 +577,9 @@
|
|
|
542
577
|
"post_comment",
|
|
543
578
|
"approve_change_request",
|
|
544
579
|
"read_change_request",
|
|
545
|
-
"merge_change_request"
|
|
580
|
+
"merge_change_request",
|
|
581
|
+
"claim",
|
|
582
|
+
"publish_release"
|
|
546
583
|
]
|
|
547
584
|
},
|
|
548
585
|
{
|
|
@@ -886,7 +923,9 @@
|
|
|
886
923
|
"post_comment",
|
|
887
924
|
"read_change_request",
|
|
888
925
|
"merge_change_request",
|
|
889
|
-
"approve_change_request"
|
|
926
|
+
"approve_change_request",
|
|
927
|
+
"claim",
|
|
928
|
+
"publish_release"
|
|
890
929
|
]
|
|
891
930
|
},
|
|
892
931
|
{
|
|
@@ -1068,7 +1107,9 @@
|
|
|
1068
1107
|
"post_comment",
|
|
1069
1108
|
"approve_change_request",
|
|
1070
1109
|
"read_change_request",
|
|
1071
|
-
"merge_change_request"
|
|
1110
|
+
"merge_change_request",
|
|
1111
|
+
"claim",
|
|
1112
|
+
"publish_release"
|
|
1072
1113
|
]
|
|
1073
1114
|
},
|
|
1074
1115
|
{
|
|
@@ -1360,7 +1401,9 @@
|
|
|
1360
1401
|
"merge_change_request",
|
|
1361
1402
|
"read_change_request",
|
|
1362
1403
|
"post_comment",
|
|
1363
|
-
"approve_change_request"
|
|
1404
|
+
"approve_change_request",
|
|
1405
|
+
"claim",
|
|
1406
|
+
"publish_release"
|
|
1364
1407
|
]
|
|
1365
1408
|
},
|
|
1366
1409
|
{
|
|
@@ -1509,6 +1552,7 @@
|
|
|
1509
1552
|
"deployed",
|
|
1510
1553
|
"rolled_back",
|
|
1511
1554
|
"deploy_failed",
|
|
1555
|
+
"rollback_failed",
|
|
1512
1556
|
"timed_out"
|
|
1513
1557
|
]
|
|
1514
1558
|
},
|
|
@@ -1564,7 +1608,8 @@
|
|
|
1564
1608
|
"$defs": {}
|
|
1565
1609
|
},
|
|
1566
1610
|
"agents": {
|
|
1567
|
-
"releaser": "releaser"
|
|
1611
|
+
"releaser": "releaser",
|
|
1612
|
+
"deployer": "deployer"
|
|
1568
1613
|
},
|
|
1569
1614
|
"checkout": "read",
|
|
1570
1615
|
"when": [],
|
|
@@ -1950,7 +1995,9 @@
|
|
|
1950
1995
|
"post_comment",
|
|
1951
1996
|
"approve_change_request",
|
|
1952
1997
|
"read_change_request",
|
|
1953
|
-
"merge_change_request"
|
|
1998
|
+
"merge_change_request",
|
|
1999
|
+
"claim",
|
|
2000
|
+
"publish_release"
|
|
1954
2001
|
],
|
|
1955
2002
|
"schedule": {
|
|
1956
2003
|
"rule": {
|
|
@@ -2401,7 +2448,9 @@
|
|
|
2401
2448
|
"write_back",
|
|
2402
2449
|
"read_change_request",
|
|
2403
2450
|
"merge_change_request",
|
|
2404
|
-
"approve_change_request"
|
|
2451
|
+
"approve_change_request",
|
|
2452
|
+
"claim",
|
|
2453
|
+
"publish_release"
|
|
2405
2454
|
]
|
|
2406
2455
|
},
|
|
2407
2456
|
{
|
|
@@ -2573,6 +2622,39 @@
|
|
|
2573
2622
|
"type": "integer",
|
|
2574
2623
|
"minimum": -9007199254740991,
|
|
2575
2624
|
"maximum": 9007199254740991
|
|
2625
|
+
},
|
|
2626
|
+
"release": {
|
|
2627
|
+
"anyOf": [
|
|
2628
|
+
{
|
|
2629
|
+
"type": "object",
|
|
2630
|
+
"properties": {
|
|
2631
|
+
"runStatus": {
|
|
2632
|
+
"type": "string"
|
|
2633
|
+
},
|
|
2634
|
+
"reason": {
|
|
2635
|
+
"type": "string"
|
|
2636
|
+
},
|
|
2637
|
+
"tag": {
|
|
2638
|
+
"anyOf": [
|
|
2639
|
+
{
|
|
2640
|
+
"type": "string"
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
"type": "null"
|
|
2644
|
+
}
|
|
2645
|
+
]
|
|
2646
|
+
}
|
|
2647
|
+
},
|
|
2648
|
+
"required": [
|
|
2649
|
+
"runStatus",
|
|
2650
|
+
"reason"
|
|
2651
|
+
],
|
|
2652
|
+
"additionalProperties": true
|
|
2653
|
+
},
|
|
2654
|
+
{
|
|
2655
|
+
"type": "null"
|
|
2656
|
+
}
|
|
2657
|
+
]
|
|
2576
2658
|
}
|
|
2577
2659
|
},
|
|
2578
2660
|
"required": [
|
|
@@ -2600,7 +2682,9 @@
|
|
|
2600
2682
|
"write_back",
|
|
2601
2683
|
"ask",
|
|
2602
2684
|
"verify",
|
|
2603
|
-
"change_request"
|
|
2685
|
+
"change_request",
|
|
2686
|
+
"claim",
|
|
2687
|
+
"publish_release"
|
|
2604
2688
|
]
|
|
2605
2689
|
},
|
|
2606
2690
|
{
|
|
@@ -2798,7 +2882,9 @@
|
|
|
2798
2882
|
"post_comment",
|
|
2799
2883
|
"approve_change_request",
|
|
2800
2884
|
"write_back",
|
|
2801
|
-
"ask"
|
|
2885
|
+
"ask",
|
|
2886
|
+
"claim",
|
|
2887
|
+
"publish_release"
|
|
2802
2888
|
],
|
|
2803
2889
|
"schedule": {
|
|
2804
2890
|
"rule": {
|
|
@@ -2964,6 +3050,8 @@
|
|
|
2964
3050
|
"read_change_request",
|
|
2965
3051
|
"merge_change_request",
|
|
2966
3052
|
"approve_change_request",
|
|
3053
|
+
"claim",
|
|
3054
|
+
"publish_release",
|
|
2967
3055
|
"create_tasks"
|
|
2968
3056
|
]
|
|
2969
3057
|
}
|
|
@@ -3032,6 +3120,15 @@
|
|
|
3032
3120
|
"shipping-and-launch"
|
|
3033
3121
|
]
|
|
3034
3122
|
},
|
|
3123
|
+
"deployer": {
|
|
3124
|
+
"instructions": "You are a deployer. Release has tagged a version, and you run the repository's own deploy script for it, once.\n\n## Boundaries\n\n- Run only the command your brief names. Do not edit files, commit, push, tag, merge or retry it: the script backs up, publishes, deploys, verifies and rolls back by itself.\n- It runs for a long time. Start it once in the background with its output in a log file, and wait for it to exit.\n- Never print, copy or summarise the variables, tokens or secrets it pulls, or the production host's address.\n\n## Output\n\nSubmit your Output with the Kitman MCP `submit_output` tool, in the shape the brief's Output schema gives.",
|
|
3125
|
+
"effort": "low",
|
|
3126
|
+
"fallbacks": [
|
|
3127
|
+
{
|
|
3128
|
+
"harness": "pi"
|
|
3129
|
+
}
|
|
3130
|
+
]
|
|
3131
|
+
},
|
|
3035
3132
|
"investigator": {
|
|
3036
3133
|
"instructions": "You are an investigator. You find out why something happened and report a conclusion you can support.\n\n## Investigation\n\n- Start from the evidence you are given: the alert, its logs, metrics and traces, and the code that produced them.\n- You have web tools: read an upstream issue, a changelog or the documentation of a dependency when the evidence points outside the repository.\n- Form a hypothesis, then look for what would prove it wrong. Report what the evidence shows, not what you expected.\n- Every conclusion cites its sources: the log line, the query, the commit or the file and line it rests on.\n- Separate what you established from what you suspect, and say how sure you are.\n- When the cause points to a code change, describe the fix precisely enough that a Build can make it, but do not make it yourself.\n\n## Boundaries\n\n- Change nothing: do not edit files, commit, push or merge. The Run owns the branch.\n- Never `git stash`: the stash stack is shared with every other worktree of the repository. To look at another revision, make a throwaway checkout with `git worktree add` and remove it after.\n- A finding written in your Output is a claim. Say what you ran and what it returned.\n\n## Output\n\nSubmit your Output with the Kitman MCP `submit_output` tool, in the shape the brief's Output schema gives. An answer with no fix is a real outcome.",
|
|
3037
3134
|
"effort": "high",
|
|
@@ -3090,7 +3187,7 @@
|
|
|
3090
3187
|
"carried": [
|
|
3091
3188
|
{
|
|
3092
3189
|
"name": "@kitman-sh/software-engineering",
|
|
3093
|
-
"version": "0.10.
|
|
3190
|
+
"version": "0.10.59",
|
|
3094
3191
|
"setup": "2a237548a69b0cd53ec3be7f687cfa8c2128335b3a655d34e185f2abce6a3c37",
|
|
3095
3192
|
"loops": [
|
|
3096
3193
|
"build",
|
|
@@ -3115,6 +3212,7 @@
|
|
|
3115
3212
|
"quality-reviewer",
|
|
3116
3213
|
"planner",
|
|
3117
3214
|
"releaser",
|
|
3215
|
+
"deployer",
|
|
3118
3216
|
"investigator",
|
|
3119
3217
|
"climber",
|
|
3120
3218
|
"surveyor",
|
package/kitman.config.ts
CHANGED
|
@@ -73,6 +73,11 @@ export default defineKit({
|
|
|
73
73
|
skills: ['git-workflow-and-versioning', 'shipping-and-launch'],
|
|
74
74
|
instructions: { file: 'agents/releaser.md' }
|
|
75
75
|
},
|
|
76
|
+
deployer: {
|
|
77
|
+
effort: 'low',
|
|
78
|
+
fallbacks,
|
|
79
|
+
instructions: { file: 'agents/deployer.md' }
|
|
80
|
+
},
|
|
76
81
|
investigator: {
|
|
77
82
|
effort: 'high',
|
|
78
83
|
fallbacks,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitman-sh/software-engineering",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.59",
|
|
4
4
|
"description": "Everything for building software: Loops, their Agents, the skills they use and the Ponytail plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"tsconfig.json"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@kitman-sh/sdk": "0.10.
|
|
20
|
+
"@kitman-sh/sdk": "0.10.59",
|
|
21
21
|
"effect": "4.1.2"
|
|
22
22
|
},
|
|
23
23
|
"kitman": {
|
|
24
24
|
"bundle": "dist/bundle.js",
|
|
25
|
-
"digest": "
|
|
25
|
+
"digest": "c865bc195025830b0c1056823b84a8a14ff3315d5c68fbe5da62ca246d862fee",
|
|
26
26
|
"protocolVersion": 5,
|
|
27
27
|
"declarations": "dist/declarations.json",
|
|
28
28
|
"setup": {
|
package/src/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
agentWork,
|
|
3
|
+
ask,
|
|
3
4
|
agentWorkOnce,
|
|
4
5
|
approveChangeRequest,
|
|
5
6
|
ApproveChangeRequestResult,
|
|
@@ -516,8 +517,9 @@ export default defineLoop({
|
|
|
516
517
|
)
|
|
517
518
|
: undefined
|
|
518
519
|
|
|
520
|
+
// A blocking Review, or anything else left unsettled, stops here with the findings for a person, not in Shepherd.
|
|
519
521
|
const landed =
|
|
520
|
-
input.land !== false
|
|
522
|
+
input.land !== false && unsettled.length === 0
|
|
521
523
|
? yield* call('land', shepherd, {
|
|
522
524
|
changeRequestId: opened.id,
|
|
523
525
|
...(input.taskId === undefined ? undefined : { taskId: input.taskId }),
|
|
@@ -525,6 +527,16 @@ export default defineLoop({
|
|
|
525
527
|
})
|
|
526
528
|
: undefined
|
|
527
529
|
|
|
530
|
+
if (input.land !== false && unsettled.length > 0)
|
|
531
|
+
yield* ask('unsettled', {
|
|
532
|
+
prompt: `The ChangeRequest ${opened.url} is open, but these still stand: ${unsettled.join('; ')}. Build will not land it.`,
|
|
533
|
+
options: [{ value: 'Taken over', description: 'A person fixes, rebuts or steers it from here.' }],
|
|
534
|
+
recommended: 'Taken over',
|
|
535
|
+
recommendationReason: 'Build stopped with these unsettled, so the next step is a person’s.',
|
|
536
|
+
// No default: an unattended Run, as full auto starts, would take it at once and finish as partial.
|
|
537
|
+
default: null
|
|
538
|
+
})
|
|
539
|
+
|
|
528
540
|
return {
|
|
529
541
|
changeRequest: opened,
|
|
530
542
|
revision: last.head.revision,
|
package/src/release.ts
CHANGED
|
@@ -43,6 +43,22 @@ const Drafted = Schema.Struct({
|
|
|
43
43
|
|
|
44
44
|
type Drafted = typeof Drafted.Type
|
|
45
45
|
|
|
46
|
+
/** What the repository's own deploy script did, as the releaser read it from its last line and exit code. */
|
|
47
|
+
const Deployed = Schema.Struct({
|
|
48
|
+
/** none: the repository has no `.kitman/deploy`, so the deploy is left to whatever sends the deploy Event. */
|
|
49
|
+
status: Schema.Literals(['deployed', 'failed', 'rolled_back', 'rollback_failed', 'none']),
|
|
50
|
+
detail: Schema.String
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const deployPrompt = (tag: string, previousTag: string | undefined) =>
|
|
54
|
+
[
|
|
55
|
+
`Deploy the release ${tag}, which Kitman has just tagged and pushed.`,
|
|
56
|
+
`If the repository has no executable \`.kitman/deploy\` at ${tag}, change nothing and answer status none.`,
|
|
57
|
+
`Otherwise run \`.kitman/deploy ${tag}${previousTag === undefined ? '' : ` ${previousTag}`}\` from the checkout of ${tag} (\`git fetch --tags\` first), and do nothing else: it backs up, publishes, deploys, verifies and rolls back by itself, and may take an hour. Run it in the background with its output in a log file, and wait for it to exit, checking every few minutes; never start it twice.`,
|
|
58
|
+
'Answer with the status its last line `kitman-deploy: <status> <detail>` names, and that detail. If it exited without that line, answer failed with its exit code and the end of its log.',
|
|
59
|
+
'Never print, copy or summarise the variables or secrets it pulls.'
|
|
60
|
+
].join('\n\n')
|
|
61
|
+
|
|
46
62
|
const Output = Schema.Struct({
|
|
47
63
|
status: Schema.Literals([
|
|
48
64
|
'gate_failed',
|
|
@@ -51,6 +67,8 @@ const Output = Schema.Struct({
|
|
|
51
67
|
'deployed',
|
|
52
68
|
'rolled_back',
|
|
53
69
|
'deploy_failed',
|
|
70
|
+
/** The deploy failed after production changed, and so did the rollback: production may be broken. */
|
|
71
|
+
'rollback_failed',
|
|
54
72
|
'timed_out'
|
|
55
73
|
]),
|
|
56
74
|
/** Why it stopped, or what happened, in a line. */
|
|
@@ -189,7 +207,7 @@ export default defineLoop({
|
|
|
189
207
|
title: 'Release',
|
|
190
208
|
input: Input,
|
|
191
209
|
output: Output,
|
|
192
|
-
agents: { releaser: 'releaser' },
|
|
210
|
+
agents: { releaser: 'releaser', deployer: 'deployer' },
|
|
193
211
|
checkout: 'read',
|
|
194
212
|
capabilities: [
|
|
195
213
|
'agent_work',
|
|
@@ -247,7 +265,9 @@ export default defineLoop({
|
|
|
247
265
|
recommended: 'Approve',
|
|
248
266
|
recommendationReason:
|
|
249
267
|
'The release is proved: its checks pass, or its ChangeRequest is green, approved and mergeable.',
|
|
250
|
-
default: null
|
|
268
|
+
default: null,
|
|
269
|
+
// A full-auto Team has handed releasing a proved change to Kitman.
|
|
270
|
+
auto: 'Approve'
|
|
251
271
|
})
|
|
252
272
|
|
|
253
273
|
if (answer === 'Approve') break
|
|
@@ -360,6 +380,35 @@ export default defineLoop({
|
|
|
360
380
|
)
|
|
361
381
|
|
|
362
382
|
const settled = { tag, notesArtifactId: notes.artifactId, revision: released.revision, url: released.url }
|
|
383
|
+
|
|
384
|
+
// The repository deploys itself from the Runner when it has a deploy script, which rolls back on its own.
|
|
385
|
+
const ran = yield* agentWork('run-deploy', {
|
|
386
|
+
agent: 'deployer',
|
|
387
|
+
prompt: deployPrompt(tag, drafted.previousTag),
|
|
388
|
+
output: Deployed,
|
|
389
|
+
timeout: '3 hours'
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
if (ran.status !== 'none') {
|
|
393
|
+
const status = ran.status === 'failed' ? 'deploy_failed' : ran.status
|
|
394
|
+
|
|
395
|
+
const outcome = {
|
|
396
|
+
deployed: 'deployed',
|
|
397
|
+
rolled_back: 'rolled back',
|
|
398
|
+
deploy_failed: 'failed to deploy',
|
|
399
|
+
rollback_failed: 'failed to deploy, and the rollback failed too'
|
|
400
|
+
}[status]
|
|
401
|
+
|
|
402
|
+
yield* artifact('report', {
|
|
403
|
+
kind: 'release_report',
|
|
404
|
+
title: `${tag} ${outcome}`,
|
|
405
|
+
text: `${tag} at ${released.revision}: ${ran.detail}. ${released.url}`,
|
|
406
|
+
data: { tag, status, revision: released.revision, url: released.url, detail: ran.detail }
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
return { status, reason: ran.detail, ...settled } satisfies Output
|
|
410
|
+
}
|
|
411
|
+
|
|
363
412
|
let status = yield* deployed('deploy', tag)
|
|
364
413
|
|
|
365
414
|
for (let round = 1; status !== 'deployed'; round++) {
|
package/src/review.ts
CHANGED
|
@@ -270,14 +270,25 @@ export default defineLoop({
|
|
|
270
270
|
const earlier = input.earlier ?? []
|
|
271
271
|
const earlierIds = new Set(earlier.map(finding => finding.id))
|
|
272
272
|
|
|
273
|
-
const
|
|
273
|
+
const answered = followUpsOf(
|
|
274
274
|
earlier,
|
|
275
275
|
outputs.flatMap(([, output]) => output.followUps ?? [])
|
|
276
276
|
)
|
|
277
277
|
|
|
278
|
-
const standing = new Set(
|
|
278
|
+
const standing = new Set(answered.flatMap(entry => (entry.status === 'fixed' ? [] : [entry.id])))
|
|
279
279
|
|
|
280
|
-
const
|
|
280
|
+
const verdicts = outputs.flatMap(([, output]) => output.verdicts ?? [])
|
|
281
|
+
const rebutted = new Set((input.rebuttals ?? []).map(rebuttal => rebuttal.findingId))
|
|
282
|
+
|
|
283
|
+
// A rebutted finding every verdict on it accepts is withdrawn, as Build's settleRound counts it.
|
|
284
|
+
const withdrawn = (id: string) => {
|
|
285
|
+
const on = verdicts.filter(verdict => verdict.findingId === id)
|
|
286
|
+
|
|
287
|
+
return rebutted.has(id) && on.length > 0 && on.every(verdict => verdict.accepted)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const carried = earlier.filter(finding => standing.has(finding.id) && !withdrawn(finding.id))
|
|
291
|
+
const followUps = answered.filter(entry => !withdrawn(entry.id))
|
|
281
292
|
|
|
282
293
|
const fresh = mergeFindings(
|
|
283
294
|
Object.fromEntries(
|
|
@@ -290,14 +301,24 @@ export default defineLoop({
|
|
|
290
301
|
|
|
291
302
|
const findings = [...carried, ...fresh]
|
|
292
303
|
|
|
304
|
+
const blockers = findings.filter(isBlocking)
|
|
305
|
+
const scope = input.files === undefined ? outputs.find(([seat]) => seat === scopeSeat)?.[1].scope : undefined
|
|
306
|
+
const failing = input.failing ?? []
|
|
307
|
+
|
|
308
|
+
// A seat's own summary is kept only when nothing blocks, so it cannot contradict the verdict.
|
|
293
309
|
const summary =
|
|
294
|
-
|
|
295
|
-
?
|
|
296
|
-
:
|
|
310
|
+
blockers.length > 0
|
|
311
|
+
? `Blocking: fix ${blockers.map(finding => finding.id).join(', ')} before merging.`
|
|
312
|
+
: scope?.met === false
|
|
313
|
+
? 'Not ready: the change does not yet do what was asked.'
|
|
314
|
+
: failing.length > 0
|
|
315
|
+
? `Not ready: ${failing.join(', ')} fails on this head.`
|
|
316
|
+
: findings.length === 0
|
|
317
|
+
? 'No findings: nothing stands in the way of merging.'
|
|
318
|
+
: ['Can merge: nothing blocks it.', ...outputs.map(([, output]) => output.summary)].join(' ')
|
|
297
319
|
|
|
298
320
|
const freshIds = new Set(fresh.map(finding => finding.id))
|
|
299
321
|
const text = reviewText(summary, findings, followUps)
|
|
300
|
-
const scope = input.files === undefined ? outputs.find(([seat]) => seat === scopeSeat)?.[1].scope : undefined
|
|
301
322
|
|
|
302
323
|
const stored = yield* artifact('findings', {
|
|
303
324
|
kind: 'review-findings',
|
|
@@ -310,7 +331,7 @@ export default defineLoop({
|
|
|
310
331
|
summary,
|
|
311
332
|
findings,
|
|
312
333
|
followUps,
|
|
313
|
-
verdicts
|
|
334
|
+
verdicts,
|
|
314
335
|
blocking: findings.some(isBlocking),
|
|
315
336
|
artifactId: stored.artifactId,
|
|
316
337
|
...(scope === undefined ? undefined : { scope })
|
|
@@ -321,7 +342,7 @@ export default defineLoop({
|
|
|
321
342
|
if (changeRequestId === undefined || head === undefined) return reviewed
|
|
322
343
|
|
|
323
344
|
const approval =
|
|
324
|
-
reviewed.blocking || scope?.met === false ||
|
|
345
|
+
reviewed.blocking || scope?.met === false || failing.length > 0
|
|
325
346
|
? undefined
|
|
326
347
|
: yield* approveChangeRequest('approve', { changeRequestId, head, summary }).pipe(
|
|
327
348
|
Effect.map(result =>
|
package/src/shepherd-output.ts
CHANGED
|
@@ -6,5 +6,9 @@ export const ShepherdOutput = Schema.Struct({
|
|
|
6
6
|
/** Why it stopped, in a line a person can act on. */
|
|
7
7
|
reason: Schema.String,
|
|
8
8
|
fixRounds: Schema.Int,
|
|
9
|
-
replies: Schema.Int
|
|
9
|
+
replies: Schema.Int,
|
|
10
|
+
/** The Release it started after merging, when it was told to release. */
|
|
11
|
+
release: Schema.optional(
|
|
12
|
+
Schema.Struct({ runStatus: Schema.String, reason: Schema.String, tag: Schema.optional(Schema.String) })
|
|
13
|
+
)
|
|
10
14
|
})
|
package/src/shepherd.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '@kitman-sh/sdk/loop'
|
|
26
26
|
import * as Effect from 'effect/Effect'
|
|
27
27
|
import * as Schema from 'effect/Schema'
|
|
28
|
+
import release from './release'
|
|
28
29
|
import review, { ReviewInput, ReviewOutput } from './review'
|
|
29
30
|
import { ShepherdOutput } from './shepherd-output'
|
|
30
31
|
|
|
@@ -160,7 +161,7 @@ export default defineLoop({
|
|
|
160
161
|
'ask',
|
|
161
162
|
'verify'
|
|
162
163
|
],
|
|
163
|
-
calls: (): readonly CalledLoop[] => [review],
|
|
164
|
+
calls: (): readonly CalledLoop[] => [review, release],
|
|
164
165
|
run: input => {
|
|
165
166
|
const changeRequestId = input.changeRequestId
|
|
166
167
|
// The ChangeRequest's own target: an offer's `branch` may be the offering Run's branch, which is this one.
|
|
@@ -486,6 +487,8 @@ export default defineLoop({
|
|
|
486
487
|
recommended: 'Merge',
|
|
487
488
|
recommendationReason: 'It is approved, green and mergeable, which is everything a merge waits for.',
|
|
488
489
|
default: 'Wait',
|
|
490
|
+
// A full-auto Team merges what its Review approved and its checks passed.
|
|
491
|
+
auto: 'Merge',
|
|
489
492
|
timeout: '1 hour'
|
|
490
493
|
})
|
|
491
494
|
|
|
@@ -500,7 +503,42 @@ export default defineLoop({
|
|
|
500
503
|
if (merged.status !== 'merged') continue
|
|
501
504
|
yield* writeBack('write-back', { milestone: 'merged' })
|
|
502
505
|
|
|
503
|
-
|
|
506
|
+
const ship = yield* ask('ship', {
|
|
507
|
+
prompt: `Merged at ${merged.revision}. Release it now?`,
|
|
508
|
+
options: [
|
|
509
|
+
{ value: 'Release', description: 'Starts Release on the merge: it gates, tags, publishes and deploys it.' },
|
|
510
|
+
{ value: 'Not now', description: 'Leaves the release to a person or a later Release.' }
|
|
511
|
+
],
|
|
512
|
+
recommended: 'Release',
|
|
513
|
+
recommendationReason: 'The merge is approved and green, so it is ready to ship.',
|
|
514
|
+
default: 'Not now',
|
|
515
|
+
auto: 'Release',
|
|
516
|
+
timeout: '1 hour'
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
if (ship === 'Not now') return done('merged', `Merged at ${merged.revision}`)
|
|
520
|
+
|
|
521
|
+
const released = yield* call('release', release, { revision: merged.revision })
|
|
522
|
+
|
|
523
|
+
// It merged either way; a Release that did not deploy, other than one a person rejected, needs a person.
|
|
524
|
+
if (released.status !== 'deployed' && released.status !== 'rejected')
|
|
525
|
+
yield* ask('release-failed', {
|
|
526
|
+
prompt: `Merged at ${merged.revision}, but its Release ended ${released.status}: ${released.reason}`,
|
|
527
|
+
options: [{ value: 'Taken over', description: 'A person looks at the release and production from here.' }],
|
|
528
|
+
recommended: 'Taken over',
|
|
529
|
+
recommendationReason: 'The merge is in, but production may not run it, or may be broken.',
|
|
530
|
+
// No default: full auto runs unattended, and would take it at once and finish as merged.
|
|
531
|
+
default: null
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
...done('merged', `Merged at ${merged.revision}; Release: ${released.reason}`),
|
|
536
|
+
release: {
|
|
537
|
+
runStatus: released.status,
|
|
538
|
+
reason: released.reason,
|
|
539
|
+
...(released.tag === undefined ? undefined : { tag: released.tag })
|
|
540
|
+
}
|
|
541
|
+
}
|
|
504
542
|
}
|
|
505
543
|
}).pipe(
|
|
506
544
|
// Kitman fails a Step that runs past its time budget with this reason.
|