azdo-cli 0.22.0-develop.798 → 0.22.0-develop.822
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 +3 -0
- package/dist/index.js +311 -45
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -139,6 +139,9 @@ azdo pipeline get-runs 12 --branch develop --limit 1
|
|
|
139
139
|
azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
|
|
140
140
|
azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
|
|
141
141
|
azdo pipeline start 12 --branch develop --parameter env=staging
|
|
142
|
+
azdo pipeline artifacts 3456 # list a run's build artifacts (name, type, size)
|
|
143
|
+
azdo pipeline artifact-download 3456 reports --path ./out # extract into ./out (--all for every artifact, --force to overwrite)
|
|
144
|
+
azdo pipeline logs 3456 --step "Trivy" --head 50 --no-progress # also --tail/--grep; --no-progress drops CR redraws
|
|
142
145
|
|
|
143
146
|
# Work item relations — types, add, remove, list
|
|
144
147
|
azdo relations types # list all relation types (Child, Parent, Related, ...)
|
package/dist/index.js
CHANGED
|
@@ -4501,11 +4501,11 @@ function branchTemplateSegments(branch) {
|
|
|
4501
4501
|
}
|
|
4502
4502
|
return segments;
|
|
4503
4503
|
}
|
|
4504
|
-
async function fetchRepositoryItemContent(context, repo, cred,
|
|
4504
|
+
async function fetchRepositoryItemContent(context, repo, cred, path5, branch) {
|
|
4505
4505
|
const url = new URL(
|
|
4506
4506
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/items`
|
|
4507
4507
|
);
|
|
4508
|
-
url.searchParams.set("path",
|
|
4508
|
+
url.searchParams.set("path", path5);
|
|
4509
4509
|
url.searchParams.set("versionDescriptor.version", branch);
|
|
4510
4510
|
url.searchParams.set("versionDescriptor.versionType", "branch");
|
|
4511
4511
|
url.searchParams.set("includeContent", "true");
|
|
@@ -4548,10 +4548,10 @@ function defaultTemplateCandidates() {
|
|
|
4548
4548
|
return paths;
|
|
4549
4549
|
}
|
|
4550
4550
|
async function firstMatchingTemplate(context, repo, cred, defaultBranch, candidates, kind) {
|
|
4551
|
-
for (const
|
|
4552
|
-
const content = await fetchRepositoryItemContent(context, repo, cred,
|
|
4551
|
+
for (const path5 of candidates) {
|
|
4552
|
+
const content = await fetchRepositoryItemContent(context, repo, cred, path5, defaultBranch);
|
|
4553
4553
|
if (content !== null) {
|
|
4554
|
-
return { path:
|
|
4554
|
+
return { path: path5, content, kind };
|
|
4555
4555
|
}
|
|
4556
4556
|
}
|
|
4557
4557
|
return null;
|
|
@@ -6348,6 +6348,7 @@ function createPrCommand() {
|
|
|
6348
6348
|
|
|
6349
6349
|
// src/commands/pipeline.ts
|
|
6350
6350
|
import { Command as Command14 } from "commander";
|
|
6351
|
+
import path3 from "path";
|
|
6351
6352
|
|
|
6352
6353
|
// src/services/pipeline-client.ts
|
|
6353
6354
|
var API_VERSION = "7.1";
|
|
@@ -6471,6 +6472,9 @@ async function getBuildStatus(context, cred, buildId) {
|
|
|
6471
6472
|
const build = await getBuild(context, cred, buildId);
|
|
6472
6473
|
return { state: mapBuildState(build.status), result: mapRunResult(build.result) };
|
|
6473
6474
|
}
|
|
6475
|
+
function recordErrors(record) {
|
|
6476
|
+
return (record.issues ?? []).filter((issue) => issue.type === "error" && issue.message).map((issue) => ({ message: issue.message ?? "", source: record.name ?? null }));
|
|
6477
|
+
}
|
|
6474
6478
|
async function getBuildTimeline(context, cred, buildId) {
|
|
6475
6479
|
const url = withApiVersion(
|
|
6476
6480
|
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/timeline`)
|
|
@@ -6482,14 +6486,20 @@ async function getBuildTimeline(context, cred, buildId) {
|
|
|
6482
6486
|
const stages = [];
|
|
6483
6487
|
const jobs = [];
|
|
6484
6488
|
const logSteps = /* @__PURE__ */ new Map();
|
|
6489
|
+
const logRecords = /* @__PURE__ */ new Map();
|
|
6490
|
+
const namesById = /* @__PURE__ */ new Map();
|
|
6485
6491
|
for (const record of records) {
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
}
|
|
6492
|
+
if (record.id && record.name) namesById.set(record.id, record.name);
|
|
6493
|
+
}
|
|
6494
|
+
for (const record of records) {
|
|
6495
|
+
errors.push(...recordErrors(record));
|
|
6491
6496
|
if (record.name && record.log?.id !== void 0) {
|
|
6492
6497
|
logSteps.set(record.log.id, record.name);
|
|
6498
|
+
logRecords.set(record.log.id, {
|
|
6499
|
+
name: record.name,
|
|
6500
|
+
type: record.type ?? null,
|
|
6501
|
+
parent: record.parentId && namesById.get(record.parentId) || null
|
|
6502
|
+
});
|
|
6493
6503
|
}
|
|
6494
6504
|
if (!record.name) continue;
|
|
6495
6505
|
const status2 = {
|
|
@@ -6509,7 +6519,7 @@ async function getBuildTimeline(context, cred, buildId) {
|
|
|
6509
6519
|
if (b.startTime === void 0) return -1;
|
|
6510
6520
|
return a.startTime < b.startTime ? -1 : 1;
|
|
6511
6521
|
});
|
|
6512
|
-
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps };
|
|
6522
|
+
return { errors, stages, jobs: jobs.map((j) => j.status), logSteps, logRecords };
|
|
6513
6523
|
}
|
|
6514
6524
|
async function listTestRuns(context, cred, buildId) {
|
|
6515
6525
|
const url = withApiVersion(new URL(`${orgProjectBase(context)}/_apis/test/runs`));
|
|
@@ -6618,16 +6628,18 @@ async function getRunLogs(context, cred, buildId) {
|
|
|
6618
6628
|
);
|
|
6619
6629
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6620
6630
|
const data = await readJsonResponse2(response);
|
|
6621
|
-
let
|
|
6631
|
+
let logRecords = /* @__PURE__ */ new Map();
|
|
6622
6632
|
try {
|
|
6623
|
-
|
|
6633
|
+
logRecords = (await getBuildTimeline(context, cred, buildId)).logRecords;
|
|
6624
6634
|
} catch {
|
|
6625
6635
|
}
|
|
6626
6636
|
return data.value.map((log) => ({
|
|
6627
6637
|
id: log.id,
|
|
6628
6638
|
createdOn: log.createdOn ?? null,
|
|
6629
6639
|
lineCount: log.lineCount ?? null,
|
|
6630
|
-
step:
|
|
6640
|
+
step: logRecords.get(log.id)?.name ?? null,
|
|
6641
|
+
type: logRecords.get(log.id)?.type ?? null,
|
|
6642
|
+
parent: logRecords.get(log.id)?.parent ?? null
|
|
6631
6643
|
}));
|
|
6632
6644
|
}
|
|
6633
6645
|
async function getRunLog(context, cred, buildId, logId) {
|
|
@@ -6640,6 +6652,122 @@ async function getRunLog(context, cred, buildId, logId) {
|
|
|
6640
6652
|
}
|
|
6641
6653
|
return response.text();
|
|
6642
6654
|
}
|
|
6655
|
+
async function listBuildArtifacts(context, cred, buildId) {
|
|
6656
|
+
const url = withApiVersion(
|
|
6657
|
+
new URL(`${orgProjectBase(context)}/_apis/build/builds/${buildId}/artifacts`)
|
|
6658
|
+
);
|
|
6659
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6660
|
+
const data = await readJsonResponse2(response);
|
|
6661
|
+
return data.value.map((artifact) => {
|
|
6662
|
+
const size = Number.parseInt(artifact.resource?.properties?.artifactsize ?? "", 10);
|
|
6663
|
+
return {
|
|
6664
|
+
id: artifact.id,
|
|
6665
|
+
name: artifact.name,
|
|
6666
|
+
type: artifact.resource?.type ?? null,
|
|
6667
|
+
sizeBytes: Number.isFinite(size) ? size : null,
|
|
6668
|
+
downloadUrl: artifact.resource?.downloadUrl ?? null
|
|
6669
|
+
};
|
|
6670
|
+
});
|
|
6671
|
+
}
|
|
6672
|
+
var INITIAL_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
6673
|
+
async function collectBody(body, declared, onProgress, initialBytes = INITIAL_BUFFER_BYTES) {
|
|
6674
|
+
const hint = Number.isSafeInteger(declared) && declared > 0 ? declared : Infinity;
|
|
6675
|
+
let buffer = new Uint8Array(Math.min(hint, initialBytes));
|
|
6676
|
+
let received = 0;
|
|
6677
|
+
for await (const chunk of body) {
|
|
6678
|
+
const needed = received + chunk.length;
|
|
6679
|
+
if (needed > buffer.length) {
|
|
6680
|
+
let capacity = Math.max(buffer.length * 2, needed);
|
|
6681
|
+
if (hint >= needed) capacity = Math.min(capacity, hint);
|
|
6682
|
+
const grown = new Uint8Array(capacity);
|
|
6683
|
+
grown.set(buffer.subarray(0, received));
|
|
6684
|
+
buffer = grown;
|
|
6685
|
+
}
|
|
6686
|
+
buffer.set(chunk, received);
|
|
6687
|
+
received = needed;
|
|
6688
|
+
onProgress?.(received);
|
|
6689
|
+
}
|
|
6690
|
+
return buffer.subarray(0, received);
|
|
6691
|
+
}
|
|
6692
|
+
async function downloadArtifactZip(cred, artifact, onProgress) {
|
|
6693
|
+
if (!artifact.downloadUrl) {
|
|
6694
|
+
throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
|
|
6695
|
+
}
|
|
6696
|
+
const url = new URL(artifact.downloadUrl);
|
|
6697
|
+
url.searchParams.set("$format", "zip");
|
|
6698
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6699
|
+
if (!response.ok) {
|
|
6700
|
+
throw httpError(response);
|
|
6701
|
+
}
|
|
6702
|
+
if (!response.body) {
|
|
6703
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
6704
|
+
}
|
|
6705
|
+
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
6706
|
+
return collectBody(response.body, declared, onProgress);
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6709
|
+
// src/services/artifact-extract.ts
|
|
6710
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
|
|
6711
|
+
import path2 from "path";
|
|
6712
|
+
import { unzipSync } from "fflate";
|
|
6713
|
+
function entryTarget(entry, artifactName) {
|
|
6714
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
6715
|
+
if (normalized.endsWith("/")) return null;
|
|
6716
|
+
const prefix = `${artifactName}/`;
|
|
6717
|
+
const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
|
|
6718
|
+
const segments = relative.split("/");
|
|
6719
|
+
if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
|
|
6720
|
+
throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
|
|
6721
|
+
}
|
|
6722
|
+
return relative;
|
|
6723
|
+
}
|
|
6724
|
+
function assertNoSymlinkEscape(root, relative) {
|
|
6725
|
+
let current = root;
|
|
6726
|
+
for (const segment of relative.split("/")) {
|
|
6727
|
+
current = path2.join(current, segment);
|
|
6728
|
+
let isLink;
|
|
6729
|
+
try {
|
|
6730
|
+
isLink = lstatSync(current).isSymbolicLink();
|
|
6731
|
+
} catch {
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
6734
|
+
if (isLink) {
|
|
6735
|
+
throw new Error(`Refusing to extract through symbolic link "${current}".`);
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
function extractArtifactZip(zip, destination, options) {
|
|
6740
|
+
const entries = unzipSync(zip);
|
|
6741
|
+
const targets = [];
|
|
6742
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6743
|
+
for (const [entry, data] of Object.entries(entries)) {
|
|
6744
|
+
const relative = entryTarget(entry, options.artifactName);
|
|
6745
|
+
if (relative === null) continue;
|
|
6746
|
+
const key = path2.posix.normalize(relative);
|
|
6747
|
+
if (seen.has(key)) {
|
|
6748
|
+
throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
|
|
6749
|
+
}
|
|
6750
|
+
seen.add(key);
|
|
6751
|
+
targets.push({ relative: key, data });
|
|
6752
|
+
}
|
|
6753
|
+
if (!options.force) {
|
|
6754
|
+
const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
|
|
6755
|
+
if (existing) {
|
|
6756
|
+
throw new Error(
|
|
6757
|
+
`"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
|
|
6758
|
+
);
|
|
6759
|
+
}
|
|
6760
|
+
}
|
|
6761
|
+
mkdirSync(destination, { recursive: true });
|
|
6762
|
+
const root = realpathSync(destination);
|
|
6763
|
+
for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
|
|
6764
|
+
for (const { relative, data } of targets) {
|
|
6765
|
+
const file = path2.join(root, relative);
|
|
6766
|
+
mkdirSync(path2.dirname(file), { recursive: true });
|
|
6767
|
+
writeFileSync(file, data);
|
|
6768
|
+
}
|
|
6769
|
+
return targets.map((t) => t.relative);
|
|
6770
|
+
}
|
|
6643
6771
|
|
|
6644
6772
|
// src/commands/pipeline.ts
|
|
6645
6773
|
var EXIT_FAILED = 1;
|
|
@@ -6976,19 +7104,26 @@ function formatRunDetail(detail) {
|
|
|
6976
7104
|
...testRows(detail)
|
|
6977
7105
|
].join("\n");
|
|
6978
7106
|
}
|
|
7107
|
+
async function withRunContext(runIdRaw, options, body) {
|
|
7108
|
+
validateOrgProjectPair(options);
|
|
7109
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7110
|
+
if (runId === null) {
|
|
7111
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7112
|
+
return;
|
|
7113
|
+
}
|
|
7114
|
+
let context;
|
|
7115
|
+
try {
|
|
7116
|
+
const resolved = await resolvePipelineContext(options);
|
|
7117
|
+
context = resolved.context;
|
|
7118
|
+
await body(runId, resolved);
|
|
7119
|
+
} catch (err) {
|
|
7120
|
+
handlePipelineError(err, context);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
6979
7123
|
function createPipelineGetRunDetailCommand() {
|
|
6980
7124
|
const command = new Command14("get-run-detail");
|
|
6981
|
-
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(
|
|
6982
|
-
|
|
6983
|
-
const runId = parsePositiveId(runIdRaw);
|
|
6984
|
-
if (runId === null) {
|
|
6985
|
-
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
6986
|
-
return;
|
|
6987
|
-
}
|
|
6988
|
-
let context;
|
|
6989
|
-
try {
|
|
6990
|
-
const resolved = await resolvePipelineContext(options);
|
|
6991
|
-
context = resolved.context;
|
|
7125
|
+
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(
|
|
7126
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
6992
7127
|
const detail = await getRunDetail(resolved.context, resolved.cred, runId);
|
|
6993
7128
|
if (options.json) {
|
|
6994
7129
|
process.stdout.write(`${JSON.stringify(detail, null, 2)}
|
|
@@ -6997,10 +7132,8 @@ function createPipelineGetRunDetailCommand() {
|
|
|
6997
7132
|
}
|
|
6998
7133
|
process.stdout.write(`${formatRunDetail(detail)}
|
|
6999
7134
|
`);
|
|
7000
|
-
}
|
|
7001
|
-
|
|
7002
|
-
}
|
|
7003
|
-
});
|
|
7135
|
+
})
|
|
7136
|
+
);
|
|
7004
7137
|
return command;
|
|
7005
7138
|
}
|
|
7006
7139
|
function grepWithContext(lines, grep, context) {
|
|
@@ -7023,17 +7156,37 @@ function grepWithContext(lines, grep, context) {
|
|
|
7023
7156
|
}
|
|
7024
7157
|
return selected;
|
|
7025
7158
|
}
|
|
7026
|
-
|
|
7159
|
+
var PROGRESS_BAR = /(?<![\d.])\d+(?:\.\d+)? [KMGT]?i?B \/ \d+(?:\.\d+)? [KMGT]?i?B \[/g;
|
|
7160
|
+
var LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z /;
|
|
7161
|
+
function collapseGluedBars(line) {
|
|
7162
|
+
const starts = [...line.matchAll(PROGRESS_BAR)].map((m) => m.index ?? 0);
|
|
7163
|
+
if (starts.length < 2) return line;
|
|
7164
|
+
const prefix = LOG_TIMESTAMP.exec(line)?.[0] ?? "";
|
|
7165
|
+
return prefix + line.slice(starts.at(-1));
|
|
7166
|
+
}
|
|
7167
|
+
function collapseProgress(line) {
|
|
7168
|
+
let end = line.length;
|
|
7169
|
+
while (end > 0 && line[end - 1] === "\r") end--;
|
|
7170
|
+
return collapseGluedBars(line.slice(line.lastIndexOf("\r", end - 1) + 1, end));
|
|
7171
|
+
}
|
|
7172
|
+
function filterLogLines(content, filters) {
|
|
7173
|
+
const { grep, tail, head, contextLines: context } = filters;
|
|
7027
7174
|
let lines = content.split("\n");
|
|
7028
7175
|
if (lines.at(-1) === "") {
|
|
7029
7176
|
lines.pop();
|
|
7030
7177
|
}
|
|
7178
|
+
if (filters.noProgress) {
|
|
7179
|
+
lines = lines.map(collapseProgress);
|
|
7180
|
+
}
|
|
7031
7181
|
if (grep) {
|
|
7032
7182
|
lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
|
|
7033
7183
|
}
|
|
7034
7184
|
if (tail !== void 0 && lines.length > tail) {
|
|
7035
7185
|
lines = lines.slice(-tail);
|
|
7036
7186
|
}
|
|
7187
|
+
if (head !== void 0 && lines.length > head) {
|
|
7188
|
+
lines = lines.slice(0, head);
|
|
7189
|
+
}
|
|
7037
7190
|
return lines;
|
|
7038
7191
|
}
|
|
7039
7192
|
function parseLogFilters(options) {
|
|
@@ -7042,9 +7195,14 @@ function parseLogFilters(options) {
|
|
|
7042
7195
|
return null;
|
|
7043
7196
|
}
|
|
7044
7197
|
const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
|
|
7045
|
-
const
|
|
7198
|
+
const noProgress = options.progress === false;
|
|
7199
|
+
const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
|
|
7046
7200
|
if (slices && !selectsSingleLog) {
|
|
7047
|
-
writeError2("--tail, --grep, and --
|
|
7201
|
+
writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
|
|
7202
|
+
return null;
|
|
7203
|
+
}
|
|
7204
|
+
if (options.head !== void 0 && options.tail !== void 0) {
|
|
7205
|
+
writeError2("Use either --head or --tail, not both.");
|
|
7048
7206
|
return null;
|
|
7049
7207
|
}
|
|
7050
7208
|
if (options.context !== void 0 && options.grep === void 0) {
|
|
@@ -7053,6 +7211,8 @@ function parseLogFilters(options) {
|
|
|
7053
7211
|
}
|
|
7054
7212
|
const tail = parseOptionalCount(options.tail, "--tail");
|
|
7055
7213
|
if (tail === null) return null;
|
|
7214
|
+
const head = parseOptionalCount(options.head, "--head");
|
|
7215
|
+
if (head === null) return null;
|
|
7056
7216
|
const contextLines = parseOptionalCount(options.context, "--context");
|
|
7057
7217
|
if (contextLines === null) return null;
|
|
7058
7218
|
let grep;
|
|
@@ -7064,7 +7224,13 @@ function parseLogFilters(options) {
|
|
|
7064
7224
|
return null;
|
|
7065
7225
|
}
|
|
7066
7226
|
}
|
|
7067
|
-
return { tail, contextLines: contextLines ?? 0, grep };
|
|
7227
|
+
return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
|
|
7228
|
+
}
|
|
7229
|
+
function describeLog(log) {
|
|
7230
|
+
const kind = log.type ?? "";
|
|
7231
|
+
const where = log.parent ? ` in ${log.parent}` : "";
|
|
7232
|
+
const detail = `${kind}${where}`.trim();
|
|
7233
|
+
return detail ? `${log.step} [${detail}]` : String(log.step);
|
|
7068
7234
|
}
|
|
7069
7235
|
function chooseStepLog(logs, step, runId) {
|
|
7070
7236
|
const needle = step.toLowerCase();
|
|
@@ -7076,7 +7242,7 @@ function chooseStepLog(logs, step, runId) {
|
|
|
7076
7242
|
return null;
|
|
7077
7243
|
}
|
|
7078
7244
|
if (chosen.length > 1) {
|
|
7079
|
-
const candidates = chosen.map((l) => `${l.id} (${l
|
|
7245
|
+
const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
|
|
7080
7246
|
writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
|
|
7081
7247
|
return null;
|
|
7082
7248
|
}
|
|
@@ -7098,8 +7264,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
|
|
|
7098
7264
|
return chooseStepLog(allLogs, options.step, runId);
|
|
7099
7265
|
}
|
|
7100
7266
|
function printSingleLog(content, filters) {
|
|
7101
|
-
if (filters.grep !== void 0 || filters.tail !== void 0) {
|
|
7102
|
-
const lines = filterLogLines(content, filters
|
|
7267
|
+
if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
|
|
7268
|
+
const lines = filterLogLines(content, filters);
|
|
7103
7269
|
if (lines.length > 0) {
|
|
7104
7270
|
process.stdout.write(`${lines.join("\n")}
|
|
7105
7271
|
`);
|
|
@@ -7111,7 +7277,7 @@ function printSingleLog(content, filters) {
|
|
|
7111
7277
|
}
|
|
7112
7278
|
function createPipelineLogsCommand() {
|
|
7113
7279
|
const command = new Command14("logs");
|
|
7114
|
-
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) => {
|
|
7280
|
+
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("--head <n>", "with --log-id/--step, print only the first 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("--no-progress", "with --log-id/--step, collapse progress-bar redraws (carriage-return or glued into one line) to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
7115
7281
|
validateOrgProjectPair(options);
|
|
7116
7282
|
const runId = parsePositiveId(runIdRaw);
|
|
7117
7283
|
if (runId === null) {
|
|
@@ -7150,7 +7316,9 @@ function createPipelineLogsCommand() {
|
|
|
7150
7316
|
String(l.id),
|
|
7151
7317
|
l.createdOn ?? "\u2014",
|
|
7152
7318
|
l.lineCount == null ? "" : `${l.lineCount} lines`,
|
|
7153
|
-
l.
|
|
7319
|
+
l.type ?? "",
|
|
7320
|
+
l.step ?? "",
|
|
7321
|
+
l.parent ? `(in ${l.parent})` : ""
|
|
7154
7322
|
]);
|
|
7155
7323
|
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
7156
7324
|
`);
|
|
@@ -7160,6 +7328,102 @@ function createPipelineLogsCommand() {
|
|
|
7160
7328
|
});
|
|
7161
7329
|
return command;
|
|
7162
7330
|
}
|
|
7331
|
+
function formatSize(bytes) {
|
|
7332
|
+
if (bytes === null) return "";
|
|
7333
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7334
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
7335
|
+
let value = bytes / 1024;
|
|
7336
|
+
let unit = 0;
|
|
7337
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7338
|
+
value /= 1024;
|
|
7339
|
+
unit++;
|
|
7340
|
+
}
|
|
7341
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
7342
|
+
}
|
|
7343
|
+
function createPipelineArtifactsCommand() {
|
|
7344
|
+
const command = new Command14("artifacts");
|
|
7345
|
+
command.description("List the build artifacts published by a pipeline run").argument("<run_id>", "pipeline run id").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(
|
|
7346
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
7347
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7348
|
+
if (options.json) {
|
|
7349
|
+
process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
|
|
7350
|
+
`);
|
|
7351
|
+
return;
|
|
7352
|
+
}
|
|
7353
|
+
if (artifacts.length === 0) {
|
|
7354
|
+
process.stdout.write(`No artifacts found for run ${runId}.
|
|
7355
|
+
`);
|
|
7356
|
+
return;
|
|
7357
|
+
}
|
|
7358
|
+
const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
|
|
7359
|
+
process.stdout.write(`${formatTable(rows)}
|
|
7360
|
+
`);
|
|
7361
|
+
})
|
|
7362
|
+
);
|
|
7363
|
+
return command;
|
|
7364
|
+
}
|
|
7365
|
+
function selectArtifacts(artifacts, name, all, runId) {
|
|
7366
|
+
const available = artifacts.map((a) => a.name).join(", ") || "(none)";
|
|
7367
|
+
if (all) return artifacts;
|
|
7368
|
+
if (name === void 0) {
|
|
7369
|
+
writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
|
|
7370
|
+
return null;
|
|
7371
|
+
}
|
|
7372
|
+
const match = artifacts.find((a) => a.name === name);
|
|
7373
|
+
if (!match) {
|
|
7374
|
+
writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
|
|
7375
|
+
return null;
|
|
7376
|
+
}
|
|
7377
|
+
return [match];
|
|
7378
|
+
}
|
|
7379
|
+
function createPipelineArtifactDownloadCommand() {
|
|
7380
|
+
const command = new Command14("artifact-download");
|
|
7381
|
+
command.description("Download a run's artifact (or --all) and extract it into a folder; no zip is left behind").argument("<run_id>", "pipeline run id").argument("[name]", "artifact name (see `pipeline artifacts`)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--path <dir>", "destination folder (default ./<name>; with --all, <dir>/<name> per artifact)").option("--all", "download every artifact of the run").option("--force", "overwrite files that already exist").option("--progress", "print byte-progress lines on stderr").option("--json", "output JSON").action(async (runIdRaw, name, options) => {
|
|
7382
|
+
validateOrgProjectPair(options);
|
|
7383
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7384
|
+
if (runId === null) {
|
|
7385
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (options.all && name !== void 0) {
|
|
7389
|
+
writeError2("Use either an artifact name or --all, not both.");
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7392
|
+
let context;
|
|
7393
|
+
try {
|
|
7394
|
+
const resolved = await resolvePipelineContext(options);
|
|
7395
|
+
context = resolved.context;
|
|
7396
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7397
|
+
const selected = selectArtifacts(artifacts, name, options.all === true, runId);
|
|
7398
|
+
if (selected === null) return;
|
|
7399
|
+
const results = await Promise.all(
|
|
7400
|
+
selected.map(async (artifact) => {
|
|
7401
|
+
const destination = path3.resolve(
|
|
7402
|
+
options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
|
|
7403
|
+
);
|
|
7404
|
+
const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
|
|
7405
|
+
`) : void 0;
|
|
7406
|
+
const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
|
|
7407
|
+
const files = extractArtifactZip(zip, destination, {
|
|
7408
|
+
artifactName: artifact.name,
|
|
7409
|
+
force: options.force === true
|
|
7410
|
+
});
|
|
7411
|
+
return { name: artifact.name, path: destination, files: files.length };
|
|
7412
|
+
})
|
|
7413
|
+
);
|
|
7414
|
+
if (options.json) {
|
|
7415
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
7416
|
+
`);
|
|
7417
|
+
return;
|
|
7418
|
+
}
|
|
7419
|
+
process.stdout.write(`${results.map((r) => r.path).join("\n")}
|
|
7420
|
+
`);
|
|
7421
|
+
} catch (err) {
|
|
7422
|
+
handlePipelineError(err, context);
|
|
7423
|
+
}
|
|
7424
|
+
});
|
|
7425
|
+
return command;
|
|
7426
|
+
}
|
|
7163
7427
|
function createPipelineTestsCommand() {
|
|
7164
7428
|
const command = new Command14("tests");
|
|
7165
7429
|
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) => {
|
|
@@ -7261,6 +7525,8 @@ function createPipelineCommand() {
|
|
|
7261
7525
|
command.addCommand(createPipelineWaitCommand());
|
|
7262
7526
|
command.addCommand(createPipelineGetRunDetailCommand());
|
|
7263
7527
|
command.addCommand(createPipelineLogsCommand());
|
|
7528
|
+
command.addCommand(createPipelineArtifactsCommand());
|
|
7529
|
+
command.addCommand(createPipelineArtifactDownloadCommand());
|
|
7264
7530
|
command.addCommand(createPipelineTestsCommand());
|
|
7265
7531
|
command.addCommand(createPipelineStartCommand());
|
|
7266
7532
|
return command;
|
|
@@ -7352,7 +7618,7 @@ function createCommentsCommand() {
|
|
|
7352
7618
|
// src/commands/download-attachment.ts
|
|
7353
7619
|
import { Command as Command16 } from "commander";
|
|
7354
7620
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
7355
|
-
import { existsSync as
|
|
7621
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7356
7622
|
import { join as join3 } from "path";
|
|
7357
7623
|
function createDownloadAttachmentCommand() {
|
|
7358
7624
|
const command = new Command16("download-attachment");
|
|
@@ -7365,7 +7631,7 @@ function createDownloadAttachmentCommand() {
|
|
|
7365
7631
|
context = resolveContext(options);
|
|
7366
7632
|
const credential = await requireAuthCredential(context.org);
|
|
7367
7633
|
const outputDir = options.output ?? ".";
|
|
7368
|
-
if (!
|
|
7634
|
+
if (!existsSync7(outputDir)) {
|
|
7369
7635
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
7370
7636
|
`);
|
|
7371
7637
|
process.exit(1);
|
|
@@ -7398,7 +7664,7 @@ function createDownloadAttachmentCommand() {
|
|
|
7398
7664
|
|
|
7399
7665
|
// src/commands/add-attachment.ts
|
|
7400
7666
|
import { Command as Command17 } from "commander";
|
|
7401
|
-
import { existsSync as
|
|
7667
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
7402
7668
|
import { readFile } from "fs/promises";
|
|
7403
7669
|
import { basename } from "path";
|
|
7404
7670
|
function createAddAttachmentCommand() {
|
|
@@ -7407,7 +7673,7 @@ function createAddAttachmentCommand() {
|
|
|
7407
7673
|
async (idStr, file, options) => {
|
|
7408
7674
|
const id = parseWorkItemId(idStr);
|
|
7409
7675
|
validateOrgProjectPair(options);
|
|
7410
|
-
if (!
|
|
7676
|
+
if (!existsSync8(file)) {
|
|
7411
7677
|
process.stderr.write(`Error: File not found: ${file}
|
|
7412
7678
|
`);
|
|
7413
7679
|
process.exit(1);
|
|
@@ -7827,7 +8093,7 @@ function createRelationsCommand() {
|
|
|
7827
8093
|
|
|
7828
8094
|
// src/services/update-check.ts
|
|
7829
8095
|
import fs2 from "fs";
|
|
7830
|
-
import
|
|
8096
|
+
import path4 from "path";
|
|
7831
8097
|
import os from "os";
|
|
7832
8098
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
7833
8099
|
var FETCH_TIMEOUT_MS = 1500;
|
|
@@ -7845,7 +8111,7 @@ function skipsUpdateCheck(commandPath) {
|
|
|
7845
8111
|
return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
|
|
7846
8112
|
}
|
|
7847
8113
|
function getCachePath() {
|
|
7848
|
-
return
|
|
8114
|
+
return path4.join(os.homedir(), ".azdo", "update-check.json");
|
|
7849
8115
|
}
|
|
7850
8116
|
function defaultReadCache() {
|
|
7851
8117
|
try {
|
|
@@ -7857,7 +8123,7 @@ function defaultReadCache() {
|
|
|
7857
8123
|
function defaultWriteCache(data) {
|
|
7858
8124
|
try {
|
|
7859
8125
|
const cachePath = getCachePath();
|
|
7860
|
-
fs2.mkdirSync(
|
|
8126
|
+
fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
|
|
7861
8127
|
fs2.writeFileSync(cachePath, data);
|
|
7862
8128
|
} catch {
|
|
7863
8129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.22.0-develop.
|
|
3
|
+
"version": "0.22.0-develop.822",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@napi-rs/keyring": "^1.3.0",
|
|
29
29
|
"@primno/dpapi": "^2.0.1",
|
|
30
30
|
"commander": "^14.0.3",
|
|
31
|
+
"fflate": "^0.8.3",
|
|
31
32
|
"jimp": "^1.6.1",
|
|
32
33
|
"node-html-markdown": "^2.0.0"
|
|
33
34
|
},
|