@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/dist/AzureDevOpsTool.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
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
4
|
import { ProcessError } from "./FlowError.js";
|
|
7
5
|
import { guarded } from "./CapabilityGuard.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
// Azure DevOps through the `az` CLI (ADR 0011), the sibling of GitHubTool's
|
|
7
|
+
// `gh` protocol: pure args builders, schema-decoded `--output json`, and
|
|
8
|
+
// capability guards around a ProcessExecutor. Credentials belong to the CLI
|
|
9
|
+
// (`az devops login`, or AZURE_DEVOPS_EXT_PAT in the process environment) —
|
|
10
|
+
// this module never reads, holds, or forwards a PAT, so no secret can reach
|
|
11
|
+
// argv, a log line, or a persisted plan.
|
|
12
|
+
export class AdoConfig extends Schema.Class("AdoConfig")({
|
|
13
|
+
orgUrl: Schema.String,
|
|
14
|
+
project: Schema.String,
|
|
15
|
+
repository: Schema.String,
|
|
16
|
+
// Only `az devops invoke` (the comments REST resource, which has no
|
|
17
|
+
// first-class `az boards` verb) needs an explicit API version; every
|
|
18
|
+
// other call is a versioned CLI command.
|
|
19
|
+
apiVersion: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("7.1-preview")))
|
|
13
20
|
}) {
|
|
14
21
|
}
|
|
15
22
|
export class WorkItem extends Schema.Class("WorkItem")({
|
|
@@ -18,7 +25,16 @@ export class WorkItem extends Schema.Class("WorkItem")({
|
|
|
18
25
|
description: Schema.String,
|
|
19
26
|
acceptanceCriteria: Schema.String,
|
|
20
27
|
state: Schema.String,
|
|
21
|
-
tags: Schema.Array(Schema.String)
|
|
28
|
+
tags: Schema.Array(Schema.String),
|
|
29
|
+
createdBy: Schema.String,
|
|
30
|
+
changedDate: Schema.String
|
|
31
|
+
}) {
|
|
32
|
+
}
|
|
33
|
+
export class WorkItemComment extends Schema.Class("WorkItemComment")({
|
|
34
|
+
id: Schema.Int,
|
|
35
|
+
author: Schema.String,
|
|
36
|
+
text: Schema.String,
|
|
37
|
+
createdDate: Schema.String
|
|
22
38
|
}) {
|
|
23
39
|
}
|
|
24
40
|
export class AdoPullRequest extends Schema.Class("AdoPullRequest")({
|
|
@@ -28,136 +44,525 @@ export class AdoPullRequest extends Schema.Class("AdoPullRequest")({
|
|
|
28
44
|
webUrl: Schema.String
|
|
29
45
|
}) {
|
|
30
46
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
// Azure DevOps branch policies are the analogue of GitHub's check rollup.
|
|
48
|
+
// There is no policy status for a timed-out run, so the outcome set is the
|
|
49
|
+
// GitHub one minus "TimedOut".
|
|
50
|
+
export const PolicyOutcome = Schema.Literals(["Success", "Failure", "Pending"]);
|
|
51
|
+
export const WorkItemState = Schema.Literals(["open", "closed", "all"]);
|
|
52
|
+
// `refs/heads/x` and `x` both name a branch across the Azure DevOps
|
|
53
|
+
// surface; the CLI wants the short form, so every ref is normalized once
|
|
54
|
+
// on the way into argv.
|
|
55
|
+
export const branchName = (ref) => ref.trim().replace(/^refs\/heads\//, "");
|
|
56
|
+
const org = (config) => [
|
|
57
|
+
"--org",
|
|
58
|
+
config.orgUrl,
|
|
59
|
+
// Without this the CLI probes the working directory's git remote and
|
|
60
|
+
// silently retargets another organization; a library must not guess.
|
|
61
|
+
"--detect",
|
|
62
|
+
"false"
|
|
63
|
+
];
|
|
64
|
+
const json = ["--output", "json"];
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Work item argv
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
export const workItemShowArgs = (config, id, expand) => [
|
|
69
|
+
"boards",
|
|
70
|
+
"work-item",
|
|
71
|
+
"show",
|
|
72
|
+
"--id",
|
|
73
|
+
String(id),
|
|
74
|
+
...(expand === undefined ? [] : ["--expand", expand]),
|
|
75
|
+
...org(config),
|
|
76
|
+
...json
|
|
77
|
+
];
|
|
78
|
+
export const fieldArgs = (fields) => {
|
|
79
|
+
const pairs = Object.entries(fields).map(([name, value]) => `${name}=${value}`);
|
|
80
|
+
return pairs.length === 0 ? [] : ["--fields", ...pairs];
|
|
81
|
+
};
|
|
82
|
+
export const workItemUpdateArgs = (config, id, fields) => [
|
|
83
|
+
"boards",
|
|
84
|
+
"work-item",
|
|
85
|
+
"update",
|
|
86
|
+
"--id",
|
|
87
|
+
String(id),
|
|
88
|
+
...fieldArgs(fields),
|
|
89
|
+
...org(config),
|
|
90
|
+
...json
|
|
91
|
+
];
|
|
92
|
+
export const workItemCommentArgs = (config, id, text) => [
|
|
93
|
+
"boards",
|
|
94
|
+
"work-item",
|
|
95
|
+
"update",
|
|
96
|
+
"--id",
|
|
97
|
+
String(id),
|
|
98
|
+
"--discussion",
|
|
99
|
+
text,
|
|
100
|
+
...org(config),
|
|
101
|
+
...json
|
|
102
|
+
];
|
|
103
|
+
export const workItemCreateArgs = (config, workItemType, title, description, tags) => [
|
|
104
|
+
"boards",
|
|
105
|
+
"work-item",
|
|
106
|
+
"create",
|
|
107
|
+
"--title",
|
|
108
|
+
title,
|
|
109
|
+
"--type",
|
|
110
|
+
workItemType,
|
|
111
|
+
"--description",
|
|
112
|
+
description,
|
|
113
|
+
"--project",
|
|
114
|
+
config.project,
|
|
115
|
+
...fieldArgs(tags.length === 0 ? {} : { "System.Tags": tags.join("; ") }),
|
|
116
|
+
...org(config),
|
|
117
|
+
...json
|
|
118
|
+
];
|
|
119
|
+
export const commentsArgs = (config, id) => [
|
|
120
|
+
"devops",
|
|
121
|
+
"invoke",
|
|
122
|
+
"--area",
|
|
123
|
+
"wit",
|
|
124
|
+
"--resource",
|
|
125
|
+
"comments",
|
|
126
|
+
"--route-parameters",
|
|
127
|
+
`project=${config.project}`,
|
|
128
|
+
`workItemId=${String(id)}`,
|
|
129
|
+
"--api-version",
|
|
130
|
+
config.apiVersion,
|
|
131
|
+
...org(config),
|
|
132
|
+
...json
|
|
133
|
+
];
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// WIQL
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// WIQL string literals are single-quoted; a quote inside a value is escaped
|
|
138
|
+
// by doubling it. Every caller-supplied value goes through here so a tag
|
|
139
|
+
// like "won't fix" cannot terminate the literal and rewrite the query.
|
|
140
|
+
export const quoteWiql = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
141
|
+
export const workItemFields = [
|
|
142
|
+
"System.Id",
|
|
143
|
+
"System.Title",
|
|
144
|
+
"System.Description",
|
|
145
|
+
"System.State",
|
|
146
|
+
"System.Tags",
|
|
147
|
+
"System.CreatedBy",
|
|
148
|
+
"System.ChangedDate",
|
|
149
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria"
|
|
150
|
+
];
|
|
151
|
+
export const wiqlFor = (filter) => {
|
|
152
|
+
const clauses = [
|
|
153
|
+
"[System.TeamProject] = @project",
|
|
154
|
+
...(filter.state === "all"
|
|
155
|
+
? []
|
|
156
|
+
: filter.state === "closed"
|
|
157
|
+
? ["[System.State] = 'Closed'"]
|
|
158
|
+
: ["[System.State] <> 'Closed'"]),
|
|
159
|
+
...(filter.tags ?? []).map((tag) => `[System.Tags] CONTAINS ${quoteWiql(tag)}`),
|
|
160
|
+
...(filter.assignedTo === undefined
|
|
161
|
+
? []
|
|
162
|
+
: [`[System.AssignedTo] = ${quoteWiql(filter.assignedTo)}`])
|
|
163
|
+
];
|
|
164
|
+
const select = workItemFields.map((name) => `[${name}]`).join(", ");
|
|
165
|
+
return (`SELECT TOP ${String(filter.limit ?? 100)} ${select} FROM WorkItems ` +
|
|
166
|
+
`WHERE ${clauses.join(" AND ")} ORDER BY [System.Id] ASC`);
|
|
167
|
+
};
|
|
168
|
+
export const queryArgs = (config, wiql) => [
|
|
169
|
+
"boards",
|
|
170
|
+
"query",
|
|
171
|
+
"--wiql",
|
|
172
|
+
wiql,
|
|
173
|
+
"--project",
|
|
174
|
+
config.project,
|
|
175
|
+
...org(config),
|
|
176
|
+
...json
|
|
177
|
+
];
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Pull request argv
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
export const prCreateArgs = (config, sourceRef, targetRef, title, description, draft = false) => [
|
|
182
|
+
"repos",
|
|
183
|
+
"pr",
|
|
184
|
+
"create",
|
|
185
|
+
"--repository",
|
|
186
|
+
config.repository,
|
|
187
|
+
"--project",
|
|
188
|
+
config.project,
|
|
189
|
+
"--source-branch",
|
|
190
|
+
branchName(sourceRef),
|
|
191
|
+
"--target-branch",
|
|
192
|
+
branchName(targetRef),
|
|
193
|
+
"--title",
|
|
194
|
+
title,
|
|
195
|
+
"--description",
|
|
196
|
+
description,
|
|
197
|
+
...(draft ? ["--draft", "true"] : []),
|
|
198
|
+
...org(config),
|
|
199
|
+
...json
|
|
200
|
+
];
|
|
201
|
+
export const prListArgs = (config, sourceRef) => [
|
|
202
|
+
"repos",
|
|
203
|
+
"pr",
|
|
204
|
+
"list",
|
|
205
|
+
"--repository",
|
|
206
|
+
config.repository,
|
|
207
|
+
"--project",
|
|
208
|
+
config.project,
|
|
209
|
+
"--status",
|
|
210
|
+
"active",
|
|
211
|
+
...(sourceRef === undefined ? [] : ["--source-branch", branchName(sourceRef)]),
|
|
212
|
+
...org(config),
|
|
213
|
+
...json
|
|
214
|
+
];
|
|
215
|
+
export const prUpdateArgs = (config, id, title, description) => [
|
|
216
|
+
"repos",
|
|
217
|
+
"pr",
|
|
218
|
+
"update",
|
|
219
|
+
"--id",
|
|
220
|
+
String(id),
|
|
221
|
+
"--title",
|
|
222
|
+
title,
|
|
223
|
+
"--description",
|
|
224
|
+
description,
|
|
225
|
+
...org(config),
|
|
226
|
+
...json
|
|
227
|
+
];
|
|
228
|
+
export const prCommentArgs = (config, id, text) => [
|
|
229
|
+
"repos",
|
|
230
|
+
"pr",
|
|
231
|
+
"thread",
|
|
232
|
+
"create",
|
|
233
|
+
"--id",
|
|
234
|
+
String(id),
|
|
235
|
+
"--content",
|
|
236
|
+
text,
|
|
237
|
+
"--project",
|
|
238
|
+
config.project,
|
|
239
|
+
...org(config),
|
|
240
|
+
...json
|
|
241
|
+
];
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Development links
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// The "Development" section of a work item is a set of ArtifactLink
|
|
246
|
+
// relations pointing at git objects. Their URLs are `vstfs:` URIs carrying
|
|
247
|
+
// GUIDs, not names — which is why `repository` below exists: a caller that
|
|
248
|
+
// knows a repository by name has to resolve its id before it can link
|
|
249
|
+
// anything to it, and has to reverse the mapping to read a link back.
|
|
250
|
+
export const GitArtifactKind = Schema.Literals(["Branch", "PullRequest", "Commit"]);
|
|
251
|
+
export class GitArtifact extends Schema.Class("GitArtifact")({
|
|
252
|
+
kind: GitArtifactKind,
|
|
253
|
+
projectId: Schema.String,
|
|
254
|
+
repositoryId: Schema.String,
|
|
255
|
+
// Branch name, pull request id, or commit sha, by kind.
|
|
256
|
+
value: Schema.String
|
|
39
257
|
}) {
|
|
40
258
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
259
|
+
export const gitArtifactKinds = ["Branch", "PullRequest", "Commit"];
|
|
260
|
+
// The CLI and the REST payloads name these links in prose, not by kind.
|
|
261
|
+
const linkNames = {
|
|
262
|
+
Branch: "Branch",
|
|
263
|
+
PullRequest: "Pull Request",
|
|
264
|
+
Commit: "Fixed in Commit"
|
|
265
|
+
};
|
|
266
|
+
export const artifactLinkName = (kind) => linkNames[kind];
|
|
267
|
+
export const artifactKindOfName = (name) => gitArtifactKinds.find((kind) => linkNames[kind].toLowerCase() === name.trim().toLowerCase());
|
|
268
|
+
const uriSegment = {
|
|
269
|
+
Branch: "Ref",
|
|
270
|
+
PullRequest: "PullRequestId",
|
|
271
|
+
Commit: "Commit"
|
|
272
|
+
};
|
|
273
|
+
// `vstfs:///Git/Ref/{project}%2F{repo}%2FGB{branch}` — the whole
|
|
274
|
+
// project/repo/value triple is ONE percent-encoded segment, which is what
|
|
275
|
+
// lets a branch name contain slashes without splitting the URI.
|
|
276
|
+
export const artifactUri = (artifact) => {
|
|
277
|
+
const value = artifact.kind === "Branch" ? `GB${artifact.value}` : artifact.value;
|
|
278
|
+
return (`vstfs:///Git/${uriSegment[artifact.kind]}/` +
|
|
279
|
+
encodeURIComponent(`${artifact.projectId}/${artifact.repositoryId}/${value}`));
|
|
280
|
+
};
|
|
281
|
+
export const parseArtifactUri = (uri) => {
|
|
282
|
+
const match = /^vstfs:\/\/\/Git\/(Ref|PullRequestId|Commit)\/(.+)$/.exec(uri.trim());
|
|
283
|
+
const segment = match?.[1];
|
|
284
|
+
const encoded = match?.[2];
|
|
285
|
+
if (segment === undefined || encoded === undefined) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const kind = gitArtifactKinds.find((candidate) => uriSegment[candidate] === segment);
|
|
289
|
+
let decoded;
|
|
290
|
+
try {
|
|
291
|
+
decoded = decodeURIComponent(encoded);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// A malformed escape is a link we cannot act on, not a crash.
|
|
295
|
+
return undefined;
|
|
54
296
|
}
|
|
55
|
-
|
|
297
|
+
// Split into exactly three: a branch name may contain further slashes.
|
|
298
|
+
const first = decoded.indexOf("/");
|
|
299
|
+
const second = decoded.indexOf("/", first + 1);
|
|
300
|
+
if (kind === undefined || first < 0 || second < 0) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const rest = decoded.slice(second + 1);
|
|
304
|
+
const value = kind === "Branch" ? (rest.startsWith("GB") ? rest.slice(2) : rest) : rest;
|
|
305
|
+
return value.length === 0
|
|
306
|
+
? undefined
|
|
307
|
+
: GitArtifact.make({
|
|
308
|
+
kind,
|
|
309
|
+
projectId: decoded.slice(0, first),
|
|
310
|
+
repositoryId: decoded.slice(first + 1, second),
|
|
311
|
+
value
|
|
312
|
+
});
|
|
56
313
|
};
|
|
57
|
-
export const
|
|
58
|
-
|
|
314
|
+
export const relationAddArgs = (config, id, artifact) => [
|
|
315
|
+
"boards",
|
|
316
|
+
"work-item",
|
|
317
|
+
"relation",
|
|
318
|
+
"add",
|
|
319
|
+
"--id",
|
|
320
|
+
String(id),
|
|
321
|
+
"--relation-type",
|
|
322
|
+
artifactLinkName(artifact.kind),
|
|
323
|
+
"--target-url",
|
|
324
|
+
artifactUri(artifact),
|
|
325
|
+
...org(config),
|
|
326
|
+
...json
|
|
327
|
+
];
|
|
328
|
+
export const repositoryShowArgs = (config, repository) => [
|
|
329
|
+
"repos",
|
|
330
|
+
"show",
|
|
331
|
+
"--repository",
|
|
332
|
+
repository,
|
|
333
|
+
"--project",
|
|
334
|
+
config.project,
|
|
335
|
+
...org(config),
|
|
336
|
+
...json
|
|
337
|
+
];
|
|
338
|
+
export class GitRepository extends Schema.Class("GitRepository")({
|
|
339
|
+
id: Schema.String,
|
|
340
|
+
name: Schema.String,
|
|
341
|
+
projectId: Schema.String,
|
|
342
|
+
projectName: Schema.String,
|
|
343
|
+
defaultBranch: Schema.String,
|
|
344
|
+
webUrl: Schema.String
|
|
345
|
+
}) {
|
|
346
|
+
}
|
|
347
|
+
export const prPolicyArgs = (config, id) => [
|
|
348
|
+
"repos",
|
|
349
|
+
"pr",
|
|
350
|
+
"policy",
|
|
351
|
+
"list",
|
|
352
|
+
"--id",
|
|
353
|
+
String(id),
|
|
354
|
+
...org(config),
|
|
355
|
+
...json
|
|
356
|
+
];
|
|
357
|
+
export const prCompleteArgs = (config, id, squash, deleteSourceBranch) => [
|
|
358
|
+
"repos",
|
|
359
|
+
"pr",
|
|
360
|
+
"update",
|
|
361
|
+
"--id",
|
|
362
|
+
String(id),
|
|
363
|
+
"--status",
|
|
364
|
+
"completed",
|
|
365
|
+
"--squash",
|
|
366
|
+
squash ? "true" : "false",
|
|
367
|
+
"--delete-source-branch",
|
|
368
|
+
deleteSourceBranch ? "true" : "false",
|
|
369
|
+
...org(config),
|
|
370
|
+
...json
|
|
371
|
+
];
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Parsing
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// System.CreatedBy is an identity object on current API versions and a bare
|
|
376
|
+
// display string on older ones; both decode to the display name.
|
|
377
|
+
const Identity = Schema.Union([
|
|
378
|
+
Schema.String,
|
|
379
|
+
Schema.Struct({
|
|
380
|
+
displayName: Schema.optionalKey(Schema.String),
|
|
381
|
+
uniqueName: Schema.optionalKey(Schema.String)
|
|
382
|
+
})
|
|
383
|
+
]);
|
|
384
|
+
const identityName = (value) => value === undefined
|
|
385
|
+
? ""
|
|
386
|
+
: typeof value === "string"
|
|
387
|
+
? value
|
|
388
|
+
: (value.displayName ?? value.uniqueName ?? "");
|
|
389
|
+
const AdoFields = Schema.Struct({
|
|
390
|
+
"System.Title": Schema.optionalKey(Schema.String),
|
|
391
|
+
"System.Description": Schema.optionalKey(Schema.String),
|
|
392
|
+
"System.State": Schema.optionalKey(Schema.String),
|
|
393
|
+
"System.Tags": Schema.optionalKey(Schema.String),
|
|
394
|
+
"System.CreatedBy": Schema.optionalKey(Identity),
|
|
395
|
+
"System.ChangedDate": Schema.optionalKey(Schema.String),
|
|
396
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria": Schema.optionalKey(Schema.String)
|
|
59
397
|
});
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
op: "add",
|
|
64
|
-
path: `/fields/${name}`,
|
|
65
|
-
value
|
|
66
|
-
}));
|
|
67
|
-
export const readWorkItemRequest = (config, id) => AdoRequest.make({
|
|
68
|
-
method: "GET",
|
|
69
|
-
url: `${witBase(config)}/workitems/${id}?$expand=relations&api-version=${config.apiVersion}`
|
|
398
|
+
const AdoWorkItem = Schema.Struct({
|
|
399
|
+
id: Schema.Int,
|
|
400
|
+
fields: AdoFields
|
|
70
401
|
});
|
|
71
|
-
export const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
402
|
+
export const parseTags = (raw) => raw
|
|
403
|
+
.split(";")
|
|
404
|
+
.map((tag) => tag.trim())
|
|
405
|
+
.filter((tag) => tag.length > 0);
|
|
406
|
+
const toWorkItem = (item) => WorkItem.make({
|
|
407
|
+
id: item.id,
|
|
408
|
+
title: item.fields["System.Title"] ?? "",
|
|
409
|
+
description: item.fields["System.Description"] ?? "",
|
|
410
|
+
acceptanceCriteria: item.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
|
|
411
|
+
state: item.fields["System.State"] ?? "",
|
|
412
|
+
tags: parseTags(item.fields["System.Tags"] ?? ""),
|
|
413
|
+
createdBy: identityName(item.fields["System.CreatedBy"]),
|
|
414
|
+
changedDate: item.fields["System.ChangedDate"] ?? ""
|
|
75
415
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
416
|
+
const decodeFailure = (message) => (error) => ProcessError.make({ message, detail: String(error) });
|
|
417
|
+
export const parseWorkItem = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoWorkItem))(payload).pipe(Effect.map(toWorkItem), Effect.mapError(decodeFailure("az boards work-item show")));
|
|
418
|
+
// `az boards query` flattens the WIQL result into a work-item array, so a
|
|
419
|
+
// queue poll is a single call rather than a fan-out over ids.
|
|
420
|
+
export const parseWorkItems = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoWorkItem)))(payload).pipe(Effect.map((items) => items.map(toWorkItem)), Effect.mapError(decodeFailure("az boards query")));
|
|
421
|
+
export const parseWorkItemIds = (payload) => parseWorkItems(payload).pipe(Effect.map((items) => items.map((item) => item.id)));
|
|
422
|
+
const AdoComments = Schema.Struct({
|
|
423
|
+
comments: Schema.Array(Schema.Struct({
|
|
424
|
+
id: Schema.Int,
|
|
425
|
+
text: Schema.optionalKey(Schema.String),
|
|
426
|
+
createdBy: Schema.optionalKey(Identity),
|
|
427
|
+
createdDate: Schema.optionalKey(Schema.String)
|
|
428
|
+
})).pipe(Schema.withConstructorDefault(Effect.succeed(Object.freeze([]))))
|
|
81
429
|
});
|
|
82
|
-
export const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
430
|
+
export const parseComments = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoComments))(payload).pipe(Effect.map((parsed) => parsed.comments.map((comment) => WorkItemComment.make({
|
|
431
|
+
id: comment.id,
|
|
432
|
+
author: identityName(comment.createdBy),
|
|
433
|
+
text: comment.text ?? "",
|
|
434
|
+
createdDate: comment.createdDate ?? ""
|
|
435
|
+
}))), Effect.mapError(decodeFailure("az devops invoke wit comments")));
|
|
436
|
+
// `relations` is absent — not empty — on a work item whose Development
|
|
437
|
+
// section has never been touched, which is every work item until this tool
|
|
438
|
+
// links one. optionalKey, because a constructor default does not apply on
|
|
439
|
+
// decode and a missing key would fail the normal case.
|
|
440
|
+
const AdoRelations = Schema.Struct({
|
|
441
|
+
relations: Schema.optionalKey(Schema.Array(Schema.Struct({
|
|
442
|
+
rel: Schema.optionalKey(Schema.String),
|
|
443
|
+
url: Schema.optionalKey(Schema.String),
|
|
444
|
+
attributes: Schema.optionalKey(Schema.Struct({ name: Schema.optionalKey(Schema.String) }))
|
|
445
|
+
})))
|
|
91
446
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})
|
|
116
|
-
export const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
447
|
+
// A work item with no Development section decodes to an empty list rather
|
|
448
|
+
// than failing: "nothing linked yet" is the normal state, not an error.
|
|
449
|
+
export const parseDevelopmentLinks = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRelations))(payload).pipe(Effect.map((parsed) => (parsed.relations ?? [])
|
|
450
|
+
.filter((relation) => (relation.rel ?? "").toLowerCase() === "artifactlink")
|
|
451
|
+
.flatMap((relation) => {
|
|
452
|
+
const artifact = parseArtifactUri(relation.url ?? "");
|
|
453
|
+
if (artifact === undefined) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
// The URI segment already fixes the kind; the attribute name is
|
|
457
|
+
// only a cross-check for the links whose segment is shared.
|
|
458
|
+
const named = artifactKindOfName(relation.attributes?.name ?? "");
|
|
459
|
+
return named === undefined || named === artifact.kind ? [artifact] : [];
|
|
460
|
+
})), Effect.mapError(decodeFailure("az boards work-item show --expand relations")));
|
|
461
|
+
const AdoRepository = Schema.Struct({
|
|
462
|
+
id: Schema.String,
|
|
463
|
+
name: Schema.String,
|
|
464
|
+
project: Schema.Struct({
|
|
465
|
+
id: Schema.String,
|
|
466
|
+
name: Schema.optionalKey(Schema.String)
|
|
467
|
+
}),
|
|
468
|
+
defaultBranch: Schema.optionalKey(Schema.String),
|
|
469
|
+
webUrl: Schema.optionalKey(Schema.String)
|
|
470
|
+
});
|
|
471
|
+
export const parseRepository = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRepository))(payload).pipe(Effect.map((repository) => GitRepository.make({
|
|
472
|
+
id: repository.id,
|
|
473
|
+
name: repository.name,
|
|
474
|
+
projectId: repository.project.id,
|
|
475
|
+
projectName: repository.project.name ?? "",
|
|
476
|
+
// Reported as a full ref; callers branch and push by short name.
|
|
477
|
+
defaultBranch: branchName(repository.defaultBranch ?? ""),
|
|
478
|
+
webUrl: repository.webUrl ?? ""
|
|
479
|
+
})), Effect.mapError(decodeFailure("az repos show")));
|
|
480
|
+
const AdoPr = Schema.Struct({
|
|
125
481
|
pullRequestId: Schema.Int,
|
|
126
482
|
repository: Schema.Struct({
|
|
127
483
|
id: Schema.String,
|
|
128
|
-
project: Schema.Struct({
|
|
129
|
-
id: Schema.String
|
|
130
|
-
})
|
|
484
|
+
project: Schema.Struct({ id: Schema.String })
|
|
131
485
|
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
486
|
+
});
|
|
487
|
+
const toPullRequest = (config, pr) => AdoPullRequest.make({
|
|
488
|
+
id: pr.pullRequestId,
|
|
489
|
+
repoId: pr.repository.id,
|
|
490
|
+
projectId: pr.repository.project.id,
|
|
136
491
|
webUrl: `${config.orgUrl}/${config.project}/_git/` +
|
|
137
|
-
`${config.repository}/pullrequest/${
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
492
|
+
`${config.repository}/pullrequest/${String(pr.pullRequestId)}`
|
|
493
|
+
});
|
|
494
|
+
export const parsePullRequest = (config, payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPr))(payload).pipe(Effect.map((pr) => toPullRequest(config, pr)), Effect.mapError(decodeFailure("az repos pr")));
|
|
495
|
+
export const parsePullRequests = (config, payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoPr)))(payload).pipe(Effect.map((prs) => prs.map((pr) => toPullRequest(config, pr))), Effect.mapError(decodeFailure("az repos pr list")));
|
|
496
|
+
const AdoPolicies = Schema.Array(Schema.Struct({
|
|
497
|
+
status: Schema.optionalKey(Schema.String)
|
|
498
|
+
}));
|
|
499
|
+
// Policy evaluation statuses: queued/running are still deciding, and
|
|
500
|
+
// rejected/broken have already decided against the PR.
|
|
501
|
+
export const outcomeFromPolicies = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPolicies))(payload).pipe(Effect.map((policies) => {
|
|
502
|
+
const statuses = policies.map((policy) => policy.status?.toLowerCase() ?? "");
|
|
503
|
+
return statuses.some((value) => ["queued", "running"].includes(value))
|
|
504
|
+
? "Pending"
|
|
505
|
+
: statuses.some((value) => ["rejected", "broken"].includes(value))
|
|
506
|
+
? "Failure"
|
|
507
|
+
: "Success";
|
|
508
|
+
}), Effect.mapError(decodeFailure("az repos pr policy list")));
|
|
509
|
+
const output = (result) => result.stdout.join("\n").trim();
|
|
510
|
+
export const mergeTags = (current, add, remove) => {
|
|
511
|
+
const removed = new Set(remove.map((tag) => tag.toLowerCase()));
|
|
512
|
+
const kept = current.filter((tag) => !removed.has(tag.toLowerCase()));
|
|
513
|
+
const present = new Set(kept.map((tag) => tag.toLowerCase()));
|
|
514
|
+
const added = add.filter((tag) => !present.has(tag.toLowerCase()) && !removed.has(tag.toLowerCase()));
|
|
515
|
+
return [...kept, ...added];
|
|
516
|
+
};
|
|
517
|
+
export const makeAzureDevOpsTool = (config, process, workDir, events) => {
|
|
518
|
+
const run = (args) => process.run(["az", ...args], workDir, {}).pipe(Effect.mapError((error) => ProcessError.make({ message: `az ${args.join(" ")}`, detail: error.message })), Effect.flatMap((result) => result.exitCode === 0
|
|
519
|
+
? Effect.succeed(output(result))
|
|
520
|
+
: Effect.fail(ProcessError.make({
|
|
521
|
+
message: `az ${args.join(" ")}`,
|
|
522
|
+
detail: [...result.stdout, ...result.stderr].join("\n").trim() ||
|
|
523
|
+
`exit code ${result.exitCode}`
|
|
524
|
+
}))));
|
|
149
525
|
const read = (operation, effect) => guarded(Capabilities.AdoRead, operation, events, effect);
|
|
150
526
|
const write = (operation, effect) => guarded(Capabilities.AdoWrite, operation, events, effect);
|
|
151
|
-
const
|
|
527
|
+
const readWorkItem = (id) => read("ado readWorkItem", run(workItemShowArgs(config, id)).pipe(Effect.flatMap(parseWorkItem)));
|
|
528
|
+
const setFields = (id, fields) => write("ado setFields", run(workItemUpdateArgs(config, id, fields)).pipe(Effect.asVoid));
|
|
529
|
+
const listPrs = (sourceRef) => run(prListArgs(config, sourceRef)).pipe(Effect.flatMap((payload) => parsePullRequests(config, payload)));
|
|
152
530
|
return {
|
|
153
|
-
readWorkItem
|
|
154
|
-
|
|
531
|
+
readWorkItem,
|
|
532
|
+
listWorkItems: (filter = {}) => read("ado listWorkItems", run(queryArgs(config, wiqlFor(filter))).pipe(Effect.flatMap(parseWorkItems))),
|
|
533
|
+
wiqlIds: (query) => read("ado wiql", run(queryArgs(config, query)).pipe(Effect.flatMap(parseWorkItemIds))),
|
|
534
|
+
readComments: (id) => read("ado readComments", run(commentsArgs(config, id)).pipe(Effect.flatMap(parseComments))),
|
|
535
|
+
developmentLinks: (id) => read("ado developmentLinks", run(workItemShowArgs(config, id, "relations")).pipe(Effect.flatMap(parseDevelopmentLinks))),
|
|
536
|
+
linkArtifact: (id, artifact) => write("ado linkArtifact", run(relationAddArgs(config, id, artifact)).pipe(Effect.asVoid)),
|
|
537
|
+
repository: (name = config.repository) => read("ado repository", run(repositoryShowArgs(config, name)).pipe(Effect.flatMap(parseRepository))),
|
|
155
538
|
setFields,
|
|
156
539
|
setState: (id, state) => setFields(id, { "System.State": state }),
|
|
157
|
-
setAcceptanceCriteria: (id, text) => setFields(id, {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
540
|
+
setAcceptanceCriteria: (id, text) => setFields(id, { "Microsoft.VSTS.Common.AcceptanceCriteria": text }),
|
|
541
|
+
editTags: (id, add, remove) => add.length === 0 && remove.length === 0
|
|
542
|
+
? Effect.void
|
|
543
|
+
: readWorkItem(id).pipe(Effect.flatMap((item) => {
|
|
544
|
+
const next = mergeTags(item.tags, add, remove);
|
|
545
|
+
return next.length === item.tags.length &&
|
|
546
|
+
next.every((tag, index) => tag === item.tags[index])
|
|
547
|
+
? Effect.void
|
|
548
|
+
: setFields(id, { "System.Tags": next.join("; ") });
|
|
549
|
+
})),
|
|
550
|
+
writeComment: (id, text) => write("ado writeComment", run(workItemCommentArgs(config, id, text)).pipe(Effect.asVoid)),
|
|
551
|
+
createWorkItem: (workItemType, title, description, tags = []) => write("ado createWorkItem", run(workItemCreateArgs(config, workItemType, title, description, tags)).pipe(Effect.flatMap(parseWorkItem))),
|
|
552
|
+
createPr: (sourceRef, targetRef, title, body, draft = false) => write("ado createPr",
|
|
553
|
+
// An active PR for the branch already IS the deliverable; creating a
|
|
554
|
+
// second one would fail on the server and lose the first's reviews.
|
|
555
|
+
Effect.flatMap(listPrs(sourceRef), (existing) => {
|
|
556
|
+
const open = existing[0];
|
|
557
|
+
return open !== undefined
|
|
558
|
+
? Effect.succeed(open)
|
|
559
|
+
: run(prCreateArgs(config, sourceRef, targetRef, title, body, draft)).pipe(Effect.flatMap((payload) => parsePullRequest(config, payload)));
|
|
560
|
+
})),
|
|
561
|
+
openPrForBranch: (sourceRef) => read("ado listPrs", listPrs(sourceRef).pipe(Effect.map((prs) => prs[0]))),
|
|
562
|
+
updatePr: (pr, title, body) => write("ado updatePr", run(prUpdateArgs(config, pr.id, title, body)).pipe(Effect.asVoid)),
|
|
563
|
+
writePrComment: (pr, body) => write("ado writePrComment", run(prCommentArgs(config, pr.id, body)).pipe(Effect.asVoid)),
|
|
564
|
+
prPolicies: (pr) => read("ado prPolicies", run(prPolicyArgs(config, pr.id)).pipe(Effect.flatMap(outcomeFromPolicies))),
|
|
565
|
+
completePr: (pr, squash = true, deleteSourceBranch = true) => write("ado completePr", run(prCompleteArgs(config, pr.id, squash, deleteSourceBranch)).pipe(Effect.asVoid))
|
|
161
566
|
};
|
|
162
567
|
};
|
|
163
568
|
//# sourceMappingURL=AzureDevOpsTool.js.map
|