azdo-cli 0.22.0-develop.807 → 0.22.0-develop.823

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +183 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -105,6 +105,7 @@ git log -1 --format=%B | azdo pr comments add --file - --pr-number 64 # "-"
105
105
  azdo pr open --title "Fix the thing" --description "Because X was broken"
106
106
  azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
107
107
  azdo pr open --title "Fix the thing" --description-file body.md # or --description-file - to pipe it in
108
+ azdo pr open --title "Fix the thing" --target master --source feature/x --draft --work-item 1234 --label bug # --work-item/--label repeat; --json adds top-level id and url
108
109
  # The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
109
110
  # the command says by how much and creates nothing. See docs/commands.md.
110
111
 
package/dist/index.js CHANGED
@@ -4130,43 +4130,26 @@ function formatContributions(composed) {
4130
4130
  function describeDescriptionBudget(composed) {
4131
4131
  return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
4132
4132
  }
4133
- async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
4134
- const existing = await listPullRequests(context, repo, cred, sourceBranch, {
4135
- status: "active",
4136
- targetBranch: "develop"
4137
- });
4138
- if (existing.length === 1) {
4139
- return {
4140
- branch: sourceBranch,
4141
- targetBranch: "develop",
4142
- created: false,
4143
- pullRequest: existing[0]
4144
- };
4145
- }
4146
- if (existing.length > 1) {
4147
- throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
4148
- }
4149
- const repository = await getRepository(context, repo, cred);
4150
- const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
4151
- const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
4152
- const composed = composeDescription(description, template);
4153
- if (composed === null) {
4154
- throw new Error("DESCRIPTION_REQUIRED");
4155
- }
4156
- if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
4157
- throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
4158
- }
4133
+ function buildOpenPayload(sourceBranch, targetBranch, title, description, options) {
4159
4134
  const payload = {
4160
4135
  sourceRefName: `refs/heads/${sourceBranch}`,
4161
- targetRefName: "refs/heads/develop",
4136
+ targetRefName: `refs/heads/${targetBranch}`,
4162
4137
  title,
4163
- description: composed.text
4138
+ description
4164
4139
  };
4140
+ if (options.isDraft) {
4141
+ payload.isDraft = true;
4142
+ }
4143
+ if (options.labels && options.labels.length > 0) {
4144
+ payload.labels = options.labels.map((name) => ({ name }));
4145
+ }
4146
+ return payload;
4147
+ }
4148
+ async function createPullRequest(context, repo, cred, payload, composed) {
4165
4149
  const url = new URL(
4166
4150
  `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
4167
4151
  );
4168
4152
  url.searchParams.set("api-version", "7.1");
4169
- let data;
4170
4153
  try {
4171
4154
  const response = await fetchWithErrors(url.toString(), {
4172
4155
  method: "POST",
@@ -4176,19 +4159,69 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
4176
4159
  },
4177
4160
  body: JSON.stringify(payload)
4178
4161
  });
4179
- data = await readJsonResponse(response);
4162
+ return await readJsonResponse(response);
4180
4163
  } catch (err) {
4181
4164
  if (err instanceof Error && err.message.startsWith("HTTP_400")) {
4182
4165
  throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
4183
4166
  }
4184
4167
  throw err;
4185
4168
  }
4186
- return {
4169
+ }
4170
+ function linkWorkItems(context, repo, cred, prId, workItemIds) {
4171
+ return Promise.all(
4172
+ workItemIds.map(
4173
+ (id) => linkWorkItemToPullRequest(context, repo, cred, prId, id).then(
4174
+ () => ({ id, linked: true }),
4175
+ (err) => ({ id, linked: false, error: err instanceof Error ? err.message : String(err) })
4176
+ )
4177
+ )
4178
+ );
4179
+ }
4180
+ async function openPullRequest(context, repo, cred, sourceBranch, title, description, options = {}) {
4181
+ const targetBranch = options.targetBranch ?? "develop";
4182
+ const existing = await listPullRequests(context, repo, cred, sourceBranch, {
4183
+ status: "active",
4184
+ targetBranch
4185
+ });
4186
+ if (existing.length === 1) {
4187
+ return {
4188
+ branch: sourceBranch,
4189
+ targetBranch,
4190
+ created: false,
4191
+ pullRequest: existing[0]
4192
+ };
4193
+ }
4194
+ if (existing.length > 1) {
4195
+ throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest2) => pullRequest2.id).join(",")}`);
4196
+ }
4197
+ const repository = await getRepository(context, repo, cred);
4198
+ const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
4199
+ const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch);
4200
+ const composed = composeDescription(description, template);
4201
+ if (composed === null) {
4202
+ throw new Error("DESCRIPTION_REQUIRED");
4203
+ }
4204
+ if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
4205
+ throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
4206
+ }
4207
+ const data = await createPullRequest(
4208
+ context,
4209
+ repo,
4210
+ cred,
4211
+ buildOpenPayload(sourceBranch, targetBranch, title, composed.text, options),
4212
+ composed
4213
+ );
4214
+ const pullRequest = mapPullRequest(context, repo, data);
4215
+ const result = {
4187
4216
  branch: sourceBranch,
4188
- targetBranch: "develop",
4217
+ targetBranch,
4189
4218
  created: true,
4190
- pullRequest: mapPullRequest(context, repo, data)
4219
+ pullRequest
4191
4220
  };
