azdo-cli 0.10.0-develop.386 → 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 +10 -1
- package/dist/index.js +1091 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,9 +56,18 @@ azdo comments add 12345 "Investigating the root cause now."
|
|
|
56
56
|
# PR comment threads — list, filter, target by number, resolve or reopen
|
|
57
57
|
azdo pr comments # active-branch PR
|
|
58
58
|
azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
|
|
59
|
-
azdo pr comments --pr-number 64 --hide-resolved
|
|
59
|
+
azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
|
|
60
|
+
azdo pr comments --code-related-only # only file/line-anchored threads
|
|
61
|
+
azdo pr status # PR checks (status + branch policies) + code-comment counts
|
|
60
62
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
61
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
|
|
62
71
|
```
|
|
63
72
|
|
|
64
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,
|
|
@@ -2652,6 +2652,21 @@ function buildPullRequestStatusesUrl(context, repo, prId) {
|
|
|
2652
2652
|
url.searchParams.set("api-version", "7.1");
|
|
2653
2653
|
return url;
|
|
2654
2654
|
}
|
|
2655
|
+
function buildProjectUrl(context) {
|
|
2656
|
+
const url = new URL(
|
|
2657
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/projects/${encodeURIComponent(context.project)}`
|
|
2658
|
+
);
|
|
2659
|
+
url.searchParams.set("api-version", "7.1");
|
|
2660
|
+
return url;
|
|
2661
|
+
}
|
|
2662
|
+
function buildPolicyEvaluationsUrl(context, projectId, prId) {
|
|
2663
|
+
const url = new URL(
|
|
2664
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/policy/evaluations`
|
|
2665
|
+
);
|
|
2666
|
+
url.searchParams.set("api-version", "7.1");
|
|
2667
|
+
url.searchParams.set("artifactId", `vstfs:///CodeReview/CodeReviewId/${projectId}/${prId}`);
|
|
2668
|
+
return url;
|
|
2669
|
+
}
|
|
2655
2670
|
function mapPullRequest(repo, pullRequest) {
|
|
2656
2671
|
return {
|
|
2657
2672
|
id: pullRequest.pullRequestId,
|
|
@@ -2690,7 +2705,49 @@ function mapPullRequestCheck(status2) {
|
|
|
2690
2705
|
targetUrl: status2.targetUrl ?? null,
|
|
2691
2706
|
createdBy: status2.createdBy?.displayName ?? null,
|
|
2692
2707
|
createdAt: status2.creationDate ?? null,
|
|
2693
|
-
updatedAt: status2.updatedDate ?? null
|
|
2708
|
+
updatedAt: status2.updatedDate ?? null,
|
|
2709
|
+
source: "status"
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
function mapPolicyEvaluationState(status2) {
|
|
2713
|
+
switch (status2) {
|
|
2714
|
+
case "approved":
|
|
2715
|
+
return "succeeded";
|
|
2716
|
+
case "rejected":
|
|
2717
|
+
return "failed";
|
|
2718
|
+
case "running":
|
|
2719
|
+
case "queued":
|
|
2720
|
+
return "pending";
|
|
2721
|
+
case "notApplicable":
|
|
2722
|
+
case "notSet":
|
|
2723
|
+
case void 0:
|
|
2724
|
+
return null;
|
|
2725
|
+
default:
|
|
2726
|
+
return status2;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
function mapPolicyEvaluationName(evaluation) {
|
|
2730
|
+
const display = evaluation.configuration?.settings?.displayName?.trim() || evaluation.configuration?.type?.displayName?.trim();
|
|
2731
|
+
if (display) {
|
|
2732
|
+
return display;
|
|
2733
|
+
}
|
|
2734
|
+
return `Policy ${evaluation.configuration?.id ?? evaluation.evaluationId ?? "?"}`;
|
|
2735
|
+
}
|
|
2736
|
+
function mapPolicyEvaluationCheck(evaluation) {
|
|
2737
|
+
const state = mapPolicyEvaluationState(evaluation.status);
|
|
2738
|
+
if (state === null) {
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
id: evaluation.configuration?.id ?? 0,
|
|
2743
|
+
state,
|
|
2744
|
+
name: mapPolicyEvaluationName(evaluation),
|
|
2745
|
+
description: null,
|
|
2746
|
+
targetUrl: null,
|
|
2747
|
+
createdBy: null,
|
|
2748
|
+
createdAt: null,
|
|
2749
|
+
updatedAt: null,
|
|
2750
|
+
source: "policy"
|
|
2694
2751
|
};
|
|
2695
2752
|
}
|
|
2696
2753
|
function mapComment(comment) {
|
|
@@ -2776,6 +2833,21 @@ async function getPullRequestChecks(context, repo, cred, prId) {
|
|
|
2776
2833
|
const data = await readJsonResponse(response);
|
|
2777
2834
|
return data.value.map(mapPullRequestCheck).filter((check) => check !== null);
|
|
2778
2835
|
}
|
|
2836
|
+
async function resolveProjectId(context, cred) {
|
|
2837
|
+
const response = await fetchWithErrors(buildProjectUrl(context).toString(), {
|
|
2838
|
+
headers: authHeaders(cred)
|
|
2839
|
+
});
|
|
2840
|
+
const data = await readJsonResponse(response);
|
|
2841
|
+
return data.id;
|
|
2842
|
+
}
|
|
2843
|
+
async function getPullRequestPolicyEvaluations(context, cred, projectId, prId) {
|
|
2844
|
+
const response = await fetchWithErrors(
|
|
2845
|
+
buildPolicyEvaluationsUrl(context, projectId, prId).toString(),
|
|
2846
|
+
{ headers: authHeaders(cred) }
|
|
2847
|
+
);
|
|
2848
|
+
const data = await readJsonResponse(response);
|
|
2849
|
+
return data.value.map(mapPolicyEvaluationCheck).filter((check) => check !== null);
|
|
2850
|
+
}
|
|
2779
2851
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
2780
2852
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
2781
2853
|
status: "active",
|
|
@@ -2884,7 +2956,10 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
2884
2956
|
}
|
|
2885
2957
|
writeError(error.message);
|
|
2886
2958
|
}
|
|
2887
|
-
function formatPullRequestChecks(checks) {
|
|
2959
|
+
function formatPullRequestChecks(checks, checksError) {
|
|
2960
|
+
if (checksError) {
|
|
2961
|
+
return [`Checks: unable to retrieve (${checksError})`];
|
|
2962
|
+
}
|
|
2888
2963
|
if (checks.length === 0) {
|
|
2889
2964
|
return ["Checks: none reported by Azure DevOps"];
|
|
2890
2965
|
}
|
|
@@ -2897,14 +2972,68 @@ function formatPullRequestChecks(checks) {
|
|
|
2897
2972
|
}
|
|
2898
2973
|
return lines;
|
|
2899
2974
|
}
|
|
2975
|
+
function countCodeComments(threads) {
|
|
2976
|
+
let open = 0;
|
|
2977
|
+
let closed = 0;
|
|
2978
|
+
for (const thread of threads) {
|
|
2979
|
+
if (thread.threadContext === null) {
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
if (isThreadResolved(thread.status)) {
|
|
2983
|
+
closed += 1;
|
|
2984
|
+
} else {
|
|
2985
|
+
open += 1;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return { open, closed };
|
|
2989
|
+
}
|
|
2990
|
+
function formatCodeCommentCounts(counts) {
|
|
2991
|
+
return `Code comments: ${counts.open} open, ${counts.closed} closed`;
|
|
2992
|
+
}
|
|
2900
2993
|
function formatPullRequestBlock(pullRequest) {
|
|
2901
2994
|
return [
|
|
2902
2995
|
`#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
|
|
2903
2996
|
`${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
|
|
2904
2997
|
pullRequest.url ?? "\u2014",
|
|
2905
|
-
...formatPullRequestChecks(pullRequest.checks)
|
|
2998
|
+
...formatPullRequestChecks(pullRequest.checks, pullRequest.checksError),
|
|
2999
|
+
formatCodeCommentCounts(pullRequest.codeCommentCounts)
|
|
2906
3000
|
].join("\n");
|
|
2907
3001
|
}
|
|
3002
|
+
async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, projectId) {
|
|
3003
|
+
let statusChecks = [];
|
|
3004
|
+
let statusOk = true;
|
|
3005
|
+
try {
|
|
3006
|
+
statusChecks = await getPullRequestChecks(context, repo, cred, pullRequest.id);
|
|
3007
|
+
} catch {
|
|
3008
|
+
statusOk = false;
|
|
3009
|
+
}
|
|
3010
|
+
let policyChecks = [];
|
|
3011
|
+
let policyOk = true;
|
|
3012
|
+
if (projectId === null) {
|
|
3013
|
+
policyOk = false;
|
|
3014
|
+
} else {
|
|
3015
|
+
try {
|
|
3016
|
+
policyChecks = await getPullRequestPolicyEvaluations(context, cred, projectId, pullRequest.id);
|
|
3017
|
+
} catch {
|
|
3018
|
+
policyOk = false;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
let codeCommentCounts;
|
|
3022
|
+
try {
|
|
3023
|
+
const threads = await getPullRequestThreads(context, repo, cred, pullRequest.id);
|
|
3024
|
+
codeCommentCounts = countCodeComments(threads);
|
|
3025
|
+
} catch {
|
|
3026
|
+
codeCommentCounts = { open: 0, closed: 0 };
|
|
3027
|
+
}
|
|
3028
|
+
const checks = [...statusChecks, ...policyChecks];
|
|
3029
|
+
const checksError = checks.length === 0 && (!statusOk || !policyOk) ? "Azure DevOps request failed" : null;
|
|
3030
|
+
return {
|
|
3031
|
+
...pullRequest,
|
|
3032
|
+
checks,
|
|
3033
|
+
codeCommentCounts,
|
|
3034
|
+
checksError
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
2908
3037
|
function threadStatusLabel(status2) {
|
|
2909
3038
|
return isThreadResolved(status2) ? "resolved" : status2;
|
|
2910
3039
|
}
|
|
@@ -2941,11 +3070,16 @@ function createPrStatusCommand() {
|
|
|
2941
3070
|
context = resolved.context;
|
|
2942
3071
|
const branch = resolved.branch;
|
|
2943
3072
|
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, branch);
|
|
3073
|
+
let projectId = null;
|
|
3074
|
+
try {
|
|
3075
|
+
projectId = await resolveProjectId(resolved.context, resolved.pat);
|
|
3076
|
+
} catch {
|
|
3077
|
+
projectId = null;
|
|
3078
|
+
}
|
|
2944
3079
|
const pullRequestsWithChecks = await Promise.all(
|
|
2945
|
-
pullRequests.map(
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
}))
|
|
3080
|
+
pullRequests.map(
|
|
3081
|
+
async (pullRequest) => buildPullRequestStatusEntry(resolved.context, resolved.repo, resolved.pat, pullRequest, projectId)
|
|
3082
|
+
)
|
|
2949
3083
|
);
|
|
2950
3084
|
const result = { branch, repository: resolved.repo, pullRequests: pullRequestsWithChecks };
|
|
2951
3085
|
if (options.json) {
|
|
@@ -3026,7 +3160,7 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
3026
3160
|
}
|
|
3027
3161
|
function createPrCommentsCommand() {
|
|
3028
3162
|
const command = new Command12("comments");
|
|
3029
|
-
configureUnwrappedHelp(command).description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--json", "output JSON").action(async (options) => {
|
|
3163
|
+
configureUnwrappedHelp(command).description("List pull request comment threads for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--exclude-resolved", "alias of --hide-resolved: exclude resolved / won't fix / closed / by design threads").option("--code-related-only", "show only threads anchored to a file/line; omit general discussion threads").option("--json", "output JSON").action(async (options) => {
|
|
3030
3164
|
validateOrgProjectPair(options);
|
|
3031
3165
|
let context;
|
|
3032
3166
|
let explicitPrId = null;
|
|
@@ -3068,8 +3202,12 @@ function createPrCommentsCommand() {
|
|
|
3068
3202
|
pullRequest = pullRequests[0];
|
|
3069
3203
|
branchLabel = resolved.branch;
|
|
3070
3204
|
}
|
|
3205
|
+
const hideResolved = options.hideResolved === true || options.excludeResolved === true;
|
|
3206
|
+
const codeRelatedOnly = options.codeRelatedOnly === true;
|
|
3071
3207
|
const allThreads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
|
|
3072
|
-
const threads =
|
|
3208
|
+
const threads = allThreads.filter(
|
|
3209
|
+
(thread) => (!hideResolved || !isThreadResolved(thread.status)) && (!codeRelatedOnly || thread.threadContext !== null)
|
|
3210
|
+
);
|
|
3073
3211
|
const result = { branch: branchLabel, pullRequest, threads };
|
|
3074
3212
|
if (options.json) {
|
|
3075
3213
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
@@ -3077,9 +3215,18 @@ function createPrCommentsCommand() {
|
|
|
3077
3215
|
return;
|
|
3078
3216
|
}
|
|
3079
3217
|
if (threads.length === 0) {
|
|
3080
|
-
if (
|
|
3081
|
-
|
|
3082
|
-
|
|
3218
|
+
if (allThreads.length > 0 && (hideResolved || codeRelatedOnly)) {
|
|
3219
|
+
const filters = [];
|
|
3220
|
+
if (codeRelatedOnly) {
|
|
3221
|
+
filters.push("code-related");
|
|
3222
|
+
}
|
|
3223
|
+
if (hideResolved) {
|
|
3224
|
+
filters.push("unresolved");
|
|
3225
|
+
}
|
|
3226
|
+
process.stdout.write(
|
|
3227
|
+
`Pull request #${pullRequest.id} has no ${filters.join(" ")} comment threads (filtered from ${allThreads.length} thread${allThreads.length === 1 ? "" : "s"}).
|
|
3228
|
+
`
|
|
3229
|
+
);
|
|
3083
3230
|
} else {
|
|
3084
3231
|
process.stdout.write(`Pull request #${pullRequest.id} has no comment threads.
|
|
3085
3232
|
`);
|
|
@@ -3221,10 +3368,922 @@ function createPrCommand() {
|
|
|
3221
3368
|
return command;
|
|
3222
3369
|
}
|
|
3223
3370
|
|
|
3224
|
-
// src/commands/
|
|
3371
|
+
// src/commands/pipeline.ts
|
|
3225
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;
|
|
3226
3670
|
function writeError2(message) {
|
|
3227
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}
|
|
3228
4287
|
`);
|
|
3229
4288
|
process.exit(1);
|
|
3230
4289
|
}
|
|
@@ -3242,7 +4301,7 @@ function formatComments(result, convertMarkdown) {
|
|
|
3242
4301
|
return lines.join("\n");
|
|
3243
4302
|
}
|
|
3244
4303
|
function createCommentsListCommand() {
|
|
3245
|
-
const command = new
|
|
4304
|
+
const command = new Command14("list");
|
|
3246
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) => {
|
|
3247
4306
|
validateOrgProjectPair(options);
|
|
3248
4307
|
const id = parseWorkItemId(idStr);
|
|
@@ -3270,12 +4329,12 @@ function createCommentsListCommand() {
|
|
|
3270
4329
|
return command;
|
|
3271
4330
|
}
|
|
3272
4331
|
function createCommentsAddCommand() {
|
|
3273
|
-
const command = new
|
|
4332
|
+
const command = new Command14("add");
|
|
3274
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) => {
|
|
3275
4334
|
validateOrgProjectPair(options);
|
|
3276
4335
|
const id = parseWorkItemId(idStr);
|
|
3277
4336
|
if (text.trim() === "") {
|
|
3278
|
-
|
|
4337
|
+
writeError3("Comment text must be a non-empty string.");
|
|
3279
4338
|
}
|
|
3280
4339
|
let context;
|
|
3281
4340
|
try {
|
|
@@ -3297,7 +4356,7 @@ function createCommentsAddCommand() {
|
|
|
3297
4356
|
return command;
|
|
3298
4357
|
}
|
|
3299
4358
|
function createCommentsCommand() {
|
|
3300
|
-
const command = new
|
|
4359
|
+
const command = new Command14("comments");
|
|
3301
4360
|
command.description("Manage Azure DevOps work item comments");
|
|
3302
4361
|
command.addCommand(createCommentsListCommand());
|
|
3303
4362
|
command.addCommand(createCommentsAddCommand());
|
|
@@ -3305,12 +4364,12 @@ function createCommentsCommand() {
|
|
|
3305
4364
|
}
|
|
3306
4365
|
|
|
3307
4366
|
// src/commands/download-attachment.ts
|
|
3308
|
-
import { Command as
|
|
4367
|
+
import { Command as Command15 } from "commander";
|
|
3309
4368
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
3310
4369
|
import { existsSync as existsSync5 } from "fs";
|
|
3311
4370
|
import { join as join3 } from "path";
|
|
3312
4371
|
function createDownloadAttachmentCommand() {
|
|
3313
|
-
const command = new
|
|
4372
|
+
const command = new Command15("download-attachment");
|
|
3314
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(
|
|
3315
4374
|
async (idStr, filename, options) => {
|
|
3316
4375
|
const id = parseWorkItemId(idStr);
|
|
@@ -3456,7 +4515,15 @@ async function getUpdateNotice(opts) {
|
|
|
3456
4515
|
}
|
|
3457
4516
|
|
|
3458
4517
|
// src/index.ts
|
|
3459
|
-
|
|
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();
|
|
3460
4527
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
3461
4528
|
program.option("--no-update-check", "Skip the check for a newer published version");
|
|
3462
4529
|
program.addCommand(createGetItemCommand());
|
|
@@ -3471,6 +4538,7 @@ program.addCommand(createSetMdFieldCommand());
|
|
|
3471
4538
|
program.addCommand(createUpsertCommand());
|
|
3472
4539
|
program.addCommand(createListFieldsCommand());
|
|
3473
4540
|
program.addCommand(createPrCommand());
|
|
4541
|
+
program.addCommand(createPipelineCommand());
|
|
3474
4542
|
program.addCommand(createCommentsCommand());
|
|
3475
4543
|
program.addCommand(createDownloadAttachmentCommand());
|
|
3476
4544
|
program.showHelpAfterError();
|