From 2932e8859870972937b8478907e3f8a55f549a9d Mon Sep 17 00:00:00 2001
From: Matt Stevens <matt@mattstevens.co.uk>
Date: Tue, 22 Sep 2020 16:18:35 +0100
Subject: [PATCH] feat(go): add support for github enterprise in go datasource
 (#7252)

---
 .../__snapshots__/index.spec.ts.snap          | 32 +++++++++++++
 lib/datasource/github-tags/index.spec.ts      | 15 ++++++
 lib/datasource/github-tags/index.ts           | 13 ++++-
 .../go/__snapshots__/index.spec.ts.snap       | 27 +++++++++++
 lib/datasource/go/index.spec.ts               | 48 +++++++++++++++++++
 lib/datasource/go/index.ts                    | 30 +++++++++++-
 6 files changed, 162 insertions(+), 3 deletions(-)

diff --git a/lib/datasource/github-tags/__snapshots__/index.spec.ts.snap b/lib/datasource/github-tags/__snapshots__/index.spec.ts.snap
index f1aae20671..dc29fe0500 100644
--- a/lib/datasource/github-tags/__snapshots__/index.spec.ts.snap
+++ b/lib/datasource/github-tags/__snapshots__/index.spec.ts.snap
@@ -138,3 +138,35 @@ Array [
   },
 ]
 `;
+
+exports[`datasource/github-tags getReleases supports ghe 1`] = `
+Object {
+  "releases": Array [
+    Object {
+      "gitRef": "v1.0.0",
+      "version": "v1.0.0",
+    },
+    Object {
+      "gitRef": "v1.1.0",
+      "version": "v1.1.0",
+    },
+  ],
+  "sourceUrl": "https://git.enterprise.com/some/dep2",
+}
+`;
+
+exports[`datasource/github-tags getReleases supports ghe 2`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/vnd.github.v3+json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "token some-token",
+      "host": "git.enterprise.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://git.enterprise.com/api/v3/repos/some/dep2/tags?per_page=100",
+  },
+]
+`;
\ No newline at end of file
diff --git a/lib/datasource/github-tags/index.spec.ts b/lib/datasource/github-tags/index.spec.ts
index 672126c8bc..e605046033 100644
--- a/lib/datasource/github-tags/index.spec.ts
+++ b/lib/datasource/github-tags/index.spec.ts
@@ -7,6 +7,7 @@ jest.mock('../../util/host-rules');
 const hostRules: any = _hostRules;
 
 const githubApiHost = 'https://api.github.com';