4221
+ if (options.workItemIds && options.workItemIds.length > 0) {
4222
+ result.workItems = await linkWorkItems(context, repo, cred, pullRequest.id, options.workItemIds);
4223
+ }
4224
+ return result;
4192
4225
  }
4193
4226
  async function updatePullRequest(context, repo, cred, prId, fields) {
4194
4227
  if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
@@ -4970,11 +5003,11 @@ function createPrStatusCommand() {
4970
5003
  });
4971
5004
  return command;
4972
5005
  }
4973
- function handlePrOpenError(err, context) {
5006
+ function handlePrOpenError(err, context, targetBranch = "develop") {
4974
5007
  const message = err instanceof Error ? err.message : "";
4975
5008
  if (message.startsWith("AMBIGUOUS_PRS:")) {
4976
5009
  const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
4977
- writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
5010
+ writeError(`Multiple active pull requests already exist for this branch targeting ${targetBranch}: ${ids}. Use pr status to review them.`);
4978
5011
  return;
4979
5012
  }
4980
5013
  if (message === "DESCRIPTION_REQUIRED") {
@@ -4987,6 +5020,47 @@ function handlePrOpenError(err, context) {
4987
5020
  }
4988
5021
  handlePrCommandError(err, context, "write");
4989
5022
  }
5023
+ function collectOption(value, previous) {
5024
+ return [...previous, value];
5025
+ }
5026
+ function stripHeadsPrefix(branch) {
5027
+ return branch.trim().replace(/^refs\/heads\//, "");
5028
+ }
5029
+ function resolveOpenOptions(options) {
5030
+ const targetBranch = options.target === void 0 ? "develop" : stripHeadsPrefix(options.target);
5031
+ const source = options.source === void 0 ? void 0 : stripHeadsPrefix(options.source);
5032
+ if (!targetBranch) {
5033
+ writeError("--target must not be empty.");
5034
+ return null;
5035
+ }
5036
+ if (source !== void 0 && !source) {
5037
+ writeError("--source must not be empty.");
5038
+ return null;
5039
+ }
5040
+ const workItemIds = [];
5041
+ for (const raw of options.workItem ?? []) {
5042
+ const id = Number(raw.trim());
5043
+ if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(id) || id <= 0) {
5044
+ writeError(`--work-item must be a positive integer, got "${raw}".`);
5045
+ return null;
5046
+ }
5047
+ if (!workItemIds.includes(id)) {
5048
+ workItemIds.push(id);
5049
+ }
5050
+ }
5051
+ const labels = [];
5052
+ for (const raw of options.label ?? []) {
5053
+ const name = raw.trim();
5054
+ if (!name) {
5055
+ writeError("--label must not be empty.");
5056
+ return null;
5057
+ }
5058
+ if (!labels.includes(name)) {
5059
+ labels.push(name);
5060
+ }
5061
+ }
5062
+ return { targetBranch, source, isDraft: options.draft === true, labels, workItemIds };
5063
+ }
4990
5064
  function resolveOpenDescription(inline, file) {
4991
5065
  if (inline !== void 0 && file !== void 0) {
4992
5066
  writeError("Cannot specify both --description and --description-file.");
@@ -5012,13 +5086,18 @@ function resolveOpenDescription(inline, file) {
5012
5086
  }
5013
5087
  function createPrOpenCommand() {
5014
5088
  const command = new Command13("open");
5015
- withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
5089
+ withCommonPrOptions(command).description("Open a pull request (default: from the current branch to develop)").option("--title <title>", "pull request title").option(
5016
5090
  "--description <description>",
5017
5091
  "pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
5018
5092
  ).option(
5019
5093
  "--description-file <path>",
5020
5094
  'read the description from a UTF-8 file instead of --description; "-" reads standard input'
5021
- ).option("--json", "output JSON").action(async (options) => {
5095
+ ).option("--target <branch>", "target branch (default: develop); refs/heads/ prefix accepted").option("--source <branch>", "source branch (default: the current branch); no local checkout needed").option("--draft", "open the pull request as a draft").option(
5096
+ "--work-item <id>",
5097
+ "link a work item to the pull request (positive integer; repeatable)",
5098
+ collectOption,
5099
+ []
5100
+ ).option("--label <label>", "add a label to the pull request (repeatable)", collectOption, []).option("--json", "output JSON; adds top-level id and url next to pullRequest").action(async (options) => {
5022
5101
  validateOrgProjectPair(options);
5023
5102
  const title = options.title?.trim();
5024
5103
  if (!title) {
@@ -5029,45 +5108,71 @@ function createPrOpenCommand() {
5029
5108
  if (description === null) {
5030
5109
  return;
5031
5110
  }
5111
+ const openOptions = resolveOpenOptions(options);
5112
+ if (openOptions === null) {
5113
+ return;
5114
+ }
5032
5115
  let context;
5033
5116
  try {
5034
- const resolved = await resolvePrCommandContext(options);
5035
- context = resolved.context;
5036
- if (resolved.branch === "develop") {
5037
- writeError("Pull request creation requires a source branch other than develop.");
5117
+ const openBranch = openOptions.source ?? getCurrentBranch();
5118
+ const target = openOptions.targetBranch;
5119
+ if (openBranch === target) {
5120
+ writeError(
5121
+ target === "develop" ? "Pull request creation requires a source branch other than develop." : `Pull request creation requires a source branch other than the target branch (${target}).`
5122
+ );
5038
5123
  return;
5039
5124
  }
5040
- const openBranch = resolved.branch;
5125
+ const resolved = await resolvePrCommandContext(options, { requireBranch: false });
5126
+ context = resolved.context;
5041
5127
  const result = await openPullRequest(
5042
5128
  resolved.context,
5043
5129
  resolved.repo,
5044
5130
  resolved.pat,
5045
5131
  openBranch,
5046
5132
  title,
5047
- description
5133
+ description,
5134
+ openOptions
5048
5135
  );
5049
5136
  if (options.json) {
5050
- process.stdout.write(`${JSON.stringify(result, null, 2)}
5137
+ const output = { ...result, id: result.pullRequest.id, url: result.pullRequest.url };
5138
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
5051
5139
  `);
5140
+ reportWorkItemLinkFailures(result);
5052
5141
  return;
5053
5142
  }
5054
5143
  if (result.created) {
5055
5144
  process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
5056
5145
  ${result.pullRequest.url ?? "\u2014"}
5057
5146
  `);
5147
+ reportWorkItemLinkFailures(result);
5058
5148
  return;
5059
5149
  }
5060
5150
  process.stdout.write(
5061
- `Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
5151
+ `Active pull request already exists for ${openBranch} -> ${result.targetBranch}: #${result.pullRequest.id}
5062
5152
  ${result.pullRequest.url ?? "\u2014"}
5063
5153
  `
5064
5154
  );
5155
+ if (openOptions.isDraft || openOptions.labels?.length || openOptions.workItemIds?.length) {
5156
+ const hint = openOptions.workItemIds?.length ? " Use pr work-items link to link a work item." : "";
5157
+ process.stdout.write(
5158
+ `--draft, --label and --work-item were not applied; the existing pull request was left unchanged.${hint}
5159
+ `
5160
+ );
5161
+ }
5065
5162
  } catch (err) {
5066
- handlePrOpenError(err, context);
5163
+ handlePrOpenError(err, context, openOptions.targetBranch);
5067
5164
  }
5068
5165
  });
5069
5166
  return command;
5070
5167
  }
5168
+ function reportWorkItemLinkFailures(result) {
5169
+ const failed = (result.workItems ?? []).filter((item) => !item.linked);
5170
+ if (failed.length === 0) return;
5171
+ const detail = failed.map((item) => `#${item.id} (${item.error ?? "unknown error"})`).join(", ");
5172
+ writeError(
5173
+ `Pull request #${result.pullRequest.id} was created but could not be linked to work item ${detail}. Retry with: azdo pr work-items link <id> --pr-number ${result.pullRequest.id}`
5174
+ );
5175
+ }
5071
5176
  function handlePrUpdateError(err, context) {
5072
5177
  const message = err instanceof Error ? err.message : "";
5073
5178
  if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
@@ -6669,6 +6774,26 @@ async function listBuildArtifacts(context, cred, buildId) {
6669
6774
  };
6670
6775
  });
6671
6776
  }
6777
+ var INITIAL_BUFFER_BYTES = 8 * 1024 * 1024;
6778
+ async function collectBody(body, declared, onProgress, initialBytes = INITIAL_BUFFER_BYTES) {
6779
+ const hint = Number.isSafeInteger(declared) && declared > 0 ? declared : Infinity;
6780
+ let buffer = new Uint8Array(Math.min(hint, initialBytes));
6781
+ let received = 0;
6782
+ for await (const chunk of body) {
6783
+ const needed = received + chunk.length;
6784
+ if (needed > buffer.length) {
6785
+ let capacity = Math.max(buffer.length * 2, needed);
6786
+ if (hint >= needed) capacity = Math.min(capacity, hint);
6787
+ const grown = new Uint8Array(capacity);
6788
+ grown.set(buffer.subarray(0, received));
6789
+ buffer = grown;
6790
+ }
6791
+ buffer.set(chunk, received);
6792
+ received = needed;
6793
+ onProgress?.(received);
6794
+ }
6795
+ return buffer.subarray(0, received);
6796
+ }
6672
6797
  async function downloadArtifactZip(cred, artifact, onProgress) {
6673
6798
  if (!artifact.downloadUrl) {
6674
6799
  throw new Error(`Artifact "${artifact.name}" (${artifact.type ?? "unknown type"}) has no download URL.`);
@@ -6682,14 +6807,8 @@ async function downloadArtifactZip(cred, artifact, onProgress) {
6682
6807
  if (!response.body) {
6683
6808
  return new Uint8Array(await response.arrayBuffer());
6684
6809
  }
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);
6810
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
6811
+ return collectBody(response.body, declared, onProgress);
6693
6812
  }
6694
6813
 
6695
6814
  // src/services/artifact-extract.ts
@@ -7142,10 +7261,18 @@ function grepWithContext(lines, grep, context) {
7142
7261
  }
7143
7262
  return selected;
7144
7263
  }
7264
+ var PROGRESS_BAR = /(?<![\d.])\d+(?:\.\d+)? [KMGT]?i?B \/ \d+(?:\.\d+)? [KMGT]?i?B \[/g;
7265
+ var LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z /;
7266
+ function collapseGluedBars(line) {
7267
+ const starts = [...line.matchAll(PROGRESS_BAR)].map((m) => m.index ?? 0);
7268
+ if (starts.length < 2) return line;
7269
+ const prefix = LOG_TIMESTAMP.exec(line)?.[0] ?? "";
7270
+ return prefix + line.slice(starts.at(-1));
7271
+ }
7145
7272
  function collapseProgress(line) {
7146
7273
  let end = line.length;
7147
7274
  while (end > 0 && line[end - 1] === "\r") end--;
7148
- return line.slice(line.lastIndexOf("\r", end - 1) + 1, end);
7275
+ return collapseGluedBars(line.slice(line.lastIndexOf("\r", end - 1) + 1, end));
7149
7276
  }
7150
7277
  function filterLogLines(content, filters) {
7151
7278
  const { grep, tail, head, contextLines: context } = filters;
@@ -7255,7 +7382,7 @@ function printSingleLog(content, filters) {
7255
7382
  }
7256
7383
  function createPipelineLogsCommand() {
7257
7384
  const command = new Command14("logs");
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) => {
7385
+ 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) => {
7259
7386
  validateOrgProjectPair(options);
7260
7387
  const runId = parsePositiveId(runIdRaw);
7261
7388
  if (runId === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.22.0-develop.807",
3
+ "version": "0.22.0-develop.823",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {