@llm4ts/flow 0.11.0 → 0.13.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 +95 -25
- package/dist/AzureDevOpsTool.d.ts.map +1 -1
- package/dist/AzureDevOpsTool.js +524 -119
- package/dist/AzureDevOpsTool.js.map +1 -1
- package/package.json +2 -2
- package/src/AzureDevOpsTool.ts +804 -188
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,759 @@ 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
|
+
// ---------------------------------------------------------------------------
|
|
89
86
|
|
|
90
|
-
export const
|
|
87
|
+
export const workItemShowArgs = (
|
|
88
|
+
config: AdoConfig,
|
|
89
|
+
id: number,
|
|
90
|
+
expand?: "relations" | "all"
|
|
91
|
+
): ReadonlyArray<string> => [
|
|
92
|
+
"boards",
|
|
93
|
+
"work-item",
|
|
94
|
+
"show",
|
|
95
|
+
"--id",
|
|
96
|
+
String(id),
|
|
97
|
+
...(expand === undefined ? [] : ["--expand", expand]),
|
|
98
|
+
...org(config),
|
|
99
|
+
...json
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
export const fieldArgs = (fields: Readonly<Record<string, string>>): ReadonlyArray<string> => {
|
|
103
|
+
const pairs = Object.entries(fields).map(([name, value]) => `${name}=${value}`)
|
|
104
|
+
return pairs.length === 0 ? [] : ["--fields", ...pairs]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const workItemUpdateArgs = (
|
|
91
108
|
config: AdoConfig,
|
|
92
109
|
id: number,
|
|
93
110
|
fields: Readonly<Record<string, string>>
|
|
94
|
-
):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
): ReadonlyArray<string> => [
|
|
112
|
+
"boards",
|
|
113
|
+
"work-item",
|
|
114
|
+
"update",
|
|
115
|
+
"--id",
|
|
116
|
+
String(id),
|
|
117
|
+
...fieldArgs(fields),
|
|
118
|
+
...org(config),
|
|
119
|
+
...json
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
export const workItemCommentArgs = (
|
|
123
|
+
config: AdoConfig,
|
|
124
|
+
id: number,
|
|
125
|
+
text: string
|
|
126
|
+
): ReadonlyArray<string> => [
|
|
127
|
+
"boards",
|
|
128
|
+
"work-item",
|
|
129
|
+
"update",
|
|
130
|
+
"--id",
|
|
131
|
+
String(id),
|
|
132
|
+
"--discussion",
|
|
133
|
+
text,
|
|
134
|
+
...org(config),
|
|
135
|
+
...json
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
export const workItemCreateArgs = (
|
|
139
|
+
config: AdoConfig,
|
|
140
|
+
workItemType: string,
|
|
141
|
+
title: string,
|
|
142
|
+
description: string,
|
|
143
|
+
tags: ReadonlyArray<string>
|
|
144
|
+
): ReadonlyArray<string> => [
|
|
145
|
+
"boards",
|
|
146
|
+
"work-item",
|
|
147
|
+
"create",
|
|
148
|
+
"--title",
|
|
149
|
+
title,
|
|
150
|
+
"--type",
|
|
151
|
+
workItemType,
|
|
152
|
+
"--description",
|
|
153
|
+
description,
|
|
154
|
+
"--project",
|
|
155
|
+
config.project,
|
|
156
|
+
...fieldArgs(tags.length === 0 ? {} : { "System.Tags": tags.join("; ") }),
|
|
157
|
+
...org(config),
|
|
158
|
+
...json
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
export const commentsArgs = (config: AdoConfig, id: number): ReadonlyArray<string> => [
|
|
162
|
+
"devops",
|
|
163
|
+
"invoke",
|
|
164
|
+
"--area",
|
|
165
|
+
"wit",
|
|
166
|
+
"--resource",
|
|
167
|
+
"comments",
|
|
168
|
+
"--route-parameters",
|
|
169
|
+
`project=${config.project}`,
|
|
170
|
+
`workItemId=${String(id)}`,
|
|
171
|
+
"--api-version",
|
|
172
|
+
config.apiVersion,
|
|
173
|
+
...org(config),
|
|
174
|
+
...json
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// WIQL
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
101
180
|
|
|
102
|
-
|
|
181
|
+
// WIQL string literals are single-quoted; a quote inside a value is escaped
|
|
182
|
+
// by doubling it. Every caller-supplied value goes through here so a tag
|
|
183
|
+
// like "won't fix" cannot terminate the literal and rewrite the query.
|
|
184
|
+
export const quoteWiql = (value: string): string => `'${value.replace(/'/g, "''")}'`
|
|
185
|
+
|
|
186
|
+
export const workItemFields: ReadonlyArray<string> = [
|
|
187
|
+
"System.Id",
|
|
188
|
+
"System.Title",
|
|
189
|
+
"System.Description",
|
|
190
|
+
"System.State",
|
|
191
|
+
"System.Tags",
|
|
192
|
+
"System.CreatedBy",
|
|
193
|
+
"System.ChangedDate",
|
|
194
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria"
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
export const wiqlFor = (filter: WorkItemFilter): string => {
|
|
198
|
+
const clauses = [
|
|
199
|
+
"[System.TeamProject] = @project",
|
|
200
|
+
...(filter.state === "all"
|
|
201
|
+
? []
|
|
202
|
+
: filter.state === "closed"
|
|
203
|
+
? ["[System.State] = 'Closed'"]
|
|
204
|
+
: ["[System.State] <> 'Closed'"]),
|
|
205
|
+
...(filter.tags ?? []).map((tag) => `[System.Tags] CONTAINS ${quoteWiql(tag)}`),
|
|
206
|
+
...(filter.assignedTo === undefined
|
|
207
|
+
? []
|
|
208
|
+
: [`[System.AssignedTo] = ${quoteWiql(filter.assignedTo)}`])
|
|
209
|
+
]
|
|
210
|
+
const select = workItemFields.map((name) => `[${name}]`).join(", ")
|
|
211
|
+
return (
|
|
212
|
+
`SELECT TOP ${String(filter.limit ?? 100)} ${select} FROM WorkItems ` +
|
|
213
|
+
`WHERE ${clauses.join(" AND ")} ORDER BY [System.Id] ASC`
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const queryArgs = (config: AdoConfig, wiql: string): ReadonlyArray<string> => [
|
|
218
|
+
"boards",
|
|
219
|
+
"query",
|
|
220
|
+
"--wiql",
|
|
221
|
+
wiql,
|
|
222
|
+
"--project",
|
|
223
|
+
config.project,
|
|
224
|
+
...org(config),
|
|
225
|
+
...json
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Pull request argv
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export const prCreateArgs = (
|
|
103
233
|
config: AdoConfig,
|
|
104
234
|
sourceRef: string,
|
|
105
235
|
targetRef: string,
|
|
106
236
|
title: string,
|
|
237
|
+
description: string,
|
|
238
|
+
draft = false
|
|
239
|
+
): ReadonlyArray<string> => [
|
|
240
|
+
"repos",
|
|
241
|
+
"pr",
|
|
242
|
+
"create",
|
|
243
|
+
"--repository",
|
|
244
|
+
config.repository,
|
|
245
|
+
"--project",
|
|
246
|
+
config.project,
|
|
247
|
+
"--source-branch",
|
|
248
|
+
branchName(sourceRef),
|
|
249
|
+
"--target-branch",
|
|
250
|
+
branchName(targetRef),
|
|
251
|
+
"--title",
|
|
252
|
+
title,
|
|
253
|
+
"--description",
|
|
254
|
+
description,
|
|
255
|
+
...(draft ? ["--draft", "true"] : []),
|
|
256
|
+
...org(config),
|
|
257
|
+
...json
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
export const prListArgs = (config: AdoConfig, sourceRef?: string): ReadonlyArray<string> => [
|
|
261
|
+
"repos",
|
|
262
|
+
"pr",
|
|
263
|
+
"list",
|
|
264
|
+
"--repository",
|
|
265
|
+
config.repository,
|
|
266
|
+
"--project",
|
|
267
|
+
config.project,
|
|
268
|
+
"--status",
|
|
269
|
+
"active",
|
|
270
|
+
...(sourceRef === undefined ? [] : ["--source-branch", branchName(sourceRef)]),
|
|
271
|
+
...org(config),
|
|
272
|
+
...json
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
export const prUpdateArgs = (
|
|
276
|
+
config: AdoConfig,
|
|
277
|
+
id: number,
|
|
278
|
+
title: string,
|
|
107
279
|
description: string
|
|
108
|
-
):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
280
|
+
): ReadonlyArray<string> => [
|
|
281
|
+
"repos",
|
|
282
|
+
"pr",
|
|
283
|
+
"update",
|
|
284
|
+
"--id",
|
|
285
|
+
String(id),
|
|
286
|
+
"--title",
|
|
287
|
+
title,
|
|
288
|
+
"--description",
|
|
289
|
+
description,
|
|
290
|
+
...org(config),
|
|
291
|
+
...json
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
export const prCommentArgs = (
|
|
295
|
+
config: AdoConfig,
|
|
296
|
+
id: number,
|
|
297
|
+
text: string
|
|
298
|
+
): ReadonlyArray<string> => [
|
|
299
|
+
"repos",
|
|
300
|
+
"pr",
|
|
301
|
+
"thread",
|
|
302
|
+
"create",
|
|
303
|
+
"--id",
|
|
304
|
+
String(id),
|
|
305
|
+
"--content",
|
|
306
|
+
text,
|
|
307
|
+
"--project",
|
|
308
|
+
config.project,
|
|
309
|
+
...org(config),
|
|
310
|
+
...json
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Development links
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
// The "Development" section of a work item is a set of ArtifactLink
|
|
318
|
+
// relations pointing at git objects. Their URLs are `vstfs:` URIs carrying
|
|
319
|
+
// GUIDs, not names — which is why `repository` below exists: a caller that
|
|
320
|
+
// knows a repository by name has to resolve its id before it can link
|
|
321
|
+
// anything to it, and has to reverse the mapping to read a link back.
|
|
322
|
+
export const GitArtifactKind = Schema.Literals(["Branch", "PullRequest", "Commit"])
|
|
323
|
+
export type GitArtifactKind = typeof GitArtifactKind.Type
|
|
324
|
+
|
|
325
|
+
export class GitArtifact extends Schema.Class<GitArtifact>("GitArtifact")({
|
|
326
|
+
kind: GitArtifactKind,
|
|
327
|
+
projectId: Schema.String,
|
|
328
|
+
repositoryId: Schema.String,
|
|
329
|
+
// Branch name, pull request id, or commit sha, by kind.
|
|
330
|
+
value: Schema.String
|
|
331
|
+
}) {}
|
|
332
|
+
|
|
333
|
+
export const gitArtifactKinds: ReadonlyArray<GitArtifactKind> = ["Branch", "PullRequest", "Commit"]
|
|
119
334
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
335
|
+
// The CLI and the REST payloads name these links in prose, not by kind.
|
|
336
|
+
const linkNames: Readonly<Record<GitArtifactKind, string>> = {
|
|
337
|
+
Branch: "Branch",
|
|
338
|
+
PullRequest: "Pull Request",
|
|
339
|
+
Commit: "Fixed in Commit"
|
|
123
340
|
}
|
|
124
341
|
|
|
125
|
-
export const
|
|
126
|
-
|
|
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
|
-
|
|
342
|
+
export const artifactLinkName = (kind: GitArtifactKind): string => linkNames[kind]
|
|
343
|
+
|
|
344
|
+
export const artifactKindOfName = (name: string): GitArtifactKind | undefined =>
|
|
345
|
+
gitArtifactKinds.find((kind) => linkNames[kind].toLowerCase() === name.trim().toLowerCase())
|
|
346
|
+
|
|
347
|
+
const uriSegment: Readonly<Record<GitArtifactKind, string>> = {
|
|
348
|
+
Branch: "Ref",
|
|
349
|
+
PullRequest: "PullRequestId",
|
|
350
|
+
Commit: "Commit"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// `vstfs:///Git/Ref/{project}%2F{repo}%2FGB{branch}` — the whole
|
|
354
|
+
// project/repo/value triple is ONE percent-encoded segment, which is what
|
|
355
|
+
// lets a branch name contain slashes without splitting the URI.
|
|
356
|
+
export const artifactUri = (artifact: GitArtifact): string => {
|
|
357
|
+
const value = artifact.kind === "Branch" ? `GB${artifact.value}` : artifact.value
|
|
358
|
+
return (
|
|
359
|
+
`vstfs:///Git/${uriSegment[artifact.kind]}/` +
|
|
360
|
+
encodeURIComponent(`${artifact.projectId}/${artifact.repositoryId}/${value}`)
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export const parseArtifactUri = (uri: string): GitArtifact | undefined => {
|
|
365
|
+
const match = /^vstfs:\/\/\/Git\/(Ref|PullRequestId|Commit)\/(.+)$/.exec(uri.trim())
|
|
366
|
+
const segment = match?.[1]
|
|
367
|
+
const encoded = match?.[2]
|
|
368
|
+
if (segment === undefined || encoded === undefined) {
|
|
369
|
+
return undefined
|
|
370
|
+
}
|
|
371
|
+
const kind = gitArtifactKinds.find((candidate) => uriSegment[candidate] === segment)
|
|
372
|
+
let decoded: string
|
|
373
|
+
try {
|
|
374
|
+
decoded = decodeURIComponent(encoded)
|
|
375
|
+
} catch {
|
|
376
|
+
// A malformed escape is a link we cannot act on, not a crash.
|
|
377
|
+
return undefined
|
|
378
|
+
}
|
|
379
|
+
// Split into exactly three: a branch name may contain further slashes.
|
|
380
|
+
const first = decoded.indexOf("/")
|
|
381
|
+
const second = decoded.indexOf("/", first + 1)
|
|
382
|
+
if (kind === undefined || first < 0 || second < 0) {
|
|
383
|
+
return undefined
|
|
384
|
+
}
|
|
385
|
+
const rest = decoded.slice(second + 1)
|
|
386
|
+
const value = kind === "Branch" ? (rest.startsWith("GB") ? rest.slice(2) : rest) : rest
|
|
387
|
+
return value.length === 0
|
|
388
|
+
? undefined
|
|
389
|
+
: GitArtifact.make({
|
|
390
|
+
kind,
|
|
391
|
+
projectId: decoded.slice(0, first),
|
|
392
|
+
repositoryId: decoded.slice(first + 1, second),
|
|
393
|
+
value
|
|
152
394
|
})
|
|
153
|
-
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export const relationAddArgs = (
|
|
398
|
+
config: AdoConfig,
|
|
399
|
+
id: number,
|
|
400
|
+
artifact: GitArtifact
|
|
401
|
+
): ReadonlyArray<string> => [
|
|
402
|
+
"boards",
|
|
403
|
+
"work-item",
|
|
404
|
+
"relation",
|
|
405
|
+
"add",
|
|
406
|
+
"--id",
|
|
407
|
+
String(id),
|
|
408
|
+
"--relation-type",
|
|
409
|
+
artifactLinkName(artifact.kind),
|
|
410
|
+
"--target-url",
|
|
411
|
+
artifactUri(artifact),
|
|
412
|
+
...org(config),
|
|
413
|
+
...json
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
export const repositoryShowArgs = (
|
|
417
|
+
config: AdoConfig,
|
|
418
|
+
repository: string
|
|
419
|
+
): ReadonlyArray<string> => [
|
|
420
|
+
"repos",
|
|
421
|
+
"show",
|
|
422
|
+
"--repository",
|
|
423
|
+
repository,
|
|
424
|
+
"--project",
|
|
425
|
+
config.project,
|
|
426
|
+
...org(config),
|
|
427
|
+
...json
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
export class GitRepository extends Schema.Class<GitRepository>("GitRepository")({
|
|
431
|
+
id: Schema.String,
|
|
432
|
+
name: Schema.String,
|
|
433
|
+
projectId: Schema.String,
|
|
434
|
+
projectName: Schema.String,
|
|
435
|
+
defaultBranch: Schema.String,
|
|
436
|
+
webUrl: Schema.String
|
|
437
|
+
}) {}
|
|
438
|
+
|
|
439
|
+
export const prPolicyArgs = (config: AdoConfig, id: number): ReadonlyArray<string> => [
|
|
440
|
+
"repos",
|
|
441
|
+
"pr",
|
|
442
|
+
"policy",
|
|
443
|
+
"list",
|
|
444
|
+
"--id",
|
|
445
|
+
String(id),
|
|
446
|
+
...org(config),
|
|
447
|
+
...json
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
export const prCompleteArgs = (
|
|
451
|
+
config: AdoConfig,
|
|
452
|
+
id: number,
|
|
453
|
+
squash: boolean,
|
|
454
|
+
deleteSourceBranch: boolean
|
|
455
|
+
): ReadonlyArray<string> => [
|
|
456
|
+
"repos",
|
|
457
|
+
"pr",
|
|
458
|
+
"update",
|
|
459
|
+
"--id",
|
|
460
|
+
String(id),
|
|
461
|
+
"--status",
|
|
462
|
+
"completed",
|
|
463
|
+
"--squash",
|
|
464
|
+
squash ? "true" : "false",
|
|
465
|
+
"--delete-source-branch",
|
|
466
|
+
deleteSourceBranch ? "true" : "false",
|
|
467
|
+
...org(config),
|
|
468
|
+
...json
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
// Parsing
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
// System.CreatedBy is an identity object on current API versions and a bare
|
|
476
|
+
// display string on older ones; both decode to the display name.
|
|
477
|
+
const Identity = Schema.Union([
|
|
478
|
+
Schema.String,
|
|
479
|
+
Schema.Struct({
|
|
480
|
+
displayName: Schema.optionalKey(Schema.String),
|
|
481
|
+
uniqueName: Schema.optionalKey(Schema.String)
|
|
482
|
+
})
|
|
483
|
+
])
|
|
484
|
+
|
|
485
|
+
const identityName = (value: typeof Identity.Type | undefined): string =>
|
|
486
|
+
value === undefined
|
|
487
|
+
? ""
|
|
488
|
+
: typeof value === "string"
|
|
489
|
+
? value
|
|
490
|
+
: (value.displayName ?? value.uniqueName ?? "")
|
|
491
|
+
|
|
492
|
+
const AdoFields = Schema.Struct({
|
|
493
|
+
"System.Title": Schema.optionalKey(Schema.String),
|
|
494
|
+
"System.Description": Schema.optionalKey(Schema.String),
|
|
495
|
+
"System.State": Schema.optionalKey(Schema.String),
|
|
496
|
+
"System.Tags": Schema.optionalKey(Schema.String),
|
|
497
|
+
"System.CreatedBy": Schema.optionalKey(Identity),
|
|
498
|
+
"System.ChangedDate": Schema.optionalKey(Schema.String),
|
|
499
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria": Schema.optionalKey(Schema.String)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const AdoWorkItem = Schema.Struct({
|
|
503
|
+
id: Schema.Int,
|
|
504
|
+
fields: AdoFields
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
export const parseTags = (raw: string): ReadonlyArray<string> =>
|
|
508
|
+
raw
|
|
509
|
+
.split(";")
|
|
510
|
+
.map((tag) => tag.trim())
|
|
511
|
+
.filter((tag) => tag.length > 0)
|
|
512
|
+
|
|
513
|
+
const toWorkItem = (item: typeof AdoWorkItem.Type): WorkItem =>
|
|
514
|
+
WorkItem.make({
|
|
515
|
+
id: item.id,
|
|
516
|
+
title: item.fields["System.Title"] ?? "",
|
|
517
|
+
description: item.fields["System.Description"] ?? "",
|
|
518
|
+
acceptanceCriteria: item.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
519
|
+
state: item.fields["System.State"] ?? "",
|
|
520
|
+
tags: parseTags(item.fields["System.Tags"] ?? ""),
|
|
521
|
+
createdBy: identityName(item.fields["System.CreatedBy"]),
|
|
522
|
+
changedDate: item.fields["System.ChangedDate"] ?? ""
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const decodeFailure =
|
|
526
|
+
(message: string) =>
|
|
527
|
+
(error: unknown): ProcessError =>
|
|
528
|
+
ProcessError.make({ message, detail: String(error) })
|
|
529
|
+
|
|
530
|
+
export const parseWorkItem = (payload: string): Effect.Effect<WorkItem, ProcessError> =>
|
|
531
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoWorkItem))(payload).pipe(
|
|
532
|
+
Effect.map(toWorkItem),
|
|
533
|
+
Effect.mapError(decodeFailure("az boards work-item show"))
|
|
154
534
|
)
|
|
155
535
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
536
|
+
// `az boards query` flattens the WIQL result into a work-item array, so a
|
|
537
|
+
// queue poll is a single call rather than a fan-out over ids.
|
|
538
|
+
export const parseWorkItems = (
|
|
539
|
+
payload: string
|
|
540
|
+
): Effect.Effect<ReadonlyArray<WorkItem>, ProcessError> =>
|
|
541
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoWorkItem)))(payload).pipe(
|
|
542
|
+
Effect.map((items) => items.map(toWorkItem)),
|
|
543
|
+
Effect.mapError(decodeFailure("az boards query"))
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
export const parseWorkItemIds = (
|
|
547
|
+
payload: string
|
|
548
|
+
): Effect.Effect<ReadonlyArray<number>, ProcessError> =>
|
|
549
|
+
parseWorkItems(payload).pipe(Effect.map((items) => items.map((item) => item.id)))
|
|
550
|
+
|
|
551
|
+
const AdoComments = Schema.Struct({
|
|
552
|
+
comments: Schema.Array(
|
|
553
|
+
Schema.Struct({
|
|
554
|
+
id: Schema.Int,
|
|
555
|
+
text: Schema.optionalKey(Schema.String),
|
|
556
|
+
createdBy: Schema.optionalKey(Identity),
|
|
557
|
+
createdDate: Schema.optionalKey(Schema.String)
|
|
558
|
+
})
|
|
559
|
+
).pipe(Schema.withConstructorDefault(Effect.succeed(Object.freeze([]))))
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
export const parseComments = (
|
|
563
|
+
payload: string
|
|
564
|
+
): Effect.Effect<ReadonlyArray<WorkItemComment>, ProcessError> =>
|
|
565
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoComments))(payload).pipe(
|
|
566
|
+
Effect.map((parsed) =>
|
|
567
|
+
parsed.comments.map((comment) =>
|
|
568
|
+
WorkItemComment.make({
|
|
569
|
+
id: comment.id,
|
|
570
|
+
author: identityName(comment.createdBy),
|
|
571
|
+
text: comment.text ?? "",
|
|
572
|
+
createdDate: comment.createdDate ?? ""
|
|
573
|
+
})
|
|
574
|
+
)
|
|
575
|
+
),
|
|
576
|
+
Effect.mapError(decodeFailure("az devops invoke wit comments"))
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
// `relations` is absent — not empty — on a work item whose Development
|
|
580
|
+
// section has never been touched, which is every work item until this tool
|
|
581
|
+
// links one. optionalKey, because a constructor default does not apply on
|
|
582
|
+
// decode and a missing key would fail the normal case.
|
|
583
|
+
const AdoRelations = Schema.Struct({
|
|
584
|
+
relations: Schema.optionalKey(
|
|
585
|
+
Schema.Array(
|
|
159
586
|
Schema.Struct({
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
})
|
|
164
|
-
)
|
|
587
|
+
rel: Schema.optionalKey(Schema.String),
|
|
588
|
+
url: Schema.optionalKey(Schema.String),
|
|
589
|
+
attributes: Schema.optionalKey(Schema.Struct({ name: Schema.optionalKey(Schema.String) }))
|
|
165
590
|
})
|
|
166
591
|
)
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// A work item with no Development section decodes to an empty list rather
|
|
596
|
+
// than failing: "nothing linked yet" is the normal state, not an error.
|
|
597
|
+
export const parseDevelopmentLinks = (
|
|
598
|
+
payload: string
|
|
599
|
+
): Effect.Effect<ReadonlyArray<GitArtifact>, ProcessError> =>
|
|
600
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRelations))(payload).pipe(
|
|
601
|
+
Effect.map((parsed) =>
|
|
602
|
+
(parsed.relations ?? [])
|
|
603
|
+
.filter((relation) => (relation.rel ?? "").toLowerCase() === "artifactlink")
|
|
604
|
+
.flatMap((relation) => {
|
|
605
|
+
const artifact = parseArtifactUri(relation.url ?? "")
|
|
606
|
+
if (artifact === undefined) {
|
|
607
|
+
return []
|
|
608
|
+
}
|
|
609
|
+
// The URI segment already fixes the kind; the attribute name is
|
|
610
|
+
// only a cross-check for the links whose segment is shared.
|
|
611
|
+
const named = artifactKindOfName(relation.attributes?.name ?? "")
|
|
612
|
+
return named === undefined || named === artifact.kind ? [artifact] : []
|
|
613
|
+
})
|
|
614
|
+
),
|
|
615
|
+
Effect.mapError(decodeFailure("az boards work-item show --expand relations"))
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
const AdoRepository = Schema.Struct({
|
|
619
|
+
id: Schema.String,
|
|
620
|
+
name: Schema.String,
|
|
621
|
+
project: Schema.Struct({
|
|
622
|
+
id: Schema.String,
|
|
623
|
+
name: Schema.optionalKey(Schema.String)
|
|
624
|
+
}),
|
|
625
|
+
defaultBranch: Schema.optionalKey(Schema.String),
|
|
626
|
+
webUrl: Schema.optionalKey(Schema.String)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
export const parseRepository = (payload: string): Effect.Effect<GitRepository, ProcessError> =>
|
|
630
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRepository))(payload).pipe(
|
|
631
|
+
Effect.map((repository) =>
|
|
632
|
+
GitRepository.make({
|
|
633
|
+
id: repository.id,
|
|
634
|
+
name: repository.name,
|
|
635
|
+
projectId: repository.project.id,
|
|
636
|
+
projectName: repository.project.name ?? "",
|
|
637
|
+
// Reported as a full ref; callers branch and push by short name.
|
|
638
|
+
defaultBranch: branchName(repository.defaultBranch ?? ""),
|
|
639
|
+
webUrl: repository.webUrl ?? ""
|
|
173
640
|
})
|
|
174
|
-
)
|
|
641
|
+
),
|
|
642
|
+
Effect.mapError(decodeFailure("az repos show"))
|
|
175
643
|
)
|
|
176
644
|
|
|
645
|
+
const AdoPr = Schema.Struct({
|
|
646
|
+
pullRequestId: Schema.Int,
|
|
647
|
+
repository: Schema.Struct({
|
|
648
|
+
id: Schema.String,
|
|
649
|
+
project: Schema.Struct({ id: Schema.String })
|
|
650
|
+
})
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
const toPullRequest = (config: AdoConfig, pr: typeof AdoPr.Type): AdoPullRequest =>
|
|
654
|
+
AdoPullRequest.make({
|
|
655
|
+
id: pr.pullRequestId,
|
|
656
|
+
repoId: pr.repository.id,
|
|
657
|
+
projectId: pr.repository.project.id,
|
|
658
|
+
webUrl:
|
|
659
|
+
`${config.orgUrl}/${config.project}/_git/` +
|
|
660
|
+
`${config.repository}/pullrequest/${String(pr.pullRequestId)}`
|
|
661
|
+
})
|
|
662
|
+
|
|
177
663
|
export const parsePullRequest = (
|
|
178
664
|
config: AdoConfig,
|
|
179
|
-
|
|
665
|
+
payload: string
|
|
180
666
|
): 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
|
-
|
|
667
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPr))(payload).pipe(
|
|
668
|
+
Effect.map((pr) => toPullRequest(config, pr)),
|
|
669
|
+
Effect.mapError(decodeFailure("az repos pr"))
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
export const parsePullRequests = (
|
|
673
|
+
config: AdoConfig,
|
|
674
|
+
payload: string
|
|
675
|
+
): Effect.Effect<ReadonlyArray<AdoPullRequest>, ProcessError> =>
|
|
676
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoPr)))(payload).pipe(
|
|
677
|
+
Effect.map((prs) => prs.map((pr) => toPullRequest(config, pr))),
|
|
678
|
+
Effect.mapError(decodeFailure("az repos pr list"))
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
const AdoPolicies = Schema.Array(
|
|
682
|
+
Schema.Struct({
|
|
683
|
+
status: Schema.optionalKey(Schema.String)
|
|
684
|
+
})
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
// Policy evaluation statuses: queued/running are still deciding, and
|
|
688
|
+
// rejected/broken have already decided against the PR.
|
|
689
|
+
export const outcomeFromPolicies = (payload: string): Effect.Effect<PolicyOutcome, ProcessError> =>
|
|
690
|
+
Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPolicies))(payload).pipe(
|
|
691
|
+
Effect.map((policies) => {
|
|
692
|
+
const statuses = policies.map((policy) => policy.status?.toLowerCase() ?? "")
|
|
693
|
+
return statuses.some((value) => ["queued", "running"].includes(value))
|
|
694
|
+
? "Pending"
|
|
695
|
+
: statuses.some((value) => ["rejected", "broken"].includes(value))
|
|
696
|
+
? "Failure"
|
|
697
|
+
: "Success"
|
|
698
|
+
}),
|
|
699
|
+
Effect.mapError(decodeFailure("az repos pr policy list"))
|
|
210
700
|
)
|
|
211
701
|
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
// Tool
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
|
|
212
706
|
export interface AzureDevOpsToolShape {
|
|
213
707
|
readonly readWorkItem: (id: number) => Effect.Effect<WorkItem, FlowError>
|
|
708
|
+
readonly listWorkItems: (
|
|
709
|
+
filter?: WorkItemFilter
|
|
710
|
+
) => Effect.Effect<ReadonlyArray<WorkItem>, FlowError>
|
|
214
711
|
readonly wiqlIds: (query: string) => Effect.Effect<ReadonlyArray<number>, FlowError>
|
|
712
|
+
readonly readComments: (id: number) => Effect.Effect<ReadonlyArray<WorkItemComment>, FlowError>
|
|
713
|
+
// The work item's Development section: the branches, pull requests, and
|
|
714
|
+
// commits linked to it. Empty when nothing has been linked yet.
|
|
715
|
+
readonly developmentLinks: (id: number) => Effect.Effect<ReadonlyArray<GitArtifact>, FlowError>
|
|
716
|
+
readonly linkArtifact: (id: number, artifact: GitArtifact) => Effect.Effect<void, FlowError>
|
|
717
|
+
// Resolves a repository's GUIDs, which every artifact link needs and no
|
|
718
|
+
// caller can know from a repository name alone.
|
|
719
|
+
readonly repository: (name?: string) => Effect.Effect<GitRepository, FlowError>
|
|
215
720
|
readonly setFields: (
|
|
216
721
|
id: number,
|
|
217
722
|
fields: Readonly<Record<string, string>>
|
|
218
723
|
) => Effect.Effect<void, FlowError>
|
|
219
724
|
readonly setState: (id: number, state: string) => Effect.Effect<void, FlowError>
|
|
220
725
|
readonly setAcceptanceCriteria: (id: number, text: string) => Effect.Effect<void, FlowError>
|
|
726
|
+
// Tags are one semicolon-joined field, so an edit is read-merge-write
|
|
727
|
+
// rather than the add/remove verbs a label API would offer.
|
|
728
|
+
readonly editTags: (
|
|
729
|
+
id: number,
|
|
730
|
+
add: ReadonlyArray<string>,
|
|
731
|
+
remove: ReadonlyArray<string>
|
|
732
|
+
) => Effect.Effect<void, FlowError>
|
|
733
|
+
readonly writeComment: (id: number, text: string) => Effect.Effect<void, FlowError>
|
|
734
|
+
readonly createWorkItem: (
|
|
735
|
+
workItemType: string,
|
|
736
|
+
title: string,
|
|
737
|
+
description: string,
|
|
738
|
+
tags?: ReadonlyArray<string>
|
|
739
|
+
) => Effect.Effect<WorkItem, FlowError>
|
|
221
740
|
readonly createPr: (
|
|
222
741
|
sourceRef: string,
|
|
223
742
|
targetRef: string,
|
|
224
743
|
title: string,
|
|
225
|
-
body: string
|
|
744
|
+
body: string,
|
|
745
|
+
draft?: boolean
|
|
226
746
|
) => Effect.Effect<AdoPullRequest, FlowError>
|
|
747
|
+
readonly openPrForBranch: (
|
|
748
|
+
sourceRef: string
|
|
749
|
+
) => Effect.Effect<AdoPullRequest | undefined, FlowError>
|
|
750
|
+
readonly updatePr: (
|
|
751
|
+
pr: AdoPullRequest,
|
|
752
|
+
title: string,
|
|
753
|
+
body: string
|
|
754
|
+
) => Effect.Effect<void, FlowError>
|
|
755
|
+
readonly writePrComment: (pr: AdoPullRequest, body: string) => Effect.Effect<void, FlowError>
|
|
756
|
+
readonly prPolicies: (pr: AdoPullRequest) => Effect.Effect<PolicyOutcome, FlowError>
|
|
757
|
+
readonly completePr: (
|
|
758
|
+
pr: AdoPullRequest,
|
|
759
|
+
squash?: boolean,
|
|
760
|
+
deleteSourceBranch?: boolean
|
|
761
|
+
) => Effect.Effect<void, FlowError>
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const output = (result: ProcessResult): string => result.stdout.join("\n").trim()
|
|
765
|
+
|
|
766
|
+
export const mergeTags = (
|
|
767
|
+
current: ReadonlyArray<string>,
|
|
768
|
+
add: ReadonlyArray<string>,
|
|
769
|
+
remove: ReadonlyArray<string>
|
|
770
|
+
): ReadonlyArray<string> => {
|
|
771
|
+
const removed = new Set(remove.map((tag) => tag.toLowerCase()))
|
|
772
|
+
const kept = current.filter((tag) => !removed.has(tag.toLowerCase()))
|
|
773
|
+
const present = new Set(kept.map((tag) => tag.toLowerCase()))
|
|
774
|
+
const added = add.filter(
|
|
775
|
+
(tag) => !present.has(tag.toLowerCase()) && !removed.has(tag.toLowerCase())
|
|
776
|
+
)
|
|
777
|
+
return [...kept, ...added]
|
|
227
778
|
}
|
|
228
779
|
|
|
229
780
|
export const makeAzureDevOpsTool = (
|
|
230
781
|
config: AdoConfig,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
782
|
+
process: ProcessExecutorShape,
|
|
783
|
+
workDir: string,
|
|
784
|
+
events: FlowEventsShape
|
|
234
785
|
): AzureDevOpsToolShape => {
|
|
235
|
-
const run = (
|
|
236
|
-
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
786
|
+
const run = (args: ReadonlyArray<string>): Effect.Effect<string, FlowError> =>
|
|
787
|
+
process.run(["az", ...args], workDir, {}).pipe(
|
|
788
|
+
Effect.mapError((error) =>
|
|
789
|
+
ProcessError.make({ message: `az ${args.join(" ")}`, detail: error.message })
|
|
790
|
+
),
|
|
791
|
+
Effect.flatMap((result) =>
|
|
792
|
+
result.exitCode === 0
|
|
793
|
+
? Effect.succeed(output(result))
|
|
794
|
+
: Effect.fail(
|
|
795
|
+
ProcessError.make({
|
|
796
|
+
message: `az ${args.join(" ")}`,
|
|
797
|
+
detail:
|
|
798
|
+
[...result.stdout, ...result.stderr].join("\n").trim() ||
|
|
799
|
+
`exit code ${result.exitCode}`
|
|
800
|
+
})
|
|
801
|
+
)
|
|
252
802
|
)
|
|
803
|
+
)
|
|
253
804
|
|
|
254
805
|
const read = <A>(
|
|
255
806
|
operation: string,
|
|
@@ -260,32 +811,97 @@ export const makeAzureDevOpsTool = (
|
|
|
260
811
|
effect: Effect.Effect<A, FlowError>
|
|
261
812
|
): Effect.Effect<A, FlowError> => guarded(Capabilities.AdoWrite, operation, events, effect)
|
|
262
813
|
|
|
814
|
+
const readWorkItem = (id: number): Effect.Effect<WorkItem, FlowError> =>
|
|
815
|
+
read("ado readWorkItem", run(workItemShowArgs(config, id)).pipe(Effect.flatMap(parseWorkItem)))
|
|
816
|
+
|
|
263
817
|
const setFields = (
|
|
264
818
|
id: number,
|
|
265
819
|
fields: Readonly<Record<string, string>>
|
|
266
820
|
): Effect.Effect<void, FlowError> =>
|
|
267
|
-
write("ado setFields", run(
|
|
821
|
+
write("ado setFields", run(workItemUpdateArgs(config, id, fields)).pipe(Effect.asVoid))
|
|
822
|
+
|
|
823
|
+
const listPrs = (sourceRef?: string): Effect.Effect<ReadonlyArray<AdoPullRequest>, FlowError> =>
|
|
824
|
+
run(prListArgs(config, sourceRef)).pipe(
|
|
825
|
+
Effect.flatMap((payload) => parsePullRequests(config, payload))
|
|
826
|
+
)
|
|
268
827
|
|
|
269
828
|
return {
|
|
270
|
-
readWorkItem
|
|
829
|
+
readWorkItem,
|
|
830
|
+
listWorkItems: (filter = {}) =>
|
|
271
831
|
read(
|
|
272
|
-
"ado
|
|
273
|
-
run(
|
|
832
|
+
"ado listWorkItems",
|
|
833
|
+
run(queryArgs(config, wiqlFor(filter))).pipe(Effect.flatMap(parseWorkItems))
|
|
274
834
|
),
|
|
275
835
|
wiqlIds: (query) =>
|
|
276
|
-
read("ado wiql", run(
|
|
836
|
+
read("ado wiql", run(queryArgs(config, query)).pipe(Effect.flatMap(parseWorkItemIds))),
|
|
837
|
+
readComments: (id) =>
|
|
838
|
+
read("ado readComments", run(commentsArgs(config, id)).pipe(Effect.flatMap(parseComments))),
|
|
839
|
+
developmentLinks: (id) =>
|
|
840
|
+
read(
|
|
841
|
+
"ado developmentLinks",
|
|
842
|
+
run(workItemShowArgs(config, id, "relations")).pipe(Effect.flatMap(parseDevelopmentLinks))
|
|
843
|
+
),
|
|
844
|
+
linkArtifact: (id, artifact) =>
|
|
845
|
+
write("ado linkArtifact", run(relationAddArgs(config, id, artifact)).pipe(Effect.asVoid)),
|
|
846
|
+
repository: (name = config.repository) =>
|
|
847
|
+
read(
|
|
848
|
+
"ado repository",
|
|
849
|
+
run(repositoryShowArgs(config, name)).pipe(Effect.flatMap(parseRepository))
|
|
850
|
+
),
|
|
277
851
|
setFields,
|
|
278
852
|
setState: (id, state) => setFields(id, { "System.State": state }),
|
|
279
853
|
setAcceptanceCriteria: (id, text) =>
|
|
280
|
-
setFields(id, {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
854
|
+
setFields(id, { "Microsoft.VSTS.Common.AcceptanceCriteria": text }),
|
|
855
|
+
editTags: (id, add, remove) =>
|
|
856
|
+
add.length === 0 && remove.length === 0
|
|
857
|
+
? Effect.void
|
|
858
|
+
: readWorkItem(id).pipe(
|
|
859
|
+
Effect.flatMap((item) => {
|
|
860
|
+
const next = mergeTags(item.tags, add, remove)
|
|
861
|
+
return next.length === item.tags.length &&
|
|
862
|
+
next.every((tag, index) => tag === item.tags[index])
|
|
863
|
+
? Effect.void
|
|
864
|
+
: setFields(id, { "System.Tags": next.join("; ") })
|
|
865
|
+
})
|
|
866
|
+
),
|
|
867
|
+
writeComment: (id, text) =>
|
|
868
|
+
write("ado writeComment", run(workItemCommentArgs(config, id, text)).pipe(Effect.asVoid)),
|
|
869
|
+
createWorkItem: (workItemType, title, description, tags = []) =>
|
|
284
870
|
write(
|
|
285
|
-
"ado
|
|
286
|
-
run(
|
|
287
|
-
Effect.flatMap(
|
|
871
|
+
"ado createWorkItem",
|
|
872
|
+
run(workItemCreateArgs(config, workItemType, title, description, tags)).pipe(
|
|
873
|
+
Effect.flatMap(parseWorkItem)
|
|
288
874
|
)
|
|
875
|
+
),
|
|
876
|
+
createPr: (sourceRef, targetRef, title, body, draft = false) =>
|
|
877
|
+
write(
|
|
878
|
+
"ado createPr",
|
|
879
|
+
// An active PR for the branch already IS the deliverable; creating a
|
|
880
|
+
// second one would fail on the server and lose the first's reviews.
|
|
881
|
+
Effect.flatMap(listPrs(sourceRef), (existing) => {
|
|
882
|
+
const open = existing[0]
|
|
883
|
+
return open !== undefined
|
|
884
|
+
? Effect.succeed(open)
|
|
885
|
+
: run(prCreateArgs(config, sourceRef, targetRef, title, body, draft)).pipe(
|
|
886
|
+
Effect.flatMap((payload) => parsePullRequest(config, payload))
|
|
887
|
+
)
|
|
888
|
+
})
|
|
889
|
+
),
|
|
890
|
+
openPrForBranch: (sourceRef) =>
|
|
891
|
+
read("ado listPrs", listPrs(sourceRef).pipe(Effect.map((prs) => prs[0]))),
|
|
892
|
+
updatePr: (pr, title, body) =>
|
|
893
|
+
write("ado updatePr", run(prUpdateArgs(config, pr.id, title, body)).pipe(Effect.asVoid)),
|
|
894
|
+
writePrComment: (pr, body) =>
|
|
895
|
+
write("ado writePrComment", run(prCommentArgs(config, pr.id, body)).pipe(Effect.asVoid)),
|
|
896
|
+
prPolicies: (pr) =>
|
|
897
|
+
read(
|
|
898
|
+
"ado prPolicies",
|
|
899
|
+
run(prPolicyArgs(config, pr.id)).pipe(Effect.flatMap(outcomeFromPolicies))
|
|
900
|
+
),
|
|
901
|
+
completePr: (pr, squash = true, deleteSourceBranch = true) =>
|
|
902
|
+
write(
|
|
903
|
+
"ado completePr",
|
|
904
|
+
run(prCompleteArgs(config, pr.id, squash, deleteSourceBranch)).pipe(Effect.asVoid)
|
|
289
905
|
)
|
|
290
906
|
}
|
|
291
907
|
}
|