@llm4ts/flow 0.10.1 → 0.12.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/README.md +5 -0
- package/dist/AzureDevOpsTool.d.ts +64 -26
- package/dist/AzureDevOpsTool.d.ts.map +1 -1
- package/dist/AzureDevOpsTool.js +375 -123
- package/dist/AzureDevOpsTool.js.map +1 -1
- package/package.json +2 -2
- package/src/AzureDevOpsTool.ts +593 -193
package/src/AzureDevOpsTool.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import * as Duration from "effect/Duration"
|
|
2
1
|
import * as Effect from "effect/Effect"
|
|
3
|
-
import * as Redacted from "effect/Redacted"
|
|
4
2
|
import * as Schema from "effect/Schema"
|
|
5
3
|
import { Capabilities } from "@llm4ts/core/Capability"
|
|
6
|
-
import type {
|
|
7
|
-
import type { JsonValue } from "@llm4ts/core/providers/CliSupport"
|
|
4
|
+
import type { ProcessExecutorShape, ProcessResult } from "@llm4ts/core/ProcessExecutor"
|
|
8
5
|
import { ProcessError, type FlowError } from "./FlowError.ts"
|
|
9
6
|
import type { FlowEventsShape } from "./FlowEvents.ts"
|
|
10
7
|
import { guarded } from "./CapabilityGuard.ts"
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// Azure DevOps through the `az` CLI (ADR 0011), the sibling of GitHubTool's
|
|
10
|
+
// `gh` protocol: pure args builders, schema-decoded `--output json`, and
|
|
11
|
+
// capability guards around a ProcessExecutor. Credentials belong to the CLI
|
|
12
|
+
// (`az devops login`, or AZURE_DEVOPS_EXT_PAT in the process environment) —
|
|
13
|
+
// this module never reads, holds, or forwards a PAT, so no secret can reach
|
|
14
|
+
// argv, a log line, or a persisted plan.
|
|
15
|
+
|
|
16
|
+
export class AdoConfig extends Schema.Class<AdoConfig>("AdoConfig")({
|
|
17
|
+
orgUrl: Schema.String,
|
|
18
|
+
project: Schema.String,
|
|
19
|
+
repository: Schema.String,
|
|
20
|
+
// Only `az devops invoke` (the comments REST resource, which has no
|
|
21
|
+
// first-class `az boards` verb) needs an explicit API version; every
|
|
22
|
+
// other call is a versioned CLI command.
|
|
23
|
+
apiVersion: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("7.1-preview")))
|
|
17
24
|
}) {}
|
|
18
25
|
|
|
19
26
|
export class WorkItem extends Schema.Class<WorkItem>("WorkItem")({
|
|
@@ -22,7 +29,16 @@ export class WorkItem extends Schema.Class<WorkItem>("WorkItem")({
|
|
|
22
29
|
description: Schema.String,
|
|
23
30
|
acceptanceCriteria: Schema.String,
|
|
24
31
|
state: Schema.String,
|
|
25
|
-
tags: Schema.Array(Schema.String)
|
|
32
|
+
tags: Schema.Array(Schema.String),
|
|
33
|
+
createdBy: Schema.String,
|
|
34
|
+
changedDate: Schema.String
|
|
35
|
+
}) {}
|
|
36
|
+
|
|
37
|
+
export class WorkItemComment extends Schema.Class<WorkItemComment>("WorkItemComment")({
|
|
38
|
+
id: Schema.Int,
|
|
39
|
+
author: Schema.String,
|
|
40
|
+
text: Schema.String,
|
|
41
|
+
createdDate: Schema.String
|
|
26
42
|
}) {}
|
|
27
43
|
|
|
28
44
|
export class AdoPullRequest extends Schema.Class<AdoPullRequest>("AdoPullRequest")({
|
|
@@ -32,224 +48,555 @@ export class AdoPullRequest extends Schema.Class<AdoPullRequest>("AdoPullRequest
|
|
|
32
48
|
webUrl: Schema.String
|
|
33
49
|
}) {}
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
disallowJsonEncode: true
|
|
41
|
-
}),
|
|
42
|
-
apiVersion: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("7.1")))
|
|
43
|
-
}) {}
|
|
51
|
+
// Azure DevOps branch policies are the analogue of GitHub's check rollup.
|
|
52
|
+
// There is no policy status for a timed-out run, so the outcome set is the
|
|
53
|
+
// GitHub one minus "TimedOut".
|
|
54
|
+
export const PolicyOutcome = Schema.Literals(["Success", "Failure", "Pending"])
|
|
55
|
+
export type PolicyOutcome = typeof PolicyOutcome.Type
|
|
44
56
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const bits = (first << 16) | ((second ?? 0) << 8) | (third ?? 0)
|
|
54
|
-
encoded += alphabet[(bits >>> 18) & 63] ?? ""
|
|
55
|
-
encoded += alphabet[(bits >>> 12) & 63] ?? ""
|
|
56
|
-
encoded += second === undefined ? "=" : (alphabet[(bits >>> 6) & 63] ?? "")
|
|
57
|
-
encoded += third === undefined ? "=" : (alphabet[bits & 63] ?? "")
|
|
58
|
-
}
|
|
59
|
-
return encoded
|
|
57
|
+
export const WorkItemState = Schema.Literals(["open", "closed", "all"])
|
|
58
|
+
export type WorkItemState = typeof WorkItemState.Type
|
|
59
|
+
|
|
60
|
+
export interface WorkItemFilter {
|
|
61
|
+
readonly tags?: ReadonlyArray<string>
|
|
62
|
+
readonly state?: WorkItemState
|
|
63
|
+
readonly assignedTo?: string
|
|
64
|
+
readonly limit?: number
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
// `refs/heads/x` and `x` both name a branch across the Azure DevOps
|
|
68
|
+
// surface; the CLI wants the short form, so every ref is normalized once
|
|
69
|
+
// on the way into argv.
|
|
70
|
+
export const branchName = (ref: string): string => ref.trim().replace(/^refs\/heads\//, "")
|
|
65
71
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
value
|
|
75
|
-
}))
|
|
76
|
-
|
|
77
|
-
export const readWorkItemRequest = (config: AdoConfig, id: number): AdoRequest =>
|
|
78
|
-
AdoRequest.make({
|
|
79
|
-
method: "GET",
|
|
80
|
-
url: `${witBase(config)}/workitems/${id}?$expand=relations&api-version=${config.apiVersion}`
|
|
81
|
-
})
|
|
72
|
+
const org = (config: AdoConfig): ReadonlyArray<string> => [
|
|
73
|
+
"--org",
|
|
74
|
+
config.orgUrl,
|
|
75
|
+
// Without this the CLI probes the working directory's git remote and
|
|
76
|
+
// silently retargets another organization; a library must not guess.
|
|
77
|
+
"--detect",
|
|
78
|
+
"false"
|
|
79
|
+
]
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
const json = ["--output", "json"]
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Work item argv
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export const workItemShowArgs = (config: AdoConfig, id: number): ReadonlyArray<string> => [
|
|
88
|
+
"boards",
|
|
89
|
+
"work-item",
|
|
90
|
+
"show",
|
|
91
|
+
"--id",
|
|
92
|
+
String(id),
|
|
93
|
+
...org(config),
|
|
94
|
+
...json
|
|
95
|
+
]
|
|
89
96
|
|
|
90
|
-
export const
|
|
97
|
+
export const fieldArgs = (fields: Readonly<Record<string, string>>): ReadonlyArray<string> => {
|
|
98
|
+
const pairs = Object.entries(fields).map(([name, value]) => `${name}=${value}`)
|
|
99
|
+
return pairs.length === 0 ? [] : ["--fields", ...pairs]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const workItemUpdateArgs = (
|
|
91
103
|
config: AdoConfig,
|
|
92
104
|
id: number,
|
|
93
105
|
fields: Readonly<Record<string, string>>
|
|
94
|
-
):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
): ReadonlyArray<string> => [
|
|
107
|
+
"boards",
|
|
108
|
+
"work-item",
|
|
109
|
+
"update",
|
|
110
|
+
"--id",
|
|
111
|
+
String(id),
|
|
112
|
+
...fieldArgs(fields),
|
|
113
|
+
...org(config),
|
|
114
|
+
...json
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
export const workItemCommentArgs = (
|
|
118
|
+
config: AdoConfig,
|
|
119
|
+
id: number,
|
|
120
|
+
text: string
|
|
121
|
+
): ReadonlyArray<string> => [
|
|
122
|
+
"boards",
|
|
123
|
+
"work-item",
|
|
124
|
+
"update",
|
|
125
|
+
"--id",
|
|
126
|
+
String(id),
|
|
127
|
+
"--discussion",
|
|
128
|
+
text,
|
|
129
|
+
...org(config),
|
|
130
|
+
...json
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
export const workItemCreateArgs = (
|
|
134
|
+
config: AdoConfig,
|
|
135
|
+
workItemType: string,
|
|
136
|
+
title: string,
|
|
137
|
+
description: string,
|
|
138
|
+
tags: ReadonlyArray<string>
|
|
139
|
+
): ReadonlyArray<string> => [
|
|
140
|
+
"boards",
|
|
141
|
+
"work-item",
|
|
142
|
+
"create",
|
|
143
|
+
"--title",
|
|
144
|
+
title,
|
|
145
|
+
"--type",
|
|
146
|
+
workItemType,
|
|
147
|
+
"--description",
|
|
148
|
+
description,
|
|
149
|
+
"--project",
|
|
150
|
+
config.project,
|
|
151
|
+
...fieldArgs(tags.length === 0 ? {} : { "System.Tags": tags.join("; ") }),
|
|
152
|
+
...org(config),
|
|
153
|
+
...json
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
export const commentsArgs = (config: AdoConfig, id: number): ReadonlyArray<string> => [
|
|
157
|
+
"devops",
|
|
158
|
+
"invoke",
|
|
159
|
+
"--area",
|
|
160
|
+
"wit",
|
|
161
|
+
"--resource",
|
|
162
|
+
"comments",
|
|
163
|
+
"--route-parameters",
|
|
164
|
+
`project=${config.project}`,
|
|
165
|
+
`workItemId=${String(id)}`,
|
|
166
|
+
"--api-version",
|
|
167
|
+
config.apiVersion,
|
|
168
|
+
...org(config),
|
|
169
|
+
...json
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// WIQL
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
101
175
|
|
|
102
|
-
|
|
176
|
+
// WIQL string literals are single-quoted; a quote inside a value is escaped
|
|
177
|
+
// by doubling it. Every caller-supplied value goes through here so a tag
|
|
178
|
+
// like "won't fix" cannot terminate the literal and rewrite the query.
|
|
179
|
+
export const quoteWiql = (value: string): string => `'${value.replace(/'/g, "''")}'`
|
|
180
|
+
|
|
181
|
+
export const workItemFields: ReadonlyArray<string> = [
|
|
182
|
+
"System.Id",
|
|
183
|
+
"System.Title",
|
|
184
|
+
"System.Description",
|
|
185
|
+
"System.State",
|
|
186
|
+
"System.Tags",
|
|
187
|
+
"System.CreatedBy",
|
|
188
|
+
"System.ChangedDate",
|
|
189
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria"
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
export const wiqlFor = (filter: WorkItemFilter): string => {
|
|
193
|
+
const clauses = [
|
|
194
|
+
"[System.TeamProject] = @project",
|
|
195
|
+
...(filter.state === "all"
|
|
196
|
+
? []
|
|
197
|
+
: filter.state === "closed"
|
|
198
|
+
? ["[System.State] = 'Closed'"]
|
|
199
|
+
: ["[System.State] <> 'Closed'"]),
|
|
200
|
+
...(filter.tags ?? []).map((tag) => `[System.Tags] CONTAINS ${quoteWiql(tag)}`),
|
|
201
|
+
...(filter.assignedTo === undefined
|
|
202
|
+
? []
|
|
203
|
+
: [`[System.AssignedTo] = ${quoteWiql(filter.assignedTo)}`])
|
|
204
|
+
]
|
|
205
|
+
const select = workItemFields.map((name) => `[${name}]`).join(", ")
|
|
206
|
+
return (
|
|
207
|
+
`SELECT TOP ${String(filter.limit ?? 100)} ${select} FROM WorkItems ` +
|
|
208
|
+
`WHERE ${clauses.join(" AND ")} ORDER BY [System.Id] ASC`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const queryArgs = (config: AdoConfig, wiql: string): ReadonlyArray<string> => [
|
|
213
|
+
"boards",
|
|
214
|
+
"query",
|
|
215
|
+
"--wiql",
|
|
216
|
+
wiql,
|
|
217
|
+
"--project",
|
|
218
|
+
config.project,
|
|
219
|
+
...org(config),
|
|
220
|
+
...json
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Pull request argv
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
export const prCreateArgs = (
|
|
103
228
|
config: AdoConfig,
|
|
104
229
|
sourceRef: string,
|
|
105
230
|
targetRef: string,
|
|
106
231
|
title: string,
|
|
232
|
+
description: string,
|
|
233
|
+
draft = false
|
|
234
|
+
): ReadonlyArray<string> => [
|
|
235
|
+
"repos",
|
|
236
|
+
"pr",
|
|
237
|
+
"create",
|
|
238
|
+
"--repository",
|
|
239
|
+
config.repository,
|
|
240
|
+
"--project",
|
|
241
|
+
config.project,
|
|
242
|
+
"--source-branch",
|
|
243
|
+
branchName(sourceRef),
|
|
244
|
+
"--target-branch",
|
|
245
|
+
branchName(targetRef),
|
|
246
|
+
"--title",
|
|
247
|
+
title,
|
|
248
|
+
"--description",
|
|
249
|
+
description,
|
|
250
|
+
...(draft ? ["--draft", "true"] : []),
|
|
251
|
+
...org(config),
|
|
252
|
+
...json
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
export const prListArgs = (config: AdoConfig, sourceRef?: string): ReadonlyArray<string> => [
|
|
256
|
+
"repos",
|
|
257
|
+
"pr",
|
|
258
|
+
"list",
|
|
259
|
+
"--repository",
|
|
260
|
+
config.repository,
|
|
261
|
+
"--project",
|
|
262
|
+
config.project,
|
|
263
|
+
"--status",
|
|
264
|
+
"active",
|
|
265
|
+
...(sourceRef === undefined ? [] : ["--source-branch", branchName(sourceRef)]),
|
|
266
|
+
...org(config),
|
|
267
|
+
...json
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
export const prUpdateArgs = (
|
|
271
|
+
config: AdoConfig,
|
|
272
|
+
id: number,
|
|
273
|
+
title: string,
|
|
107
274
|
description: string
|
|
108
|
-
):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
275
|
+
): ReadonlyArray<string> => [
|
|
276
|
+
"repos",
|
|
277
|
+
"pr",
|
|
278
|
+
"update",
|
|
279
|
+
"--id",
|
|
280
|
+
String(id),
|
|
281
|
+
"--title",
|
|
282
|
+
title,
|
|
283
|
+
"--description",
|
|
284
|
+
description,
|
|
285
|
+
...org(config),
|
|
286
|
+
...json
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
export const prCommentArgs = (
|
|
290
|
+
config: AdoConfig,
|
|
291
|
+
id: number,
|
|
292
|
+
text: string
|
|
293
|
+
): ReadonlyArray<string> => [
|
|
294
|
+
"repos",
|
|
295
|
+
"pr",
|
|
296
|
+
"thread",
|
|
297
|
+
"create",
|
|
298
|
+
"--id",
|
|
299
|
+
String(id),
|
|
300
|
+
"--content",
|
|
301
|
+
text,
|
|
302
|
+
"--project",
|
|
303
|
+
config.project,
|
|
304
|
+
...org(config),
|
|
305
|
+
...json
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
export const prPolicyArgs = (config: AdoConfig, id: number): ReadonlyArray<string> => [
|
|
309
|
+
"repos",
|
|
310
|
+
"pr",
|
|
311
|
+
"policy",
|
|
312
|
+
"list",
|
|
313
|
+
"--id",
|
|
314
|
+
String(id),
|
|
315
|
+
...org(config),
|
|
316
|
+
...json
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
export const prCompleteArgs = (
|
|
320
|
+
config: AdoConfig,
|
|
321
|
+
id: number,
|
|
322
|
+
squash: boolean,
|
|
323
|
+
deleteSourceBranch: boolean
|
|
324
|
+
): ReadonlyArray<string> => [
|
|
325
|
+
"repos",
|
|
326
|
+
"pr",
|
|
327
|
+
"update",
|
|
328
|
+
"--id",
|
|
329
|
+
String(id),
|
|
330
|
+
"--status",
|
|
331
|
+
"completed",
|
|
332
|
+
"--squash",
|
|
333
|
+
squash ? "true" : "false",
|
|
334
|
+
"--delete-source-branch",
|
|
335
|
+
deleteSourceBranch ? "true" : "false",
|
|
336
|
+
...org(config),
|
|
337
|
+
...json
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Parsing
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
// System.CreatedBy is an identity object on current API versions and a bare
|
|
345
|
+
// display string on older ones; both decode to the display name.
|
|
346
|
+
const Identity = Schema.Union([
|
|
347
|
+
Schema.String,
|
|
348
|
+
Schema.Struct({
|
|
349
|
+
displayName: Schema.optionalKey(Schema.String),
|
|
350
|
+
uniqueName: Schema.optionalKey(Schema.String)
|
|
118
351
|
})
|
|
352
|
+
])
|
|
119
353
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
354
|
+
const identityName = (value: typeof Identity.Type | undefined): string =>
|
|
355
|
+
value === undefined
|
|
356
|
+
? ""
|
|
357
|
+
: typeof value === "string"
|
|
358
|
+
? value
|
|
359
|
+
: (value.displayName ?? value.uniqueName ?? "")
|
|
124
360
|
|
|
125
|
-
|
|
126
|
-
Schema.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
361
|
+
const AdoFields = Schema.Struct({
|
|
362
|
+
"System.Title": Schema.optionalKey(Schema.String),
|
|
363
|
+
"System.Description": Schema.optionalKey(Schema.String),
|
|
364
|
+
"System.State": Schema.optionalKey(Schema.String),
|
|
365
|
+
"System.Tags": Schema.optionalKey(Schema.String),
|
|
366
|
+
"System.CreatedBy": Schema.optionalKey(Identity),
|
|
367
|
+
"System.ChangedDate": Schema.optionalKey(Schema.String),
|
|
368
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria": Schema.optionalKey(Schema.String)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
const AdoWorkItem = Schema.Struct({
|
|
372
|
+
id: Schema.Int,
|
|
373
|
+
fields: AdoFields
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
export const parseTags = (raw: string): ReadonlyArray<string> =>
|
|
377
|
+
raw
|
|
378
|
+
.split(";")
|
|
379
|
+
.map((tag) => tag.trim())
|
|
380
|
+
.filter((tag) => tag.length > 0)
|
|
381
|
+
|
|
382
|
+
const toWorkItem = (item: typeof AdoWorkItem.Type): WorkItem =>
|
|
383
|
+
WorkItem.make({
|
|
384
|
+
id: item.id,
|
|
385
|
+
title: item.fields["System.Title"] ?? "",
|
|
386
|
+
description: item.fields["System.Description"] ?? "",
|
|
387
|
+
acceptanceCriteria: item.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
388
|
+
state: item.fields["System.State"] ?? "",
|
|
389
|
+
tags: parseTags(item.fields["System.Tags"] ?? ""),
|
|
390
|
+
createdBy: identityName(item.fields["System.CreatedBy"]),
|
|
391
|
+
changedDate: item.fields["System.ChangedDate"] ?? ""
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
const decodeFailure =
|
|
395
|
+
(message: string) =>
|
|
396
|
+
(error: unknown): ProcessError =>
|
|
397
|
+
ProcessError.make({ message, detail: String(error) })
|
|
398
|
+
|
|
399
|
+
export const parseWorkItem = (payload: string): Effect.Effect<WorkItem, ProcessError> =>
|
|
400
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoWorkItem))(payload).pipe(
|
|
401
|
+
Effect.map(toWorkItem),
|
|
402
|
+
Effect.mapError(decodeFailure("az boards work-item show"))
|
|
154
403
|
)
|
|
155
404
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
405
|
+
// `az boards query` flattens the WIQL result into a work-item array, so a
|
|
406
|
+
// queue poll is a single call rather than a fan-out over ids.
|
|
407
|
+
export const parseWorkItems = (
|
|
408
|
+
payload: string
|
|
409
|
+
): Effect.Effect<ReadonlyArray<WorkItem>, ProcessError> =>
|
|
410
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoWorkItem)))(payload).pipe(
|
|
411
|
+
Effect.map((items) => items.map(toWorkItem)),
|
|
412
|
+
Effect.mapError(decodeFailure("az boards query"))
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
export const parseWorkItemIds = (
|
|
416
|
+
payload: string
|
|
417
|
+
): Effect.Effect<ReadonlyArray<number>, ProcessError> =>
|
|
418
|
+
parseWorkItems(payload).pipe(Effect.map((items) => items.map((item) => item.id)))
|
|
419
|
+
|
|
420
|
+
const AdoComments = Schema.Struct({
|
|
421
|
+
comments: Schema.Array(
|
|
422
|
+
Schema.Struct({
|
|
423
|
+
id: Schema.Int,
|
|
424
|
+
text: Schema.optionalKey(Schema.String),
|
|
425
|
+
createdBy: Schema.optionalKey(Identity),
|
|
426
|
+
createdDate: Schema.optionalKey(Schema.String)
|
|
427
|
+
})
|
|
428
|
+
).pipe(Schema.withConstructorDefault(Effect.succeed(Object.freeze([]))))
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
export const parseComments = (
|
|
432
|
+
payload: string
|
|
433
|
+
): Effect.Effect<ReadonlyArray<WorkItemComment>, ProcessError> =>
|
|
434
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoComments))(payload).pipe(
|
|
435
|
+
Effect.map((parsed) =>
|
|
436
|
+
parsed.comments.map((comment) =>
|
|
437
|
+
WorkItemComment.make({
|
|
438
|
+
id: comment.id,
|
|
439
|
+
author: identityName(comment.createdBy),
|
|
440
|
+
text: comment.text ?? "",
|
|
441
|
+
createdDate: comment.createdDate ?? ""
|
|
442
|
+
})
|
|
443
|
+
)
|
|
444
|
+
),
|
|
445
|
+
Effect.mapError(decodeFailure("az devops invoke wit comments"))
|
|
175
446
|
)
|
|
176
447
|
|
|
448
|
+
const AdoPr = Schema.Struct({
|
|
449
|
+
pullRequestId: Schema.Int,
|
|
450
|
+
repository: Schema.Struct({
|
|
451
|
+
id: Schema.String,
|
|
452
|
+
project: Schema.Struct({ id: Schema.String })
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const toPullRequest = (config: AdoConfig, pr: typeof AdoPr.Type): AdoPullRequest =>
|
|
457
|
+
AdoPullRequest.make({
|
|
458
|
+
id: pr.pullRequestId,
|
|
459
|
+
repoId: pr.repository.id,
|
|
460
|
+
projectId: pr.repository.project.id,
|
|
461
|
+
webUrl:
|
|
462
|
+
`${config.orgUrl}/${config.project}/_git/` +
|
|
463
|
+
`${config.repository}/pullrequest/${String(pr.pullRequestId)}`
|
|
464
|
+
})
|
|
465
|
+
|
|
177
466
|
export const parsePullRequest = (
|
|
178
467
|
config: AdoConfig,
|
|
179
|
-
|
|
468
|
+
payload: string
|
|
180
469
|
): Effect.Effect<AdoPullRequest, ProcessError> =>
|
|
181
|
-
Schema.decodeUnknownEffect(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
470
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPr))(payload).pipe(
|
|
471
|
+
Effect.map((pr) => toPullRequest(config, pr)),
|
|
472
|
+
Effect.mapError(decodeFailure("az repos pr"))
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
export const parsePullRequests = (
|
|
476
|
+
config: AdoConfig,
|
|
477
|
+
payload: string
|
|
478
|
+
): Effect.Effect<ReadonlyArray<AdoPullRequest>, ProcessError> =>
|
|
479
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoPr)))(payload).pipe(
|
|
480
|
+
Effect.map((prs) => prs.map((pr) => toPullRequest(config, pr))),
|
|
481
|
+
Effect.mapError(decodeFailure("az repos pr list"))
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
const AdoPolicies = Schema.Array(
|
|
485
|
+
Schema.Struct({
|
|
486
|
+
status: Schema.optionalKey(Schema.String)
|
|
487
|
+
})
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
// Policy evaluation statuses: queued/running are still deciding, and
|
|
491
|
+
// rejected/broken have already decided against the PR.
|
|
492
|
+
export const outcomeFromPolicies = (payload: string): Effect.Effect<PolicyOutcome, ProcessError> =>
|
|
493
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPolicies))(payload).pipe(
|
|
494
|
+
Effect.map((policies) => {
|
|
495
|
+
const statuses = policies.map((policy) => policy.status?.toLowerCase() ?? "")
|
|
496
|
+
return statuses.some((value) => ["queued", "running"].includes(value))
|
|
497
|
+
? "Pending"
|
|
498
|
+
: statuses.some((value) => ["rejected", "broken"].includes(value))
|
|
499
|
+
? "Failure"
|
|
500
|
+
: "Success"
|
|
501
|
+
}),
|
|
502
|
+
Effect.mapError(decodeFailure("az repos pr policy list"))
|
|
210
503
|
)
|
|
211
504
|
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Tool
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
212
509
|
export interface AzureDevOpsToolShape {
|
|
213
510
|
readonly readWorkItem: (id: number) => Effect.Effect<WorkItem, FlowError>
|
|
511
|
+
readonly listWorkItems: (
|
|
512
|
+
filter?: WorkItemFilter
|
|
513
|
+
) => Effect.Effect<ReadonlyArray<WorkItem>, FlowError>
|
|
214
514
|
readonly wiqlIds: (query: string) => Effect.Effect<ReadonlyArray<number>, FlowError>
|
|
515
|
+
readonly readComments: (id: number) => Effect.Effect<ReadonlyArray<WorkItemComment>, FlowError>
|
|
215
516
|
readonly setFields: (
|
|
216
517
|
id: number,
|
|
217
518
|
fields: Readonly<Record<string, string>>
|
|
218
519
|
) => Effect.Effect<void, FlowError>
|
|
219
520
|
readonly setState: (id: number, state: string) => Effect.Effect<void, FlowError>
|
|
220
521
|
readonly setAcceptanceCriteria: (id: number, text: string) => Effect.Effect<void, FlowError>
|
|
522
|
+
// Tags are one semicolon-joined field, so an edit is read-merge-write
|
|
523
|
+
// rather than the add/remove verbs a label API would offer.
|
|
524
|
+
readonly editTags: (
|
|
525
|
+
id: number,
|
|
526
|
+
add: ReadonlyArray<string>,
|
|
527
|
+
remove: ReadonlyArray<string>
|
|
528
|
+
) => Effect.Effect<void, FlowError>
|
|
529
|
+
readonly writeComment: (id: number, text: string) => Effect.Effect<void, FlowError>
|
|
530
|
+
readonly createWorkItem: (
|
|
531
|
+
workItemType: string,
|
|
532
|
+
title: string,
|
|
533
|
+
description: string,
|
|
534
|
+
tags?: ReadonlyArray<string>
|
|
535
|
+
) => Effect.Effect<WorkItem, FlowError>
|
|
221
536
|
readonly createPr: (
|
|
222
537
|
sourceRef: string,
|
|
223
538
|
targetRef: string,
|
|
224
539
|
title: string,
|
|
225
|
-
body: string
|
|
540
|
+
body: string,
|
|
541
|
+
draft?: boolean
|
|
226
542
|
) => Effect.Effect<AdoPullRequest, FlowError>
|
|
543
|
+
readonly openPrForBranch: (
|
|
544
|
+
sourceRef: string
|
|
545
|
+
) => Effect.Effect<AdoPullRequest | undefined, FlowError>
|
|
546
|
+
readonly updatePr: (
|
|
547
|
+
pr: AdoPullRequest,
|
|
548
|
+
title: string,
|
|
549
|
+
body: string
|
|
550
|
+
) => Effect.Effect<void, FlowError>
|
|
551
|
+
readonly writePrComment: (pr: AdoPullRequest, body: string) => Effect.Effect<void, FlowError>
|
|
552
|
+
readonly prPolicies: (pr: AdoPullRequest) => Effect.Effect<PolicyOutcome, FlowError>
|
|
553
|
+
readonly completePr: (
|
|
554
|
+
pr: AdoPullRequest,
|
|
555
|
+
squash?: boolean,
|
|
556
|
+
deleteSourceBranch?: boolean
|
|
557
|
+
) => Effect.Effect<void, FlowError>
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const output = (result: ProcessResult): string => result.stdout.join("\n").trim()
|
|
561
|
+
|
|
562
|
+
export const mergeTags = (
|
|
563
|
+
current: ReadonlyArray<string>,
|
|
564
|
+
add: ReadonlyArray<string>,
|
|
565
|
+
remove: ReadonlyArray<string>
|
|
566
|
+
): ReadonlyArray<string> => {
|
|
567
|
+
const removed = new Set(remove.map((tag) => tag.toLowerCase()))
|
|
568
|
+
const kept = current.filter((tag) => !removed.has(tag.toLowerCase()))
|
|
569
|
+
const present = new Set(kept.map((tag) => tag.toLowerCase()))
|
|
570
|
+
const added = add.filter(
|
|
571
|
+
(tag) => !present.has(tag.toLowerCase()) && !removed.has(tag.toLowerCase())
|
|
572
|
+
)
|
|
573
|
+
return [...kept, ...added]
|
|
227
574
|
}
|
|
228
575
|
|
|
229
576
|
export const makeAzureDevOpsTool = (
|
|
230
577
|
config: AdoConfig,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
578
|
+
process: ProcessExecutorShape,
|
|
579
|
+
workDir: string,
|
|
580
|
+
events: FlowEventsShape
|
|
234
581
|
): AzureDevOpsToolShape => {
|
|
235
|
-
const run = (
|
|
236
|
-
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
582
|
+
const run = (args: ReadonlyArray<string>): Effect.Effect<string, FlowError> =>
|
|
583
|
+
process.run(["az", ...args], workDir, {}).pipe(
|
|
584
|
+
Effect.mapError((error) =>
|
|
585
|
+
ProcessError.make({ message: `az ${args.join(" ")}`, detail: error.message })
|
|
586
|
+
),
|
|
587
|
+
Effect.flatMap((result) =>
|
|
588
|
+
result.exitCode === 0
|
|
589
|
+
? Effect.succeed(output(result))
|
|
590
|
+
: Effect.fail(
|
|
591
|
+
ProcessError.make({
|
|
592
|
+
message: `az ${args.join(" ")}`,
|
|
593
|
+
detail:
|
|
594
|
+
[...result.stdout, ...result.stderr].join("\n").trim() ||
|
|
595
|
+
`exit code ${result.exitCode}`
|
|
596
|
+
})
|
|
597
|
+
)
|
|
252
598
|
)
|
|
599
|
+
)
|
|
253
600
|
|
|
254
601
|
const read = <A>(
|
|
255
602
|
operation: string,
|
|
@@ -260,32 +607,85 @@ export const makeAzureDevOpsTool = (
|
|
|
260
607
|
effect: Effect.Effect<A, FlowError>
|
|
261
608
|
): Effect.Effect<A, FlowError> => guarded(Capabilities.AdoWrite, operation, events, effect)
|
|
262
609
|
|
|
610
|
+
const readWorkItem = (id: number): Effect.Effect<WorkItem, FlowError> =>
|
|
611
|
+
read("ado readWorkItem", run(workItemShowArgs(config, id)).pipe(Effect.flatMap(parseWorkItem)))
|
|
612
|
+
|
|
263
613
|
const setFields = (
|
|
264
614
|
id: number,
|
|
265
615
|
fields: Readonly<Record<string, string>>
|
|
266
616
|
): Effect.Effect<void, FlowError> =>
|
|
267
|
-
write("ado setFields", run(
|
|
617
|
+
write("ado setFields", run(workItemUpdateArgs(config, id, fields)).pipe(Effect.asVoid))
|
|
618
|
+
|
|
619
|
+
const listPrs = (sourceRef?: string): Effect.Effect<ReadonlyArray<AdoPullRequest>, FlowError> =>
|
|
620
|
+
run(prListArgs(config, sourceRef)).pipe(
|
|
621
|
+
Effect.flatMap((payload) => parsePullRequests(config, payload))
|
|
622
|
+
)
|
|
268
623
|
|
|
269
624
|
return {
|
|
270
|
-
readWorkItem
|
|
625
|
+
readWorkItem,
|
|
626
|
+
listWorkItems: (filter = {}) =>
|
|
271
627
|
read(
|
|
272
|
-
"ado
|
|
273
|
-
run(
|
|
628
|
+
"ado listWorkItems",
|
|
629
|
+
run(queryArgs(config, wiqlFor(filter))).pipe(Effect.flatMap(parseWorkItems))
|
|
274
630
|
),
|
|
275
631
|
wiqlIds: (query) =>
|
|
276
|
-
read("ado wiql", run(
|
|
632
|
+
read("ado wiql", run(queryArgs(config, query)).pipe(Effect.flatMap(parseWorkItemIds))),
|
|
633
|
+
readComments: (id) =>
|
|
634
|
+
read("ado readComments", run(commentsArgs(config, id)).pipe(Effect.flatMap(parseComments))),
|
|
277
635
|
setFields,
|
|
278
636
|
setState: (id, state) => setFields(id, { "System.State": state }),
|
|
279
637
|
setAcceptanceCriteria: (id, text) =>
|
|
280
|
-
setFields(id, {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
638
|
+
setFields(id, { "Microsoft.VSTS.Common.AcceptanceCriteria": text }),
|
|
639
|
+
editTags: (id, add, remove) =>
|
|
640
|
+
add.length === 0 && remove.length === 0
|
|
641
|
+
? Effect.void
|
|
642
|
+
: readWorkItem(id).pipe(
|
|
643
|
+
Effect.flatMap((item) => {
|
|
644
|
+
const next = mergeTags(item.tags, add, remove)
|
|
645
|
+
return next.length === item.tags.length &&
|
|
646
|
+
next.every((tag, index) => tag === item.tags[index])
|
|
647
|
+
? Effect.void
|
|
648
|
+
: setFields(id, { "System.Tags": next.join("; ") })
|
|
649
|
+
})
|
|
650
|
+
),
|
|
651
|
+
writeComment: (id, text) =>
|
|
652
|
+
write("ado writeComment", run(workItemCommentArgs(config, id, text)).pipe(Effect.asVoid)),
|
|
653
|
+
createWorkItem: (workItemType, title, description, tags = []) =>
|
|
284
654
|
write(
|
|
285
|
-
"ado
|
|
286
|
-
run(
|
|
287
|
-
Effect.flatMap(
|
|
655
|
+
"ado createWorkItem",
|
|
656
|
+
run(workItemCreateArgs(config, workItemType, title, description, tags)).pipe(
|
|
657
|
+
Effect.flatMap(parseWorkItem)
|
|
288
658
|
)
|
|
659
|
+
),
|
|
660
|
+
createPr: (sourceRef, targetRef, title, body, draft = false) =>
|
|
661
|
+
write(
|
|
662
|
+
"ado createPr",
|
|
663
|
+
// An active PR for the branch already IS the deliverable; creating a
|
|
664
|
+
// second one would fail on the server and lose the first's reviews.
|
|
665
|
+
Effect.flatMap(listPrs(sourceRef), (existing) => {
|
|
666
|
+
const open = existing[0]
|
|
667
|
+
return open !== undefined
|
|
668
|
+
? Effect.succeed(open)
|
|
669
|
+
: run(prCreateArgs(config, sourceRef, targetRef, title, body, draft)).pipe(
|
|
670
|
+
Effect.flatMap((payload) => parsePullRequest(config, payload))
|
|
671
|
+
)
|
|
672
|
+
})
|
|
673
|
+
),
|
|
674
|
+
openPrForBranch: (sourceRef) =>
|
|
675
|
+
read("ado listPrs", listPrs(sourceRef).pipe(Effect.map((prs) => prs[0]))),
|
|
676
|
+
updatePr: (pr, title, body) =>
|
|
677
|
+
write("ado updatePr", run(prUpdateArgs(config, pr.id, title, body)).pipe(Effect.asVoid)),
|
|
678
|
+
writePrComment: (pr, body) =>
|
|
679
|
+
write("ado writePrComment", run(prCommentArgs(config, pr.id, body)).pipe(Effect.asVoid)),
|
|
680
|
+
prPolicies: (pr) =>
|
|
681
|
+
read(
|
|
682
|
+
"ado prPolicies",
|
|
683
|
+
run(prPolicyArgs(config, pr.id)).pipe(Effect.flatMap(outcomeFromPolicies))
|
|
684
|
+
),
|
|
685
|
+
completePr: (pr, squash = true, deleteSourceBranch = true) =>
|
|
686
|
+
write(
|
|
687
|
+
"ado completePr",
|
|
688
|
+
run(prCompleteArgs(config, pr.id, squash, deleteSourceBranch)).pipe(Effect.asVoid)
|
|
289
689
|
)
|
|
290
690
|
}
|
|
291
691
|
}
|