azdo-cli 0.24.0-rescue-develop-20261106T154004Z.916 → 0.25.0-develop.933
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
CHANGED
|
@@ -162,6 +162,7 @@ azdo relations list 1000 --json # JSON: { workItemId, relations: [..
|
|
|
162
162
|
| Linux credential store setup | [docs/linux-credential-store.md](docs/linux-credential-store.md) |
|
|
163
163
|
| Full command reference — every command, alias and option, credential resolution order, and the `--json` output contracts | [docs/commands.md](docs/commands.md) |
|
|
164
164
|
| Development setup | [docs/development.md](docs/development.md) |
|
|
165
|
+
| Project operations and automation (Dev Container, VM, Hermes) | [instructions/README.md](instructions/README.md) |
|
|
165
166
|
|
|
166
167
|
## License
|
|
167
168
|
|
|
@@ -1047,6 +1047,17 @@ var DpapiEntry = class {
|
|
|
1047
1047
|
}
|
|
1048
1048
|
};
|
|
1049
1049
|
|
|
1050
|
+
// src/lib/async.ts
|
|
1051
|
+
function runSequentially(items, task) {
|
|
1052
|
+
return items.reduce(
|
|
1053
|
+
(previous, item, index) => previous.then(() => task(item, index)),
|
|
1054
|
+
Promise.resolve()
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
function asPromise(fn) {
|
|
1058
|
+
return Promise.resolve().then(fn);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1050
1061
|
// src/services/credential-store.ts
|
|
1051
1062
|
var SERVICE = "azdo-cli";
|
|
1052
1063
|
var LEGACY_ACCOUNT = "pat";
|
|
@@ -1187,7 +1198,7 @@ function serializeCredential(cred) {
|
|
|
1187
1198
|
}
|
|
1188
1199
|
return JSON.stringify(cred);
|
|
1189
1200
|
}
|
|
1190
|
-
|
|
1201
|
+
function maybeMigrateLegacy(targetOrg) {
|
|
1191
1202
|
if (activeCredentialStore() === "dpapi") return null;
|
|
1192
1203
|
const config = loadConfig();
|
|
1193
1204
|
if (!config.org || config.org !== targetOrg) {
|
|
@@ -1235,30 +1246,31 @@ async function getPat(org) {
|
|
|
1235
1246
|
if (cred.kind === "pat") return cred.token;
|
|
1236
1247
|
return null;
|
|
1237
1248
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
return parseStoredValue(migrated);
|
|
1245
|
-
}
|
|
1246
|
-
return parseStoredValue(value);
|
|
1249
|
+
function getStoredCredential(org) {
|
|
1250
|
+
return asPromise(() => {
|
|
1251
|
+
const entry = entryFor(accountFor(org));
|
|
1252
|
+
const value = wrapUnavailable(() => entry.getPassword()) ?? maybeMigrateLegacy(org);
|
|
1253
|
+
return value === null ? null : parseStoredValue(value);
|
|
1254
|
+
});
|
|
1247
1255
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1256
|
+
function storePat(org, pat) {
|
|
1257
|
+
return asPromise(() => {
|
|
1258
|
+
const cred = { kind: "pat", token: pat };
|
|
1259
|
+
const entry = entryFor(accountFor(org));
|
|
1260
|
+
wrapUnavailable(() => entry.setPassword(serializeCredential(cred)));
|
|
1261
|
+
appendAuthAuditEvent({
|
|
1262
|
+
event: "auth.store",
|
|
1263
|
+
org,
|
|
1264
|
+
backend: probeBackend(),
|
|
1265
|
+
masked_pat: maskedDisplay(pat)
|
|
1266
|
+
});
|
|
1257
1267
|
});
|
|
1258
1268
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1269
|
+
function storeOAuthCredential(org, cred) {
|
|
1270
|
+
return asPromise(() => {
|
|
1271
|
+
const entry = entryFor(accountFor(org));
|
|
1272
|
+
wrapUnavailable(() => entry.setPassword(serializeCredential(cred)));
|
|
1273
|
+
});
|
|
1262
1274
|
}
|
|
1263
1275
|
async function deletePat(org) {
|
|
1264
1276
|
const entry = entryFor(accountFor(org));
|
|
@@ -1290,13 +1302,16 @@ async function deletePat(org) {
|
|
|
1290
1302
|
}
|
|
1291
1303
|
try {
|
|
1292
1304
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
1293
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
1305
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-4WISWRPJ.js");
|
|
1294
1306
|
unlinkSync2(lockPath2(org));
|
|
1295
1307
|
} catch {
|
|
1296
1308
|
}
|
|
1297
1309
|
return true;
|
|
1298
1310
|
}
|
|
1299
|
-
|
|
1311
|
+
function listOrgsWithStoredPat() {
|
|
1312
|
+
return asPromise(listOrgsWithStoredPatSync);
|
|
1313
|
+
}
|
|
1314
|
+
function listOrgsWithStoredPatSync() {
|
|
1300
1315
|
const seen = /* @__PURE__ */ new Set();
|
|
1301
1316
|
for (const ev of readAuditEvents()) {
|
|
1302
1317
|
if (ev.event === "auth.store" || ev.event === "oauth-login-success") {
|
|
@@ -1387,25 +1402,26 @@ function lockPath(org) {
|
|
|
1387
1402
|
async function defaultAcquireLock(org, waitMs = DEFAULT_LOCK_WAIT_MS) {
|
|
1388
1403
|
const path4 = lockPath(org);
|
|
1389
1404
|
mkdirSync(locksDir(), { recursive: true, mode: 448 });
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1405
|
+
return tryAcquireLock(path4, Date.now() + waitMs);
|
|
1406
|
+
}
|
|
1407
|
+
async function tryAcquireLock(path4, deadline) {
|
|
1408
|
+
try {
|
|
1409
|
+
const fd = openSync2(path4, "wx");
|
|
1410
|
+
closeSync2(fd);
|
|
1411
|
+
return {
|
|
1412
|
+
release: () => {
|
|
1413
|
+
try {
|
|
1414
|
+
unlinkSync(path4);
|
|
1415
|
+
} catch {
|
|
1401
1416
|
}
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
if (err.code !== "EEXIST") throw err;
|
|
1421
|
+
if (Date.now() > deadline) return null;
|
|
1408
1422
|
}
|
|
1423
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1424
|
+
return tryAcquireLock(path4, deadline);
|
|
1409
1425
|
}
|
|
1410
1426
|
function classifyRefreshFailure(error) {
|
|
1411
1427
|
const code = typeof error === "string" ? error : error.error ?? "";
|
|
@@ -1543,6 +1559,7 @@ export {
|
|
|
1543
1559
|
CredentialStoreUnavailableError,
|
|
1544
1560
|
CredentialMissingError,
|
|
1545
1561
|
CredentialRefreshError,
|
|
1562
|
+
runSequentially,
|
|
1546
1563
|
appendAuthAuditEvent,
|
|
1547
1564
|
readAuditEvents,
|
|
1548
1565
|
SETTINGS,
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
resolveOAuthConfig,
|
|
38
38
|
resolveScopedConfig,
|
|
39
39
|
runAuthCodeFlow,
|
|
40
|
+
runSequentially,
|
|
40
41
|
setConfigValue,
|
|
41
42
|
setOrgScopedValue,
|
|
42
43
|
storeOAuthCredential,
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
tokenResponseToCredential,
|
|
46
47
|
unsetConfigValue,
|
|
47
48
|
unsetOrgScopedValue
|
|
48
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-WCO6SF63.js";
|
|
49
50
|
|
|
50
51
|
// src/program.ts
|
|
51
52
|
import { Command as Command20 } from "commander";
|
|
@@ -154,6 +155,14 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
156
157
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
158
|
+
writeCommandError(error, id, context, scope);
|
|
159
|
+
if (exit) {
|
|
160
|
+
process.exit(1);
|
|
161
|
+
} else {
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function writeCommandError(error, id, context, scope) {
|
|
157
166
|
const msg = error.message;
|
|
158
167
|
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
159
168
|
if (isSentinel(msg, "AUTH_FAILED")) {
|
|
@@ -193,11 +202,6 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
193
202
|
process.stderr.write(`Error: ${msg}
|
|
194
203
|
`);
|
|
195
204
|
}
|
|
196
|
-
if (exit) {
|
|
197
|
-
process.exit(1);
|
|
198
|
-
} else {
|
|
199
|
-
process.exitCode = 1;
|
|
200
|
-
}
|
|
201
205
|
}
|
|
202
206
|
var STDIN_PATH = "-";
|
|
203
207
|
function readTextSource(file) {
|
|
@@ -340,9 +344,8 @@ async function downloadImagesFromFields(fields, args, credential) {
|
|
|
340
344
|
}
|
|
341
345
|
}
|
|
342
346
|
const results = [];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
index += 1;
|
|
347
|
+
await runSequentially(references, async (reference, position) => {
|
|
348
|
+
const index = position + 1;
|
|
346
349
|
try {
|
|
347
350
|
const bytes = await downloadAttachment(reference.url, credential);
|
|
348
351
|
const processed = await processImageBytes(bytes, options.maxWidth);
|
|
@@ -363,7 +366,7 @@ async function downloadImagesFromFields(fields, args, credential) {
|
|
|
363
366
|
error: err instanceof Error ? err.message : String(err)
|
|
364
367
|
});
|
|
365
368
|
}
|
|
366
|
-
}
|
|
369
|
+
});
|
|
367
370
|
return results;
|
|
368
371
|
}
|
|
369
372
|
async function runImageDownload(fields, args, credential) {
|
|
@@ -846,23 +849,24 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
|
846
849
|
}
|
|
847
850
|
return stringifyFieldValue(value);
|
|
848
851
|
}
|
|
852
|
+
async function listWorkItemCommentPages(context, id, cred, continuationToken, comments) {
|
|
853
|
+
const response = await fetchWithErrors(
|
|
854
|
+
buildWorkItemCommentsListUrl(context, id, continuationToken ?? void 0).toString(),
|
|
855
|
+
{ headers: authHeaders(cred) }
|
|
856
|
+
);
|
|
857
|
+
if (!response.ok) {
|
|
858
|
+
throw httpError(response);
|
|
859
|
+
}
|
|
860
|
+
const data = await response.json();
|
|
861
|
+
comments.push(
|
|
862
|
+
...(data.comments ?? []).map((comment) => mapWorkItemComment(comment, id)).filter((comment) => !comment.isDeleted)
|
|
863
|
+
);
|
|
864
|
+
const next = readContinuationToken(response, data);
|
|
865
|
+
if (next === null) return comments;
|
|
866
|
+
return listWorkItemCommentPages(context, id, cred, next, comments);
|
|
867
|
+
}
|
|
849
868
|
async function listWorkItemComments(context, id, cred) {
|
|
850
|
-
const comments = [];
|
|
851
|
-
let continuationToken = null;
|
|
852
|
-
do {
|
|
853
|
-
const response = await fetchWithErrors(
|
|
854
|
-
buildWorkItemCommentsListUrl(context, id, continuationToken ?? void 0).toString(),
|
|
855
|
-
{ headers: authHeaders(cred) }
|
|
856
|
-
);
|
|
857
|
-
if (!response.ok) {
|
|
858
|
-
throw httpError(response);
|
|
859
|
-
}
|
|
860
|
-
const data = await response.json();
|
|
861
|
-
comments.push(
|
|
862
|
-
...(data.comments ?? []).map((comment) => mapWorkItemComment(comment, id)).filter((comment) => !comment.isDeleted)
|
|
863
|
-
);
|
|
864
|
-
continuationToken = readContinuationToken(response, data);
|
|
865
|
-
} while (continuationToken !== null);
|
|
869
|
+
const comments = await listWorkItemCommentPages(context, id, cred, null, []);
|
|
866
870
|
return {
|
|
867
871
|
workItemId: id,
|
|
868
872
|
count: comments.length,
|
|
@@ -1442,29 +1446,31 @@ async function loginWithOAuth(org, opts = {}) {
|
|
|
1442
1446
|
async function logout(opts = {}) {
|
|
1443
1447
|
if (opts.all) {
|
|
1444
1448
|
const orgs = await listOrgsWithStoredPat();
|
|
1445
|
-
|
|
1446
|
-
for (const o of orgs) {
|
|
1447
|
-
const cred2 = await getStoredCredential(o);
|
|
1448
|
-
const ok2 = await deletePat(o);
|
|
1449
|
-
if (ok2 && cred2 !== null) {
|
|
1450
|
-
removed.push({ org: o, kind: cred2.kind });
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
return { removed };
|
|
1449
|
+
return { removed: await logoutOrgsInOrder(orgs, 0, []) };
|
|
1454
1450
|
}
|
|
1455
1451
|
if (!opts.org) {
|
|
1456
1452
|
throw new Error("logout requires an org or --all");
|
|
1457
1453
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1454
|
+
return { removed: await logoutOrg(opts.org) };
|
|
1455
|
+
}
|
|
1456
|
+
async function logoutOrgsInOrder(orgs, index, removed) {
|
|
1457
|
+
const org = orgs[index];
|
|
1458
|
+
if (org === void 0) return removed;
|
|
1459
|
+
removed.push(...await logoutOrg(org));
|
|
1460
|
+
return logoutOrgsInOrder(orgs, index + 1, removed);
|
|
1461
|
+
}
|
|
1462
|
+
async function logoutOrg(org) {
|
|
1463
|
+
const cred = await getStoredCredential(org);
|
|
1464
|
+
const ok = await deletePat(org);
|
|
1465
|
+
return ok && cred !== null ? [{ org, kind: cred.kind }] : [];
|
|
1461
1466
|
}
|
|
1462
1467
|
async function status() {
|
|
1463
1468
|
const orgs = await listOrgsWithStoredPat();
|
|
1469
|
+
const creds = await Promise.all(orgs.map((org) => getStoredCredential(org)));
|
|
1464
1470
|
const out = [];
|
|
1465
|
-
|
|
1466
|
-
const cred =
|
|
1467
|
-
if (cred === null)
|
|
1471
|
+
orgs.forEach((org, i) => {
|
|
1472
|
+
const cred = creds[i];
|
|
1473
|
+
if (cred === null) return;
|
|
1468
1474
|
if (cred.kind === "pat") {
|
|
1469
1475
|
out.push({ org, kind: "pat", backend: probeBackend() });
|
|
1470
1476
|
} else {
|
|
@@ -1477,7 +1483,7 @@ async function status() {
|
|
|
1477
1483
|
backend: probeBackend()
|
|
1478
1484
|
});
|
|
1479
1485
|
}
|
|
1480
|
-
}
|
|
1486
|
+
});
|
|
1481
1487
|
return { orgs: out };
|
|
1482
1488
|
}
|
|
1483
1489
|
async function exportCredential(org) {
|
|
@@ -3128,10 +3134,8 @@ ${renderSettingsHelp()}`);
|
|
|
3128
3134
|
const ask = createAsk(rl);
|
|
3129
3135
|
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
3130
3136
|
process.stderr.write("=======================================\n\n");
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
await promptForSetting(cfg, setting, ask);
|
|
3134
|
-
}
|
|
3137
|
+
const wizardSettings = SETTINGS.filter((setting) => setting.key !== "credentialStore");
|
|
3138
|
+
await runSequentially(wizardSettings, (setting) => promptForSetting(cfg, setting, ask));
|
|
3135
3139
|
rl.close();
|
|
3136
3140
|
process.stderr.write("Configuration complete!\n");
|
|
3137
3141
|
});
|
|
@@ -3771,7 +3775,8 @@ function stringifyValue(value) {
|
|
|
3771
3775
|
if (value === null || value === void 0) return "";
|
|
3772
3776
|
if (typeof value === "string") return value;
|
|
3773
3777
|
if (typeof value === "object") return JSON.stringify(value);
|
|
3774
|
-
if (typeof value === "symbol"
|
|
3778
|
+
if (typeof value === "symbol") return value.toString();
|
|
3779
|
+
if (typeof value === "function") return `[function ${value.name || "anonymous"}]`;
|
|
3775
3780
|
return String(value);
|
|
3776
3781
|
}
|
|
3777
3782
|
function formatRichValue(raw) {
|
|
@@ -4296,7 +4301,7 @@ async function getPullRequestThread(context, repo, cred, prId, threadId) {
|
|
|
4296
4301
|
const data = await readJsonResponse(response);
|
|
4297
4302
|
return toActiveCommentThread(data);
|
|
4298
4303
|
}
|
|
4299
|
-
async function createPullRequestThread(context, repo, cred, prId, content, status2) {
|
|
4304
|
+
async function createPullRequestThread(context, repo, cred, prId, content, status2, anchor) {
|
|
4300
4305
|
const url = new URL(
|
|
4301
4306
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
4302
4307
|
);
|
|
@@ -4307,6 +4312,10 @@ async function createPullRequestThread(context, repo, cred, prId, content, statu
|
|
|
4307
4312
|
if (status2 !== void 0) {
|
|
4308
4313
|
payload.status = status2;
|
|
4309
4314
|
}
|
|
4315
|
+
if (anchor !== void 0) {
|
|
4316
|
+
payload.threadContext = anchor.threadContext;
|
|
4317
|
+
payload.pullRequestThreadContext = anchor.pullRequestThreadContext;
|
|
4318
|
+
}
|
|
4310
4319
|
const response = await fetchWithErrors(url.toString(), {
|
|
4311
4320
|
method: "POST",
|
|
4312
4321
|
headers: {
|
|
@@ -4318,6 +4327,42 @@ async function createPullRequestThread(context, repo, cred, prId, content, statu
|
|
|
4318
4327
|
const data = await readJsonResponse(response);
|
|
4319
4328
|
return toActiveCommentThread(data);
|
|
4320
4329
|
}
|
|
4330
|
+
function buildIterationsUrl(context, repo, prId, suffix = "") {
|
|
4331
|
+
const url = new URL(
|
|
4332
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/iterations${suffix}`
|
|
4333
|
+
);
|
|
4334
|
+
url.searchParams.set("api-version", "7.1");
|
|
4335
|
+
return url;
|
|
4336
|
+
}
|
|
4337
|
+
async function getLatestPullRequestIterationId(context, repo, cred, prId) {
|
|
4338
|
+
const url = buildIterationsUrl(context, repo, prId);
|
|
4339
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
4340
|
+
const data = await readJsonResponse(response);
|
|
4341
|
+
const ids = (data.value ?? []).map((iteration) => iteration.id);
|
|
4342
|
+
if (ids.length === 0) {
|
|
4343
|
+
throw new Error(`Pull request #${prId} has no iterations.`);
|
|
4344
|
+
}
|
|
4345
|
+
return Math.max(...ids);
|
|
4346
|
+
}
|
|
4347
|
+
async function getPullRequestIterationChanges(context, repo, cred, prId, iterationId) {
|
|
4348
|
+
const changes = [];
|
|
4349
|
+
let skip = 0;
|
|
4350
|
+
for (; ; ) {
|
|
4351
|
+
const url = buildIterationsUrl(context, repo, prId, `/${iterationId}/changes`);
|
|
4352
|
+
url.searchParams.set("$top", "2000");
|
|
4353
|
+
if (skip > 0) {
|
|
4354
|
+
url.searchParams.set("$skip", String(skip));
|
|
4355
|
+
}
|
|
4356
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
4357
|
+
const page = await readJsonResponse(response);
|
|
4358
|
+
changes.push(...page.changeEntries ?? []);
|
|
4359
|
+
const next = page.nextSkip ?? 0;
|
|
4360
|
+
if (next <= skip) {
|
|
4361
|
+
return changes;
|
|
4362
|
+
}
|
|
4363
|
+
skip = next;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4321
4366
|
function buildThreadCommentUrl(context, repo, prId, threadId) {
|
|
4322
4367
|
const url = new URL(
|
|
4323
4368
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments`
|
|
@@ -4641,6 +4686,39 @@ async function resolvePullRequestTemplate(context, repo, cred, defaultBranch, ta
|
|
|
4641
4686
|
return firstMatchingTemplate(context, repo, cred, defaultBranch, defaultTemplateCandidates(), "default");
|
|
4642
4687
|
}
|
|
4643
4688
|
|
|
4689
|
+
// src/services/thread-anchor.ts
|
|
4690
|
+
var LINE_END_OFFSET = 1e3;
|
|
4691
|
+
function normalizeThreadFilePath(raw) {
|
|
4692
|
+
if (raw.trim() === "") {
|
|
4693
|
+
return null;
|
|
4694
|
+
}
|
|
4695
|
+
let path5 = raw.replaceAll("\\", "/");
|
|
4696
|
+
while (path5.startsWith("./")) {
|
|
4697
|
+
path5 = path5.slice(2);
|
|
4698
|
+
}
|
|
4699
|
+
path5 = path5.replace(/^\/+/, "");
|
|
4700
|
+
return path5 === "" ? null : `/${path5}`;
|
|
4701
|
+
}
|
|
4702
|
+
function buildThreadAnchorContext(filePath, line, endLine, side) {
|
|
4703
|
+
const start = { line, offset: 1 };
|
|
4704
|
+
const end = { line: endLine, offset: LINE_END_OFFSET };
|
|
4705
|
+
return side === "right" ? { filePath, rightFileStart: start, rightFileEnd: end } : { filePath, leftFileStart: start, leftFileEnd: end };
|
|
4706
|
+
}
|
|
4707
|
+
function buildPullRequestThreadContext(iterationId, changeTrackingId) {
|
|
4708
|
+
const context = {
|
|
4709
|
+
iterationContext: { firstComparingIteration: iterationId, secondComparingIteration: iterationId }
|
|
4710
|
+
};
|
|
4711
|
+
if (changeTrackingId !== void 0) {
|
|
4712
|
+
context.changeTrackingId = changeTrackingId;
|
|
4713
|
+
}
|
|
4714
|
+
return context;
|
|
4715
|
+
}
|
|
4716
|
+
function findChangeTrackingId(changes, filePath, side) {
|
|
4717
|
+
const byPath = (change) => change.item?.path === filePath;
|
|
4718
|
+
const match = side === "left" ? changes.find((change) => change.originalPath === filePath) ?? changes.find(byPath) : changes.find(byPath);
|
|
4719
|
+
return match?.changeTrackingId;
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4644
4722
|
// src/commands/pr.ts
|
|
4645
4723
|
function parsePositivePrNumber(raw) {
|
|
4646
4724
|
if (!/^\d+$/.test(raw)) {
|
|
@@ -5794,6 +5872,130 @@ var CREATABLE_THREAD_STATUSES = [
|
|
|
5794
5872
|
"byDesign",
|
|
5795
5873
|
"pending"
|
|
5796
5874
|
];
|
|
5875
|
+
var THREAD_ANCHOR_SIDES = ["right", "left"];
|
|
5876
|
+
function parseCommentAddStatus(raw) {
|
|
5877
|
+
if (raw === void 0) {
|
|
5878
|
+
return void 0;
|
|
5879
|
+
}
|
|
5880
|
+
const match = CREATABLE_THREAD_STATUSES.find((candidate) => candidate === raw);
|
|
5881
|
+
if (match === void 0) {
|
|
5882
|
+
writeError(`Invalid --status "${raw}"; expected one of ${CREATABLE_THREAD_STATUSES.join(", ")}.`);
|
|
5883
|
+
return null;
|
|
5884
|
+
}
|
|
5885
|
+
return match;
|
|
5886
|
+
}
|
|
5887
|
+
function parseLineOption(flag, raw) {
|
|
5888
|
+
const value = parsePositivePrNumber(raw);
|
|
5889
|
+
if (value === null) {
|
|
5890
|
+
writeError(`Invalid ${flag} "${raw}"; expected a positive integer (1-based line number).`);
|
|
5891
|
+
}
|
|
5892
|
+
return value;
|
|
5893
|
+
}
|
|
5894
|
+
function parseCommentAnchorOptions(options) {
|
|
5895
|
+
if (options.filePath === void 0) {
|
|
5896
|
+
const stray = [
|
|
5897
|
+
["--line", options.line],
|
|
5898
|
+
["--end-line", options.endLine],
|
|
5899
|
+
["--side", options.side],
|
|
5900
|
+
["--allow-unchanged-file", options.allowUnchangedFile]
|
|
5901
|
+
].find(([, value]) => value !== void 0);
|
|
5902
|
+
if (stray !== void 0) {
|
|
5903
|
+
writeError(`${stray[0]} requires --file-path.`);
|
|
5904
|
+
return null;
|
|
5905
|
+
}
|
|
5906
|
+
return void 0;
|
|
5907
|
+
}
|
|
5908
|
+
if (options.line === void 0) {
|
|
5909
|
+
writeError("--file-path requires --line.");
|
|
5910
|
+
return null;
|
|
5911
|
+
}
|
|
5912
|
+
const filePath = normalizeThreadFilePath(options.filePath);
|
|
5913
|
+
if (filePath === null) {
|
|
5914
|
+
writeError(`Invalid --file-path "${options.filePath}"; expected a repository file path.`);
|
|
5915
|
+
return null;
|
|
5916
|
+
}
|
|
5917
|
+
const line = parseLineOption("--line", options.line);
|
|
5918
|
+
if (line === null) {
|
|
5919
|
+
return null;
|
|
5920
|
+
}
|
|
5921
|
+
const endLine = options.endLine === void 0 ? line : parseLineOption("--end-line", options.endLine);
|
|
5922
|
+
if (endLine === null) {
|
|
5923
|
+
return null;
|
|
5924
|
+
}
|
|
5925
|
+
if (endLine < line) {
|
|
5926
|
+
writeError(`--end-line (${endLine}) must not be less than --line (${line}).`);
|
|
5927
|
+
return null;
|
|
5928
|
+
}
|
|
5929
|
+
const side = THREAD_ANCHOR_SIDES.find((candidate) => candidate === (options.side ?? "right"));
|
|
5930
|
+
if (side === void 0) {
|
|
5931
|
+
writeError(`Invalid --side "${options.side}"; expected ${THREAD_ANCHOR_SIDES.join(" or ")}.`);
|
|
5932
|
+
return null;
|
|
5933
|
+
}
|
|
5934
|
+
return { filePath, line, endLine, side, allowUnchangedFile: options.allowUnchangedFile === true };
|
|
5935
|
+
}
|
|
5936
|
+
async function resolveCommentAnchor(target, request) {
|
|
5937
|
+
const prId = target.pullRequest.id;
|
|
5938
|
+
const iterationId = await getLatestPullRequestIterationId(target.context, target.repo, target.pat, prId);
|
|
5939
|
+
const changes = await getPullRequestIterationChanges(target.context, target.repo, target.pat, prId, iterationId);
|
|
5940
|
+
const changeTrackingId = findChangeTrackingId(changes, request.filePath, request.side);
|
|
5941
|
+
if (changeTrackingId === void 0 && !request.allowUnchangedFile) {
|
|
5942
|
+
writeError(
|
|
5943
|
+
`${request.filePath} is not among the files changed by pull request #${prId} (iteration ${iterationId}). Check the path, or pass --allow-unchanged-file to post the comment anyway.`,
|
|
5944
|
+
3
|
|
5945
|
+
);
|
|
5946
|
+
return null;
|
|
5947
|
+
}
|
|
5948
|
+
return {
|
|
5949
|
+
request,
|
|
5950
|
+
iterationId,
|
|
5951
|
+
changeTrackingId,
|
|
5952
|
+
threadContext: buildThreadAnchorContext(request.filePath, request.line, request.endLine, request.side),
|
|
5953
|
+
pullRequestThreadContext: buildPullRequestThreadContext(iterationId, changeTrackingId)
|
|
5954
|
+
};
|
|
5955
|
+
}
|
|
5956
|
+
function describeCommentAnchor(anchor) {
|
|
5957
|
+
const { filePath, line, endLine, side } = anchor.request;
|
|
5958
|
+
const lines = line === endLine ? `line ${line}` : `lines ${line}-${endLine}`;
|
|
5959
|
+
const tracking = anchor.changeTrackingId === void 0 ? "no changeTrackingId" : `changeTrackingId ${anchor.changeTrackingId}`;
|
|
5960
|
+
return ` at ${filePath} ${lines} (${side} side, iteration ${anchor.iterationId}, ${tracking})`;
|
|
5961
|
+
}
|
|
5962
|
+
function warnIfUntracked(anchor) {
|
|
5963
|
+
if (anchor !== void 0 && anchor.changeTrackingId === void 0) {
|
|
5964
|
+
process.stderr.write(
|
|
5965
|
+
`Note: ${anchor.request.filePath} is not among the pull request's changed files, so the thread is posted without a changeTrackingId and will not follow the line across later pushes.
|
|
5966
|
+
`
|
|
5967
|
+
);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
function withAnchorFields(result, anchor) {
|
|
5971
|
+
if (anchor === void 0) {
|
|
5972
|
+
return result;
|
|
5973
|
+
}
|
|
5974
|
+
return {
|
|
5975
|
+
...result,
|
|
5976
|
+
threadContext: anchor.threadContext,
|
|
5977
|
+
pullRequestThreadContext: anchor.pullRequestThreadContext,
|
|
5978
|
+
changeTracked: anchor.changeTrackingId !== void 0
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
function writeCommentAddDryRun(prId, body, status2, anchor, json) {
|
|
5982
|
+
if (json) {
|
|
5983
|
+
const dryResult = withAnchorFields(
|
|
5984
|
+
{ pullRequestId: prId, threadId: null, commentId: null, status: status2 ?? null, content: body, dryRun: true },
|
|
5985
|
+
anchor
|
|
5986
|
+
);
|
|
5987
|
+
process.stdout.write(`${JSON.stringify(dryResult, null, 2)}
|
|
5988
|
+
`);
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5991
|
+
const statusSuffix = status2 === void 0 ? "" : ` with status ${status2}`;
|
|
5992
|
+
const anchorSuffix = anchor === void 0 ? "" : describeCommentAnchor(anchor);
|
|
5993
|
+
process.stdout.write(
|
|
5994
|
+
`Dry run: would post a new comment thread${statusSuffix} on pull request #${prId}${anchorSuffix} (${body.length} chars).
|
|
5995
|
+
${body}
|
|
5996
|
+
`
|
|
5997
|
+
);
|
|
5998
|
+
}
|
|
5797
5999
|
async function runCommentAdd(text, options) {
|
|
5798
6000
|
let context;
|
|
5799
6001
|
try {
|
|
@@ -5801,16 +6003,13 @@ async function runCommentAdd(text, options) {
|
|
|
5801
6003
|
if (body === null) {
|
|
5802
6004
|
return;
|
|
5803
6005
|
}
|
|
5804
|
-
|
|
5805
|
-
if (
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
return;
|
|
5812
|
-
}
|
|
5813
|
-
status2 = match;
|
|
6006
|
+
const status2 = parseCommentAddStatus(options.status);
|
|
6007
|
+
if (status2 === null) {
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6010
|
+
const anchorRequest = parseCommentAnchorOptions(options);
|
|
6011
|
+
if (anchorRequest === null) {
|
|
6012
|
+
return;
|
|
5814
6013
|
}
|
|
5815
6014
|
const target = await resolvePullRequestTarget(options, {
|
|
5816
6015
|
onContextResolved: (resolved) => {
|
|
@@ -5820,26 +6019,13 @@ async function runCommentAdd(text, options) {
|
|
|
5820
6019
|
if (target === null) {
|
|
5821
6020
|
return;
|
|
5822
6021
|
}
|
|
6022
|
+
const anchor = anchorRequest === void 0 ? void 0 : await resolveCommentAnchor(target, anchorRequest);
|
|
6023
|
+
if (anchor === null) {
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
warnIfUntracked(anchor);
|
|
5823
6027
|
if (options.dryRun === true) {
|
|
5824
|
-
|
|
5825
|
-
pullRequestId: target.pullRequest.id,
|
|
5826
|
-
threadId: null,
|
|
5827
|
-
commentId: null,
|
|
5828
|
-
status: status2 ?? null,
|
|
5829
|
-
content: body,
|
|
5830
|
-
dryRun: true
|
|
5831
|
-
};
|
|
5832
|
-
if (options.json) {
|
|
5833
|
-
process.stdout.write(`${JSON.stringify(dryResult, null, 2)}
|
|
5834
|
-
`);
|
|
5835
|
-
return;
|
|
5836
|
-
}
|
|
5837
|
-
const statusSuffix = status2 === void 0 ? "" : ` with status ${status2}`;
|
|
5838
|
-
process.stdout.write(
|
|
5839
|
-
`Dry run: would post a new comment thread${statusSuffix} on pull request #${target.pullRequest.id} (${body.length} chars).
|
|
5840
|
-
${body}
|
|
5841
|
-
`
|
|
5842
|
-
);
|
|
6028
|
+
writeCommentAddDryRun(target.pullRequest.id, body, status2, anchor, options.json);
|
|
5843
6029
|
return;
|
|
5844
6030
|
}
|
|
5845
6031
|
const thread = await createPullRequestThread(
|
|
@@ -5848,24 +6034,29 @@ ${body}
|
|
|
5848
6034
|
target.pat,
|
|
5849
6035
|
target.pullRequest.id,
|
|
5850
6036
|
body,
|
|
5851
|
-
status2
|
|
6037
|
+
status2,
|
|
6038
|
+
...anchor === void 0 ? [] : [{ threadContext: anchor.threadContext, pullRequestThreadContext: anchor.pullRequestThreadContext }]
|
|
5852
6039
|
);
|
|
5853
6040
|
const created = thread.comments[0];
|
|
5854
|
-
const result =
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
6041
|
+
const result = withAnchorFields(
|
|
6042
|
+
{
|
|
6043
|
+
pullRequestId: target.pullRequest.id,
|
|
6044
|
+
threadId: thread.id,
|
|
6045
|
+
commentId: created?.id ?? null,
|
|
6046
|
+
status: thread.status,
|
|
6047
|
+
content: created?.content ?? body,
|
|
6048
|
+
dryRun: false
|
|
6049
|
+
},
|
|
6050
|
+
anchor
|
|
6051
|
+
);
|
|
5862
6052
|
if (options.json) {
|
|
5863
6053
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
5864
6054
|
`);
|
|
5865
6055
|
return;
|
|
5866
6056
|
}
|
|
6057
|
+
const anchorSuffix = anchor === void 0 ? "" : describeCommentAnchor(anchor);
|
|
5867
6058
|
process.stdout.write(
|
|
5868
|
-
`Comment posted to pull request #${target.pullRequest.id} (thread #${thread.id}).
|
|
6059
|
+
`Comment posted to pull request #${target.pullRequest.id} (thread #${thread.id})${anchorSuffix}.
|
|
5869
6060
|
`
|
|
5870
6061
|
);
|
|
5871
6062
|
} catch (err) {
|
|
@@ -5877,18 +6068,21 @@ function buildCommentAddCommand(name, description) {
|
|
|
5877
6068
|
withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("[text]", "body of the new comment; omit when using --file").option("--file <path>", "read the comment body from a UTF-8 file instead of the inline argument").option(
|
|
5878
6069
|
"--status <status>",
|
|
5879
6070
|
`thread status (${CREATABLE_THREAD_STATUSES.join(" | ")}); omit for a plain, non-resolvable comment`
|
|
6071
|
+
).option("--file-path <path>", "anchor the thread to this repository file (Files tab); requires --line").option("--line <n>", "first line of the anchored range (1-based)").option("--end-line <n>", "last line of the anchored range (default: --line)").option("--side <side>", "right (new version, default) or left (old version, for deleted lines)").option(
|
|
6072
|
+
"--allow-unchanged-file",
|
|
6073
|
+
"post even when --file-path is not among the pull request's changed files (the thread will not follow the line across pushes)"
|
|
5880
6074
|
).option("--dry-run", "resolve the target pull request and print what would be posted, without writing anything").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (text, _options, command2) => {
|
|
5881
6075
|
await runCommentAdd(text, mergedPrOptions(command2));
|
|
5882
6076
|
});
|
|
5883
6077
|
return command;
|
|
5884
6078
|
}
|
|
5885
6079
|
function createPrCommentsAddCommand() {
|
|
5886
|
-
return buildCommentAddCommand("add", "Post a new comment thread on the pull request overview");
|
|
6080
|
+
return buildCommentAddCommand("add", "Post a new comment thread on the pull request overview, or on a file and line");
|
|
5887
6081
|
}
|
|
5888
6082
|
function createPrCommentAddCommand() {
|
|
5889
6083
|
return buildCommentAddCommand(
|
|
5890
6084
|
"comment-add",
|
|
5891
|
-
'Post a new comment thread on the pull request overview (alias of "azdo pr comments add")'
|
|
6085
|
+
'Post a new comment thread on the pull request overview, or on a file and line (alias of "azdo pr comments add")'
|
|
5892
6086
|
);
|
|
5893
6087
|
}
|
|
5894
6088
|
function parseExplicitCommentId(options) {
|
|
@@ -6178,13 +6372,15 @@ async function attachWorkItemIds(context, repo, cred, pullRequests) {
|
|
|
6178
6372
|
const result = new Array(pullRequests.length);
|
|
6179
6373
|
let next = 0;
|
|
6180
6374
|
const worker = async () => {
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
next += 1;
|
|
6184
|
-
const pullRequest = pullRequests[index];
|
|
6185
|
-
const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
|
|
6186
|
-
result[index] = { ...pullRequest, workItemIds };
|
|
6375
|
+
if (next >= pullRequests.length) {
|
|
6376
|
+
return;
|
|
6187
6377
|
}
|
|
6378
|
+
const index = next;
|
|
6379
|
+
next += 1;
|
|
6380
|
+
const pullRequest = pullRequests[index];
|
|
6381
|
+
const workItemIds = await getPullRequestWorkItemIds(context, repo, cred, pullRequest.id);
|
|
6382
|
+
result[index] = { ...pullRequest, workItemIds };
|
|
6383
|
+
await worker();
|
|
6188
6384
|
};
|
|
6189
6385
|
await Promise.all(Array.from({ length: Math.min(WORK_ITEM_LOOKUP_CONCURRENCY, pullRequests.length) }, worker));
|
|
6190
6386
|
return result;
|
|
@@ -6680,26 +6876,27 @@ async function getTestSummary(context, cred, buildId) {
|
|
|
6680
6876
|
var MAX_FAILED_TESTS = 50;
|
|
6681
6877
|
async function getFailedTests(context, cred, buildId) {
|
|
6682
6878
|
const testRuns = await listTestRuns(context, cred, buildId);
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6879
|
+
return collectFailedTests(context, cred, testRuns, 0, []);
|
|
6880
|
+
}
|
|
6881
|
+
async function collectFailedTests(context, cred, testRuns, index, failed) {
|
|
6882
|
+
const testRun = testRuns[index];
|
|
6883
|
+
if (testRun === void 0 || failed.length >= MAX_FAILED_TESTS) return failed;
|
|
6884
|
+
const resultsUrl = withApiVersion(
|
|
6885
|
+
new URL(`${orgProjectBase(context)}/_apis/test/runs/${testRun.id}/results`)
|
|
6886
|
+
);
|
|
6887
|
+
resultsUrl.searchParams.set("outcomes", "Failed");
|
|
6888
|
+
resultsUrl.searchParams.set("$top", String(MAX_FAILED_TESTS - failed.length));
|
|
6889
|
+
const resultsResponse = await fetchWithErrors(resultsUrl.toString(), {
|
|
6890
|
+
headers: authHeaders(cred)
|
|
6891
|
+
});
|
|
6892
|
+
const resultsData = await readJsonResponse2(resultsResponse);
|
|
6893
|
+
for (const result of resultsData.value) {
|
|
6894
|
+
failed.push({
|
|
6895
|
+
name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
|
|
6896
|
+
errorMessage: result.errorMessage ?? null
|
|
6693
6897
|
});
|
|
6694
|
-
const resultsData = await readJsonResponse2(resultsResponse);
|
|
6695
|
-
for (const result of resultsData.value) {
|
|
6696
|
-
failed.push({
|
|
6697
|
-
name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
|
|
6698
|
-
errorMessage: result.errorMessage ?? null
|
|
6699
|
-
});
|
|
6700
|
-
}
|
|
6701
6898
|
}
|
|
6702
|
-
return failed;
|
|
6899
|
+
return collectFailedTests(context, cred, testRuns, index + 1, failed);
|
|
6703
6900
|
}
|
|
6704
6901
|
function secondsBetween(start, finish) {
|
|
6705
6902
|
if (!start || !finish) return null;
|
|
@@ -6972,6 +7169,17 @@ function sleep(ms) {
|
|
|
6972
7169
|
setTimeout(resolve2, ms);
|
|
6973
7170
|
});
|
|
6974
7171
|
}
|
|
7172
|
+
async function pollRunUntilDone(context, cred, runId, timing) {
|
|
7173
|
+
const status2 = await getBuildStatus(context, cred, runId);
|
|
7174
|
+
if (status2.state === "completed") {
|
|
7175
|
+
return { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
7176
|
+
}
|
|
7177
|
+
if (Date.now() >= timing.deadline) {
|
|
7178
|
+
return { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
7179
|
+
}
|
|
7180
|
+
await sleep(timing.pollMs);
|
|
7181
|
+
return pollRunUntilDone(context, cred, runId, timing);
|
|
7182
|
+
}
|
|
6975
7183
|
function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
|
|
6976
7184
|
const widths = [];
|
|
6977
7185
|
for (const row of rows) {
|
|
@@ -7134,19 +7342,10 @@ function createPipelineWaitCommand() {
|
|
|
7134
7342
|
const resolved = await resolvePipelineContext(options);
|
|
7135
7343
|
context = resolved.context;
|
|
7136
7344
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
7142
|
-
break;
|
|
7143
|
-
}
|
|
7144
|
-
if (Date.now() >= deadline) {
|
|
7145
|
-
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
7146
|
-
break;
|
|
7147
|
-
}
|
|
7148
|
-
await sleep(pollSec * 1e3);
|
|
7149
|
-
}
|
|
7345
|
+
const waitResult = await pollRunUntilDone(resolved.context, resolved.cred, runId, {
|
|
7346
|
+
deadline,
|
|
7347
|
+
pollMs: pollSec * 1e3
|
|
7348
|
+
});
|
|
7150
7349
|
applyWaitExitCode(waitResult);
|
|
7151
7350
|
if (options.json) {
|
|
7152
7351
|
process.stdout.write(`${JSON.stringify(waitResult, null, 2)}
|