From 2ed30c076431a293a617b8682c5ef0efa5f97662 Mon Sep 17 00:00:00 2001
From: Tom Fay <tom@teamfay.co.uk>
Date: Wed, 15 Feb 2023 06:14:47 +0000
Subject: [PATCH] fix(manager/azure-pipelines): Enable azure repository
 extraction (#20380)

---
 .../manager/azure-pipelines/extract.spec.ts   | 52 +++++++++++++++++++
 .../manager/azure-pipelines/extract.ts        | 36 ++++++++++++-
 2 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/lib/modules/manager/azure-pipelines/extract.spec.ts b/lib/modules/manager/azure-pipelines/extract.spec.ts
index 82ff78295a..84b198a997 100644
--- a/lib/modules/manager/azure-pipelines/extract.spec.ts
+++ b/lib/modules/manager/azure-pipelines/extract.spec.ts
@@ -1,4 +1,5 @@
 import { Fixtures } from '../../../../test/fixtures';
+import { GlobalConfig } from '../../../config/global';
 import { AzurePipelinesTasksDatasource } from '../../datasource/azure-pipelines-tasks';
 import {
   extractAzurePipelinesTasks,
@@ -19,6 +20,10 @@ const azurePipelinesJobs = Fixtures.get('azure-pipelines-jobs.yaml');
 const azurePipelinesSteps = Fixtures.get('azure-pipelines-steps.yaml');
 
 describe('modules/manager/azure-pipelines/extract', () => {
+  afterEach(() => {
+    GlobalConfig.reset();
+  });
+
   it('should parse a valid azure-pipelines file', () => {
     const file = parseAzurePipelines(azurePipelines, azurePipelinesFilename);
     expect(file).not.toBeNull();
@@ -72,6 +77,53 @@ describe('modules/manager/azure-pipelines/extract', () => {
         })
       ).toBeNull();
     });
+
+    it('should extract Azure repository information if project in name', () => {
+      GlobalConfig.set({
+        platform: 'azure',
+        endpoint: 'https://dev.azure.com/renovate-org',
+      });
+
+      expect(
+        extractRepository({
+          type: 'git',
+          name: 'project/repo',
+          ref: 'refs/tags/v1.0.0',
+        })
+      ).toMatchObject({
+        depName: 'project/repo',
+        packageName: 'https://dev.azure.com/renovate-org/project/_git/repo',
+      });
+    });
+
+    it('should return null if repository type is git and project not in name', () => {
+      GlobalConfig.set({
+        platform: 'azure',
+        endpoint: 'https://dev.azure.com/renovate-org',
+      });
+
+      expect(
+        extractRepository({
+          type: 'git',
+          name: 'repo',
+          ref: 'refs/tags/v1.0.0',
+        })
+      ).toBeNull();
+    });
+
+    it('should extract return null for git repo type if platform not Azure', () => {
+      GlobalConfig.set({
+        platform: 'github',
+      });
+
+      expect(
+        extractRepository({
+          type: 'git',
+          name: 'project/repo',
+          ref: 'refs/tags/v1.0.0',
+        })
+      ).toBeNull();
+    });
   });
 
   describe('extractContainer()', () => {
diff --git a/lib/modules/manager/azure-pipelines/extract.ts b/lib/modules/manager/azure-pipelines/extract.ts
index 50cfc49fc4..512ca41124 100644
--- a/lib/modules/manager/azure-pipelines/extract.ts
+++ b/lib/modules/manager/azure-pipelines/extract.ts
@@ -1,7 +1,9 @@
 import { load } from 'js-yaml';
+import { GlobalConfig } from '../../../config/global';
 import { logger } from '../../../logger';
 import { coerceArray } from '../../../util/array';
 import { regEx } from '../../../util/regex';
+import { joinUrlParts } from '../../../util/url';
 import { AzurePipelinesTasksDatasource } from '../../datasource/azure-pipelines-tasks';
 import { GitTagsDatasource } from '../../datasource/git-tags';
 import { getDep } from '../dockerfile/extract';
@@ -13,7 +15,37 @@ const AzurePipelinesTaskRegex = regEx(/^(?<name>[^@]+)@(?<version>.*)$/);
 export function extractRepository(
   repository: Repository
 ): PackageDependency | null {
-  if (repository.type !== 'github') {
+  let repositoryUrl = null;
+
+  if (repository.type === 'github') {
+    repositoryUrl = `https://github.com/${repository.name}.git`;
+  } else if (repository.type === 'git') {
+    // "git" type indicates an AzureDevOps repository.
+    // The repository URL is only deducible if we are running on AzureDevOps (so can use the endpoint)
+    // and the name is of the form `Project/Repository`.
+    // The name could just be the repository name, in which case AzureDevOps defaults to the
+    // same project, which is not currently accessible here. It could be deduced later by exposing
+    // the repository URL to managers.
+    // https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/resources-repositories-repository?view=azure-pipelines#types
+    const { platform, endpoint } = GlobalConfig.get();
+    if (platform === 'azure' && endpoint) {
+      if (repository.name.includes('/')) {
+        const [projectName, repoName] = repository.name.split('/');
+        repositoryUrl = joinUrlParts(
+          endpoint,
+          encodeURIComponent(projectName),
+          '_git',
+          encodeURIComponent(repoName)
+        );
+      } else {
+        logger.debug(
+          'Renovate cannot update repositories that do not include the project name'
+        );
+      }
+    }
+  }
+
+  if (repositoryUrl === null) {
     return null;
   }
 
@@ -27,7 +59,7 @@ export function extractRepository(
     datasource: GitTagsDatasource.id,
     depName: repository.name,
     depType: 'gitTags',
-    packageName: `https://github.com/${repository.name}.git`,
+    packageName: repositoryUrl,
     replaceString: repository.ref,
   };
 }
-- 
GitLab