From a54cc479500642a23bf877865d6b973b49dd2ffc Mon Sep 17 00:00:00 2001
From: Gabriel-Ladzaretti
 <97394622+Gabriel-Ladzaretti@users.noreply.github.com>
Date: Mon, 22 Aug 2022 23:16:00 +0300
Subject: [PATCH] feat(repo/changelogs): allow user configuration of source url
 (#16873)

---
 docs/usage/configuration-options.md           | 24 ++++++++
 lib/config/options/index.ts                   | 10 ++++
 lib/config/types.ts                           |  1 +
 .../update/pr/changelog/common.spec.ts        | 14 +++++
 .../repository/update/pr/changelog/common.ts  |  7 +++
 .../update/pr/changelog/github.spec.ts        | 59 +++++++++++++++++++
 .../update/pr/changelog/gitlab.spec.ts        | 33 +++++++++++
 .../repository/update/pr/changelog/index.ts   |  6 +-
 .../update/pr/changelog/source-github.ts      |  4 +-
 .../update/pr/changelog/source-gitlab.ts      |  4 +-
 10 files changed, 157 insertions(+), 5 deletions(-)
 create mode 100644 lib/workers/repository/update/pr/changelog/common.spec.ts
 create mode 100644 lib/workers/repository/update/pr/changelog/common.ts

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 1fdb5949fd..78a95f31b0 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1876,6 +1876,30 @@ For example to apply a special label for Major updates:
 }
 ```
 
+### customChangelogUrl
+
+Use this field to set the source URL for a package, including overriding an existing one.
+Source URLs are necessary in order to look up release notes.
+
+Using this field we can specify the exact url to fetch release notes from.
+
+Example setting source URL for package "dummy":
+
+```json
+{
+  "packageRules": [
+    {
+      "matchPackageNames": ["dummy"],
+      "customChangelogUrl": "https://github.com/org/dummy"
+    }
+  ]
+}
+```
+
+<!-- prettier-ignore -->
+!!! note
+Renovate can fetch changelogs from GitHub and GitLab platforms only, and setting the URL to an unsupported host/platform type won't change that.
+
 ### replacementName
 
 This config option only works with the `npm` manager.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index a8736faccc..a744bc9dde 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -1185,6 +1185,16 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'customChangelogUrl',
+    description:
+      'If set, Renovate will use this url to fetch changelogs for a matched dependency. Valid only within a `packageRules` object.',
+    type: 'string',
+    stage: 'pr',
+    parent: 'packageRules',
+    cli: false,
+    env: false,
+  },
   {
     name: 'pinDigests',
     description: 'Whether to add digests to Dockerfile source images.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 65ccc58323..a955ec2ee7 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -34,6 +34,7 @@ export interface RenovateSharedConfig {
   commitMessage?: string;
   commitMessagePrefix?: string;
   confidential?: boolean;
+  customChangelogUrl?: string;
   draftPR?: boolean;
   enabled?: boolean;
   enabledManagers?: string[];
diff --git a/lib/workers/repository/update/pr/changelog/common.spec.ts b/lib/workers/repository/update/pr/changelog/common.spec.ts
new file mode 100644
index 0000000000..ca4613d24c
--- /dev/null
+++ b/lib/workers/repository/update/pr/changelog/common.spec.ts
@@ -0,0 +1,14 @@
+import { slugifyUrl } from './common';
+
+describe('workers/repository/update/pr/changelog/common', () => {
+  it.each`
+    url                                                    | expected
+    ${'https://github-enterprise.example.com/çhãlk/chálk'} | ${'https-github-enterprise-example-com-chalk-chalk'}
+    ${'https://github.com/chalk/chalk'}                    | ${'https-github-com-chalk-chalk'}
+    ${'https://github-enterprise.example.com/'}            | ${'https-github-enterprise-example-com'}
+    ${'https://github.com/sindresorhus/delay'}             | ${'https-github-com-sindresorhus-delay'}
+    ${'https://github.com/🔥/∂u/∂t/equals/α∇^2u'}          | ${'https-github-com-du-dt-equals-a2u'}
+  `('isSingleVersion("$url") === $expected', ({ url, expected }) => {
+    expect(slugifyUrl(url)).toBe(expected);
+  });
+});
diff --git a/lib/workers/repository/update/pr/changelog/common.ts b/lib/workers/repository/update/pr/changelog/common.ts
new file mode 100644
index 0000000000..cd2f8f4d3f
--- /dev/null
+++ b/lib/workers/repository/update/pr/changelog/common.ts
@@ -0,0 +1,7 @@
+import slugify from 'slugify';
+import { regEx } from '../../../../../util/regex';
+
+export function slugifyUrl(url: string): string {
+  const r = regEx(/(:?[:/.])+/g);
+  return slugify(url.replace(r, ' '));
+}
diff --git a/lib/workers/repository/update/pr/changelog/github.spec.ts b/lib/workers/repository/update/pr/changelog/github.spec.ts
index cb5ab72dc5..7d0c6d6f2c 100644
--- a/lib/workers/repository/update/pr/changelog/github.spec.ts
+++ b/lib/workers/repository/update/pr/changelog/github.spec.ts
@@ -265,6 +265,34 @@ describe('workers/repository/update/pr/changelog/github', () => {
       });
     });
 
+    it('supports overwriting sourceUrl for supports github enterprise and github.com changelog', async () => {
+      const sourceUrl = upgrade.sourceUrl;
+      const replacementSourceUrl = 'https://github.com/sindresorhus/got';
+      const config = {
+        ...upgrade,
+        endpoint: 'https://github-enterprise.example.com/',
+        customChangelogUrl: replacementSourceUrl,
+      };
+      hostRules.add({
+        hostType: PlatformId.Github,
+        token: 'super_secret',
+        matchHost: 'https://github-enterprise.example.com/',
+      });
+      expect(await getChangeLogJSON(config)).toMatchObject({
+        hasReleaseNotes: true,
+        project: {
+          apiBaseUrl: 'https://api.github.com/',
+          baseUrl: 'https://github.com/',
+          depName: 'renovate',
+          repository: 'sindresorhus/got',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://github.com/sindresorhus/got',
+          type: 'github',
+        },
+      });
+      expect(upgrade.sourceUrl).toBe(sourceUrl); // ensure unmodified function argument
+    });
+
     it('supports github enterprise and github enterprise changelog', async () => {
       hostRules.add({
         hostType: PlatformId.Github,
@@ -298,6 +326,37 @@ describe('workers/repository/update/pr/changelog/github', () => {
       });
     });
 
+    it('supports overwriting sourceUrl for github enterprise and github enterprise changelog', async () => {
+      const sourceUrl = 'https://github-enterprise.example.com/chalk/chalk';
+      const replacementSourceUrl =
+        'https://github-enterprise.example.com/sindresorhus/got';
+      const config = {
+        ...upgrade,
+        sourceUrl,
+        endpoint: 'https://github-enterprise.example.com/',
+        customChangelogUrl: replacementSourceUrl,
+      };
+      hostRules.add({
+        hostType: PlatformId.Github,
+        matchHost: 'https://github-enterprise.example.com/',
+        token: 'abc',
+      });
+      process.env.GITHUB_ENDPOINT = '';
+      expect(await getChangeLogJSON(config)).toMatchObject({
+        hasReleaseNotes: true,
+        project: {
+          apiBaseUrl: 'https://github-enterprise.example.com/api/v3/',
+          baseUrl: 'https://github-enterprise.example.com/',
+          depName: 'renovate',
+          repository: 'sindresorhus/got',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://github-enterprise.example.com/sindresorhus/got',
+          type: 'github',
+        },
+      });
+      expect(config.sourceUrl).toBe(sourceUrl); // ensure unmodified function argument
+    });
+
     it('works with same version releases but different prefix', async () => {
       const githubTagsMock = jest.spyOn(
         CacheableGithubTags.prototype,
diff --git a/lib/workers/repository/update/pr/changelog/gitlab.spec.ts b/lib/workers/repository/update/pr/changelog/gitlab.spec.ts
index 0e27e9f651..c9196a53e6 100644
--- a/lib/workers/repository/update/pr/changelog/gitlab.spec.ts
+++ b/lib/workers/repository/update/pr/changelog/gitlab.spec.ts
@@ -315,5 +315,38 @@ describe('workers/repository/update/pr/changelog/gitlab', () => {
         ],
       });
     });
+
+    it('supports overwriting sourceUrl for self-hosted gitlab changelog', async () => {
+      httpMock.scope('https://git.test.com').persist().get(/.*/).reply(200, []);
+      const sourceUrl = 'https://git.test.com/meno/dropzone/';
+      const replacementSourceUrl =
+        'https://git.test.com/replacement/sourceurl/';
+      const config = {
+        ...upgrade,
+        platform: PlatformId.Gitlab,
+        endpoint: 'https://git.test.com/api/v4/',
+        sourceUrl,
+        customChangelogUrl: replacementSourceUrl,
+      };
+      hostRules.add({
+        hostType: PlatformId.Gitlab,
+        matchHost: 'https://git.test.com/',
+        token: 'abc',
+      });
+      process.env.GITHUB_ENDPOINT = '';
+      expect(await getChangeLogJSON(config)).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://git.test.com/api/v4/',
+          baseUrl: 'https://git.test.com/',
+          depName: 'renovate',
+          repository: 'replacement/sourceurl',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://git.test.com/replacement/sourceurl/',
+          type: 'gitlab',
+        },
+      });
+      expect(config.sourceUrl).toBe(sourceUrl); // ensure unmodified function argument
+    });
   });
 });
diff --git a/lib/workers/repository/update/pr/changelog/index.ts b/lib/workers/repository/update/pr/changelog/index.ts
index fcca0fc7f5..a0e07f0c22 100644
--- a/lib/workers/repository/update/pr/changelog/index.ts
+++ b/lib/workers/repository/update/pr/changelog/index.ts
@@ -9,9 +9,11 @@ import type { ChangeLogResult } from './types';
 export * from './types';
 
 export async function getChangeLogJSON(
-  config: BranchUpgradeConfig
+  _config: BranchUpgradeConfig
 ): Promise<ChangeLogResult | null> {
-  const { sourceUrl, versioning, currentVersion, newVersion } = config;
+  const sourceUrl = _config.customChangelogUrl ?? _config.sourceUrl!;
+  const config: BranchUpgradeConfig = { ..._config, sourceUrl };
+  const { versioning, currentVersion, newVersion } = config;
   try {
     if (!(sourceUrl && currentVersion && newVersion)) {
       return null;
diff --git a/lib/workers/repository/update/pr/changelog/source-github.ts b/lib/workers/repository/update/pr/changelog/source-github.ts
index 7631f6cd2e..d8dabe224e 100644
--- a/lib/workers/repository/update/pr/changelog/source-github.ts
+++ b/lib/workers/repository/update/pr/changelog/source-github.ts
@@ -10,6 +10,7 @@ import * as packageCache from '../../../../../util/cache/package';
 import * as hostRules from '../../../../../util/host-rules';
 import { regEx } from '../../../../../util/regex';
 import type { BranchUpgradeConfig } from '../../../../types';
+import { slugifyUrl } from './common';
 import { getTags } from './github';
 import { addReleaseNotes } from './release-notes';
 import { getInRangeReleases } from './releases';
@@ -120,8 +121,9 @@ export async function getChangeLogJSON(
   }
 
   const cacheNamespace = 'changelog-github-release';
+
   function getCacheKey(prev: string, next: string): string {
-    return `${manager}:${depName}:${prev}:${next}`;
+    return `${slugifyUrl(sourceUrl)}:${depName}:${prev}:${next}`;
   }
 
   const changelogReleases: ChangeLogRelease[] = [];
diff --git a/lib/workers/repository/update/pr/changelog/source-gitlab.ts b/lib/workers/repository/update/pr/changelog/source-gitlab.ts
index 2cd1b38c27..e55b681849 100644
--- a/lib/workers/repository/update/pr/changelog/source-gitlab.ts
+++ b/lib/workers/repository/update/pr/changelog/source-gitlab.ts
@@ -7,6 +7,7 @@ import * as memCache from '../../../../../util/cache/memory';
 import * as packageCache from '../../../../../util/cache/package';
 import { regEx } from '../../../../../util/regex';
 import type { BranchUpgradeConfig } from '../../../../types';
+import { slugifyUrl } from './common';
 import { getTags } from './gitlab';
 import { addReleaseNotes } from './release-notes';
 import { getInRangeReleases } from './releases';
@@ -38,7 +39,6 @@ export async function getChangeLogJSON(
   const newVersion = config.newVersion!;
   const sourceUrl = config.sourceUrl!;
   const depName = config.depName!;
-  const manager = config.manager;
   const sourceDirectory = config.sourceDirectory!;
 
   logger.trace('getChangeLogJSON for gitlab');
@@ -95,7 +95,7 @@ export async function getChangeLogJSON(
   }
 
   function getCacheKey(prev: string, next: string): string {
-    return `${manager}:${depName}:${prev}:${next}`;
+    return `${slugifyUrl(sourceUrl)}:${depName}:${prev}:${next}`;
   }
 
   const changelogReleases: ChangeLogRelease[] = [];
-- 
GitLab