@cat-indev/catops-cli 0.0.1-alpha.10 → 0.0.1-alpha.12
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 +173 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/services/azdo-api.d.ts +212 -0
- package/dist/services/azdo-api.js +652 -0
- package/dist/services/azdo-api.js.map +1 -1
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +6 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/yaml.d.ts +47 -0
- package/dist/services/yaml.js +146 -0
- package/dist/services/yaml.js.map +1 -0
- package/package.json +7 -2
|
@@ -242,6 +242,658 @@ class AzureDevOpsApi {
|
|
|
242
242
|
body: { query: wiql }
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
|
+
// =========================================================================
|
|
246
|
+
// Git Repositories — existence checks + creation
|
|
247
|
+
// =========================================================================
|
|
248
|
+
/** Verifica si un repositorio existe. Devuelve el repo o `undefined`. */
|
|
249
|
+
async repoExists(project, repoName, opts) {
|
|
250
|
+
try {
|
|
251
|
+
const res = await this.request("GET", `/git/repositories/${encodeURIComponent(repoName)}`, { ...opts, project: project ?? opts?.project });
|
|
252
|
+
return res.body;
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Crea un repositorio en un proyecto. */
|
|
262
|
+
createRepository(projectName, projectId, repoName, opts) {
|
|
263
|
+
return this.request("POST", "/git/repositories", {
|
|
264
|
+
...opts,
|
|
265
|
+
body: {
|
|
266
|
+
name: repoName,
|
|
267
|
+
project: { id: projectId, name: projectName }
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// =========================================================================
|
|
272
|
+
// Branches — creation
|
|
273
|
+
// =========================================================================
|
|
274
|
+
/** Crea una branch nueva a partir de un commit ID. */
|
|
275
|
+
createBranch(project, repoName, branchName, fromObjectId, opts) {
|
|
276
|
+
return this.request("POST", `/git/repositories/${encodeURIComponent(repoName)}/refs`, {
|
|
277
|
+
...opts,
|
|
278
|
+
project: project ?? opts?.project,
|
|
279
|
+
body: [
|
|
280
|
+
{
|
|
281
|
+
name: `refs/heads/${branchName}`,
|
|
282
|
+
newObjectId: fromObjectId,
|
|
283
|
+
oldObjectId: "0000000000000000000000000000000000000000"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// =========================================================================
|
|
289
|
+
// Commits — latest
|
|
290
|
+
// =========================================================================
|
|
291
|
+
/** Obtiene el último commit de un repo. Devuelve `null` si no hay commits. */
|
|
292
|
+
async getLatestCommit(project, repoNameOrId, opts) {
|
|
293
|
+
const res = await this.request("GET", `/git/repositories/${encodeURIComponent(repoNameOrId)}/commits`, { ...opts, project: project ?? opts?.project, query: { $top: 1, ...opts?.query } });
|
|
294
|
+
if (!res.body.value.length)
|
|
295
|
+
return null;
|
|
296
|
+
const commit = res.body.value[0];
|
|
297
|
+
return {
|
|
298
|
+
commitId: commit.commitId,
|
|
299
|
+
message: commit.comment,
|
|
300
|
+
date: commit.author.date,
|
|
301
|
+
author: { name: commit.author.name, date: commit.author.date }
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// =========================================================================
|
|
305
|
+
// Files — existence check
|
|
306
|
+
// =========================================================================
|
|
307
|
+
/** Verifica si un archivo existe en una branch. Devuelve el item o `undefined`. */
|
|
308
|
+
async fileExists(project, repoName, branchName, filePath, opts) {
|
|
309
|
+
try {
|
|
310
|
+
const res = await this.request("GET", `/git/repositories/${encodeURIComponent(repoName)}/items`, {
|
|
311
|
+
...opts,
|
|
312
|
+
project: project ?? opts?.project,
|
|
313
|
+
query: { path: filePath, "versionDescriptor.version": branchName, ...opts?.query }
|
|
314
|
+
});
|
|
315
|
+
return res.body;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// =========================================================================
|
|
325
|
+
// Files — create / update
|
|
326
|
+
// =========================================================================
|
|
327
|
+
/** Crea o actualiza un archivo en una branch (push de un solo archivo). */
|
|
328
|
+
async createOrUpdateFile(options, opts) {
|
|
329
|
+
let ref;
|
|
330
|
+
try {
|
|
331
|
+
const refName = options.branch.startsWith("refs/heads/") ? options.branch : `refs/heads/${options.branch}`;
|
|
332
|
+
const branchRes = await this.request("GET", `/git/repositories/${encodeURIComponent(options.repo)}/refs/${refName}`, { ...opts, project: options.project });
|
|
333
|
+
ref = branchRes.body?.value?.[0]?.objectId;
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
337
|
+
ref = undefined;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const exists = await this.fileExists(options.project, options.repo, options.branch, options.filePath, opts);
|
|
344
|
+
const isBuffer = Buffer.isBuffer(options.fileContent);
|
|
345
|
+
return this.request("POST", `/git/repositories/${encodeURIComponent(options.repo)}/pushes`, {
|
|
346
|
+
...opts,
|
|
347
|
+
project: options.project,
|
|
348
|
+
body: {
|
|
349
|
+
refUpdates: [
|
|
350
|
+
{
|
|
351
|
+
name: `refs/heads/${options.branch}`,
|
|
352
|
+
oldObjectId: !ref ? "0000000000000000000000000000000000000000" : ref
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
commits: [
|
|
356
|
+
{
|
|
357
|
+
comment: options.comment ?? "Automatic update",
|
|
358
|
+
changes: [
|
|
359
|
+
{
|
|
360
|
+
changeType: exists ? "edit" : "add",
|
|
361
|
+
item: { path: options.filePath },
|
|
362
|
+
newContent: {
|
|
363
|
+
content: isBuffer ? options.fileContent.toString("base64") : options.fileContent,
|
|
364
|
+
contentType: isBuffer ? "base64Encoded" : "rawtext"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Crea un archivo en un repo. Si el contenido es una de las plantillas
|
|
375
|
+
* conocidas (ej. `$README:TEMPLATE`), genera un README.md con el contenido
|
|
376
|
+
* por defecto.
|
|
377
|
+
*/
|
|
378
|
+
async createFileInRepo(project, repo, branch, fileName, filePath, fileContent, opts) {
|
|
379
|
+
const contentTemplates = {
|
|
380
|
+
"$README:TEMPLATE": { name: "README.md", type: "readme" }
|
|
381
|
+
};
|
|
382
|
+
if (contentTemplates[fileContent]) {
|
|
383
|
+
return this.createOrUpdateFile({
|
|
384
|
+
project,
|
|
385
|
+
repo,
|
|
386
|
+
branch,
|
|
387
|
+
filePath: `${filePath}/${contentTemplates[fileContent].name}`,
|
|
388
|
+
fileContent: `# ${repo}\n\nProject repository.`
|
|
389
|
+
}, opts);
|
|
390
|
+
}
|
|
391
|
+
return this.createOrUpdateFile({
|
|
392
|
+
project,
|
|
393
|
+
repo,
|
|
394
|
+
branch,
|
|
395
|
+
filePath: `${filePath}/${fileName}`.replace(/\/+/g, "/"),
|
|
396
|
+
fileContent,
|
|
397
|
+
comment: "Automated by prepare ci/cd process"
|
|
398
|
+
}, opts);
|
|
399
|
+
}
|
|
400
|
+
/** Crea un repositorio y lo inicializa con un README. */
|
|
401
|
+
async createAndInitRepository(projectName, projectId, repository, opts) {
|
|
402
|
+
const existing = await this.repoExists(projectId, repository, opts);
|
|
403
|
+
let repoData = existing;
|
|
404
|
+
if (!existing) {
|
|
405
|
+
const res = await this.createRepository(projectName, projectId, repository, opts);
|
|
406
|
+
repoData = res.body;
|
|
407
|
+
}
|
|
408
|
+
const repoId = repoData.id;
|
|
409
|
+
await this.createFileInRepo(projectId, repoId, "main", "README.md", "/", "$README:TEMPLATE", opts);
|
|
410
|
+
return repoData;
|
|
411
|
+
}
|
|
412
|
+
// =========================================================================
|
|
413
|
+
// Pushes — bulk push (multiple changes)
|
|
414
|
+
// =========================================================================
|
|
415
|
+
/** Hace un push con múltiples cambios (add/edit/delete) en un solo commit. */
|
|
416
|
+
async pushChanges(options, opts) {
|
|
417
|
+
let ref;
|
|
418
|
+
try {
|
|
419
|
+
const refName = options.branch.startsWith("refs/heads/") ? options.branch : `refs/heads/${options.branch}`;
|
|
420
|
+
const branchRes = await this.request("GET", `/git/repositories/${encodeURIComponent(options.repository)}/refs/${refName}`, {
|
|
421
|
+
...opts,
|
|
422
|
+
project: options.project
|
|
423
|
+
});
|
|
424
|
+
ref = branchRes.body?.value?.[0]?.objectId;
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
428
|
+
ref = undefined;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return this.request("POST", `/git/repositories/${encodeURIComponent(options.repository)}/pushes`, {
|
|
435
|
+
...opts,
|
|
436
|
+
project: options.project,
|
|
437
|
+
body: {
|
|
438
|
+
refUpdates: [
|
|
439
|
+
{
|
|
440
|
+
name: `refs/heads/${options.branch}`,
|
|
441
|
+
oldObjectId: ref
|
|
442
|
+
}
|
|
443
|
+
],
|
|
444
|
+
commits: [
|
|
445
|
+
{
|
|
446
|
+
comment: options.comment,
|
|
447
|
+
changes: options.changes
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// =========================================================================
|
|
454
|
+
// Templates — download + merge
|
|
455
|
+
// =========================================================================
|
|
456
|
+
/** Descarga un repositorio completo como buffer ZIP. */
|
|
457
|
+
async downloadRepositoryZip(options, opts) {
|
|
458
|
+
const path = options.scopePath ?? "/";
|
|
459
|
+
return this.request("GET", `/git/repositories/${encodeURIComponent(options.repository)}/items`, {
|
|
460
|
+
...opts,
|
|
461
|
+
project: options.project,
|
|
462
|
+
query: {
|
|
463
|
+
path,
|
|
464
|
+
"versionDescriptor[versionOptions]": 0,
|
|
465
|
+
"versionDescriptor[versionType]": 0,
|
|
466
|
+
"versionDescriptor[version]": options.branch,
|
|
467
|
+
resolveLfs: true,
|
|
468
|
+
"$format": "zip",
|
|
469
|
+
download: true,
|
|
470
|
+
...opts?.query
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// =========================================================================
|
|
475
|
+
// Webhooks
|
|
476
|
+
// =========================================================================
|
|
477
|
+
/** Verifica si existe un webhook de push para un repo/branch/URL dados. */
|
|
478
|
+
async webhookExists(projectId, repositoryId, branchName, webhookUrl, opts) {
|
|
479
|
+
try {
|
|
480
|
+
const res = await this.request("POST", "/hooks/subscriptionsQuery", {
|
|
481
|
+
...opts,
|
|
482
|
+
organizationLevel: true,
|
|
483
|
+
apiVersion: "7.2-preview.1",
|
|
484
|
+
body: {
|
|
485
|
+
publisherId: "tfs",
|
|
486
|
+
publisherInputFilters: [
|
|
487
|
+
{
|
|
488
|
+
conditions: [
|
|
489
|
+
{ inputId: "projectId", operator: 0, inputValue: projectId }
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
return res.body.results.find(e => e.publisherInputs.projectId === projectId
|
|
496
|
+
&& e.publisherInputs.repository === repositoryId
|
|
497
|
+
&& e.publisherInputs.branch === branchName
|
|
498
|
+
&& e.consumerInputs.url === webhookUrl);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Crea un webhook de push para un repositorio. */
|
|
505
|
+
createWebhook(projectId, repoId, sourceBranch, webhookUrl, opts) {
|
|
506
|
+
return this.request("POST", "/hooks/subscriptions", {
|
|
507
|
+
...opts,
|
|
508
|
+
organizationLevel: true,
|
|
509
|
+
apiVersion: "7.2-preview.1",
|
|
510
|
+
body: {
|
|
511
|
+
consumerActionId: "httpRequest",
|
|
512
|
+
consumerId: "webHooks",
|
|
513
|
+
consumerInputs: { url: webhookUrl },
|
|
514
|
+
eventType: "git.push",
|
|
515
|
+
publisherId: "tfs",
|
|
516
|
+
publisherInputs: {
|
|
517
|
+
repository: repoId,
|
|
518
|
+
branch: sourceBranch,
|
|
519
|
+
pushedBy: "",
|
|
520
|
+
projectId
|
|
521
|
+
},
|
|
522
|
+
resourceVersion: "1.0",
|
|
523
|
+
scope: 1
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
// =========================================================================
|
|
528
|
+
// Environments
|
|
529
|
+
// =========================================================================
|
|
530
|
+
/** Crea un environment en un proyecto. */
|
|
531
|
+
createEnvironment(project, name, description = "", opts) {
|
|
532
|
+
return this.request("POST", "/distributedtask/environments", {
|
|
533
|
+
...opts,
|
|
534
|
+
project: project ?? opts?.project,
|
|
535
|
+
apiVersion: "5.2-preview.1",
|
|
536
|
+
body: { name, description }
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/** Busca un grupo de identidades por nombre (SAM account name). */
|
|
540
|
+
async findIdentityGroup(groupName, opts) {
|
|
541
|
+
const res = await this.request("POST", "/IdentityPicker/Identities", {
|
|
542
|
+
...opts,
|
|
543
|
+
organizationLevel: true,
|
|
544
|
+
apiVersion: "5.0-preview.1",
|
|
545
|
+
body: {
|
|
546
|
+
query: groupName,
|
|
547
|
+
identityTypes: ["group"],
|
|
548
|
+
operationScopes: ["ims", "source"],
|
|
549
|
+
options: { MinResults: 1, MaxResults: 20 },
|
|
550
|
+
properties: ["DisplayName", "SamAccountName", "SubjectDescriptor"]
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
const group = res.body.results[0].identities.find(g => g.samAccountName === groupName);
|
|
554
|
+
if (!group) {
|
|
555
|
+
throw new Error(`Group '${groupName}' not found.`);
|
|
556
|
+
}
|
|
557
|
+
return group;
|
|
558
|
+
}
|
|
559
|
+
/** Crea una approval check para un environment. */
|
|
560
|
+
createEnvironmentApproval(project, environmentId, environmentName, approver, opts) {
|
|
561
|
+
return this.request("POST", "/pipelines/checks/configurations", {
|
|
562
|
+
...opts,
|
|
563
|
+
project: project ?? opts?.project,
|
|
564
|
+
apiVersion: "5.2-preview.1",
|
|
565
|
+
body: {
|
|
566
|
+
type: { id: "8C6F20A7-A545-4486-9777-F762FAFE0D4D", name: "Approval" },
|
|
567
|
+
settings: {
|
|
568
|
+
approvers: [
|
|
569
|
+
{
|
|
570
|
+
displayName: approver.displayName,
|
|
571
|
+
id: approver.localId,
|
|
572
|
+
descriptor: approver.subjectDescriptor,
|
|
573
|
+
imageUrl: approver.imageUrl ?? "",
|
|
574
|
+
uniqueName: approver.uniqueName ?? ""
|
|
575
|
+
}
|
|
576
|
+
],
|
|
577
|
+
executionOrder: 1,
|
|
578
|
+
instructions: "",
|
|
579
|
+
blockedApprovers: [],
|
|
580
|
+
minRequiredApprovers: 0,
|
|
581
|
+
requesterCannotBeApprover: false,
|
|
582
|
+
definitionRef: {
|
|
583
|
+
id: "26014962-64a0-49f4-885b-4b874119a5cc"
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
resource: { type: "environment", id: environmentId, name: environmentName },
|
|
587
|
+
timeout: 43200
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Crea un environment con approvals.
|
|
593
|
+
* Si `approverGroup` es `"none"`, no agrega approvals.
|
|
594
|
+
*/
|
|
595
|
+
async createEnvironmentWithApprovals(options, opts) {
|
|
596
|
+
const envRes = await this.createEnvironment(options.project, options.environmentName, options.description ?? "", opts);
|
|
597
|
+
const environment = envRes.body;
|
|
598
|
+
if (options.approverGroup === "none")
|
|
599
|
+
return environment;
|
|
600
|
+
const group = await this.findIdentityGroup(options.approverGroup, opts);
|
|
601
|
+
await this.createEnvironmentApproval(options.project, environment.id, options.environmentName, group, opts);
|
|
602
|
+
return environment;
|
|
603
|
+
}
|
|
604
|
+
// =========================================================================
|
|
605
|
+
// Permissions — committer regex + deny
|
|
606
|
+
// =========================================================================
|
|
607
|
+
/** Crea una política de validación de email del committer. */
|
|
608
|
+
applyCommitterRegexPermissions(projectId, opts) {
|
|
609
|
+
return this.request("POST", "/policy/Configurations", {
|
|
610
|
+
...opts,
|
|
611
|
+
apiVersion: "5.0-preview.1",
|
|
612
|
+
body: {
|
|
613
|
+
type: { id: "77ed4bd3-b063-4689-934a-175e4d0a78d7" },
|
|
614
|
+
revision: 1,
|
|
615
|
+
isDeleted: false,
|
|
616
|
+
isBlocking: true,
|
|
617
|
+
isEnabled: true,
|
|
618
|
+
settings: {
|
|
619
|
+
authorEmailPatterns: null,
|
|
620
|
+
scope: [{ repositoryId: null }]
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/** Actualiza una política de validación de email del committer. */
|
|
626
|
+
updateCommitterRegexPermissions(projectId, policyId, allowedEmailPatterns, opts) {
|
|
627
|
+
return this.request("POST", `/policy/Configurations/${policyId}`, {
|
|
628
|
+
...opts,
|
|
629
|
+
apiVersion: "5.0-preview.1",
|
|
630
|
+
body: {
|
|
631
|
+
isEnabled: true,
|
|
632
|
+
isBlocking: true,
|
|
633
|
+
isDeleted: false,
|
|
634
|
+
settings: {
|
|
635
|
+
authorEmailPatterns: allowedEmailPatterns,
|
|
636
|
+
scope: [{ repositoryId: null }]
|
|
637
|
+
},
|
|
638
|
+
revision: 3,
|
|
639
|
+
id: policyId,
|
|
640
|
+
type: {
|
|
641
|
+
id: "77ed4bd3-b063-4689-934a-175e4d0a78d7",
|
|
642
|
+
displayName: "Commit author email validation"
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/** Obtiene los grupos con permisos de contribuidor del proyecto. */
|
|
648
|
+
async getContributorGroups(projectId, projectName, opts) {
|
|
649
|
+
const res = await this.request("POST", "/Contribution/HierarchyQuery", {
|
|
650
|
+
...opts,
|
|
651
|
+
organizationLevel: true,
|
|
652
|
+
apiVersion: "5.0-preview.1",
|
|
653
|
+
body: {
|
|
654
|
+
contributionIds: ["ms.vss-admin-web.security-view-members-data-provider"],
|
|
655
|
+
dataProviderContext: {
|
|
656
|
+
properties: {
|
|
657
|
+
permissionSetId: "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87",
|
|
658
|
+
permissionSetToken: `repoV2/${projectId}/`,
|
|
659
|
+
sourcePage: {
|
|
660
|
+
url: `https://dev.azure.com/${this.baseUrl.split("/").pop()}/${encodeURIComponent(projectName)}/_settings/repositories?_a=permissions`,
|
|
661
|
+
routeId: "ms.vss-admin-web.project-admin-hub-route",
|
|
662
|
+
routeValues: {
|
|
663
|
+
project: projectName,
|
|
664
|
+
adminPivot: "repositories",
|
|
665
|
+
controller: "ContributedPage",
|
|
666
|
+
action: "Execute"
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
return res.body.dataProviders["ms.vss-admin-web.security-view-members-data-provider"].identities;
|
|
674
|
+
}
|
|
675
|
+
/** Deniega permisos a una entidad por SID. */
|
|
676
|
+
denyPermission(projectId, entityId, permissionCode, opts) {
|
|
677
|
+
return this.request("POST", "/AccessControlEntries/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87", {
|
|
678
|
+
...opts,
|
|
679
|
+
organizationLevel: true,
|
|
680
|
+
apiVersion: "5.0-preview.1",
|
|
681
|
+
body: {
|
|
682
|
+
token: `repoV2/${projectId}/`,
|
|
683
|
+
merge: true,
|
|
684
|
+
accessControlEntries: [
|
|
685
|
+
{
|
|
686
|
+
descriptor: `Microsoft.TeamFoundation.Identity;${entityId}`,
|
|
687
|
+
allow: 0,
|
|
688
|
+
deny: permissionCode,
|
|
689
|
+
extendedInfo: {
|
|
690
|
+
effectiveAllow: 0,
|
|
691
|
+
effectiveDeny: permissionCode,
|
|
692
|
+
inheritedAllow: 0,
|
|
693
|
+
inheritedDeny: permissionCode
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** Deniega permisos a un usuario por email. */
|
|
701
|
+
denyPermissionByEmail(projectId, userEmail, permissionCode, opts) {
|
|
702
|
+
return this.request("POST", "/AccessControlEntries/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87", {
|
|
703
|
+
...opts,
|
|
704
|
+
organizationLevel: true,
|
|
705
|
+
apiVersion: "5.0-preview.1",
|
|
706
|
+
body: {
|
|
707
|
+
token: `repoV2/${projectId}/`,
|
|
708
|
+
merge: true,
|
|
709
|
+
accessControlEntries: [
|
|
710
|
+
{
|
|
711
|
+
descriptor: `Microsoft.IdentityModel.Claims.ClaimsIdentity;e95d19cb-8725-4b0b-8ce2-ff42be9ae6e9\\\\${userEmail}`,
|
|
712
|
+
allow: 0,
|
|
713
|
+
deny: permissionCode,
|
|
714
|
+
extendedInfo: {
|
|
715
|
+
effectiveAllow: 0,
|
|
716
|
+
effectiveDeny: permissionCode,
|
|
717
|
+
inheritedAllow: 0,
|
|
718
|
+
inheritedDeny: permissionCode
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Aplica permisos a todos los proyectos: crea la política de regex,
|
|
727
|
+
* obtiene los grupos, y deniega los permisos de Force Push y Create Branch.
|
|
728
|
+
*/
|
|
729
|
+
async applyPermissions(opts) {
|
|
730
|
+
const projectsRes = await this.listProjects(opts);
|
|
731
|
+
const projects = projectsRes.body.value;
|
|
732
|
+
const summary = [];
|
|
733
|
+
for (const project of projects) {
|
|
734
|
+
const policyRes = await this.applyCommitterRegexPermissions(project.id, opts);
|
|
735
|
+
const policyId = policyRes.body;
|
|
736
|
+
await this.updateCommitterRegexPermissions(project.id, policyId, [], opts);
|
|
737
|
+
const groups = await this.getContributorGroups(project.id, project.name, opts);
|
|
738
|
+
for (const group of groups) {
|
|
739
|
+
if (group.sid) {
|
|
740
|
+
await this.denyPermission(project.id, group.sid, 32768, opts);
|
|
741
|
+
await this.denyPermission(project.id, group.sid, 128, opts);
|
|
742
|
+
}
|
|
743
|
+
else if (group.metaType === "member") {
|
|
744
|
+
if (group.sid) {
|
|
745
|
+
await this.denyPermission(project.id, group.sid, 32768, opts);
|
|
746
|
+
}
|
|
747
|
+
if (group.mailAddress) {
|
|
748
|
+
await this.denyPermissionByEmail(project.id, group.mailAddress, 128, opts);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
summary.push({ project: project.name, groupsProcessed: groups.length });
|
|
753
|
+
}
|
|
754
|
+
return summary;
|
|
755
|
+
}
|
|
756
|
+
// =========================================================================
|
|
757
|
+
// Agent Pools
|
|
758
|
+
// =========================================================================
|
|
759
|
+
/** Asocia un agent pool existente a un proyecto (crea una queue). */
|
|
760
|
+
async addAgentPoolToProject(project, agentPoolName, opts) {
|
|
761
|
+
const poolsRes = await this.request("GET", "/distributedtask/pools", {
|
|
762
|
+
...opts,
|
|
763
|
+
organizationLevel: true,
|
|
764
|
+
apiVersion: "7.1-preview.1"
|
|
765
|
+
});
|
|
766
|
+
const pool = poolsRes.body.value.find(p => p.name === agentPoolName);
|
|
767
|
+
if (!pool) {
|
|
768
|
+
throw new Error(`Agent Pool '${agentPoolName}' not found.`);
|
|
769
|
+
}
|
|
770
|
+
return this.request("POST", "/distributedtask/queues", {
|
|
771
|
+
...opts,
|
|
772
|
+
project: project ?? opts?.project,
|
|
773
|
+
apiVersion: "7.1-preview.1",
|
|
774
|
+
query: { authorizePipelines: true, ...opts?.query },
|
|
775
|
+
body: { name: agentPoolName, pool: { id: pool.id } }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/** Autoriza todos los pipelines para usar una queue. */
|
|
779
|
+
authorizeAgentPool(project, queueId, opts) {
|
|
780
|
+
return this.request("PATCH", `/pipelines/pipelinePermissions/queue/${queueId}`, {
|
|
781
|
+
...opts,
|
|
782
|
+
project: project ?? opts?.project,
|
|
783
|
+
apiVersion: "7.1-preview.1",
|
|
784
|
+
body: {
|
|
785
|
+
resource: { type: "queue", id: String(queueId) },
|
|
786
|
+
allPipelines: { authorized: true, authorizedBy: null, authorizedOn: null },
|
|
787
|
+
pipelines: []
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
// =========================================================================
|
|
792
|
+
// Variable Groups
|
|
793
|
+
// =========================================================================
|
|
794
|
+
/** Crea un variable group. Si ya existe con el mismo nombre, lo devuelve sin crear. */
|
|
795
|
+
async createVariableGroup(project, name, variables = {}, opts) {
|
|
796
|
+
const existingRes = await this.request("GET", "/distributedtask/variablegroups", { ...opts, project: project ?? opts?.project, apiVersion: "7.1-preview.1" });
|
|
797
|
+
const existing = existingRes.body.value.find(vg => vg.name === name);
|
|
798
|
+
if (existing)
|
|
799
|
+
return existing;
|
|
800
|
+
const res = await this.request("POST", "/distributedtask/variablegroups", {
|
|
801
|
+
...opts,
|
|
802
|
+
project: project ?? opts?.project,
|
|
803
|
+
apiVersion: "7.1-preview.1",
|
|
804
|
+
body: { name, variables }
|
|
805
|
+
});
|
|
806
|
+
return res.body;
|
|
807
|
+
}
|
|
808
|
+
// =========================================================================
|
|
809
|
+
// Build Folders
|
|
810
|
+
// =========================================================================
|
|
811
|
+
/** Crea una carpeta de build. Si ya existe, la devuelve. */
|
|
812
|
+
async createBuildFolder(project, folderPath, opts) {
|
|
813
|
+
const normalizedPath = folderPath.startsWith("\\") ? folderPath : `\\${folderPath}`;
|
|
814
|
+
const foldersRes = await this.request("GET", "/build/folders", { ...opts, project: project ?? opts?.project, apiVersion: "7.2-preview.2" });
|
|
815
|
+
const existing = foldersRes.body.value.find(f => f.path === normalizedPath);
|
|
816
|
+
if (existing)
|
|
817
|
+
return existing;
|
|
818
|
+
const encodedPath = encodeURIComponent(normalizedPath);
|
|
819
|
+
const res = await this.request("PUT", `/build/folders?path=${encodedPath}`, {
|
|
820
|
+
...opts,
|
|
821
|
+
project: project ?? opts?.project,
|
|
822
|
+
apiVersion: "6.0-preview.2",
|
|
823
|
+
body: { path: normalizedPath }
|
|
824
|
+
});
|
|
825
|
+
return res.body;
|
|
826
|
+
}
|
|
827
|
+
// =========================================================================
|
|
828
|
+
// Pipelines — creation
|
|
829
|
+
// =========================================================================
|
|
830
|
+
/** Crea un pipeline YAML asociado a un repositorio. */
|
|
831
|
+
async createPipeline(options, opts) {
|
|
832
|
+
const reposRes = await this.listRepos({ ...opts, project: options.project });
|
|
833
|
+
const repo = reposRes.body.value.find(r => r.name === options.repository);
|
|
834
|
+
if (!repo) {
|
|
835
|
+
throw new Error(`Repository '${options.repository}' not found.`);
|
|
836
|
+
}
|
|
837
|
+
return this.request("POST", "/pipelines", {
|
|
838
|
+
...opts,
|
|
839
|
+
project: options.project,
|
|
840
|
+
apiVersion: "7.1-preview.1",
|
|
841
|
+
body: {
|
|
842
|
+
name: options.name,
|
|
843
|
+
folder: options.folder ?? "\\",
|
|
844
|
+
configuration: {
|
|
845
|
+
type: "yaml",
|
|
846
|
+
path: options.yamlPath,
|
|
847
|
+
repository: {
|
|
848
|
+
id: repo.id,
|
|
849
|
+
type: "azureReposGit",
|
|
850
|
+
name: repo.name,
|
|
851
|
+
defaultBranch: options.defaultBranch ?? "refs/heads/main"
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
// =========================================================================
|
|
858
|
+
// Service Connections
|
|
859
|
+
// =========================================================================
|
|
860
|
+
/** Comparte un service endpoint con un proyecto. */
|
|
861
|
+
async shareServiceConnection(project, resourceId, resourceName, opts) {
|
|
862
|
+
const projectRes = await this.getProject(project, { ...opts, organizationLevel: true });
|
|
863
|
+
if (!projectRes.body) {
|
|
864
|
+
throw new Error(`Project '${project}' not found.`);
|
|
865
|
+
}
|
|
866
|
+
return this.request("PATCH", `/serviceendpoint/endpoints/${resourceId}`, {
|
|
867
|
+
...opts,
|
|
868
|
+
organizationLevel: true,
|
|
869
|
+
apiVersion: "6.0-preview.4",
|
|
870
|
+
body: [
|
|
871
|
+
{
|
|
872
|
+
description: "",
|
|
873
|
+
name: `${resourceName}-${projectRes.body.name}`,
|
|
874
|
+
projectReference: { id: projectRes.body.id, name: projectRes.body.name }
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
// =========================================================================
|
|
880
|
+
// Tags
|
|
881
|
+
// =========================================================================
|
|
882
|
+
/** Lista todas las tags de un repositorio. */
|
|
883
|
+
async getTags(project, repository, opts) {
|
|
884
|
+
const res = await this.request("GET", `/git/repositories/${encodeURIComponent(repository)}/refs`, {
|
|
885
|
+
...opts,
|
|
886
|
+
project: project ?? opts?.project,
|
|
887
|
+
query: { filter: "tags/", ...opts?.query }
|
|
888
|
+
});
|
|
889
|
+
return res.body.value.map(tag => ({
|
|
890
|
+
name: tag.name.replace("refs/tags/", ""),
|
|
891
|
+
ref: tag.name,
|
|
892
|
+
objectId: tag.objectId,
|
|
893
|
+
creator: tag.creator,
|
|
894
|
+
peeledObjectId: tag.peeledObjectId
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
245
897
|
}
|
|
246
898
|
exports.AzureDevOpsApi = AzureDevOpsApi;
|
|
247
899
|
//# sourceMappingURL=azdo-api.js.map
|