azdo-cli 0.10.0-develop.394 → 0.10.0-develop.423
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 +7 -0
- package/dist/index.js +932 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,13 @@ azdo pr comments --code-related-only # only file/line-anchored threads
|
|
|
61
61
|
azdo pr status # PR checks (status + branch policies) + code-comment counts
|
|
62
62
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
63
63
|
azdo pr comment-reopen 17 --pr-number 64
|
|
64
|
+
|
|
65
|
+
# Pipelines — list, inspect runs, wait (exit code = result), start
|
|
66
|
+
azdo pipeline list --filter ci
|
|
67
|
+
azdo pipeline get-runs 12 --branch develop --limit 1
|
|
68
|
+
azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
|
|
69
|
+
azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
|
|
70
|
+
azdo pipeline start 12 --branch develop --parameter env=staging
|
|
64
71
|
```
|
|
65
72
|
|
|
66
73
|
## Documentation
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "./chunk-C7RAZJHV.js";
|
|
32
32
|
|
|
33
33
|
// src/index.ts
|
|
34
|
-
import { Command as
|
|
34
|
+
import { Command as Command16 } from "commander";
|
|
35
35
|
|
|
36
36
|
// src/version.ts
|
|
37
37
|
import { readFileSync } from "fs";
|
|
@@ -479,13 +479,13 @@ async function classifyDeviceTokenResponse(response) {
|
|
|
479
479
|
async function pollForDeviceToken(deviceCode, oauthConfig, initialIntervalSec, expiresAtMs, deps) {
|
|
480
480
|
const fetchFn = deps.fetch ?? fetch;
|
|
481
481
|
const now = deps.now ?? (() => Date.now());
|
|
482
|
-
const
|
|
482
|
+
const sleep2 = deps.sleep ?? defaultSleep;
|
|
483
483
|
let intervalSec = Math.max(MIN_INTERVAL_SEC, initialIntervalSec);
|
|
484
484
|
for (; ; ) {
|
|
485
485
|
if (now() >= expiresAtMs) {
|
|
486
486
|
throw new DeviceCodeFlowError("expired_token", "device-code flow expired before authorisation completed");
|
|
487
487
|
}
|
|
488
|
-
await
|
|
488
|
+
await sleep2(intervalSec * 1e3);
|
|
489
489
|
const body = new URLSearchParams({
|
|
490
490
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
491
491
|
client_id: oauthConfig.clientId,
|
|
@@ -3368,10 +3368,922 @@ function createPrCommand() {
|
|
|
3368
3368
|
return command;
|
|
3369
3369
|
}
|
|
3370
3370
|
|
|
3371
|
-
// src/commands/
|
|
3371
|
+
// src/commands/pipeline.ts
|
|
3372
3372
|
import { Command as Command13 } from "commander";
|
|
3373
|
+
|
|
3374
|
+
// src/services/pipeline-client.ts
|
|
3375
|
+
var API_VERSION = "7.1";
|
|
3376
|
+
function orgProjectBase(context) {
|
|
3377
|
+
return `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}`;
|
|
3378
|
+
}
|
|
3379
|
+
function withApiVersion(url) {
|
|
3380
|
+
url.searchParams.set("api-version", API_VERSION);
|
|
3381
|
+
return url;
|
|
3382
|
+
}
|
|
3383
|
+
async function readJsonResponse2(response) {
|
|
3384
|
+
if (!response.ok) {
|
|
3385
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3386
|
+
}
|
|
3387
|
+
return await response.json();
|
|
3388
|
+
}
|
|
3389
|
+
function mapPipeline(pipeline) {
|
|
3390
|
+
return {
|
|
3391
|
+
id: pipeline.id,
|
|
3392
|
+
name: pipeline.name,
|
|
3393
|
+
folder: pipeline.folder?.trim() ? pipeline.folder : null
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
function mapRunState(state) {
|
|
3397
|
+
if (state === "inProgress" || state === "completed") {
|
|
3398
|
+
return state;
|
|
3399
|
+
}
|
|
3400
|
+
return "unknown";
|
|
3401
|
+
}
|
|
3402
|
+
function mapRunResult(result) {
|
|
3403
|
+
if (result === "succeeded" || result === "failed" || result === "canceled") {
|
|
3404
|
+
return result;
|
|
3405
|
+
}
|
|
3406
|
+
if (result === "partiallySucceeded") {
|
|
3407
|
+
return "failed";
|
|
3408
|
+
}
|
|
3409
|
+
return null;
|
|
3410
|
+
}
|
|
3411
|
+
function mapBuildState(status2) {
|
|
3412
|
+
if (status2 === "completed") return "completed";
|
|
3413
|
+
if (status2 === void 0 || status2 === "none") return "unknown";
|
|
3414
|
+
return "inProgress";
|
|
3415
|
+
}
|
|
3416
|
+
function mapBuildSummary(build) {
|
|
3417
|
+
return {
|
|
3418
|
+
id: build.id,
|
|
3419
|
+
name: build.buildNumber ?? null,
|
|
3420
|
+
state: mapBuildState(build.status),
|
|
3421
|
+
result: mapRunResult(build.result),
|
|
3422
|
+
createdDate: build.queueTime ?? build.startTime ?? null,
|
|
3423
|
+
finishedDate: build.finishTime ?? null,
|
|
3424
|
+
sourceBranch: build.sourceBranch ?? null,
|
|
3425
|
+
sourceCommit: build.sourceVersion ?? null
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
function normalizeRef(branch) {
|
|
3429
|
+
return branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
|
|
3430
|
+
}
|
|
3431
|
+
async function getPipelineDefinitions(context, cred) {
|
|
3432
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/pipelines`));
|
|
3433
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3434
|
+
const data = await readJsonResponse2(response);
|
|
3435
|
+
return data.value.map(mapPipeline);
|
|
3436
|
+
}
|
|
3437
|
+
var COMMIT_LOOKBACK = 200;
|
|
3438
|
+
async function getPipelineRuns(context, cred, query) {
|
|
3439
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds`));
|
|
3440
|
+
if (query.definitionId !== void 0) {
|
|
3441
|
+
url.searchParams.set("definitions", String(query.definitionId));
|
|
3442
|
+
}
|
|
3443
|
+
if (query.prNumber !== void 0) {
|
|
3444
|
+
url.searchParams.set("branchName", `refs/pull/${query.prNumber}/merge`);
|
|
3445
|
+
} else if (query.branch) {
|
|
3446
|
+
url.searchParams.set("branchName", normalizeRef(query.branch));
|
|
3447
|
+
}
|
|
3448
|
+
url.searchParams.set("queryOrder", "queueTimeDescending");
|
|
3449
|
+
url.searchParams.set("$top", String(query.commit ? COMMIT_LOOKBACK : query.top));
|
|
3450
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3451
|
+
const data = await readJsonResponse2(response);
|
|
3452
|
+
let runs = data.value.map(mapBuildSummary);
|
|
3453
|
+
if (query.commit) {
|
|
3454
|
+
const needle = query.commit.toLowerCase();
|
|
3455
|
+
runs = runs.filter((run) => run.sourceCommit?.toLowerCase().startsWith(needle));
|
|
3456
|
+
}
|
|
3457
|
+
return runs.slice(0, query.top);
|
|
3458
|
+
}
|
|
3459
|
+
async function runPipeline(context, cred, pipelineId, opts) {
|
|
3460
|
+
const url = withApiVersion(
|
|
3461
|
+
new URL(`${orgProjectBase(context)}/_apis/pipelines/${pipelineId}/runs`)
|
|
3462
|
+
);
|
|
3463
|
+
const body = {};
|
|
3464
|
+
if (opts.branch) {
|
|
3465
|
+
const refName = opts.branch.startsWith("refs/") ? opts.branch : `refs/heads/${opts.branch}`;
|
|
3466
|
+
body.resources = { repositories: { self: { refName } } };
|
|
3467
|
+
}
|
|
3468
|
+
if (opts.parameters && Object.keys(opts.parameters).length > 0) {
|
|
3469
|
+
body.templateParameters = opts.parameters;
|
|
3470
|
+
}
|
|
3471
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3472
|
+
method: "POST",
|
|
3473
|
+
headers: { ...authHeaders(cred), "Content-Type": "application/json" },
|
|
3474
|
+
body: JSON.stringify(body)
|
|
3475
|
+
});
|
|
3476
|
+
const data = await readJsonResponse2(response);
|
|
3477
|
+
return {
|
|
3478
|
+
id: data.id,
|
|
3479
|
+
state: mapRunState(data.state),
|
|
3480
|
+
webUrl: data._links?.web?.href ?? null
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function buildUrl(context, buildId) {
|
|
3484
|
+
return withApiVersion(new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}`));
|
|
3485
|
+
}
|
|
3486
|
+
async function getBuild(context, cred, buildId) {
|
|
3487
|
+
const response = await fetchWithErrors(buildUrl(context, buildId).toString(), {
|
|
3488
|
+
headers: authHeaders(cred)
|
|
3489
|
+
});
|
|
3490
|
+
return readJsonResponse2(response);
|
|
3491
|
+
}
|
|
3492
|
+
async function getBuildStatus(context, cred, buildId) {
|
|
3493
|
+
const build = await getBuild(context, cred, buildId);
|
|
3494
|
+
return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
|
|
3495
|
+
}
|
|
3496
|
+
async function getBuildTimeline(context, cred, buildId) {
|
|
3497
|
+
const url = withApiVersion(
|
|
3498
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
|
|
3499
|
+
);
|
|
3500
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3501
|
+
const data = await readJsonResponse2(response);
|
|
3502
|
+
const records = data.records ?? [];
|
|
3503
|
+
const errors = [];
|
|
3504
|
+
const stages = [];
|
|
3505
|
+
const jobs = [];
|
|
3506
|
+
const logSteps = /* @__PURE__ */ new Map();
|
|
3507
|
+
for (const record of records) {
|
|
3508
|
+
for (const issue of record.issues ?? []) {
|
|
3509
|
+
if (issue.type === "error" && issue.message) {
|
|
3510
|
+
errors.push({ message: issue.message, source: record.name ?? null });
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
if (record.name && record.log?.id !== void 0) {
|
|
3514
|
+
logSteps.set(record.log.id, record.name);
|
|
3515
|
+
}
|
|
3516
|
+
if (!record.name) continue;
|
|
3517
|
+
const status2 = {
|
|
3518
|
+
name: record.name,
|
|
3519
|
+
state: record.state ?? "unknown",
|
|
3520
|
+
result: record.result ?? null
|
|
3521
|
+
};
|
|
3522
|
+
if (record.type === "Stage") {
|
|
3523
|
+
stages.push(status2);
|
|
3524
|
+
} else if (record.type === "Job") {
|
|
3525
|
+
jobs.push({ startTime: record.startTime, status: status2 });
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
jobs.sort((a, b) => {
|
|
3529
|
+
if (a.startTime === b.startTime) return 0;
|
|
3530
|
+
if (a.startTime === void 0) return 1;
|
|
3531
|
+
if (b.startTime === void 0) return -1;
|
|
3532
|
+
return a.startTime < b.startTime ? -1 : 1;
|
|
3533
|
+
});
|
|
3534
|
+
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
|
|
3535
|
+
}
|
|
3536
|
+
async function listTestRuns(context, cred, buildId) {
|
|
3537
|
+
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
|
|
3538
|
+
url.searchParams.set("buildUri", `vstfs:///Build/Build/${buildId}`);
|
|
3539
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3540
|
+
const data = await readJsonResponse2(response);
|
|
3541
|
+
return data.value;
|
|
3542
|
+
}
|
|
3543
|
+
async function getTestSummary(context, cred, buildId) {
|
|
3544
|
+
const testRuns = await listTestRuns(context, cred, buildId);
|
|
3545
|
+
let total = 0;
|
|
3546
|
+
let failed = 0;
|
|
3547
|
+
for (const run of testRuns) {
|
|
3548
|
+
const runTotal = run.totalTests ?? 0;
|
|
3549
|
+
total += runTotal;
|
|
3550
|
+
const passedOrSkipped = (run.passedTests ?? 0) + (run.notApplicableTests ?? 0) + (run.incompleteTests ?? 0);
|
|
3551
|
+
failed += Math.max(0, runTotal - passedOrSkipped);
|
|
3552
|
+
}
|
|
3553
|
+
if (total === 0) {
|
|
3554
|
+
return { present: false, total: 0, failed: 0, failedTests: [] };
|
|
3555
|
+
}
|
|
3556
|
+
return { present: true, total, failed, failedTests: [] };
|
|
3557
|
+
}
|
|
3558
|
+
var MAX_FAILED_TESTS = 50;
|
|
3559
|
+
async function getFailedTests(context, cred, buildId) {
|
|
3560
|
+
const testRuns = await listTestRuns(context, cred, buildId);
|
|
3561
|
+
const failed = [];
|
|
3562
|
+
for (const testRun of testRuns) {
|
|
3563
|
+
if (failed.length >= MAX_FAILED_TESTS) break;
|
|
3564
|
+
const resultsUrl = withApiVersion(
|
|
3565
|
+
new URL(`${orgProjectBase(context)}/_apis/test/runs/${testRun.id}/results`)
|
|
3566
|
+
);
|
|
3567
|
+
resultsUrl.searchParams.set("outcomes", "Failed");
|
|
3568
|
+
resultsUrl.searchParams.set("$top", String(MAX_FAILED_TESTS - failed.length));
|
|
3569
|
+
const resultsResponse = await fetchWithErrors(resultsUrl.toString(), {
|
|
3570
|
+
headers: authHeaders(cred)
|
|
3571
|
+
});
|
|
3572
|
+
const resultsData = await readJsonResponse2(resultsResponse);
|
|
3573
|
+
for (const result of resultsData.value) {
|
|
3574
|
+
failed.push({
|
|
3575
|
+
name: result.testCaseTitle ?? result.automatedTestName ?? "(unnamed test)",
|
|
3576
|
+
errorMessage: result.errorMessage ?? null
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
return failed;
|
|
3581
|
+
}
|
|
3582
|
+
function secondsBetween(start, finish) {
|
|
3583
|
+
if (!start || !finish) return null;
|
|
3584
|
+
const ms = Date.parse(finish) - Date.parse(start);
|
|
3585
|
+
return Number.isFinite(ms) ? Math.round(ms / 1e3) : null;
|
|
3586
|
+
}
|
|
3587
|
+
async function getRunDetail(context, cred, buildId) {
|
|
3588
|
+
const build = await getBuild(context, cred, buildId);
|
|
3589
|
+
let errors = [];
|
|
3590
|
+
let stages = [];
|
|
3591
|
+
let jobs = [];
|
|
3592
|
+
let errorsAvailable = true;
|
|
3593
|
+
try {
|
|
3594
|
+
const timeline = await getBuildTimeline(context, cred, buildId);
|
|
3595
|
+
errors = timeline.errors;
|
|
3596
|
+
stages = timeline.stages;
|
|
3597
|
+
jobs = timeline.jobs;
|
|
3598
|
+
} catch {
|
|
3599
|
+
errorsAvailable = false;
|
|
3600
|
+
}
|
|
3601
|
+
let tests = { present: false, total: 0, failed: 0, failedTests: [] };
|
|
3602
|
+
let testsAvailable = true;
|
|
3603
|
+
try {
|
|
3604
|
+
tests = await getTestSummary(context, cred, buildId);
|
|
3605
|
+
if (tests.failed > 0) {
|
|
3606
|
+
try {
|
|
3607
|
+
tests = { ...tests, failedTests: await getFailedTests(context, cred, buildId) };
|
|
3608
|
+
} catch {
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
} catch {
|
|
3612
|
+
testsAvailable = false;
|
|
3613
|
+
}
|
|
3614
|
+
return {
|
|
3615
|
+
id: build.id,
|
|
3616
|
+
name: build.buildNumber ?? null,
|
|
3617
|
+
state: mapBuildState(build.status),
|
|
3618
|
+
result: mapRunResult(build.result),
|
|
3619
|
+
// createdDate is the queue time, matching the run-list mapping.
|
|
3620
|
+
createdDate: build.queueTime ?? build.startTime ?? null,
|
|
3621
|
+
startedDate: build.startTime ?? null,
|
|
3622
|
+
finishedDate: build.finishTime ?? null,
|
|
3623
|
+
durationSeconds: secondsBetween(build.startTime, build.finishTime),
|
|
3624
|
+
reason: build.reason ?? null,
|
|
3625
|
+
requestedFor: build.requestedFor?.displayName ?? null,
|
|
3626
|
+
sourceBranch: build.sourceBranch ?? null,
|
|
3627
|
+
sourceCommit: build.sourceVersion ?? null,
|
|
3628
|
+
webUrl: build._links?.web?.href ?? null,
|
|
3629
|
+
errors,
|
|
3630
|
+
errorsAvailable,
|
|
3631
|
+
stages,
|
|
3632
|
+
jobs,
|
|
3633
|
+
tests,
|
|
3634
|
+
testsAvailable
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
async function getRunLogs(context, cred, buildId) {
|
|
3638
|
+
const url = withApiVersion(
|
|
3639
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs`)
|
|
3640
|
+
);
|
|
3641
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3642
|
+
const data = await readJsonResponse2(response);
|
|
3643
|
+
let logSteps = /* @__PURE__ */ new Map();
|
|
3644
|
+
try {
|
|
3645
|
+
logSteps = (await getBuildTimeline(context, cred, buildId)).logSteps;
|
|
3646
|
+
} catch {
|
|
3647
|
+
}
|
|
3648
|
+
return data.value.map((log) => ({
|
|
3649
|
+
id: log.id,
|
|
3650
|
+
createdOn: log.createdOn ?? null,
|
|
3651
|
+
lineCount: log.lineCount ?? null,
|
|
3652
|
+
step: logSteps.get(log.id) ?? null
|
|
3653
|
+
}));
|
|
3654
|
+
}
|
|
3655
|
+
async function getRunLog(context, cred, buildId, logId) {
|
|
3656
|
+
const url = withApiVersion(
|
|
3657
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/logs/${logId}`)
|
|
3658
|
+
);
|
|
3659
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3660
|
+
if (!response.ok) {
|
|
3661
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3662
|
+
}
|
|
3663
|
+
return response.text();
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/commands/pipeline.ts
|
|
3667
|
+
var EXIT_FAILED = 1;
|
|
3668
|
+
var EXIT_CANCELED = 2;
|
|
3669
|
+
var EXIT_TIMEOUT = 124;
|
|
3373
3670
|
function writeError2(message) {
|
|
3374
3671
|
process.stderr.write(`Error: ${message}
|
|
3672
|
+
`);
|
|
3673
|
+
process.exitCode = 1;
|
|
3674
|
+
}
|
|
3675
|
+
function handlePipelineError(err, context) {
|
|
3676
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3677
|
+
if (error.message === "AUTH_FAILED") {
|
|
3678
|
+
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
if (error.message === "PERMISSION_DENIED") {
|
|
3682
|
+
writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
if (error.message === "NETWORK_ERROR") {
|
|
3686
|
+
writeError2("Could not connect to Azure DevOps. Check your network connection.");
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
if (error.message.startsWith("NOT_FOUND")) {
|
|
3690
|
+
writeError2(`Resource not found in ${context?.org}/${context?.project}.`);
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (error.message.startsWith("HTTP_")) {
|
|
3694
|
+
writeError2(`Azure DevOps request failed with ${error.message}.`);
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
writeError2(error.message);
|
|
3698
|
+
}
|
|
3699
|
+
function parsePositiveId(raw) {
|
|
3700
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
3701
|
+
const n = Number.parseInt(raw, 10);
|
|
3702
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
3703
|
+
}
|
|
3704
|
+
function parseOptionalCount(value, flag) {
|
|
3705
|
+
if (value === void 0) return void 0;
|
|
3706
|
+
const parsed = parsePositiveId(value);
|
|
3707
|
+
if (parsed === null) {
|
|
3708
|
+
writeError2(`Invalid ${flag} "${value}"; expected a positive integer.`);
|
|
3709
|
+
return null;
|
|
3710
|
+
}
|
|
3711
|
+
return parsed;
|
|
3712
|
+
}
|
|
3713
|
+
function formatBranchName2(refName) {
|
|
3714
|
+
if (!refName) return "\u2014";
|
|
3715
|
+
return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
|
|
3716
|
+
}
|
|
3717
|
+
async function resolvePipelineContext(options) {
|
|
3718
|
+
const context = resolveContext(options);
|
|
3719
|
+
const cred = await requireAuthCredential(context.org);
|
|
3720
|
+
return { context, cred };
|
|
3721
|
+
}
|
|
3722
|
+
function sleep(ms) {
|
|
3723
|
+
return new Promise((resolve2) => {
|
|
3724
|
+
setTimeout(resolve2, ms);
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
function formatTable(rows, rightAlign = /* @__PURE__ */ new Set()) {
|
|
3728
|
+
const widths = [];
|
|
3729
|
+
for (const row of rows) {
|
|
3730
|
+
row.forEach((cell, i) => {
|
|
3731
|
+
widths[i] = Math.max(widths[i] ?? 0, cell.length);
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
return rows.map(
|
|
3735
|
+
(row) => row.map((cell, i) => rightAlign.has(i) ? cell.padStart(widths[i]) : cell.padEnd(widths[i])).join(" ").trimEnd()
|
|
3736
|
+
).join("\n");
|
|
3737
|
+
}
|
|
3738
|
+
function createPipelineListCommand() {
|
|
3739
|
+
const command = new Command13("list");
|
|
3740
|
+
command.description("List Azure DevOps pipeline definitions").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--filter <name>", "filter definitions by name (case-insensitive substring)").option("--json", "output JSON").action(async (options) => {
|
|
3741
|
+
validateOrgProjectPair(options);
|
|
3742
|
+
let context;
|
|
3743
|
+
try {
|
|
3744
|
+
const resolved = await resolvePipelineContext(options);
|
|
3745
|
+
context = resolved.context;
|
|
3746
|
+
let definitions = await getPipelineDefinitions(resolved.context, resolved.cred);
|
|
3747
|
+
if (options.filter) {
|
|
3748
|
+
const needle = options.filter.toLowerCase();
|
|
3749
|
+
definitions = definitions.filter((d) => d.name.toLowerCase().includes(needle));
|
|
3750
|
+
}
|
|
3751
|
+
if (options.json) {
|
|
3752
|
+
process.stdout.write(`${JSON.stringify(definitions, null, 2)}
|
|
3753
|
+
`);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
if (definitions.length === 0) {
|
|
3757
|
+
process.stdout.write("No pipelines found.\n");
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
const hasFolder = definitions.some((d) => d.folder);
|
|
3761
|
+
const rows = definitions.map(
|
|
3762
|
+
(d) => hasFolder ? [String(d.id), d.name, d.folder ?? ""] : [String(d.id), d.name]
|
|
3763
|
+
);
|
|
3764
|
+
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
3765
|
+
`);
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
handlePipelineError(err, context);
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
return command;
|
|
3771
|
+
}
|
|
3772
|
+
function runRow(run) {
|
|
3773
|
+
const status2 = run.result ? `${run.state}/${run.result}` : run.state;
|
|
3774
|
+
return [
|
|
3775
|
+
String(run.id),
|
|
3776
|
+
`[${status2}]`,
|
|
3777
|
+
run.createdDate ?? "\u2014",
|
|
3778
|
+
formatBranchName2(run.sourceBranch),
|
|
3779
|
+
run.sourceCommit ? run.sourceCommit.slice(0, 8) : "\u2014"
|
|
3780
|
+
];
|
|
3781
|
+
}
|
|
3782
|
+
var COMMIT_SHA_PATTERN = /^[0-9a-f]{6,40}$/i;
|
|
3783
|
+
function parseGetRunsInputs(defIdRaw, options) {
|
|
3784
|
+
let defId;
|
|
3785
|
+
if (defIdRaw !== void 0) {
|
|
3786
|
+
const parsed = parsePositiveId(defIdRaw);
|
|
3787
|
+
if (parsed === null) {
|
|
3788
|
+
writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
|
|
3789
|
+
return null;
|
|
3790
|
+
}
|
|
3791
|
+
defId = parsed;
|
|
3792
|
+
} else if (options.commit === void 0 && options.pr === void 0) {
|
|
3793
|
+
writeError2("Definition id is required unless --commit or --pr is given.");
|
|
3794
|
+
return null;
|
|
3795
|
+
}
|
|
3796
|
+
const limit = parseOptionalCount(options.limit, "--limit");
|
|
3797
|
+
if (limit === null) return null;
|
|
3798
|
+
const prNumber = parseOptionalCount(options.pr, "--pr");
|
|
3799
|
+
if (prNumber === null) return null;
|
|
3800
|
+
if (options.commit !== void 0 && !COMMIT_SHA_PATTERN.test(options.commit)) {
|
|
3801
|
+
writeError2(`Invalid --commit "${options.commit}"; expected 6-40 hex characters.`);
|
|
3802
|
+
return null;
|
|
3803
|
+
}
|
|
3804
|
+
if (options.branch !== void 0 && prNumber !== void 0) {
|
|
3805
|
+
writeError2("Use either --branch or --pr, not both.");
|
|
3806
|
+
return null;
|
|
3807
|
+
}
|
|
3808
|
+
return { defId, limit: limit ?? 10, prNumber };
|
|
3809
|
+
}
|
|
3810
|
+
function createPipelineGetRunsCommand() {
|
|
3811
|
+
const command = new Command13("get-runs");
|
|
3812
|
+
command.description("List recent runs for a pipeline definition (newest first)").argument("[def_id]", "pipeline definition id (optional with --commit or --pr)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--limit <n>", "maximum number of runs to show (default 10)").option("--branch <branch>", "only show runs for this source branch").option("--commit <sha>", "only show runs that built this commit (full or abbreviated SHA)").option("--pr <number>", "only show runs for this pull request").option("--json", "output JSON").action(async (defIdRaw, options) => {
|
|
3813
|
+
validateOrgProjectPair(options);
|
|
3814
|
+
const inputs = parseGetRunsInputs(defIdRaw, options);
|
|
3815
|
+
if (inputs === null) {
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
let context;
|
|
3819
|
+
try {
|
|
3820
|
+
const resolved = await resolvePipelineContext(options);
|
|
3821
|
+
context = resolved.context;
|
|
3822
|
+
const runs = await getPipelineRuns(resolved.context, resolved.cred, {
|
|
3823
|
+
definitionId: inputs.defId,
|
|
3824
|
+
branch: options.branch,
|
|
3825
|
+
prNumber: inputs.prNumber,
|
|
3826
|
+
commit: options.commit,
|
|
3827
|
+
top: inputs.limit
|
|
3828
|
+
});
|
|
3829
|
+
if (options.json) {
|
|
3830
|
+
process.stdout.write(`${JSON.stringify(runs, null, 2)}
|
|
3831
|
+
`);
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (runs.length === 0) {
|
|
3835
|
+
process.stdout.write(
|
|
3836
|
+
inputs.defId === void 0 ? "No runs found matching the filters.\n" : `No runs found for pipeline ${inputs.defId}.
|
|
3837
|
+
`
|
|
3838
|
+
);
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
process.stdout.write(`${formatTable(runs.map(runRow), /* @__PURE__ */ new Set([0]))}
|
|
3842
|
+
`);
|
|
3843
|
+
} catch (err) {
|
|
3844
|
+
handlePipelineError(err, context);
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
return command;
|
|
3848
|
+
}
|
|
3849
|
+
function applyWaitExitCode(result) {
|
|
3850
|
+
if (result.timedOut) {
|
|
3851
|
+
process.exitCode = EXIT_TIMEOUT;
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
switch (result.result) {
|
|
3855
|
+
case "succeeded":
|
|
3856
|
+
return;
|
|
3857
|
+
case "canceled":
|
|
3858
|
+
process.exitCode = EXIT_CANCELED;
|
|
3859
|
+
return;
|
|
3860
|
+
case "failed":
|
|
3861
|
+
default:
|
|
3862
|
+
process.exitCode = EXIT_FAILED;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
function createPipelineWaitCommand() {
|
|
3866
|
+
const command = new Command13("wait");
|
|
3867
|
+
command.description("Wait for a pipeline run to finish; exit code reflects the result (0 success, non-zero otherwise)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--timeout <seconds>", "maximum seconds to wait (default 1800)").option("--poll-interval <seconds>", "seconds between status checks (default 5)").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
3868
|
+
validateOrgProjectPair(options);
|
|
3869
|
+
const runId = parsePositiveId(runIdRaw);
|
|
3870
|
+
if (runId === null) {
|
|
3871
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
const timeoutSec = options.timeout === void 0 ? 1800 : Number(options.timeout);
|
|
3875
|
+
const pollSec = options.pollInterval === void 0 ? 5 : Number(options.pollInterval);
|
|
3876
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
|
|
3877
|
+
writeError2(`Invalid --timeout "${options.timeout}"; expected a non-negative number.`);
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
if (!Number.isFinite(pollSec) || pollSec <= 0) {
|
|
3881
|
+
writeError2(`Invalid --poll-interval "${options.pollInterval}"; expected a positive number.`);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
let context;
|
|
3885
|
+
try {
|
|
3886
|
+
const resolved = await resolvePipelineContext(options);
|
|
3887
|
+
context = resolved.context;
|
|
3888
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3889
|
+
let waitResult = null;
|
|
3890
|
+
for (; ; ) {
|
|
3891
|
+
const status2 = await getBuildStatus(resolved.context, resolved.cred, runId);
|
|
3892
|
+
if (status2.state === "completed") {
|
|
3893
|
+
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: false };
|
|
3894
|
+
break;
|
|
3895
|
+
}
|
|
3896
|
+
if (Date.now() >= deadline) {
|
|
3897
|
+
waitResult = { id: runId, state: status2.state, result: status2.result, timedOut: true };
|
|
3898
|
+
break;
|
|
3899
|
+
}
|
|
3900
|
+
await sleep(pollSec * 1e3);
|
|
3901
|
+
}
|
|
3902
|
+
applyWaitExitCode(waitResult);
|
|
3903
|
+
if (options.json) {
|
|
3904
|
+
process.stdout.write(`${JSON.stringify(waitResult, null, 2)}
|
|
3905
|
+
`);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
if (waitResult.timedOut) {
|
|
3909
|
+
process.stdout.write(`Run ${runId} did not finish within ${timeoutSec}s (still ${waitResult.state}).
|
|
3910
|
+
`);
|
|
3911
|
+
} else {
|
|
3912
|
+
process.stdout.write(`Run ${runId} finished: ${waitResult.result ?? waitResult.state}.
|
|
3913
|
+
`);
|
|
3914
|
+
}
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
handlePipelineError(err, context);
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
return command;
|
|
3920
|
+
}
|
|
3921
|
+
function timelineRows(items, available) {
|
|
3922
|
+
if (!available) {
|
|
3923
|
+
return [" unavailable"];
|
|
3924
|
+
}
|
|
3925
|
+
if (items.length === 0) {
|
|
3926
|
+
return [" (none)"];
|
|
3927
|
+
}
|
|
3928
|
+
return items.map((item) => ` - ${item.name} [${item.result ?? item.state}]`);
|
|
3929
|
+
}
|
|
3930
|
+
function formatDuration(totalSeconds) {
|
|
3931
|
+
const h = Math.floor(totalSeconds / 3600);
|
|
3932
|
+
const m = Math.floor(totalSeconds % 3600 / 60);
|
|
3933
|
+
const s = totalSeconds % 60;
|
|
3934
|
+
if (h > 0) return `${h}h${m}m${s}s`;
|
|
3935
|
+
if (m > 0) return `${m}m${s}s`;
|
|
3936
|
+
return `${s}s`;
|
|
3937
|
+
}
|
|
3938
|
+
function errorRows(detail) {
|
|
3939
|
+
if (!detail.errorsAvailable) {
|
|
3940
|
+
return [" unavailable"];
|
|
3941
|
+
}
|
|
3942
|
+
if (detail.errors.length === 0) {
|
|
3943
|
+
return [" (none)"];
|
|
3944
|
+
}
|
|
3945
|
+
return detail.errors.map((error) => {
|
|
3946
|
+
const source = error.source ? `[${error.source}] ` : "";
|
|
3947
|
+
return ` - ${source}${error.message}`;
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
function failedTestRow(test) {
|
|
3951
|
+
if (!test.errorMessage) {
|
|
3952
|
+
return ` - ${test.name}`;
|
|
3953
|
+
}
|
|
3954
|
+
const firstLine = test.errorMessage.split("\n", 1)[0].trim();
|
|
3955
|
+
return ` - ${test.name}: ${firstLine}`;
|
|
3956
|
+
}
|
|
3957
|
+
function testRows(detail) {
|
|
3958
|
+
if (!detail.testsAvailable) {
|
|
3959
|
+
return [" unavailable"];
|
|
3960
|
+
}
|
|
3961
|
+
if (detail.tests.present) {
|
|
3962
|
+
return [
|
|
3963
|
+
` ${detail.tests.failed} failing of ${detail.tests.total}`,
|
|
3964
|
+
...detail.tests.failedTests.map(failedTestRow)
|
|
3965
|
+
];
|
|
3966
|
+
}
|
|
3967
|
+
return [" no tests present"];
|
|
3968
|
+
}
|
|
3969
|
+
function formatRunDetail(detail) {
|
|
3970
|
+
const status2 = detail.result ? `${detail.state}/${detail.result}` : detail.state;
|
|
3971
|
+
const name = detail.name ? ` ${detail.name}` : "";
|
|
3972
|
+
const duration = detail.durationSeconds == null ? "\u2014" : formatDuration(detail.durationSeconds);
|
|
3973
|
+
return [
|
|
3974
|
+
`Run #${detail.id} [${status2}]${name}`,
|
|
3975
|
+
`Queued: ${detail.createdDate ?? "\u2014"} Started: ${detail.startedDate ?? "\u2014"} Finished: ${detail.finishedDate ?? "\u2014"}`,
|
|
3976
|
+
`Duration: ${duration} Reason: ${detail.reason ?? "\u2014"} Requested for: ${detail.requestedFor ?? "\u2014"}`,
|
|
3977
|
+
`Branch: ${formatBranchName2(detail.sourceBranch)} Commit: ${detail.sourceCommit ?? "unavailable"}`,
|
|
3978
|
+
...detail.webUrl ? [`Link: ${detail.webUrl}`] : [],
|
|
3979
|
+
"",
|
|
3980
|
+
"Stages:",
|
|
3981
|
+
...timelineRows(detail.stages, detail.errorsAvailable),
|
|
3982
|
+
"",
|
|
3983
|
+
"Jobs:",
|
|
3984
|
+
...timelineRows(detail.jobs, detail.errorsAvailable),
|
|
3985
|
+
"",
|
|
3986
|
+
"Errors:",
|
|
3987
|
+
...errorRows(detail),
|
|
3988
|
+
"",
|
|
3989
|
+
"Tests:",
|
|
3990
|
+
...testRows(detail)
|
|
3991
|
+
].join("\n");
|
|
3992
|
+
}
|
|
3993
|
+
function createPipelineGetRunDetailCommand() {
|
|
3994
|
+
const command = new Command13("get-run-detail");
|
|
3995
|
+
command.description("Show a detailed summary of a single pipeline run (errors, failing tests, stages)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
3996
|
+
validateOrgProjectPair(options);
|
|
3997
|
+
const runId = parsePositiveId(runIdRaw);
|
|
3998
|
+
if (runId === null) {
|
|
3999
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
let context;
|
|
4003
|
+
try {
|
|
4004
|
+
const resolved = await resolvePipelineContext(options);
|
|
4005
|
+
context = resolved.context;
|
|
4006
|
+
const detail = await getRunDetail(resolved.context, resolved.cred, runId);
|
|
4007
|
+
if (options.json) {
|
|
4008
|
+
process.stdout.write(`${JSON.stringify(detail, null, 2)}
|
|
4009
|
+
`);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
process.stdout.write(`${formatRunDetail(detail)}
|
|
4013
|
+
`);
|
|
4014
|
+
} catch (err) {
|
|
4015
|
+
handlePipelineError(err, context);
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
return command;
|
|
4019
|
+
}
|
|
4020
|
+
function grepWithContext(lines, grep, context) {
|
|
4021
|
+
const include = /* @__PURE__ */ new Set();
|
|
4022
|
+
lines.forEach((line, i) => {
|
|
4023
|
+
if (grep.test(line)) {
|
|
4024
|
+
for (let j = Math.max(0, i - context); j <= Math.min(lines.length - 1, i + context); j++) {
|
|
4025
|
+
include.add(j);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
const selected = [];
|
|
4030
|
+
let prev = -1;
|
|
4031
|
+
for (const i of [...include].sort((a, b) => a - b)) {
|
|
4032
|
+
if (selected.length > 0 && i > prev + 1) {
|
|
4033
|
+
selected.push("--");
|
|
4034
|
+
}
|
|
4035
|
+
selected.push(lines[i]);
|
|
4036
|
+
prev = i;
|
|
4037
|
+
}
|
|
4038
|
+
return selected;
|
|
4039
|
+
}
|
|
4040
|
+
function filterLogLines(content, grep, tail, context) {
|
|
4041
|
+
let lines = content.split("\n");
|
|
4042
|
+
if (lines.at(-1) === "") {
|
|
4043
|
+
lines.pop();
|
|
4044
|
+
}
|
|
4045
|
+
if (grep) {
|
|
4046
|
+
lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
|
|
4047
|
+
}
|
|
4048
|
+
if (tail !== void 0 && lines.length > tail) {
|
|
4049
|
+
lines = lines.slice(-tail);
|
|
4050
|
+
}
|
|
4051
|
+
return lines;
|
|
4052
|
+
}
|
|
4053
|
+
function parseLogFilters(options) {
|
|
4054
|
+
if (options.logId !== void 0 && options.step !== void 0) {
|
|
4055
|
+
writeError2("Use either --log-id or --step, not both.");
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
|
|
4059
|
+
const slices = options.tail !== void 0 || options.grep !== void 0 || options.context !== void 0;
|
|
4060
|
+
if (slices && !selectsSingleLog) {
|
|
4061
|
+
writeError2("--tail, --grep, and --context require --log-id or --step.");
|
|
4062
|
+
return null;
|
|
4063
|
+
}
|
|
4064
|
+
if (options.context !== void 0 && options.grep === void 0) {
|
|
4065
|
+
writeError2("--context requires --grep.");
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
const tail = parseOptionalCount(options.tail, "--tail");
|
|
4069
|
+
if (tail === null) return null;
|
|
4070
|
+
const contextLines = parseOptionalCount(options.context, "--context");
|
|
4071
|
+
if (contextLines === null) return null;
|
|
4072
|
+
let grep;
|
|
4073
|
+
if (options.grep !== void 0) {
|
|
4074
|
+
try {
|
|
4075
|
+
grep = new RegExp(options.grep);
|
|
4076
|
+
} catch {
|
|
4077
|
+
writeError2(`Invalid --grep "${options.grep}"; expected a valid regular expression.`);
|
|
4078
|
+
return null;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
return { tail, contextLines: contextLines ?? 0, grep };
|
|
4082
|
+
}
|
|
4083
|
+
function chooseStepLog(logs, step, runId) {
|
|
4084
|
+
const needle = step.toLowerCase();
|
|
4085
|
+
const matches = logs.filter((l) => l.step?.toLowerCase().includes(needle));
|
|
4086
|
+
const exact = matches.filter((l) => l.step?.toLowerCase() === needle);
|
|
4087
|
+
const chosen = exact.length === 1 ? exact : matches;
|
|
4088
|
+
if (chosen.length === 0) {
|
|
4089
|
+
writeError2(`No log matches step "${step}" in run ${runId}.`);
|
|
4090
|
+
return null;
|
|
4091
|
+
}
|
|
4092
|
+
if (chosen.length > 1) {
|
|
4093
|
+
const candidates = chosen.map((l) => `${l.id} (${l.step})`).join(", ");
|
|
4094
|
+
writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
|
|
4095
|
+
return null;
|
|
4096
|
+
}
|
|
4097
|
+
return chosen[0].id;
|
|
4098
|
+
}
|
|
4099
|
+
async function resolveRequestedLogId(resolved, runId, options) {
|
|
4100
|
+
if (options.logId !== void 0) {
|
|
4101
|
+
const parsed = parsePositiveId(options.logId);
|
|
4102
|
+
if (parsed === null) {
|
|
4103
|
+
writeError2(`Invalid --log-id "${options.logId}"; expected a positive integer.`);
|
|
4104
|
+
return null;
|
|
4105
|
+
}
|
|
4106
|
+
return parsed;
|
|
4107
|
+
}
|
|
4108
|
+
if (options.step === void 0) {
|
|
4109
|
+
return void 0;
|
|
4110
|
+
}
|
|
4111
|
+
const allLogs = await getRunLogs(resolved.context, resolved.cred, runId);
|
|
4112
|
+
return chooseStepLog(allLogs, options.step, runId);
|
|
4113
|
+
}
|
|
4114
|
+
function printSingleLog(content, filters) {
|
|
4115
|
+
if (filters.grep !== void 0 || filters.tail !== void 0) {
|
|
4116
|
+
const lines = filterLogLines(content, filters.grep, filters.tail, filters.contextLines);
|
|
4117
|
+
if (lines.length > 0) {
|
|
4118
|
+
process.stdout.write(`${lines.join("\n")}
|
|
4119
|
+
`);
|
|
4120
|
+
}
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
process.stdout.write(content.endsWith("\n") ? content : `${content}
|
|
4124
|
+
`);
|
|
4125
|
+
}
|
|
4126
|
+
function createPipelineLogsCommand() {
|
|
4127
|
+
const command = new Command13("logs");
|
|
4128
|
+
command.description("List a pipeline run's logs, or print a specific log with --log-id").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--log-id <id>", "print the content of this log id").option("--step <name>", "print the log of the step/job matching this name (case-insensitive substring)").option("--tail <n>", "with --log-id/--step, print only the last N lines").option("--grep <pattern>", "with --log-id/--step, print only lines matching this regular expression").option("--context <n>", "with --grep, also print N lines around each match (grep -C)").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
4129
|
+
validateOrgProjectPair(options);
|
|
4130
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4131
|
+
if (runId === null) {
|
|
4132
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
const filters = parseLogFilters(options);
|
|
4136
|
+
if (filters === null) {
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
let context;
|
|
4140
|
+
try {
|
|
4141
|
+
const resolved = await resolvePipelineContext(options);
|
|
4142
|
+
context = resolved.context;
|
|
4143
|
+
const logId = await resolveRequestedLogId(resolved, runId, options);
|
|
4144
|
+
if (logId === null) {
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (logId !== void 0) {
|
|
4148
|
+
const content = await getRunLog(resolved.context, resolved.cred, runId, logId);
|
|
4149
|
+
printSingleLog(content, filters);
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
const logs = await getRunLogs(resolved.context, resolved.cred, runId);
|
|
4153
|
+
if (options.json) {
|
|
4154
|
+
process.stdout.write(`${JSON.stringify(logs, null, 2)}
|
|
4155
|
+
`);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
if (logs.length === 0) {
|
|
4159
|
+
process.stdout.write(`No logs found for run ${runId}.
|
|
4160
|
+
`);
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
const rows = logs.map((l) => [
|
|
4164
|
+
String(l.id),
|
|
4165
|
+
l.createdOn ?? "\u2014",
|
|
4166
|
+
l.lineCount == null ? "" : `${l.lineCount} lines`,
|
|
4167
|
+
l.step ?? ""
|
|
4168
|
+
]);
|
|
4169
|
+
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
4170
|
+
`);
|
|
4171
|
+
} catch (err) {
|
|
4172
|
+
handlePipelineError(err, context);
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
return command;
|
|
4176
|
+
}
|
|
4177
|
+
function createPipelineTestsCommand() {
|
|
4178
|
+
const command = new Command13("tests");
|
|
4179
|
+
command.description("Show a run's test results: summary plus failing tests by name (no log grepping needed)").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--failed", "list only the failing tests").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
4180
|
+
validateOrgProjectPair(options);
|
|
4181
|
+
const runId = parsePositiveId(runIdRaw);
|
|
4182
|
+
if (runId === null) {
|
|
4183
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
let context;
|
|
4187
|
+
try {
|
|
4188
|
+
const resolved = await resolvePipelineContext(options);
|
|
4189
|
+
context = resolved.context;
|
|
4190
|
+
const summary = await getTestSummary(resolved.context, resolved.cred, runId);
|
|
4191
|
+
const failedTests = summary.failed > 0 ? await getFailedTests(resolved.context, resolved.cred, runId) : [];
|
|
4192
|
+
if (options.json) {
|
|
4193
|
+
process.stdout.write(`${JSON.stringify({ ...summary, failedTests }, null, 2)}
|
|
4194
|
+
`);
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (!summary.present) {
|
|
4198
|
+
process.stdout.write(`No test results published for run ${runId}.
|
|
4199
|
+
`);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
if (!options.failed) {
|
|
4203
|
+
process.stdout.write(`Run #${runId}: ${summary.failed} failing of ${summary.total} tests
|
|
4204
|
+
`);
|
|
4205
|
+
}
|
|
4206
|
+
if (failedTests.length > 0) {
|
|
4207
|
+
process.stdout.write(`${failedTests.map(failedTestRow).join("\n")}
|
|
4208
|
+
`);
|
|
4209
|
+
} else if (options.failed) {
|
|
4210
|
+
process.stdout.write("No failing tests.\n");
|
|
4211
|
+
}
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
handlePipelineError(err, context);
|
|
4214
|
+
}
|
|
4215
|
+
});
|
|
4216
|
+
return command;
|
|
4217
|
+
}
|
|
4218
|
+
function parseParameters(values) {
|
|
4219
|
+
const result = {};
|
|
4220
|
+
for (const entry of values ?? []) {
|
|
4221
|
+
const eq = entry.indexOf("=");
|
|
4222
|
+
if (eq <= 0) {
|
|
4223
|
+
return null;
|
|
4224
|
+
}
|
|
4225
|
+
const key = entry.slice(0, eq);
|
|
4226
|
+
const value = entry.slice(eq + 1);
|
|
4227
|
+
result[key] = value;
|
|
4228
|
+
}
|
|
4229
|
+
return result;
|
|
4230
|
+
}
|
|
4231
|
+
function collectParameter(value, previous) {
|
|
4232
|
+
return previous.concat([value]);
|
|
4233
|
+
}
|
|
4234
|
+
function createPipelineStartCommand() {
|
|
4235
|
+
const command = new Command13("start");
|
|
4236
|
+
command.description("Queue a new run of a pipeline definition").argument("<def_id>", "pipeline definition id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--branch <branch>", "branch to run against (default: pipeline default branch)").option("--parameter <key=value>", "template parameter (repeatable)", collectParameter, []).option("--json", "output JSON").action(async (defIdRaw, options) => {
|
|
4237
|
+
validateOrgProjectPair(options);
|
|
4238
|
+
const defId = parsePositiveId(defIdRaw);
|
|
4239
|
+
if (defId === null) {
|
|
4240
|
+
writeError2(`Invalid definition id "${defIdRaw}"; expected a positive integer.`);
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
const parameters = parseParameters(options.parameter);
|
|
4244
|
+
if (parameters === null) {
|
|
4245
|
+
writeError2("Invalid --parameter; expected key=value.");
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
let context;
|
|
4249
|
+
try {
|
|
4250
|
+
const resolved = await resolvePipelineContext(options);
|
|
4251
|
+
context = resolved.context;
|
|
4252
|
+
const result = await runPipeline(resolved.context, resolved.cred, defId, {
|
|
4253
|
+
branch: options.branch,
|
|
4254
|
+
parameters
|
|
4255
|
+
});
|
|
4256
|
+
if (options.json) {
|
|
4257
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4258
|
+
`);
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
process.stdout.write(`Queued run #${result.id} [${result.state}]
|
|
4262
|
+
${result.webUrl ?? "\u2014"}
|
|
4263
|
+
`);
|
|
4264
|
+
} catch (err) {
|
|
4265
|
+
handlePipelineError(err, context);
|
|
4266
|
+
}
|
|
4267
|
+
});
|
|
4268
|
+
return command;
|
|
4269
|
+
}
|
|
4270
|
+
function createPipelineCommand() {
|
|
4271
|
+
const command = new Command13("pipeline");
|
|
4272
|
+
command.description("Manage Azure DevOps pipelines");
|
|
4273
|
+
command.addCommand(createPipelineListCommand());
|
|
4274
|
+
command.addCommand(createPipelineGetRunsCommand());
|
|
4275
|
+
command.addCommand(createPipelineWaitCommand());
|
|
4276
|
+
command.addCommand(createPipelineGetRunDetailCommand());
|
|
4277
|
+
command.addCommand(createPipelineLogsCommand());
|
|
4278
|
+
command.addCommand(createPipelineTestsCommand());
|
|
4279
|
+
command.addCommand(createPipelineStartCommand());
|
|
4280
|
+
return command;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
// src/commands/comments.ts
|
|
4284
|
+
import { Command as Command14 } from "commander";
|
|
4285
|
+
function writeError3(message) {
|
|
4286
|
+
process.stderr.write(`Error: ${message}
|
|
3375
4287
|
`);
|
|
3376
4288
|
process.exit(1);
|
|
3377
4289
|
}
|
|
@@ -3389,7 +4301,7 @@ function formatComments(result, convertMarkdown) {
|
|
|
3389
4301
|
return lines.join("\n");
|
|
3390
4302
|
}
|
|
3391
4303
|
function createCommentsListCommand() {
|
|
3392
|
-
const command = new
|
|
4304
|
+
const command = new Command14("list");
|
|
3393
4305
|
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "convert HTML comment bodies to markdown").action(async (idStr, options) => {
|
|
3394
4306
|
validateOrgProjectPair(options);
|
|
3395
4307
|
const id = parseWorkItemId(idStr);
|
|
@@ -3417,12 +4329,12 @@ function createCommentsListCommand() {
|
|
|
3417
4329
|
return command;
|
|
3418
4330
|
}
|
|
3419
4331
|
function createCommentsAddCommand() {
|
|
3420
|
-
const command = new
|
|
4332
|
+
const command = new Command14("add");
|
|
3421
4333
|
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, text, options) => {
|
|
3422
4334
|
validateOrgProjectPair(options);
|
|
3423
4335
|
const id = parseWorkItemId(idStr);
|
|
3424
4336
|
if (text.trim() === "") {
|
|
3425
|
-
|
|
4337
|
+
writeError3("Comment text must be a non-empty string.");
|
|
3426
4338
|
}
|
|
3427
4339
|
let context;
|
|
3428
4340
|
try {
|
|
@@ -3444,7 +4356,7 @@ function createCommentsAddCommand() {
|
|
|
3444
4356
|
return command;
|
|
3445
4357
|
}
|
|
3446
4358
|
function createCommentsCommand() {
|
|
3447
|
-
const command = new
|
|
4359
|
+
const command = new Command14("comments");
|
|
3448
4360
|
command.description("Manage Azure DevOps work item comments");
|
|
3449
4361
|
command.addCommand(createCommentsListCommand());
|
|
3450
4362
|
command.addCommand(createCommentsAddCommand());
|
|
@@ -3452,12 +4364,12 @@ function createCommentsCommand() {
|
|
|
3452
4364
|
}
|
|
3453
4365
|
|
|
3454
4366
|
// src/commands/download-attachment.ts
|
|
3455
|
-
import { Command as
|
|
4367
|
+
import { Command as Command15 } from "commander";
|
|
3456
4368
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
3457
4369
|
import { existsSync as existsSync5 } from "fs";
|
|
3458
4370
|
import { join as join3 } from "path";
|
|
3459
4371
|
function createDownloadAttachmentCommand() {
|
|
3460
|
-
const command = new
|
|
4372
|
+
const command = new Command15("download-attachment");
|
|
3461
4373
|
command.description("Download an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to download").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--output <dir>", "target directory for the downloaded file").action(
|
|
3462
4374
|
async (idStr, filename, options) => {
|
|
3463
4375
|
const id = parseWorkItemId(idStr);
|
|
@@ -3603,7 +4515,15 @@ async function getUpdateNotice(opts) {
|
|
|
3603
4515
|
}
|
|
3604
4516
|
|
|
3605
4517
|
// src/index.ts
|
|
3606
|
-
|
|
4518
|
+
function exitOnEpipe(err) {
|
|
4519
|
+
if (err.code === "EPIPE") {
|
|
4520
|
+
process.exit(0);
|
|
4521
|
+
}
|
|
4522
|
+
throw err;
|
|
4523
|
+
}
|
|
4524
|
+
process.stdout.on("error", exitOnEpipe);
|
|
4525
|
+
process.stderr.on("error", exitOnEpipe);
|
|
4526
|
+
var program = new Command16();
|
|
3607
4527
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
3608
4528
|
program.option("--no-update-check", "Skip the check for a newer published version");
|
|
3609
4529
|
program.addCommand(createGetItemCommand());
|
|
@@ -3618,6 +4538,7 @@ program.addCommand(createSetMdFieldCommand());
|
|
|
3618
4538
|
program.addCommand(createUpsertCommand());
|
|
3619
4539
|
program.addCommand(createListFieldsCommand());
|
|
3620
4540
|
program.addCommand(createPrCommand());
|
|
4541
|
+
program.addCommand(createPipelineCommand());
|
|
3621
4542
|
program.addCommand(createCommentsCommand());
|
|
3622
4543
|
program.addCommand(createDownloadAttachmentCommand());
|
|
3623
4544
|
program.showHelpAfterError();
|