@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.
@@ -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