+const githubEnterpriseApiHost = 'https://git.enterprise.com';
 
 describe('datasource/github-tags', () => {
   beforeEach(() => {
@@ -102,5 +103,19 @@ describe('datasource/github-tags', () => {
       expect(res.releases).toHaveLength(2);
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
+
+    it('supports ghe', async () => {
+      const body = [{ name: 'v1.0.0' }, { name: 'v1.1.0' }];
+      httpMock
+        .scope(githubEnterpriseApiHost)
+        .get(`/api/v3/repos/${depName}/tags?per_page=100`)
+        .reply(200, body);
+      const res = await github.getReleases({
+        registryUrl: 'https://git.enterprise.com',
+        lookupName: depName,
+      });
+      expect(res).toMatchSnapshot();
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
   });
 });
diff --git a/lib/datasource/github-tags/index.ts b/lib/datasource/github-tags/index.ts
index 281dd34c3a..921d3f6587 100644
--- a/lib/datasource/github-tags/index.ts
+++ b/lib/datasource/github-tags/index.ts
@@ -1,3 +1,4 @@
+import URL from 'url';
 import { logger } from '../../logger';
 import * as packageCache from '../../util/cache/package';
 import { GithubHttp } from '../../util/http/github';
@@ -119,6 +120,7 @@ export async function getDigest(
  *  - Return a dependency object containing sourceUrl string and releases array
  */
 export async function getReleases({
+  registryUrl: depHost,
   lookupName: repo,
 }: GetReleasesConfig): Promise<ReleaseResult | null> {
   const cachedResult = await packageCache.get<ReleaseResult>(
@@ -129,8 +131,15 @@ export async function getReleases({
   if (cachedResult) {
     return cachedResult;
   }
+
+  // default to GitHub.com if no GHE host is specified.
+  const sourceUrlBase = depHost ?? `https://github.com/`;
+  const apiBaseUrl = depHost
+    ? URL.resolve(depHost, 'api/v3/')
+    : `https://api.github.com/`;
+
   // tag
-  const url = `https://api.github.com/repos/${repo}/tags?per_page=100`;
+  const url = URL.resolve(apiBaseUrl, `repos/${repo}/tags?per_page=100`);
   type GitHubTag = {
     name: string;
   }[];
@@ -141,7 +150,7 @@ export async function getReleases({
     })
   ).body.map((o) => o.name);
   const dependency: ReleaseResult = {
-    sourceUrl: 'https://github.com/' + repo,
+    sourceUrl: URL.resolve(sourceUrlBase, repo),
     releases: null,
   };
   dependency.releases = versions.map((version) => ({
diff --git a/lib/datasource/go/__snapshots__/index.spec.ts.snap b/lib/datasource/go/__snapshots__/index.spec.ts.snap
index 9789204307..9614386770 100644
--- a/lib/datasource/go/__snapshots__/index.spec.ts.snap
+++ b/lib/datasource/go/__snapshots__/index.spec.ts.snap
@@ -166,6 +166,33 @@ Array [
 ]
 `;
 
+exports[`datasource/go getReleases support ghe 1`] = `
+Object {
+  "releases": Array [
+    Object {
+      "version": "v1.0.0",
+    },
+    Object {
+      "version": "v2.0.0",
+    },
+  ],
+}
+`;
+
+exports[`datasource/go getReleases support ghe 2`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept-encoding": "gzip, deflate",
+      "host": "git.enterprise.com",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://git.enterprise.com/example/module?go-get=1",
+  },
+]
+`;
+
 exports[`datasource/go getReleases works for known servers 1`] = `
 Array [
   Array [
diff --git a/lib/datasource/go/index.spec.ts b/lib/datasource/go/index.spec.ts
index eacac1a4f1..0f07bec49f 100644
--- a/lib/datasource/go/index.spec.ts
+++ b/lib/datasource/go/index.spec.ts
@@ -24,6 +24,20 @@ Nothing to see here; <a href="https://godoc.org/golang.org/x/text">move along</a
 </body>
 </html>`;
 
+const resGitHubEnterprise = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+
+<title>Go remote import path metadata</title>
+<meta name="go-import" content="git.enterprise.com/example/module git https://git.enterprise.com/example/module.git">
+</head>
+
+<body>
+<!-- Metadata for Go remote import path -->
+</body>
+</html>`;
+
 describe('datasource/go', () => {
   beforeEach(() => {
     httpMock.setup();
@@ -145,6 +159,40 @@ describe('datasource/go', () => {
       expect(res).toBeDefined();
       expect(httpMock.getTrace()).toMatchSnapshot();
     });
+    it('support ghe', async () => {
+      httpMock
+        .scope('https://git.enterprise.com/')
+        .get('/example/module?go-get=1')
+        .reply(200, resGitHubEnterprise);
+      github.getReleases.mockResolvedValueOnce({
+        releases: [{ version: 'v1.0.0' }, { version: 'v2.0.0' }],
+      });
+      const res = await getPkgReleases({
+        datasource,
+        depName: 'git.enterprise.com/example/module',
+      });
+      expect(res).toMatchSnapshot();
+      expect(res).not.toBeNull();
+      expect(res).toBeDefined();
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
+    it('returns null for go-import prefix mismatch', async () => {
+      httpMock
+        .scope('https://git.enterprise.com/')
+        .get('/example/module?go-get=1')
+        .reply(
+          200,
+          resGitHubEnterprise.replace(
+            'git.enterprise.com/example/module',
+            'git.enterprise.com/badexample/badmodule'
+          )
+        );
+      const res = await getPkgReleases({
+        datasource,
+        depName: 'git.enterprise.com/example/module',
+      });
+      expect(res).toBeNull();
+    });
     it('skips wrong package', async () => {
       httpMock
         .scope('https://golang.org/')
diff --git a/lib/datasource/go/index.ts b/lib/datasource/go/index.ts
index 75348d3634..e249819088 100644
--- a/lib/datasource/go/index.ts
+++ b/lib/datasource/go/index.ts
@@ -1,3 +1,4 @@
+import URL from 'url';
 import { logger } from '../../logger';
 import { Http } from '../../util/http';
 import { regEx } from '../../util/regex';
@@ -35,6 +36,7 @@ async function getDatasource(goModule: string): Promise<DataSource | null> {
       lookupName,
     };
   }
+
   const pkgUrl = `https://${goModule}?go-get=1`;
   const res = (await http.get(pkgUrl)).body;
   const sourceMatch = regEx(
@@ -64,7 +66,33 @@ async function getDatasource(goModule: string): Promise<DataSource | null> {
       };
     }
   } else {
-    logger.trace({ goModule }, 'No go-source header found');
+    // GitHub Enterprise only returns a go-import meta
+    const importMatch = regEx(
+      `<meta\\s+name="go-import"\\s+content="([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)">`
+    ).exec(res);
+    if (importMatch) {
+      const [, prefix, , goImportURL] = importMatch;
+      if (!goModule.startsWith(prefix)) {
+        logger.trace({ goModule }, 'go-import header prefix not match');
+        return null;
+      }
+      logger.debug({ goModule, goImportURL }, 'Go lookup import url');
+
+      // get server base url from import url
+      const parsedUrl = URL.parse(goImportURL);
+
+      // split the go module from the URL: host/go/module -> go/module
+      const split = goModule.split('/');
+      const lookupName = split[1] + '/' + split[2];
+
+      return {
+        datasource: github.id,
+        registryUrl: `${parsedUrl.protocol}//${parsedUrl.host}`,
+        lookupName,
+      };
+    }
+
+    logger.trace({ goModule }, 'No go-source or go-import header found');
   }
   return null;
 }
-- 
GitLab