azdo-cli 0.22.0-develop.798 → 0.22.0-develop.807
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 +289 -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,108 @@ 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
|
+
async function downloadArtifactZip(cred, artifact, onProgress) {
|
|
6673
|
+
if (!artifact.downloadUrl) {
|
|
6674
|
+
throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
|
|
6675
|
+
}
|
|
6676
|
+
const url = new URL(artifact.downloadUrl);
|
|
6677
|
+
url.searchParams.set("$format", "zip");
|
|
6678
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
6679
|
+
if (!response.ok) {
|
|
6680
|
+
throw httpError(response);
|
|
6681
|
+
}
|
|
6682
|
+
if (!response.body) {
|
|
6683
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
6684
|
+
}
|
|
6685
|
+
const chunks = [];
|
|
6686
|
+
let received = 0;
|
|
6687
|
+
for await (const chunk of response.body) {
|
|
6688
|
+
chunks.push(chunk);
|
|
6689
|
+
received += chunk.length;
|
|
6690
|
+
onProgress?.(received);
|
|
6691
|
+
}
|
|
6692
|
+
return Buffer.concat(chunks);
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
// src/services/artifact-extract.ts
|
|
6696
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync, realpathSync, writeFileSync } from "fs";
|
|
6697
|
+
import path2 from "path";
|
|
6698
|
+
import { unzipSync } from "fflate";
|
|
6699
|
+
function entryTarget(entry, artifactName) {
|
|
6700
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
6701
|
+
if (normalized.endsWith("/")) return null;
|
|
6702
|
+
const prefix = `${artifactName}/`;
|
|
6703
|
+
const relative = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
|
|
6704
|
+
const segments = relative.split("/");
|
|
6705
|
+
if (relative.startsWith("/") || /^[A-Za-z]:/.test(relative) || segments.includes("..")) {
|
|
6706
|
+
throw new Error(`Refusing to extract "${entry}": path escapes the destination folder.`);
|
|
6707
|
+
}
|
|
6708
|
+
return relative;
|
|
6709
|
+
}
|
|
6710
|
+
function assertNoSymlinkEscape(root, relative) {
|
|
6711
|
+
let current = root;
|
|
6712
|
+
for (const segment of relative.split("/")) {
|
|
6713
|
+
current = path2.join(current, segment);
|
|
6714
|
+
let isLink;
|
|
6715
|
+
try {
|
|
6716
|
+
isLink = lstatSync(current).isSymbolicLink();
|
|
6717
|
+
} catch {
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
if (isLink) {
|
|
6721
|
+
throw new Error(`Refusing to extract through symbolic link "${current}".`);
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
function extractArtifactZip(zip, destination, options) {
|
|
6726
|
+
const entries = unzipSync(zip);
|
|
6727
|
+
const targets = [];
|
|
6728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6729
|
+
for (const [entry, data] of Object.entries(entries)) {
|
|
6730
|
+
const relative = entryTarget(entry, options.artifactName);
|
|
6731
|
+
if (relative === null) continue;
|
|
6732
|
+
const key = path2.posix.normalize(relative);
|
|
6733
|
+
if (seen.has(key)) {
|
|
6734
|
+
throw new Error(`Refusing to extract: several archive entries resolve to "${key}".`);
|
|
6735
|
+
}
|
|
6736
|
+
seen.add(key);
|
|
6737
|
+
targets.push({ relative: key, data });
|
|
6738
|
+
}
|
|
6739
|
+
if (!options.force) {
|
|
6740
|
+
const existing = targets.find((t) => existsSync6(path2.join(destination, t.relative)));
|
|
6741
|
+
if (existing) {
|
|
6742
|
+
throw new Error(
|
|
6743
|
+
`"${path2.join(destination, existing.relative)}" already exists. Use --force to overwrite.`
|
|
6744
|
+
);
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
mkdirSync(destination, { recursive: true });
|
|
6748
|
+
const root = realpathSync(destination);
|
|
6749
|
+
for (const { relative } of targets) assertNoSymlinkEscape(root, relative);
|
|
6750
|
+
for (const { relative, data } of targets) {
|
|
6751
|
+
const file = path2.join(root, relative);
|
|
6752
|
+
mkdirSync(path2.dirname(file), { recursive: true });
|
|
6753
|
+
writeFileSync(file, data);
|
|
6754
|
+
}
|
|
6755
|
+
return targets.map((t) => t.relative);
|
|
6756
|
+
}
|
|
6643
6757
|
|
|
6644
6758
|
// src/commands/pipeline.ts
|
|
6645
6759
|
var EXIT_FAILED = 1;
|
|
@@ -6976,19 +7090,26 @@ function formatRunDetail(detail) {
|
|
|
6976
7090
|
...testRows(detail)
|
|
6977
7091
|
].join("\n");
|
|
6978
7092
|
}
|
|
7093
|
+
async function withRunContext(runIdRaw, options, body) {
|
|
7094
|
+
validateOrgProjectPair(options);
|
|
7095
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7096
|
+
if (runId === null) {
|
|
7097
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7098
|
+
return;
|
|
7099
|
+
}
|
|
7100
|
+
let context;
|
|
7101
|
+
try {
|
|
7102
|
+
const resolved = await resolvePipelineContext(options);
|
|
7103
|
+
context = resolved.context;
|
|
7104
|
+
await body(runId, resolved);
|
|
7105
|
+
} catch (err) {
|
|
7106
|
+
handlePipelineError(err, context);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
6979
7109
|
function createPipelineGetRunDetailCommand() {
|
|
6980
7110
|
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;
|
|
7111
|
+
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(
|
|
7112
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
6992
7113
|
const detail = await getRunDetail(resolved.context, resolved.cred, runId);
|
|
6993
7114
|
if (options.json) {
|
|
6994
7115
|
process.stdout.write(`${JSON.stringify(detail, null, 2)}
|
|
@@ -6997,10 +7118,8 @@ function createPipelineGetRunDetailCommand() {
|
|
|
6997
7118
|
}
|
|
6998
7119
|
process.stdout.write(`${formatRunDetail(detail)}
|
|
6999
7120
|
`);
|
|
7000
|
-
}
|
|
7001
|
-
|
|
7002
|
-
}
|
|
7003
|
-
});
|
|
7121
|
+
})
|
|
7122
|
+
);
|
|
7004
7123
|
return command;
|
|
7005
7124
|
}
|
|
7006
7125
|
function grepWithContext(lines, grep, context) {
|
|
@@ -7023,17 +7142,29 @@ function grepWithContext(lines, grep, context) {
|
|
|
7023
7142
|
}
|
|
7024
7143
|
return selected;
|
|
7025
7144
|
}
|
|
7026
|
-
function
|
|
7145
|
+
function collapseProgress(line) {
|
|
7146
|
+
let end = line.length;
|
|
7147
|
+
while (end > 0 && line[end - 1] === "\r") end--;
|
|
7148
|
+
return line.slice(line.lastIndexOf("\r", end - 1) + 1, end);
|
|
7149
|
+
}
|
|
7150
|
+
function filterLogLines(content, filters) {
|
|
7151
|
+
const { grep, tail, head, contextLines: context } = filters;
|
|
7027
7152
|
let lines = content.split("\n");
|
|
7028
7153
|
if (lines.at(-1) === "") {
|
|
7029
7154
|
lines.pop();
|
|
7030
7155
|
}
|
|
7156
|
+
if (filters.noProgress) {
|
|
7157
|
+
lines = lines.map(collapseProgress);
|
|
7158
|
+
}
|
|
7031
7159
|
if (grep) {
|
|
7032
7160
|
lines = context > 0 ? grepWithContext(lines, grep, context) : lines.filter((line) => grep.test(line));
|
|
7033
7161
|
}
|
|
7034
7162
|
if (tail !== void 0 && lines.length > tail) {
|
|
7035
7163
|
lines = lines.slice(-tail);
|
|
7036
7164
|
}
|
|
7165
|
+
if (head !== void 0 && lines.length > head) {
|
|
7166
|
+
lines = lines.slice(0, head);
|
|
7167
|
+
}
|
|
7037
7168
|
return lines;
|
|
7038
7169
|
}
|
|
7039
7170
|
function parseLogFilters(options) {
|
|
@@ -7042,9 +7173,14 @@ function parseLogFilters(options) {
|
|
|
7042
7173
|
return null;
|
|
7043
7174
|
}
|
|
7044
7175
|
const selectsSingleLog = options.logId !== void 0 || options.step !== void 0;
|
|
7045
|
-
const
|
|
7176
|
+
const noProgress = options.progress === false;
|
|
7177
|
+
const slices = options.tail !== void 0 || options.head !== void 0 || options.grep !== void 0 || options.context !== void 0 || noProgress;
|
|
7046
7178
|
if (slices && !selectsSingleLog) {
|
|
7047
|
-
writeError2("--tail, --grep, and --
|
|
7179
|
+
writeError2("--tail, --head, --grep, --context, and --no-progress require --log-id or --step.");
|
|
7180
|
+
return null;
|
|
7181
|
+
}
|
|
7182
|
+
if (options.head !== void 0 && options.tail !== void 0) {
|
|
7183
|
+
writeError2("Use either --head or --tail, not both.");
|
|
7048
7184
|
return null;
|
|
7049
7185
|
}
|
|
7050
7186
|
if (options.context !== void 0 && options.grep === void 0) {
|
|
@@ -7053,6 +7189,8 @@ function parseLogFilters(options) {
|
|
|
7053
7189
|
}
|
|
7054
7190
|
const tail = parseOptionalCount(options.tail, "--tail");
|
|
7055
7191
|
if (tail === null) return null;
|
|
7192
|
+
const head = parseOptionalCount(options.head, "--head");
|
|
7193
|
+
if (head === null) return null;
|
|
7056
7194
|
const contextLines = parseOptionalCount(options.context, "--context");
|
|
7057
7195
|
if (contextLines === null) return null;
|
|
7058
7196
|
let grep;
|
|
@@ -7064,7 +7202,13 @@ function parseLogFilters(options) {
|
|
|
7064
7202
|
return null;
|
|
7065
7203
|
}
|
|
7066
7204
|
}
|
|
7067
|
-
return { tail, contextLines: contextLines ?? 0, grep };
|
|
7205
|
+
return { tail, head, noProgress, contextLines: contextLines ?? 0, grep };
|
|
7206
|
+
}
|
|
7207
|
+
function describeLog(log) {
|
|
7208
|
+
const kind = log.type ?? "";
|
|
7209
|
+
const where = log.parent ? ` in ${log.parent}` : "";
|
|
7210
|
+
const detail = `${kind}${where}`.trim();
|
|
7211
|
+
return detail ? `${log.step} [${detail}]` : String(log.step);
|
|
7068
7212
|
}
|
|
7069
7213
|
function chooseStepLog(logs, step, runId) {
|
|
7070
7214
|
const needle = step.toLowerCase();
|
|
@@ -7076,7 +7220,7 @@ function chooseStepLog(logs, step, runId) {
|
|
|
7076
7220
|
return null;
|
|
7077
7221
|
}
|
|
7078
7222
|
if (chosen.length > 1) {
|
|
7079
|
-
const candidates = chosen.map((l) => `${l.id} (${l
|
|
7223
|
+
const candidates = chosen.map((l) => `${l.id} (${describeLog(l)})`).join(", ");
|
|
7080
7224
|
writeError2(`Step "${step}" matches multiple logs: ${candidates}. Be more specific or use --log-id.`);
|
|
7081
7225
|
return null;
|
|
7082
7226
|
}
|
|
@@ -7098,8 +7242,8 @@ async function resolveRequestedLogId(resolved, runId, options) {
|
|
|
7098
7242
|
return chooseStepLog(allLogs, options.step, runId);
|
|
7099
7243
|
}
|
|
7100
7244
|
function printSingleLog(content, filters) {
|
|
7101
|
-
if (filters.grep !== void 0 || filters.tail !== void 0) {
|
|
7102
|
-
const lines = filterLogLines(content, filters
|
|
7245
|
+
if (filters.grep !== void 0 || filters.tail !== void 0 || filters.head !== void 0 || filters.noProgress) {
|
|
7246
|
+
const lines = filterLogLines(content, filters);
|
|
7103
7247
|
if (lines.length > 0) {
|
|
7104
7248
|
process.stdout.write(`${lines.join("\n")}
|
|
7105
7249
|
`);
|
|
@@ -7111,7 +7255,7 @@ function printSingleLog(content, filters) {
|
|
|
7111
7255
|
}
|
|
7112
7256
|
function createPipelineLogsCommand() {
|
|
7113
7257
|
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) => {
|
|
7258
|
+
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 carriage-return progress redraws to their final state").option("--json", "output JSON").action(async (runIdRaw, options) => {
|
|
7115
7259
|
validateOrgProjectPair(options);
|
|
7116
7260
|
const runId = parsePositiveId(runIdRaw);
|
|
7117
7261
|
if (runId === null) {
|
|
@@ -7150,7 +7294,9 @@ function createPipelineLogsCommand() {
|
|
|
7150
7294
|
String(l.id),
|
|
7151
7295
|
l.createdOn ?? "\u2014",
|
|
7152
7296
|
l.lineCount == null ? "" : `${l.lineCount} lines`,
|
|
7153
|
-
l.
|
|
7297
|
+
l.type ?? "",
|
|
7298
|
+
l.step ?? "",
|
|
7299
|
+
l.parent ? `(in ${l.parent})` : ""
|
|
7154
7300
|
]);
|
|
7155
7301
|
process.stdout.write(`${formatTable(rows, /* @__PURE__ */ new Set([0]))}
|
|
7156
7302
|
`);
|
|
@@ -7160,6 +7306,102 @@ function createPipelineLogsCommand() {
|
|
|
7160
7306
|
});
|
|
7161
7307
|
return command;
|
|
7162
7308
|
}
|
|
7309
|
+
function formatSize(bytes) {
|
|
7310
|
+
if (bytes === null) return "";
|
|
7311
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7312
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
7313
|
+
let value = bytes / 1024;
|
|
7314
|
+
let unit = 0;
|
|
7315
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7316
|
+
value /= 1024;
|
|
7317
|
+
unit++;
|
|
7318
|
+
}
|
|
7319
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
7320
|
+
}
|
|
7321
|
+
function createPipelineArtifactsCommand() {
|
|
7322
|
+
const command = new Command14("artifacts");
|
|
7323
|
+
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(
|
|
7324
|
+
(runIdRaw, options) => withRunContext(runIdRaw, options, async (runId, resolved) => {
|
|
7325
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7326
|
+
if (options.json) {
|
|
7327
|
+
process.stdout.write(`${JSON.stringify(artifacts, null, 2)}
|
|
7328
|
+
`);
|
|
7329
|
+
return;
|
|
7330
|
+
}
|
|
7331
|
+
if (artifacts.length === 0) {
|
|
7332
|
+
process.stdout.write(`No artifacts found for run ${runId}.
|
|
7333
|
+
`);
|
|
7334
|
+
return;
|
|
7335
|
+
}
|
|
7336
|
+
const rows = artifacts.map((a) => [a.name, a.type ?? "", formatSize(a.sizeBytes)]);
|
|
7337
|
+
process.stdout.write(`${formatTable(rows)}
|
|
7338
|
+
`);
|
|
7339
|
+
})
|
|
7340
|
+
);
|
|
7341
|
+
return command;
|
|
7342
|
+
}
|
|
7343
|
+
function selectArtifacts(artifacts, name, all, runId) {
|
|
7344
|
+
const available = artifacts.map((a) => a.name).join(", ") || "(none)";
|
|
7345
|
+
if (all) return artifacts;
|
|
7346
|
+
if (name === void 0) {
|
|
7347
|
+
writeError2(`Give an artifact name or --all. Available artifacts in run ${runId}: ${available}.`);
|
|
7348
|
+
return null;
|
|
7349
|
+
}
|
|
7350
|
+
const match = artifacts.find((a) => a.name === name);
|
|
7351
|
+
if (!match) {
|
|
7352
|
+
writeError2(`Run ${runId} has no artifact "${name}". Available artifacts: ${available}.`);
|
|
7353
|
+
return null;
|
|
7354
|
+
}
|
|
7355
|
+
return [match];
|
|
7356
|
+
}
|
|
7357
|
+
function createPipelineArtifactDownloadCommand() {
|
|
7358
|
+
const command = new Command14("artifact-download");
|
|
7359
|
+
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) => {
|
|
7360
|
+
validateOrgProjectPair(options);
|
|
7361
|
+
const runId = parsePositiveId(runIdRaw);
|
|
7362
|
+
if (runId === null) {
|
|
7363
|
+
writeError2(`Invalid run id "${runIdRaw}"; expected a positive integer.`);
|
|
7364
|
+
return;
|
|
7365
|
+
}
|
|
7366
|
+
if (options.all && name !== void 0) {
|
|
7367
|
+
writeError2("Use either an artifact name or --all, not both.");
|
|
7368
|
+
return;
|
|
7369
|
+
}
|
|
7370
|
+
let context;
|
|
7371
|
+
try {
|
|
7372
|
+
const resolved = await resolvePipelineContext(options);
|
|
7373
|
+
context = resolved.context;
|
|
7374
|
+
const artifacts = await listBuildArtifacts(resolved.context, resolved.cred, runId);
|
|
7375
|
+
const selected = selectArtifacts(artifacts, name, options.all === true, runId);
|
|
7376
|
+
if (selected === null) return;
|
|
7377
|
+
const results = await Promise.all(
|
|
7378
|
+
selected.map(async (artifact) => {
|
|
7379
|
+
const destination = path3.resolve(
|
|
7380
|
+
options.all ? path3.join(options.path ?? ".", artifact.name) : options.path ?? artifact.name
|
|
7381
|
+
);
|
|
7382
|
+
const onProgress = options.progress ? (bytes) => process.stderr.write(`${artifact.name}: ${bytes} bytes
|
|
7383
|
+
`) : void 0;
|
|
7384
|
+
const zip = await downloadArtifactZip(resolved.cred, artifact, onProgress);
|
|
7385
|
+
const files = extractArtifactZip(zip, destination, {
|
|
7386
|
+
artifactName: artifact.name,
|
|
7387
|
+
force: options.force === true
|
|
7388
|
+
});
|
|
7389
|
+
return { name: artifact.name, path: destination, files: files.length };
|
|
7390
|
+
})
|
|
7391
|
+
);
|
|
7392
|
+
if (options.json) {
|
|
7393
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
7394
|
+
`);
|
|
7395
|
+
return;
|
|
7396
|
+
}
|
|
7397
|
+
process.stdout.write(`${results.map((r) => r.path).join("\n")}
|
|
7398
|
+
`);
|
|
7399
|
+
} catch (err) {
|
|
7400
|
+
handlePipelineError(err, context);
|
|
7401
|
+
}
|
|
7402
|
+
});
|
|
7403
|
+
return command;
|
|
7404
|
+
}
|
|
7163
7405
|
function createPipelineTestsCommand() {
|
|
7164
7406
|
const command = new Command14("tests");
|
|
7165
7407
|
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 +7503,8 @@ function createPipelineCommand() {
|
|
|
7261
7503
|
command.addCommand(createPipelineWaitCommand());
|
|
7262
7504
|
command.addCommand(createPipelineGetRunDetailCommand());
|
|
7263
7505
|
command.addCommand(createPipelineLogsCommand());
|
|
7506
|
+
command.addCommand(createPipelineArtifactsCommand());
|
|
7507
|
+
command.addCommand(createPipelineArtifactDownloadCommand());
|
|
7264
7508
|
command.addCommand(createPipelineTestsCommand());
|
|
7265
7509
|
command.addCommand(createPipelineStartCommand());
|
|
7266
7510
|
return command;
|
|
@@ -7352,7 +7596,7 @@ function createCommentsCommand() {
|
|
|
7352
7596
|
// src/commands/download-attachment.ts
|
|
7353
7597
|
import { Command as Command16 } from "commander";
|
|
7354
7598
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
7355
|
-
import { existsSync as
|
|
7599
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7356
7600
|
import { join as join3 } from "path";
|
|
7357
7601
|
function createDownloadAttachmentCommand() {
|
|
7358
7602
|
const command = new Command16("download-attachment");
|
|
@@ -7365,7 +7609,7 @@ function createDownloadAttachmentCommand() {
|
|
|
7365
7609
|
context = resolveContext(options);
|
|
7366
7610
|
const credential = await requireAuthCredential(context.org);
|
|
7367
7611
|
const outputDir = options.output ?? ".";
|
|
7368
|
-
if (!
|
|
7612
|
+
if (!existsSync7(outputDir)) {
|
|
7369
7613
|
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
7370
7614
|
`);
|
|
7371
7615
|
process.exit(1);
|
|
@@ -7398,7 +7642,7 @@ function createDownloadAttachmentCommand() {
|
|
|
7398
7642
|
|
|
7399
7643
|
// src/commands/add-attachment.ts
|
|
7400
7644
|
import { Command as Command17 } from "commander";
|
|
7401
|
-
import { existsSync as
|
|
7645
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
7402
7646
|
import { readFile } from "fs/promises";
|
|
7403
7647
|
import { basename } from "path";
|
|
7404
7648
|
function createAddAttachmentCommand() {
|
|
@@ -7407,7 +7651,7 @@ function createAddAttachmentCommand() {
|
|
|
7407
7651
|
async (idStr, file, options) => {
|
|
7408
7652
|
const id = parseWorkItemId(idStr);
|
|
7409
7653
|
validateOrgProjectPair(options);
|
|
7410
|
-
if (!
|
|
7654
|
+
if (!existsSync8(file)) {
|
|
7411
7655
|
process.stderr.write(`Error: File not found: ${file}
|
|
7412
7656
|
`);
|
|
7413
7657
|
process.exit(1);
|
|
@@ -7827,7 +8071,7 @@ function createRelationsCommand() {
|
|
|
7827
8071
|
|
|
7828
8072
|
// src/services/update-check.ts
|
|
7829
8073
|
import fs2 from "fs";
|
|
7830
|
-
import
|
|
8074
|
+
import path4 from "path";
|
|
7831
8075
|
import os from "os";
|
|
7832
8076
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
7833
8077
|
var FETCH_TIMEOUT_MS = 1500;
|
|
@@ -7845,7 +8089,7 @@ function skipsUpdateCheck(commandPath) {
|
|
|
7845
8089
|
return UPDATE_CHECK_EXEMPT_COMMANDS.has(withoutRoot.join(" "));
|
|
7846
8090
|
}
|
|
7847
8091
|
function getCachePath() {
|
|
7848
|
-
return
|
|
8092
|
+
return path4.join(os.homedir(), ".azdo", "update-check.json");
|
|
7849
8093
|
}
|
|
7850
8094
|
function defaultReadCache() {
|
|
7851
8095
|
try {
|
|
@@ -7857,7 +8101,7 @@ function defaultReadCache() {
|
|
|
7857
8101
|
function defaultWriteCache(data) {
|
|
7858
8102
|
try {
|
|
7859
8103
|
const cachePath = getCachePath();
|
|
7860
|
-
fs2.mkdirSync(
|
|
8104
|
+
fs2.mkdirSync(path4.dirname(cachePath), { recursive: true });
|
|
7861
8105
|
fs2.writeFileSync(cachePath, data);
|
|
7862
8106
|
} catch {
|
|
7863
8107
|
}
|
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.807",
|
|
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
|
},
|