azdo-cli 0.10.0-develop.423 → 0.10.0-develop.479
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
CHANGED
|
@@ -58,7 +58,7 @@ azdo pr comments # active-branch PR
|
|
|
58
58
|
azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
|
|
59
59
|
azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
|
|
60
60
|
azdo pr comments --code-related-only # only file/line-anchored threads
|
|
61
|
-
azdo pr status # PR checks (status + branch policies) + code-comment counts
|
|
61
|
+
azdo pr status # PR checks (status + branch policies + pipeline builds) + code-comment counts
|
|
62
62
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
63
63
|
azdo pr comment-reopen 17 --pr-number 64
|
|
64
64
|
|
|
@@ -65,6 +65,7 @@ var SETTINGS = [
|
|
|
65
65
|
}
|
|
66
66
|
];
|
|
67
67
|
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
68
|
+
var SCOPED_KEYS = ["project", "fields", "markdown"];
|
|
68
69
|
function getConfigPath() {
|
|
69
70
|
return path.join(os.homedir(), ".azdo", "config.json");
|
|
70
71
|
}
|
|
@@ -88,6 +89,20 @@ function loadConfig() {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
function saveConfig(config) {
|
|
92
|
+
if (config.organizations) {
|
|
93
|
+
const normalised = {};
|
|
94
|
+
for (const [k, v] of Object.entries(config.organizations)) {
|
|
95
|
+
if (v && Object.keys(v).length > 0) {
|
|
96
|
+
normalised[k.toLowerCase()] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(normalised).length > 0) {
|
|
100
|
+
config = { ...config, organizations: normalised };
|
|
101
|
+
} else {
|
|
102
|
+
const { organizations: _, ...rest } = config;
|
|
103
|
+
config = rest;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
91
106
|
const configPath = getConfigPath();
|
|
92
107
|
const dir = path.dirname(configPath);
|
|
93
108
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -126,6 +141,115 @@ function unsetConfigValue(key) {
|
|
|
126
141
|
delete config[key];
|
|
127
142
|
saveConfig(config);
|
|
128
143
|
}
|
|
144
|
+
function resolveScopedConfig(org) {
|
|
145
|
+
const config = loadConfig();
|
|
146
|
+
const base = {
|
|
147
|
+
project: config.project,
|
|
148
|
+
fields: config.fields,
|
|
149
|
+
markdown: config.markdown,
|
|
150
|
+
org: config.org
|
|
151
|
+
};
|
|
152
|
+
if (!org) return base;
|
|
153
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
154
|
+
if (!scope) return base;
|
|
155
|
+
return {
|
|
156
|
+
org: config.org,
|
|
157
|
+
project: scope.project ?? config.project,
|
|
158
|
+
fields: scope.fields ?? config.fields,
|
|
159
|
+
markdown: scope.markdown ?? config.markdown
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function validateScopedKey(key) {
|
|
163
|
+
if (!SCOPED_KEYS.includes(key)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid scoped key "${key}". Valid scoped keys: ${SCOPED_KEYS.join(", ")}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function applyValueToScope(scope, key, value) {
|
|
170
|
+
if (key === "fields") {
|
|
171
|
+
return { ...scope, fields: value.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
172
|
+
}
|
|
173
|
+
if (key === "markdown") {
|
|
174
|
+
if (value !== "true" && value !== "false") {
|
|
175
|
+
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
176
|
+
}
|
|
177
|
+
return { ...scope, markdown: value === "true" };
|
|
178
|
+
}
|
|
179
|
+
return { ...scope, [key]: value };
|
|
180
|
+
}
|
|
181
|
+
function setOrgScopedValue(org, key, value) {
|
|
182
|
+
validateScopedKey(key);
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const lc = org.toLowerCase();
|
|
185
|
+
const existing = config.organizations?.[lc] ?? {};
|
|
186
|
+
const updated = applyValueToScope(existing, key, value);
|
|
187
|
+
saveConfig({
|
|
188
|
+
...config,
|
|
189
|
+
organizations: { ...config.organizations, [lc]: updated }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function unsetOrgScopedValue(org, key) {
|
|
193
|
+
validateScopedKey(key);
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const lc = org.toLowerCase();
|
|
196
|
+
const scope = config.organizations?.[lc];
|
|
197
|
+
if (!scope) return;
|
|
198
|
+
const { [key]: _, ...rest } = scope;
|
|
199
|
+
saveConfig({
|
|
200
|
+
...config,
|
|
201
|
+
organizations: { ...config.organizations, [lc]: rest }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function getOrgScopedValue(org, key) {
|
|
205
|
+
validateScopedKey(key);
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
208
|
+
return scope?.[key];
|
|
209
|
+
}
|
|
210
|
+
function readScope(config, name) {
|
|
211
|
+
if (name === "default") {
|
|
212
|
+
const { org: _o, organizations: _orgs, ...defaults } = config;
|
|
213
|
+
return defaults;
|
|
214
|
+
}
|
|
215
|
+
return config.organizations?.[name.toLowerCase()] ?? {};
|
|
216
|
+
}
|
|
217
|
+
function copyOrgScope(from, to, force = false) {
|
|
218
|
+
const config = loadConfig();
|
|
219
|
+
const source = readScope(config, from);
|
|
220
|
+
const toLc = to.toLowerCase();
|
|
221
|
+
const dest = config.organizations?.[toLc] ?? {};
|
|
222
|
+
if (!force) {
|
|
223
|
+
const collisions = Object.keys(source).filter(
|
|
224
|
+
(k) => dest[k] !== void 0
|
|
225
|
+
);
|
|
226
|
+
if (collisions.length > 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Scope "${toLc}" already has values for: ${collisions.join(", ")}. Use --force to overwrite.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
saveConfig({
|
|
233
|
+
...config,
|
|
234
|
+
organizations: {
|
|
235
|
+
...config.organizations,
|
|
236
|
+
[toLc]: { ...dest, ...source }
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function moveOrgScope(from, to, force = false) {
|
|
241
|
+
copyOrgScope(from, to, force);
|
|
242
|
+
if (from !== "default") {
|
|
243
|
+
deleteOrgScope(from);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function deleteOrgScope(name) {
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
const lc = name.toLowerCase();
|
|
249
|
+
if (!config.organizations?.[lc]) return;
|
|
250
|
+
const { [lc]: _, ...rest } = config.organizations;
|
|
251
|
+
saveConfig({ ...config, organizations: rest });
|
|
252
|
+
}
|
|
129
253
|
|
|
130
254
|
// src/services/oauth-config.ts
|
|
131
255
|
var DEFAULT_OAUTH_CLIENT_ID = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
|
|
@@ -864,7 +988,7 @@ async function deletePat(org) {
|
|
|
864
988
|
}
|
|
865
989
|
try {
|
|
866
990
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
867
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
991
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-7FQ4VAIS.js");
|
|
868
992
|
unlinkSync2(lockPath2(org));
|
|
869
993
|
} catch {
|
|
870
994
|
}
|
|
@@ -1061,6 +1185,13 @@ export {
|
|
|
1061
1185
|
getConfigValue,
|
|
1062
1186
|
setConfigValue,
|
|
1063
1187
|
unsetConfigValue,
|
|
1188
|
+
resolveScopedConfig,
|
|
1189
|
+
setOrgScopedValue,
|
|
1190
|
+
unsetOrgScopedValue,
|
|
1191
|
+
getOrgScopedValue,
|
|
1192
|
+
copyOrgScope,
|
|
1193
|
+
moveOrgScope,
|
|
1194
|
+
deleteOrgScope,
|
|
1064
1195
|
maskedDisplay,
|
|
1065
1196
|
normalizePat,
|
|
1066
1197
|
AZDO_RESOURCE_ID,
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,19 @@ import {
|
|
|
6
6
|
SETTINGS,
|
|
7
7
|
appendAuthAuditEvent,
|
|
8
8
|
buildScopeString,
|
|
9
|
+
copyOrgScope,
|
|
9
10
|
defaultScopes,
|
|
11
|
+
deleteOrgScope,
|
|
10
12
|
deletePat,
|
|
11
13
|
firstPartyShippedScopes,
|
|
12
14
|
getConfigValue,
|
|
15
|
+
getOrgScopedValue,
|
|
13
16
|
getPat,
|
|
14
17
|
getStoredCredential,
|
|
15
18
|
listOrgsWithStoredPat,
|
|
16
19
|
loadConfig,
|
|
17
20
|
maskedDisplay,
|
|
21
|
+
moveOrgScope,
|
|
18
22
|
normalizePat,
|
|
19
23
|
openUrl,
|
|
20
24
|
probeBackend,
|
|
@@ -22,13 +26,16 @@ import {
|
|
|
22
26
|
readTokenResponse,
|
|
23
27
|
refreshIfNeeded,
|
|
24
28
|
resolveOAuthConfig,
|
|
29
|
+
resolveScopedConfig,
|
|
25
30
|
runAuthCodeFlow,
|
|
26
31
|
setConfigValue,
|
|
32
|
+
setOrgScopedValue,
|
|
27
33
|
storeOAuthCredential,
|
|
28
34
|
storePat,
|
|
29
35
|
tokenResponseToCredential,
|
|
30
|
-
unsetConfigValue
|
|
31
|
-
|
|
36
|
+
unsetConfigValue,
|
|
37
|
+
unsetOrgScopedValue
|
|
38
|
+
} from "./chunk-XVXMDWQE.js";
|
|
32
39
|
|
|
33
40
|
// src/index.ts
|
|
34
41
|
import { Command as Command16 } from "commander";
|
|
@@ -245,28 +252,66 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
245
252
|
}
|
|
246
253
|
return await response.json();
|
|
247
254
|
}
|
|
248
|
-
async function
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
|
|
255
|
+
async function getOrgFieldNames(context, cred) {
|
|
256
|
+
const url = new URL(
|
|
257
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/fields`
|
|
258
|
+
);
|
|
259
|
+
url.searchParams.set("api-version", "7.1");
|
|
260
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`HTTP_${response.status}`);
|
|
257
263
|
}
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
return (data.value ?? []).map((f) => f.referenceName);
|
|
266
|
+
}
|
|
267
|
+
function buildCombinedDescription(fields) {
|
|
268
|
+
const parts = [];
|
|
269
|
+
if (fields["System.Description"]) {
|
|
270
|
+
parts.push({ label: "Description", value: fields["System.Description"] });
|
|
271
|
+
}
|
|
272
|
+
if (fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
|
|
273
|
+
parts.push({ label: "Acceptance Criteria", value: fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
|
|
274
|
+
}
|
|
275
|
+
if (fields["Microsoft.VSTS.TCM.ReproSteps"]) {
|
|
276
|
+
parts.push({ label: "Repro Steps", value: fields["Microsoft.VSTS.TCM.ReproSteps"] });
|
|
260
277
|
}
|
|
261
|
-
if (
|
|
262
|
-
|
|
278
|
+
if (parts.length === 0) return null;
|
|
279
|
+
if (parts.length === 1) return parts[0].value;
|
|
280
|
+
return parts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
|
|
281
|
+
}
|
|
282
|
+
async function fetchWorkItemWithFallback(context, id, cred, normalizedExtraFields) {
|
|
283
|
+
try {
|
|
284
|
+
const data = await fetchWorkItemResponse(context, id, cred, {
|
|
285
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
|
|
286
|
+
});
|
|
287
|
+
return { data, effectiveExtraFields: normalizedExtraFields };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
290
|
+
if (!msg.includes("TF51535")) throw err;
|
|
291
|
+
const orgFieldNames = await getOrgFieldNames(context, cred);
|
|
292
|
+
const orgFieldsLower = new Set(orgFieldNames.map((n) => n.toLowerCase()));
|
|
293
|
+
const missing = normalizedExtraFields.filter((f) => !orgFieldsLower.has(f.toLowerCase()));
|
|
294
|
+
const effectiveExtraFields = normalizedExtraFields.filter((f) => orgFieldsLower.has(f.toLowerCase()));
|
|
295
|
+
for (const f of missing) {
|
|
296
|
+
process.stderr.write(`azdo: warning: field '${f}' does not exist in organization '${context.org}' and was skipped
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
const data = await fetchWorkItemResponse(context, id, cred, {
|
|
300
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...effectiveExtraFields])
|
|
301
|
+
});
|
|
302
|
+
return { data, effectiveExtraFields };
|
|
263
303
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
304
|
+
}
|
|
305
|
+
async function getWorkItem(context, id, cred, extraFields) {
|
|
306
|
+
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
307
|
+
let effectiveExtraFields = normalizedExtraFields;
|
|
308
|
+
let data;
|
|
309
|
+
if (normalizedExtraFields.length > 0) {
|
|
310
|
+
({ data, effectiveExtraFields } = await fetchWorkItemWithFallback(context, id, cred, normalizedExtraFields));
|
|
311
|
+
} else {
|
|
312
|
+
data = await fetchWorkItemResponse(context, id, cred, { includeRelations: true });
|
|
269
313
|
}
|
|
314
|
+
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, { includeRelations: true }) : data;
|
|
270
315
|
return {
|
|
271
316
|
id: data.id,
|
|
272
317
|
rev: data.rev,
|
|
@@ -274,11 +319,11 @@ async function getWorkItem(context, id, cred, extraFields) {
|
|
|
274
319
|
state: data.fields["System.State"],
|
|
275
320
|
type: data.fields["System.WorkItemType"],
|
|
276
321
|
assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
|
|
277
|
-
description:
|
|
322
|
+
description: buildCombinedDescription(data.fields),
|
|
278
323
|
areaPath: data.fields["System.AreaPath"],
|
|
279
324
|
iterationPath: data.fields["System.IterationPath"],
|
|
280
325
|
url: data._links.html.href,
|
|
281
|
-
extraFields:
|
|
326
|
+
extraFields: effectiveExtraFields.length > 0 ? buildExtraFields(data.fields, effectiveExtraFields) : null,
|
|
282
327
|
attachments: extractAttachments(relationsData.relations)
|
|
283
328
|
};
|
|
284
329
|
}
|
|
@@ -765,23 +810,39 @@ async function status() {
|
|
|
765
810
|
}
|
|
766
811
|
|
|
767
812
|
// src/services/git-remote.ts
|
|
768
|
-
import {
|
|
813
|
+
import { execFileSync } from "child_process";
|
|
814
|
+
import fs from "fs";
|
|
815
|
+
import path from "path";
|
|
769
816
|
|
|
770
817
|
// src/services/remote-warning.ts
|
|
771
|
-
var WARNING = "azdo: warning: origin includes embedded credentials; consider removing them with 'git remote set-url origin <clean-url>'\n";
|
|
772
818
|
var warned = false;
|
|
773
|
-
function
|
|
819
|
+
function buildWarning(remoteName) {
|
|
820
|
+
return `azdo: warning: ${remoteName} includes embedded credentials; consider removing them with 'git remote set-url ${remoteName} <clean-url>'
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
function noticeCredentialBearingRemote(remoteName = "origin") {
|
|
774
824
|
if (warned) {
|
|
775
825
|
return;
|
|
776
826
|
}
|
|
777
827
|
warned = true;
|
|
778
828
|
try {
|
|
779
|
-
process.stderr.write(
|
|
829
|
+
process.stderr.write(buildWarning(remoteName));
|
|
780
830
|
} catch {
|
|
781
831
|
}
|
|
782
832
|
}
|
|
783
833
|
|
|
784
834
|
// src/services/git-remote.ts
|
|
835
|
+
var GIT_BINARY = (() => {
|
|
836
|
+
const known = process.platform === "win32" ? [String.raw`C:\Program Files\Git\bin\git.exe`, String.raw`C:\Program Files (x86)\Git\bin\git.exe`] : ["/usr/bin/git", "/usr/local/bin/git", "/opt/homebrew/bin/git"];
|
|
837
|
+
return known.find((p) => {
|
|
838
|
+
try {
|
|
839
|
+
execFileSync(p, ["--version"], { stdio: "ignore" });
|
|
840
|
+
return true;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}) ?? "git";
|
|
845
|
+
})();
|
|
785
846
|
var patterns = [
|
|
786
847
|
// HTTPS (current): https://[user[:token]@]dev.azure.com/{org}/{project}/_git/{repo}[.git]
|
|
787
848
|
/^https?:\/\/(?:[^@/]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/,
|
|
@@ -794,41 +855,137 @@ var patterns = [
|
|
|
794
855
|
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}[.git]
|
|
795
856
|
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/
|
|
796
857
|
];
|
|
797
|
-
var
|
|
798
|
-
function
|
|
858
|
+
var httpsEmbeddedSecret = /^https?:\/\/[^:@/]+:[^@/]+@/;
|
|
859
|
+
function parseSingleRemoteLine(line) {
|
|
860
|
+
const trimmed = line.trim();
|
|
861
|
+
if (!trimmed) return null;
|
|
862
|
+
const tabIdx = trimmed.indexOf(" ");
|
|
863
|
+
if (tabIdx === -1) return null;
|
|
864
|
+
const remoteName = trimmed.slice(0, tabIdx);
|
|
865
|
+
const afterTab = trimmed.slice(tabIdx + 1);
|
|
866
|
+
const urlEnd = afterTab.lastIndexOf(" (");
|
|
867
|
+
const url = urlEnd === -1 ? afterTab : afterTab.slice(0, urlEnd);
|
|
868
|
+
return { remoteName, url };
|
|
869
|
+
}
|
|
870
|
+
function matchAzdoRemote(remoteName, url) {
|
|
799
871
|
for (const pattern of patterns) {
|
|
800
872
|
const match = pattern.exec(url);
|
|
801
|
-
if (match)
|
|
802
|
-
|
|
803
|
-
|
|
873
|
+
if (!match) continue;
|
|
874
|
+
const project = match[2];
|
|
875
|
+
if (/^DefaultCollection$/i.test(project)) return null;
|
|
876
|
+
return { remoteName, org: match[1], project, hasEmbeddedSecret: httpsEmbeddedSecret.test(url) };
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
function parseAllAzdoRemotes(output) {
|
|
881
|
+
const seen = /* @__PURE__ */ new Set();
|
|
882
|
+
const results = [];
|
|
883
|
+
for (const line of output.split("\n")) {
|
|
884
|
+
const parsed = parseSingleRemoteLine(line);
|
|
885
|
+
if (!parsed) continue;
|
|
886
|
+
const { remoteName, url } = parsed;
|
|
887
|
+
if (seen.has(remoteName)) continue;
|
|
888
|
+
const candidate = matchAzdoRemote(remoteName, url);
|
|
889
|
+
if (candidate) {
|
|
890
|
+
seen.add(remoteName);
|
|
891
|
+
results.push(candidate);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return results;
|
|
895
|
+
}
|
|
896
|
+
function selectRemote(candidates) {
|
|
897
|
+
if (candidates.length === 0) {
|
|
898
|
+
throw new Error("No Azure DevOps remote found. Provide --org and --project explicitly.");
|
|
899
|
+
}
|
|
900
|
+
const origin = candidates.find((c) => c.remoteName === "origin");
|
|
901
|
+
if (origin) return origin;
|
|
902
|
+
if (candidates.length === 1) return candidates[0];
|
|
903
|
+
const first = candidates[0];
|
|
904
|
+
const allSame = candidates.every(
|
|
905
|
+
(c) => c.org.toLowerCase() === first.org.toLowerCase() && c.project.toLowerCase() === first.project.toLowerCase()
|
|
906
|
+
);
|
|
907
|
+
if (allSame) return first;
|
|
908
|
+
const lines = candidates.map((c) => ` ${c.remoteName.padEnd(8)} \u2192 ${c.org}/${c.project}`).join("\n");
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Multiple Azure DevOps remotes found with different org/project:
|
|
911
|
+
${lines}
|
|
912
|
+
Use --org/--project (or 'git remote rename <name> origin') to disambiguate.`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function readGitConfigContent() {
|
|
916
|
+
const gitDirEnv = process.env.GIT_DIR;
|
|
917
|
+
if (gitDirEnv) {
|
|
918
|
+
return fs.readFileSync(path.join(gitDirEnv, "config"), "utf-8");
|
|
919
|
+
}
|
|
920
|
+
let dir = process.cwd();
|
|
921
|
+
for (; ; ) {
|
|
922
|
+
const gitPath = path.join(dir, ".git");
|
|
923
|
+
try {
|
|
924
|
+
const stat = fs.statSync(gitPath);
|
|
925
|
+
if (stat.isDirectory()) {
|
|
926
|
+
return fs.readFileSync(path.join(gitPath, "config"), "utf-8");
|
|
804
927
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
928
|
+
if (stat.isFile()) {
|
|
929
|
+
const ref = fs.readFileSync(gitPath, "utf-8");
|
|
930
|
+
const m = /^gitdir:[ \t]*([^\r\n]+)/m.exec(ref);
|
|
931
|
+
if (m) {
|
|
932
|
+
return fs.readFileSync(path.join(path.resolve(dir, m[1].trim()), "config"), "utf-8");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
const parent = path.dirname(dir);
|
|
938
|
+
if (parent === dir) break;
|
|
939
|
+
dir = parent;
|
|
940
|
+
}
|
|
941
|
+
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
942
|
+
}
|
|
943
|
+
function gitConfigToRemoteLines(configContent) {
|
|
944
|
+
const lines = [];
|
|
945
|
+
let currentRemote = null;
|
|
946
|
+
let emittedUrl = false;
|
|
947
|
+
for (const line of configContent.split("\n")) {
|
|
948
|
+
const sectionMatch = /^\[remote\s+"([^"]+)"\]/.exec(line);
|
|
949
|
+
if (sectionMatch) {
|
|
950
|
+
currentRemote = sectionMatch[1];
|
|
951
|
+
emittedUrl = false;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (line.startsWith("[")) {
|
|
955
|
+
currentRemote = null;
|
|
956
|
+
emittedUrl = false;
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (currentRemote && !emittedUrl) {
|
|
960
|
+
const urlMatch = /^[ \t]+url[ \t]*=[ \t]*([^\r\n]+)/.exec(line);
|
|
961
|
+
if (urlMatch) {
|
|
962
|
+
lines.push(`${currentRemote} ${urlMatch[1].trim()} (fetch)`);
|
|
963
|
+
emittedUrl = true;
|
|
808
964
|
}
|
|
809
|
-
return { org: match[1], project };
|
|
810
965
|
}
|
|
811
966
|
}
|
|
812
|
-
return
|
|
967
|
+
return lines.join("\n");
|
|
813
968
|
}
|
|
814
969
|
function detectAzdoContext() {
|
|
815
|
-
let
|
|
970
|
+
let configContent;
|
|
816
971
|
try {
|
|
817
|
-
|
|
972
|
+
configContent = readGitConfigContent();
|
|
818
973
|
} catch {
|
|
819
974
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
820
975
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
976
|
+
const remoteLines = gitConfigToRemoteLines(configContent);
|
|
977
|
+
const candidates = parseAllAzdoRemotes(remoteLines);
|
|
978
|
+
const selected = selectRemote(candidates);
|
|
979
|
+
if (selected.hasEmbeddedSecret) {
|
|
980
|
+
noticeCredentialBearingRemote(selected.remoteName);
|
|
824
981
|
}
|
|
825
|
-
return
|
|
982
|
+
return { org: selected.org, project: selected.project };
|
|
826
983
|
}
|
|
827
984
|
function parseRepoName(url) {
|
|
828
985
|
for (const pattern of patterns) {
|
|
829
986
|
const match = pattern.exec(url);
|
|
830
987
|
if (match) {
|
|
831
|
-
if (
|
|
988
|
+
if (httpsEmbeddedSecret.test(url)) {
|
|
832
989
|
noticeCredentialBearingRemote();
|
|
833
990
|
}
|
|
834
991
|
return match[3];
|
|
@@ -839,7 +996,7 @@ function parseRepoName(url) {
|
|
|
839
996
|
function detectRepoName() {
|
|
840
997
|
let remoteUrl;
|
|
841
998
|
try {
|
|
842
|
-
remoteUrl =
|
|
999
|
+
remoteUrl = execFileSync(GIT_BINARY, ["remote", "get-url", "origin"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
843
1000
|
} catch {
|
|
844
1001
|
throw new Error('Not in a git repository. Check that git remote "origin" exists and try again.');
|
|
845
1002
|
}
|
|
@@ -850,7 +1007,7 @@ function detectRepoName() {
|
|
|
850
1007
|
return repo;
|
|
851
1008
|
}
|
|
852
1009
|
function getCurrentBranch() {
|
|
853
|
-
const branch =
|
|
1010
|
+
const branch = execFileSync(GIT_BINARY, ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
854
1011
|
if (branch === "HEAD") {
|
|
855
1012
|
throw new Error("Not on a named branch. Check out a named branch and try again.");
|
|
856
1013
|
}
|
|
@@ -886,7 +1043,7 @@ function formatResolutionError() {
|
|
|
886
1043
|
return [
|
|
887
1044
|
"Could not resolve an Azure DevOps organization. Options (in priority order):",
|
|
888
1045
|
" 1. Pass --org <name> on the command line.",
|
|
889
|
-
" 2. Run this command from a git repo
|
|
1046
|
+
" 2. Run this command from a git repo that has an Azure DevOps remote.",
|
|
890
1047
|
" 3. Run `azdo config set org <name>` once to set a persistent default."
|
|
891
1048
|
].join("\n");
|
|
892
1049
|
}
|
|
@@ -899,9 +1056,9 @@ function resolveContext(options) {
|
|
|
899
1056
|
gitContext = detectAzdoContext();
|
|
900
1057
|
} catch {
|
|
901
1058
|
}
|
|
902
|
-
const config = loadConfig();
|
|
903
1059
|
const org = resolvedOrg?.org;
|
|
904
|
-
const
|
|
1060
|
+
const scopedCfg = resolveScopedConfig(org);
|
|
1061
|
+
const project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || scopedCfg.project;
|
|
905
1062
|
if (org && project) {
|
|
906
1063
|
return { org, project };
|
|
907
1064
|
}
|
|
@@ -1354,9 +1511,10 @@ function createGetItemCommand() {
|
|
|
1354
1511
|
try {
|
|
1355
1512
|
context = resolveContext(options);
|
|
1356
1513
|
const credential = await requireAuthCredential(context.org);
|
|
1357
|
-
const
|
|
1514
|
+
const scopedCfg = resolveScopedConfig(context.org);
|
|
1515
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
|
|
1358
1516
|
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1359
|
-
const markdownEnabled = options.markdown ??
|
|
1517
|
+
const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
|
|
1360
1518
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1361
1519
|
process.stdout.write(output + "\n");
|
|
1362
1520
|
if (imageOptions.enabled) {
|
|
@@ -1836,18 +1994,47 @@ function formatConfigValue(value, unsetFallback = "") {
|
|
|
1836
1994
|
}
|
|
1837
1995
|
return Array.isArray(value) ? value.join(",") : value;
|
|
1838
1996
|
}
|
|
1997
|
+
function buildConfigListEntries(cfg) {
|
|
1998
|
+
const entries = SETTINGS.map((s) => ({
|
|
1999
|
+
scope: "default",
|
|
2000
|
+
key: s.key,
|
|
2001
|
+
value: cfg[s.key]
|
|
2002
|
+
}));
|
|
2003
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
2004
|
+
for (const [k, v] of Object.entries(scope)) {
|
|
2005
|
+
entries.push({ scope: orgName, key: k, value: v });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return entries;
|
|
2009
|
+
}
|
|
1839
2010
|
function writeConfigList(cfg) {
|
|
1840
2011
|
const keyWidth = 10;
|
|
1841
2012
|
const valueWidth = 30;
|
|
2013
|
+
const scopeWidth = 12;
|
|
2014
|
+
process.stdout.write(
|
|
2015
|
+
`${"scope".padEnd(scopeWidth)}${"key".padEnd(keyWidth)}${"value".padEnd(valueWidth)}description
|
|
2016
|
+
`
|
|
2017
|
+
);
|
|
1842
2018
|
for (const setting of SETTINGS) {
|
|
1843
2019
|
const raw = cfg[setting.key];
|
|
1844
2020
|
const value = formatConfigValue(raw, "(not set)");
|
|
1845
2021
|
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
1846
2022
|
process.stdout.write(
|
|
1847
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
2023
|
+
`${"default".padEnd(scopeWidth)}${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1848
2024
|
`
|
|
1849
2025
|
);
|
|
1850
2026
|
}
|
|
2027
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
2028
|
+
const orgScope = scope;
|
|
2029
|
+
const scopedSettings = Object.entries(orgScope);
|
|
2030
|
+
for (const [k, v] of scopedSettings) {
|
|
2031
|
+
const value = formatConfigValue(v, "(not set)");
|
|
2032
|
+
process.stdout.write(
|
|
2033
|
+
`${orgName.padEnd(scopeWidth)}${k.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}
|
|
2034
|
+
`
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1851
2038
|
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
1852
2039
|
if (hasUnset) {
|
|
1853
2040
|
process.stdout.write(
|
|
@@ -1891,17 +2078,22 @@ function createConfigCommand() {
|
|
|
1891
2078
|
const config = new Command4("config");
|
|
1892
2079
|
config.description("Manage CLI settings");
|
|
1893
2080
|
const set = new Command4("set");
|
|
1894
|
-
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
|
|
2081
|
+
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
|
|
1895
2082
|
try {
|
|
1896
|
-
|
|
2083
|
+
if (options.org) {
|
|
2084
|
+
setOrgScopedValue(options.org, key, value);
|
|
2085
|
+
} else {
|
|
2086
|
+
setConfigValue(key, value);
|
|
2087
|
+
}
|
|
1897
2088
|
if (options.json) {
|
|
1898
|
-
const output = { key, value };
|
|
2089
|
+
const output = { key, value, scope: options.org ?? "default" };
|
|
1899
2090
|
if (key === "fields") {
|
|
1900
2091
|
output.value = value.split(",").map((s) => s.trim());
|
|
1901
2092
|
}
|
|
1902
2093
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
1903
2094
|
} else {
|
|
1904
|
-
|
|
2095
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2096
|
+
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
1905
2097
|
`);
|
|
1906
2098
|
}
|
|
1907
2099
|
} catch (err) {
|
|
@@ -1912,12 +2104,12 @@ function createConfigCommand() {
|
|
|
1912
2104
|
}
|
|
1913
2105
|
});
|
|
1914
2106
|
const get = new Command4("get");
|
|
1915
|
-
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2107
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1916
2108
|
try {
|
|
1917
|
-
const value = getConfigValue(key);
|
|
2109
|
+
const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
|
|
1918
2110
|
if (options.json) {
|
|
1919
2111
|
process.stdout.write(
|
|
1920
|
-
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
2112
|
+
JSON.stringify({ key, value: value ?? null, scope: options.org ?? "default" }) + "\n"
|
|
1921
2113
|
);
|
|
1922
2114
|
} else if (value === void 0) {
|
|
1923
2115
|
process.stdout.write(`Setting "${key}" is not configured.
|
|
@@ -1925,7 +2117,7 @@ function createConfigCommand() {
|
|
|
1925
2117
|
} else if (Array.isArray(value)) {
|
|
1926
2118
|
process.stdout.write(value.join(",") + "\n");
|
|
1927
2119
|
} else {
|
|
1928
|
-
process.stdout.write(value + "\n");
|
|
2120
|
+
process.stdout.write(String(value) + "\n");
|
|
1929
2121
|
}
|
|
1930
2122
|
} catch (err) {
|
|
1931
2123
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1938,19 +2130,25 @@ function createConfigCommand() {
|
|
|
1938
2130
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1939
2131
|
const cfg = loadConfig();
|
|
1940
2132
|
if (options.json) {
|
|
1941
|
-
|
|
2133
|
+
const entries = buildConfigListEntries(cfg);
|
|
2134
|
+
process.stdout.write(JSON.stringify(entries) + "\n");
|
|
1942
2135
|
return;
|
|
1943
2136
|
}
|
|
1944
2137
|
writeConfigList(cfg);
|
|
1945
2138
|
});
|
|
1946
2139
|
const unset = new Command4("unset");
|
|
1947
|
-
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2140
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1948
2141
|
try {
|
|
1949
|
-
|
|
2142
|
+
if (options.org) {
|
|
2143
|
+
unsetOrgScopedValue(options.org, key);
|
|
2144
|
+
} else {
|
|
2145
|
+
unsetConfigValue(key);
|
|
2146
|
+
}
|
|
1950
2147
|
if (options.json) {
|
|
1951
|
-
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
2148
|
+
process.stdout.write(JSON.stringify({ key, unset: true, scope: options.org ?? "default" }) + "\n");
|
|
1952
2149
|
} else {
|
|
1953
|
-
|
|
2150
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2151
|
+
process.stdout.write(`Unset "${key}"${scopeTag}
|
|
1954
2152
|
`);
|
|
1955
2153
|
}
|
|
1956
2154
|
} catch (err) {
|
|
@@ -1982,10 +2180,52 @@ function createConfigCommand() {
|
|
|
1982
2180
|
rl.close();
|
|
1983
2181
|
process.stderr.write("Configuration complete!\n");
|
|
1984
2182
|
});
|
|
2183
|
+
const orgCopy = new Command4("org-copy");
|
|
2184
|
+
orgCopy.description('Copy settings from one scope to another (use "default" as source to copy top-level settings)').argument("<from>", 'source scope name or "default"').argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2185
|
+
try {
|
|
2186
|
+
copyOrgScope(from, to, options.force ?? false);
|
|
2187
|
+
process.stdout.write(`Copied scope "${from}" to "${to}"
|
|
2188
|
+
`);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2191
|
+
process.stderr.write(`Error: ${message}
|
|
2192
|
+
`);
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
const orgMove = new Command4("org-move");
|
|
2197
|
+
orgMove.description("Move settings from one org scope to another").argument("<from>", "source org name").argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2198
|
+
try {
|
|
2199
|
+
moveOrgScope(from, to, options.force ?? false);
|
|
2200
|
+
process.stdout.write(`Moved scope "${from}" to "${to}"
|
|
2201
|
+
`);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2204
|
+
process.stderr.write(`Error: ${message}
|
|
2205
|
+
`);
|
|
2206
|
+
process.exit(1);
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
const orgDelete = new Command4("org-delete");
|
|
2210
|
+
orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
|
|
2211
|
+
try {
|
|
2212
|
+
deleteOrgScope(name);
|
|
2213
|
+
process.stdout.write(`Deleted scope "${name}"
|
|
2214
|
+
`);
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2217
|
+
process.stderr.write(`Error: ${message}
|
|
2218
|
+
`);
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
1985
2222
|
config.addCommand(set);
|
|
1986
2223
|
config.addCommand(get);
|
|
1987
2224
|
config.addCommand(list);
|
|
1988
2225
|
config.addCommand(unset);
|
|
2226
|
+
config.addCommand(orgCopy);
|
|
2227
|
+
config.addCommand(orgMove);
|
|
2228
|
+
config.addCommand(orgDelete);
|
|
1989
2229
|
config.addCommand(wizard);
|
|
1990
2230
|
return config;
|
|
1991
2231
|
}
|
|
@@ -2667,6 +2907,16 @@ function buildPolicyEvaluationsUrl(context, projectId, prId) {
|
|
|
2667
2907
|
url.searchParams.set("artifactId", `vstfs:///CodeReview/CodeReviewId/${projectId}/${prId}`);
|
|
2668
2908
|
return url;
|
|
2669
2909
|
}
|
|
2910
|
+
function buildPullRequestBuildsUrl(context, prId) {
|
|
2911
|
+
const url = new URL(
|
|
2912
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/build/builds`
|
|
2913
|
+
);
|
|
2914
|
+
url.searchParams.set("branchName", `refs/pull/${prId}/merge`);
|
|
2915
|
+
url.searchParams.set("queryOrder", "queueTimeDescending");
|
|
2916
|
+
url.searchParams.set("$top", "50");
|
|
2917
|
+
url.searchParams.set("api-version", "7.1");
|
|
2918
|
+
return url;
|
|
2919
|
+
}
|
|
2670
2920
|
function mapPullRequest(repo, pullRequest) {
|
|
2671
2921
|
return {
|
|
2672
2922
|
id: pullRequest.pullRequestId,
|
|
@@ -2726,6 +2976,22 @@ function mapPolicyEvaluationState(status2) {
|
|
|
2726
2976
|
return status2;
|
|
2727
2977
|
}
|
|
2728
2978
|
}
|
|
2979
|
+
function mapBuildToCheckState(build) {
|
|
2980
|
+
if (build.status !== "completed") {
|
|
2981
|
+
return "pending";
|
|
2982
|
+
}
|
|
2983
|
+
switch (build.result) {
|
|
2984
|
+
case "succeeded":
|
|
2985
|
+
case "partiallySucceeded":
|
|
2986
|
+
return "succeeded";
|
|
2987
|
+
case "failed":
|
|
2988
|
+
return "failed";
|
|
2989
|
+
case "canceled":
|
|
2990
|
+
return "error";
|
|
2991
|
+
default:
|
|
2992
|
+
return "pending";
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2729
2995
|
function mapPolicyEvaluationName(evaluation) {
|
|
2730
2996
|
const display = evaluation.configuration?.settings?.displayName?.trim() || evaluation.configuration?.type?.displayName?.trim();
|
|
2731
2997
|
if (display) {
|
|
@@ -2747,7 +3013,8 @@ function mapPolicyEvaluationCheck(evaluation) {
|
|
|
2747
3013
|
createdBy: null,
|
|
2748
3014
|
createdAt: null,
|
|
2749
3015
|
updatedAt: null,
|
|
2750
|
-
source: "policy"
|
|
3016
|
+
source: "policy",
|
|
3017
|
+
isBlocking: evaluation.configuration?.isBlocking ?? null
|
|
2751
3018
|
};
|
|
2752
3019
|
}
|
|
2753
3020
|
function mapComment(comment) {
|
|
@@ -2769,7 +3036,7 @@ function mapThread(thread) {
|
|
|
2769
3036
|
}
|
|
2770
3037
|
return {
|
|
2771
3038
|
id: thread.id,
|
|
2772
|
-
status: thread.status,
|
|
3039
|
+
status: thread.status ?? "unknown",
|
|
2773
3040
|
threadContext: thread.threadContext?.filePath ?? null,
|
|
2774
3041
|
comments
|
|
2775
3042
|
};
|
|
@@ -2777,7 +3044,7 @@ function mapThread(thread) {
|
|
|
2777
3044
|
function toActiveCommentThread(thread) {
|
|
2778
3045
|
return {
|
|
2779
3046
|
id: thread.id,
|
|
2780
|
-
status: thread.status,
|
|
3047
|
+
status: thread.status ?? "unknown",
|
|
2781
3048
|
threadContext: thread.threadContext?.filePath ?? null,
|
|
2782
3049
|
comments: thread.comments.map(mapComment).filter((comment) => comment !== null)
|
|
2783
3050
|
};
|
|
@@ -2848,6 +3115,24 @@ async function getPullRequestPolicyEvaluations(context, cred, projectId, prId) {
|
|
|
2848
3115
|
const data = await readJsonResponse(response);
|
|
2849
3116
|
return data.value.map(mapPolicyEvaluationCheck).filter((check) => check !== null);
|
|
2850
3117
|
}
|
|
3118
|
+
async function getPullRequestBuilds(context, cred, prId) {
|
|
3119
|
+
const response = await fetchWithErrors(buildPullRequestBuildsUrl(context, prId).toString(), {
|
|
3120
|
+
headers: authHeaders(cred)
|
|
3121
|
+
});
|
|
3122
|
+
const data = await readJsonResponse(response);
|
|
3123
|
+
return data.value.map((build) => ({
|
|
3124
|
+
id: build.id,
|
|
3125
|
+
state: mapBuildToCheckState(build),
|
|
3126
|
+
name: build.definition?.name ?? `Build #${build.id}`,
|
|
3127
|
+
description: null,
|
|
3128
|
+
targetUrl: build._links?.web?.href ?? null,
|
|
3129
|
+
createdBy: null,
|
|
3130
|
+
createdAt: build.queueTime ?? null,
|
|
3131
|
+
updatedAt: build.finishTime ?? null,
|
|
3132
|
+
source: "build",
|
|
3133
|
+
isBlocking: null
|
|
3134
|
+
}));
|
|
3135
|
+
}
|
|
2851
3136
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
2852
3137
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
2853
3138
|
status: "active",
|
|
@@ -2965,7 +3250,8 @@ function formatPullRequestChecks(checks, checksError) {
|
|
|
2965
3250
|
}
|
|
2966
3251
|
const lines = ["Checks:"];
|
|
2967
3252
|
for (const check of checks) {
|
|
2968
|
-
|
|
3253
|
+
const optionalTag = check.isBlocking === false ? " [optional]" : "";
|
|
3254
|
+
lines.push(`- [${check.state}] ${check.name}${optionalTag}`);
|
|
2969
3255
|
if ((check.state === "failed" || check.state === "error") && check.description) {
|
|
2970
3256
|
lines.push(` Detail: ${check.description}`);
|
|
2971
3257
|
}
|
|
@@ -3018,6 +3304,13 @@ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, pro
|
|
|
3018
3304
|
policyOk = false;
|
|
3019
3305
|
}
|
|
3020
3306
|
}
|
|
3307
|
+
let buildChecks = [];
|
|
3308
|
+
let buildsOk = true;
|
|
3309
|
+
try {
|
|
3310
|
+
buildChecks = await getPullRequestBuilds(context, cred, pullRequest.id);
|
|
3311
|
+
} catch {
|
|
3312
|
+
buildsOk = false;
|
|
3313
|
+
}
|
|
3021
3314
|
let codeCommentCounts;
|
|
3022
3315
|
try {
|
|
3023
3316
|
const threads = await getPullRequestThreads(context, repo, cred, pullRequest.id);
|
|
@@ -3025,8 +3318,8 @@ async function buildPullRequestStatusEntry(context, repo, cred, pullRequest, pro
|
|
|
3025
3318
|
} catch {
|
|
3026
3319
|
codeCommentCounts = { open: 0, closed: 0 };
|
|
3027
3320
|
}
|
|
3028
|
-
const checks = [...statusChecks, ...policyChecks];
|
|
3029
|
-
const checksError = checks.length === 0 && (!statusOk || !policyOk) ? "Azure DevOps request failed" : null;
|
|
3321
|
+
const checks = [...statusChecks, ...policyChecks, ...buildChecks];
|
|
3322
|
+
const checksError = checks.length === 0 && (!statusOk || !policyOk || !buildsOk) ? "Azure DevOps request failed" : null;
|
|
3030
3323
|
return {
|
|
3031
3324
|
...pullRequest,
|
|
3032
3325
|
checks,
|
|
@@ -4411,18 +4704,18 @@ function createDownloadAttachmentCommand() {
|
|
|
4411
4704
|
}
|
|
4412
4705
|
|
|
4413
4706
|
// src/services/update-check.ts
|
|
4414
|
-
import
|
|
4415
|
-
import
|
|
4707
|
+
import fs2 from "fs";
|
|
4708
|
+
import path2 from "path";
|
|
4416
4709
|
import os from "os";
|
|
4417
4710
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
4418
4711
|
var FETCH_TIMEOUT_MS = 1500;
|
|
4419
4712
|
var REGISTRY_URL = "https://registry.npmjs.org/azdo-cli/latest";
|
|
4420
4713
|
function getCachePath() {
|
|
4421
|
-
return
|
|
4714
|
+
return path2.join(os.homedir(), ".azdo", "update-check.json");
|
|
4422
4715
|
}
|
|
4423
4716
|
function defaultReadCache() {
|
|
4424
4717
|
try {
|
|
4425
|
-
return
|
|
4718
|
+
return fs2.readFileSync(getCachePath(), "utf-8");
|
|
4426
4719
|
} catch {
|
|
4427
4720
|
return null;
|
|
4428
4721
|
}
|
|
@@ -4430,8 +4723,8 @@ function defaultReadCache() {
|
|
|
4430
4723
|
function defaultWriteCache(data) {
|
|
4431
4724
|
try {
|
|
4432
4725
|
const cachePath = getCachePath();
|
|
4433
|
-
|
|
4434
|
-
|
|
4726
|
+
fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
|
|
4727
|
+
fs2.writeFileSync(cachePath, data);
|
|
4435
4728
|
} catch {
|
|
4436
4729
|
}
|
|
4437
4730
|
}
|