azdo-cli 0.24.0-feature-059-project-operations-guide.922 → 0.24.0-feature-061-fix-sonar.925
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,7 +162,6 @@ 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) |
|
|
166
165
|
|
|
167
166
|
## License
|
|
168
167
|
|
|
@@ -1047,6 +1047,21 @@ 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
|
+
try {
|
|
1059
|
+
return Promise.resolve(fn());
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1050
1065
|
// src/services/credential-store.ts
|
|
1051
1066
|
var SERVICE = "azdo-cli";
|
|
1052
1067
|
var LEGACY_ACCOUNT = "pat";
|
|
@@ -1187,7 +1202,7 @@ function serializeCredential(cred) {
|
|
|
1187
1202
|
}
|
|
1188
1203
|
return JSON.stringify(cred);
|
|
1189
1204
|
}
|
|
1190
|
-
|
|
1205
|
+
function maybeMigrateLegacy(targetOrg) {
|
|
1191
1206
|
if (activeCredentialStore() === "dpapi") return null;
|
|
1192
1207
|
const config = loadConfig();
|
|
1193
1208
|
if (!config.org || config.org !== targetOrg) {
|
|
@@ -1235,30 +1250,31 @@ async function getPat(org) {
|
|
|
1235
1250
|
if (cred.kind === "pat") return cred.token;
|
|
1236
1251
|
return null;
|
|
1237
1252
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
return parseStoredValue(migrated);
|
|
1245
|
-
}
|
|
1246
|
-
return parseStoredValue(value);
|
|
1253
|
+
function getStoredCredential(org) {
|
|
1254
|
+
return asPromise(() => {
|
|
1255
|
+
const entry = entryFor(accountFor(org));
|
|
1256
|
+
const value = wrapUnavailable(() => entry.getPassword()) ?? maybeMigrateLegacy(org);
|
|
1257
|
+
return value === null ? null : parseStoredValue(value);
|
|
1258
|
+
});
|
|
1247
1259
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1260
|
+
function storePat(org, pat) {
|
|
1261
|
+
return asPromise(() => {
|
|
1262
|
+
const cred = { kind: "pat", token: pat };
|
|
1263
|
+
const entry = entryFor(accountFor(org));
|
|
1264
|
+
wrapUnavailable(() => entry.setPassword(serializeCredential(cred)));
|
|
1265
|
+
appendAuthAuditEvent({
|
|
1266
|
+
event: "auth.store",
|
|
1267
|
+
org,
|
|
1268
|
+
backend: probeBackend(),
|
|
1269
|
+
masked_pat: maskedDisplay(pat)
|
|
1270
|
+
});
|
|
1257
1271
|
});
|
|
1258
1272
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1273
|
+
function storeOAuthCredential(org, cred) {
|
|
1274
|
+
return asPromise(() => {
|
|
1275
|
+
const entry = entryFor(accountFor(org));
|
|
1276
|
+
wrapUnavailable(() => entry.setPassword(serializeCredential(cred)));
|
|
1277
|
+
});
|
|
1262
1278
|
}
|
|
1263
1279
|
async function deletePat(org) {
|
|
1264
1280
|
const entry = entryFor(accountFor(org));
|
|
@@ -1290,13 +1306,16 @@ async function deletePat(org) {
|
|
|
1290
1306
|
}
|
|
1291
1307
|
try {
|
|
1292
1308
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
1293
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
1309
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-MFVKQFJI.js");
|
|
1294
1310
|
unlinkSync2(lockPath2(org));
|
|
1295
1311
|
} catch {
|
|
1296
1312
|
}
|
|
1297
1313
|
return true;
|
|
1298
1314
|
}
|
|
1299
|
-
|
|
1315
|
+
function listOrgsWithStoredPat() {
|
|
1316
|
+
return asPromise(listOrgsWithStoredPatSync);
|
|
1317
|
+
}
|
|
1318
|
+
function listOrgsWithStoredPatSync() {
|
|
1300
1319
|
const seen = /* @__PURE__ */ new Set();
|
|
1301
1320
|
for (const ev of readAuditEvents()) {
|
|
1302
1321
|
if (ev.event === "auth.store" || ev.event === "oauth-login-success") {
|
|
@@ -1387,25 +1406,26 @@ function lockPath(org) {
|
|
|
1387
1406
|
async function defaultAcquireLock(org, waitMs = DEFAULT_LOCK_WAIT_MS) {
|
|
1388
1407
|
const path4 = lockPath(org);
|
|
1389
1408
|
mkdirSync(locksDir(), { recursive: true, mode: 448 });
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1409
|
+
return tryAcquireLock(path4, Date.now() + waitMs);
|
|
1410
|
+
}
|
|
1411
|
+
async function tryAcquireLock(path4, deadline) {
|
|
1412
|
+
try {
|
|
1413
|
+
const fd = openSync2(path4, "wx");
|
|
1414
|
+
closeSync2(fd);
|
|
1415
|
+
return {
|
|
1416
|
+
release: () => {
|
|
1417
|
+
try {
|
|
1418
|
+
unlinkSync(path4);
|
|
1419
|
+
} catch {
|
|
1401
1420
|
}
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
if (err.code !== "EEXIST") throw err;
|
|
1425
|
+
if (Date.now() > deadline) return null;
|
|
1408
1426
|
}
|
|
1427
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1428
|
+
return tryAcquireLock(path4, deadline);
|
|
1409
1429
|
}
|
|
1410
1430
|
function classifyRefreshFailure(error) {
|
|
1411
1431
|
const code = typeof error === "string" ? error : error.error ?? "";
|
|
@@ -1543,6 +1563,7 @@ export {
|
|
|
1543
1563
|
CredentialStoreUnavailableError,
|
|
1544
1564
|
CredentialMissingError,
|
|
1545
1565
|
CredentialRefreshError,
|
|
1566
|
+
runSequentially,
|
|
1546
1567
|
appendAuthAuditEvent,
|
|
1547
1568
|
readAuditEvents,
|
|
1548
1569
|
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-5PPUTWGL.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,22 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
|
846
849
|
}
|
|
847
850
|
return stringifyFieldValue(value);
|
|
848
851
|
}
|
|
852
|
+
async function listWorkItemCommentPages(context, id, cred, continuationToken) {
|
|
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
|
+
const comments = (data.comments ?? []).map((comment) => mapWorkItemComment(comment, id)).filter((comment) => !comment.isDeleted);
|
|
862
|
+
const next = readContinuationToken(response, data);
|
|
863
|
+
if (next === null) return comments;
|
|
864
|
+
return [...comments, ...await listWorkItemCommentPages(context, id, cred, next)];
|
|
865
|
+
}
|
|
849
866
|
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);
|
|
867
|
+
const comments = await listWorkItemCommentPages(context, id, cred, null);
|
|
866
868
|
return {
|
|
867
869
|
workItemId: id,
|
|
868
870
|
count: comments.length,
|
|
@@ -1442,29 +1444,26 @@ async function loginWithOAuth(org, opts = {}) {
|
|
|
1442
1444
|
async function logout(opts = {}) {
|
|
1443
1445
|
if (opts.all) {
|
|
1444
1446
|
const orgs = await listOrgsWithStoredPat();
|
|
1445
|
-
const
|
|
1446
|
-
|
|
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 };
|
|
1447
|
+
const perOrg = await Promise.all(orgs.map((o) => logoutOrg(o)));
|
|
1448
|
+
return { removed: perOrg.flat() };
|
|
1454
1449
|
}
|
|
1455
1450
|
if (!opts.org) {
|
|
1456
1451
|
throw new Error("logout requires an org or --all");
|
|
1457
1452
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1453
|
+
return { removed: await logoutOrg(opts.org) };
|
|
1454
|
+
}
|
|
1455
|
+
async function logoutOrg(org) {
|
|
1456
|
+
const cred = await getStoredCredential(org);
|
|
1457
|
+
const ok = await deletePat(org);
|
|
1458
|
+
return ok && cred !== null ? [{ org, kind: cred.kind }] : [];
|
|
1461
1459
|
}
|
|
1462
1460
|
async function status() {
|
|
1463
1461
|
const orgs = await listOrgsWithStoredPat();
|
|
1462
|
+
const creds = await Promise.all(orgs.map((org) => getStoredCredential(org)));
|
|
1464
1463
|
const out = [];
|
|
1465
|
-
|
|
1466
|
-
const cred =
|
|
1467
|
-
if (cred === null)
|
|
1464
|
+
orgs.forEach((org, i) => {
|
|
1465
|
+
const cred = creds[i];
|
|
1466
|
+
if (cred === null) return;
|
|
1468
1467
|
if (cred.kind === "pat") {
|
|
1469
1468
|
out.push({ org, kind: "pat", backend: probeBackend() });
|
|
1470
1469
|
} else {
|
|
@@ -1477,7 +1476,7 @@ async function status() {
|
|
|
1477
1476
|
backend: probeBackend()
|
|
1478
1477
|
});
|
|
1479
1478
|
}
|
|
1480
|
-
}
|
|
1479
|
+
});
|
|
1481
1480
|
return { orgs: out };
|
|
1482
1481
|
}
|
|
1483
1482
|
async function exportCredential(org) {
|
|
@@ -3128,10 +3127,8 @@ ${renderSettingsHelp()}`);
|
|
|
3128
3127
|
const ask = createAsk(rl);
|
|
3129
3128
|
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
3130
3129
|
process.stderr.write("=======================================\n\n");
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
await promptForSetting(cfg, setting, ask);
|
|
3134
|
-
}
|
|
3130
|
+
const wizardSettings = SETTINGS.filter((setting) => setting.key !== "credentialStore");
|
|
3131
|
+
await runSequentially(wizardSettings, (setting) => promptForSetting(cfg, setting, ask));
|
|
3135
3132
|
rl.close();
|
|
3136
3133
|
process.stderr.write("Configuration complete!\n");
|
|
3137
3134
|
});
|
|
@@ -3771,7 +3768,8 @@ function stringifyValue(value) {
|
|
|
3771
3768
|
if (value === null || value === void 0) return "";
|
|
3772
3769
|
if (typeof value === "string") return value;
|
|
3773
3770
|
if (typeof value === "object") return JSON.stringify(value);
|
|
3774
|
-
if (typeof value === "symbol"
|
|
3771
|
+
if (typeof value === "symbol") return value.toString();
|
|
3772
|
+
if (typeof value === "function") return `[function ${value.name || "anonymous"}]`;
|
|
3775
3773
|
return String(value);
|
|
3776
3774
|
}
|
|
3777
3775
|
function formatRichValue(raw) {
|
|
@@ -6871,26 +6869,25 @@ async function getTestSummary(context, cred, buildId) {
|
|
|
6871
6869
|
var MAX_FAILED_TESTS = 50;
|
|
6872
6870
|
async function getFailedTests(context, cred, buildId) {
|
|
6873
6871
|
const testRuns = await listTestRuns(context, cred, buildId);
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
return failed;
|
|
6872
|
+
return collectFailedTests(context, cred, testRuns, []);
|
|
6873
|
+
}
|
|
6874
|
+
async function collectFailedTests(context, cred, testRuns, failed) {
|
|
6875
|
+
const [testRun, ...rest] = testRuns;
|
|
6876
|
+
if (testRun === void 0 || failed.length >= MAX_FAILED_TESTS) return failed;
|
|
6877
|
+
const resultsUrl = withApiVersion(
|
|
6878
|
+
new URL(`${orgProjectBase(context)}/_apis/test/runs/${testRun.id}/results`)
|
|
6879
|
+
);
|
|
6880
|
+
resultsUrl.searchParams.set("outcomes", "Failed");
|
|
6881
|
+
resultsUrl.searchParams.set("$top", String(MAX_FAILED_TESTS - failed.length));
|
|
6882
|
+
const resultsResponse = await fetchWithErrors(resultsUrl.toString(), {
|
|
6883
|
+
headers: authHeaders(cred)
|
|
6884
|
+
});
|
|
6885
|
+
const resultsData = await readJsonResponse2(resultsResponse);
|
|
6886
|
+
const page = resultsData.value.map((result) => ({
|
|
6887
|
+
name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
|
|
6888
|
+
errorMessage: result.errorMessage ?? null
|
|
6889
|
+
}));
|
|
6890
|
+
return collectFailedTests(context, cred, rest, [...failed, ...page]);
|
|
6894
6891
|
}
|
|
6895
6892
|
function secondsBetween(start, finish) {
|
|
6896
6893
|
if (!start || !finish) return null;
|
|
@@ -7163,6 +7160,17 @@ function sleep(ms) {
|
|
|
7163
7160
|
setTimeout(resolve2, ms);
|
|
7164
7161
|
});
|
|
7165
7162
|
}
|
|
7163
|
+
async function pollRunUntilDone(context, cred, runId, timing) {
|
|
7164
|
+
const status2 = await getBuildStatus(context, cred, runId);
|
|
7165
|
+
if (status2.state === "completed") {
|
|
7166
|
+
return { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
7167
|
+
}
|
|
7168
|
+
if (Date.now() >= timing.deadline) {
|
|
7169
|
+
return { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
7170
|
+
}
|
|
7171
|
+
await sleep(timing.pollMs);
|
|
7172
|
+
return pollRunUntilDone(context, cred, runId, timing);
|
|
7173
|
+
}
|
|
7166
7174
|
function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
|
|
7167
7175
|
const widths = [];
|
|
7168
7176
|
for (const row of rows) {
|
|
@@ -7325,19 +7333,10 @@ function createPipelineWaitCommand() {
|
|
|
7325
7333
|
const resolved = await resolvePipelineContext(options);
|
|
7326
7334
|
context = resolved.context;
|
|
7327
7335
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
7333
|
-
break;
|
|
7334
|
-
}
|
|
7335
|
-
if (Date.now() >= deadline) {
|
|
7336
|
-
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
7337
|
-
break;
|
|
7338
|
-
}
|
|
7339
|
-
await sleep(pollSec * 1e3);
|
|
7340
|
-
}
|
|
7336
|
+
const waitResult = await pollRunUntilDone(resolved.context, resolved.cred, runId, {
|
|
7337
|
+
deadline,
|
|
7338
|
+
pollMs: pollSec * 1e3
|
|
7339
|
+
});
|
|
7341
7340
|
applyWaitExitCode(waitResult);
|
|
7342
7341
|
if (options.json) {
|
|
7343
7342
|
process.stdout.write(`${JSON.stringify(waitResult, null, 2)}
|