From 9c51ff6733a54b469eab32edaedf3af7cda0d8e3 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Fri, 24 Jun 2022 11:07:34 +0300
Subject: [PATCH] feat(manager/helm): make change log url retrieval more
 flexible (#16210)

* feat(helm): improve fallback on sourceUrl selection

* fix(helm): use home and source from latest release

instead of the first. Otherwise those fields can't be ever changed.

* test(helm): test charts with no home metadata

* test(helm): test releases semver sorting

* fix(helm): select last release by semver

* Apply suggestions from code review

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* fix(type): make homeMatch keys optional

* Fixes

Co-authored-by: Stefano Zaninetta <stefano.zaninetta@nagra.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../datasource/helm/__fixtures__/index.yaml   | 70 +++++++++++++++++++
 .../datasource/helm/__fixtures__/sample.yaml  | 17 +++++
 lib/modules/datasource/helm/common.spec.ts    | 12 ++--
 lib/modules/datasource/helm/common.ts         | 47 +++++++++----
 lib/modules/datasource/helm/index.spec.ts     | 18 +++++
 lib/modules/datasource/helm/index.ts          |  8 ++-
 6 files changed, 151 insertions(+), 21 deletions(-)

diff --git a/lib/modules/datasource/helm/__fixtures__/index.yaml b/lib/modules/datasource/helm/__fixtures__/index.yaml
index 870b7b419e..6f8c60e890 100644
--- a/lib/modules/datasource/helm/__fixtures__/index.yaml
+++ b/lib/modules/datasource/helm/__fixtures__/index.yaml
@@ -1872,3 +1872,73 @@ entries:
     urls:
     - https://charts.helm.sh/stable/packages/ambassador-1.0.0.tgz
     version: 1.0.0
+  cluster-autoscaler:
+  - apiVersion: v2
+    appVersion: 1.21.1
+    created: "2021-12-14T18:07:21.619018562Z"
+    description: Scales Kubernetes worker nodes within autoscaling groups.
+    digest: 5685825bce34653919c9d23ca833b99f2ba359aad6dc89afacdf32208c4d0e16
+    home: https://www.autoscaler.io/9.10.9
+    icon: https://github.com/kubernetes/kubernetes/raw/master/logo/logo.png
+    maintainers:
+    - email: e.bailey@sportradar.com
+      name: yurrriq
+    - email: mgoodness@gmail.com
+      name: mgoodness
+    - email: guyjtempleton@googlemail.com
+      name: gjtempleton
+    - email: scott.crooks@gmail.com
+      name: sc250024
+    name: cluster-autoscaler
+    sources:
+    - https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#9.10.9
+    type: application
+    urls:
+    - cluster-autoscaler-9.10.9.tgz
+    version: 9.10.9
+  - apiVersion: v2
+    appVersion: 1.21.1
+    created: "2021-12-22T11:20:10.489588334Z"
+    description: Scales Kubernetes worker nodes within autoscaling groups.
+    digest: 0da701c485890b77ec45853aa3b5ad8644ff7c71d6cd2d5ee65ee54d118ca99b
+    home: https://www.autoscaler.io/9.11.0
+    icon: https://github.com/kubernetes/kubernetes/raw/master/logo/logo.png
+    maintainers:
+    - email: e.bailey@sportradar.com
+      name: yurrriq
+    - email: mgoodness@gmail.com
+      name: mgoodness
+    - email: guyjtempleton@googlemail.com
+      name: gjtempleton
+    - email: scott.crooks@gmail.com
+      name: sc250024
+    name: cluster-autoscaler
+    sources:
+    - https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#9.11.0
+    type: application
+    urls:
+    - cluster-autoscaler-9.11.0.tgz
+    version: 9.11.0
+  - apiVersion: v2
+    appVersion: 1.21.1
+    created: "2021-11-01T22:54:28.760738726Z"
+    description: Scales Kubernetes worker nodes within autoscaling groups.
+    digest: 446efc118d134bb3dbc2d30ff10a07bbcaa33726bdabc60ebf2a5fd8b1871392
+    home: https://www.autoscaler.io/9.10.8
+    icon: https://github.com/kubernetes/kubernetes/raw/master/logo/logo.png
+    maintainers:
+    - email: e.bailey@sportradar.com
+      name: yurrriq
+    - email: mgoodness@gmail.com
+      name: mgoodness
+    - email: guyjtempleton@googlemail.com
+      name: gjtempleton
+    - email: scott.crooks@gmail.com
+      name: sc250024
+    name: cluster-autoscaler
+    sources:
+    - https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#9.10.8
+    type: application
+    urls:
+    - cluster-autoscaler-9.10.8.tgz
+    version: 9.10.8
diff --git a/lib/modules/datasource/helm/__fixtures__/sample.yaml b/lib/modules/datasource/helm/__fixtures__/sample.yaml
index 9e3c6d97bb..9e2afe89d9 100644
--- a/lib/modules/datasource/helm/__fixtures__/sample.yaml
+++ b/lib/modules/datasource/helm/__fixtures__/sample.yaml
@@ -93,3 +93,20 @@ entries:
   - home:
     urls:
     - some.tgz
+  dummy-no-home:
+  - description: dummy chart without home metadata
+    name: dummy-no-home
+    sources:
+    - https://github.com/dummyorg/dummymonorepo/tree/main/charts/dummy-no-home
+    version: 0.0.42
+    urls:
+    - some.tgz
+  dummy-no-chart-repo:
+  - description: dummy chart without a valid chart repo URL
+    name: dummy-no-chart-repo
+    home: https://github.com/dummyorg/dummymonorepo/tree/main/charts/dummy-no-chart-repo
+    sources:
+    - https://some.test
+    version: 0.0.42
+    urls:
+    - some.tgz
diff --git a/lib/modules/datasource/helm/common.spec.ts b/lib/modules/datasource/helm/common.spec.ts
index 48d4d0906d..d0f33613d6 100644
--- a/lib/modules/datasource/helm/common.spec.ts
+++ b/lib/modules/datasource/helm/common.spec.ts
@@ -11,11 +11,13 @@ const repo = load(loadFixture('sample.yaml'), {
 describe('modules/datasource/helm/common', () => {
   describe('findSourceUrl', () => {
     test.each`
-      input         | output
-      ${'airflow'}  | ${{ sourceUrl: 'https://github.com/bitnami/charts', sourceDirectory: 'bitnami/airflow' }}
-      ${'coredns'}  | ${{ sourceUrl: 'https://github.com/coredns/helm', sourceDirectory: undefined }}
-      ${'pgadmin4'} | ${{ sourceUrl: 'https://github.com/rowanruseler/helm-charts', sourceDirectory: undefined }}
-      ${'dummy'}    | ${{}}
+      input                    | output
+      ${'airflow'}             | ${{ sourceUrl: 'https://github.com/bitnami/charts', sourceDirectory: 'bitnami/airflow' }}
+      ${'coredns'}             | ${{ sourceUrl: 'https://github.com/coredns/helm', sourceDirectory: undefined }}
+      ${'pgadmin4'}            | ${{ sourceUrl: 'https://github.com/rowanruseler/helm-charts', sourceDirectory: undefined }}
+      ${'dummy-no-home'}       | ${{ sourceUrl: 'https://github.com/dummyorg/dummymonorepo', sourceDirectory: 'charts/dummy-no-home' }}
+      ${'dummy-no-chart-repo'} | ${{ sourceUrl: 'https://github.com/dummyorg/dummymonorepo', sourceDirectory: 'charts/dummy-no-chart-repo' }}
+      ${'dummy'}               | ${{}}
     `(
       '$input -> $output',
       ({ input, output }: { input: string; output: string }) => {
diff --git a/lib/modules/datasource/helm/common.ts b/lib/modules/datasource/helm/common.ts
index 4dfc00984b..0a82fd9988 100644
--- a/lib/modules/datasource/helm/common.ts
+++ b/lib/modules/datasource/helm/common.ts
@@ -16,13 +16,11 @@ export function findSourceUrl(release: HelmRelease): RepoSource {
     return { sourceUrl: releaseMatch[1] };
   }
 
-  if (release.home) {
-    const githubUrlMatch = githubUrl.exec(release.home);
-    if (githubUrlMatch?.groups && chartRepo.test(githubUrlMatch?.groups.repo)) {
-      return {
-        sourceUrl: githubUrlMatch.groups.url,
-        sourceDirectory: githubUrlMatch.groups.path,
-      };
+  const homeMatchGroups = release.home && githubUrl.exec(release.home)?.groups;
+  if (homeMatchGroups) {
+    const { url: sourceUrl, path: sourceDirectory, repo } = homeMatchGroups;
+    if (chartRepo.test(repo)) {
+      return { sourceUrl, sourceDirectory };
     }
   }
 
@@ -31,15 +29,36 @@ export function findSourceUrl(release: HelmRelease): RepoSource {
   }
 
   for (const url of release.sources) {
-    const githubUrlMatch = githubUrl.exec(url);
-    if (githubUrlMatch?.groups && chartRepo.test(githubUrlMatch?.groups.repo)) {
-      return {
-        sourceUrl: githubUrlMatch.groups.url,
-        sourceDirectory: githubUrlMatch.groups.path,
-      };
+    const githubUrlMatchGroups = githubUrl.exec(url)?.groups;
+    if (githubUrlMatchGroups) {
+      const {
+        url: sourceUrl,
+        path: sourceDirectory,
+        repo,
+      } = githubUrlMatchGroups;
+      if (chartRepo.test(repo)) {
+        return { sourceUrl, sourceDirectory };
+      }
+    }
+  }
+
+  // fallback: if neither home nor sources are a chart repo URL, use githubUrl (if present)
+  if (homeMatchGroups) {
+    const { url: sourceUrl, path: sourceDirectory } = homeMatchGroups;
+    if (sourceUrl && sourceDirectory) {
+      return { sourceUrl, sourceDirectory };
+    }
+  }
+
+  for (const source of release.sources) {
+    const firstSourceMatch = githubUrl.exec(source)?.groups;
+    if (firstSourceMatch) {
+      const { url: sourceUrl, path: sourceDirectory } = firstSourceMatch;
+      if (sourceUrl && sourceDirectory) {
+        return { sourceUrl, sourceDirectory };
+      }
     }
   }
 
-  // fallback
   return { sourceUrl: release.sources[0] };
 }
diff --git a/lib/modules/datasource/helm/index.spec.ts b/lib/modules/datasource/helm/index.spec.ts
index 54ea10e119..3d03c3b992 100644
--- a/lib/modules/datasource/helm/index.spec.ts
+++ b/lib/modules/datasource/helm/index.spec.ts
@@ -189,5 +189,23 @@ describe('modules/datasource/helm/index', () => {
         releases: expect.toBeArrayOfSize(27),
       });
     });
+
+    it('returns home and source metadata of the most recent version', async () => {
+      httpMock
+        .scope('https://example-repository.com')
+        .get('/index.yaml')
+        .reply(200, indexYaml);
+      const releases = await getPkgReleases({
+        datasource: HelmDatasource.id,
+        depName: 'cluster-autoscaler',
+        registryUrls: ['https://example-repository.com'],
+      });
+      expect(releases).not.toBeNull();
+      expect(releases).toMatchObject({
+        homepage: 'https://www.autoscaler.io/9.11.0',
+        sourceDirectory: 'cluster-autoscaler#9.11.0',
+        sourceUrl: 'https://github.com/kubernetes/autoscaler',
+      });
+    });
   });
 });
diff --git a/lib/modules/datasource/helm/index.ts b/lib/modules/datasource/helm/index.ts
index 6b0018f698..2d719d5ebd 100644
--- a/lib/modules/datasource/helm/index.ts
+++ b/lib/modules/datasource/helm/index.ts
@@ -1,5 +1,6 @@
 import is from '@sindresorhus/is';
 import { load } from 'js-yaml';
+import { gt } from 'semver';
 import { logger } from '../../../logger';
 import { cache } from '../../../util/cache/package/decorator';
 import { ensureTrailingSlash } from '../../../util/url';
@@ -62,9 +63,12 @@ export class HelmDatasource extends Datasource {
       }
       const result: HelmRepositoryData = {};
       for (const [name, releases] of Object.entries(doc.entries)) {
-        const { sourceUrl, sourceDirectory } = findSourceUrl(releases[0]);
+        const latestRelease = releases.sort((r0, r1) =>
+          gt(r0.version, r1.version) ? -1 : 1
+        )[0];
+        const { sourceUrl, sourceDirectory } = findSourceUrl(latestRelease);
         result[name] = {
-          homepage: releases[0].home,
+          homepage: latestRelease.home,
           sourceUrl,
           sourceDirectory,
           releases: releases.map((release) => ({
-- 
GitLab