From 62bc64970af9ef2ca8cc18da39c262c444d4cb8e Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Thu, 17 Sep 2020 10:06:06 +0200
Subject: [PATCH] feat: extractVersion (#7307)

Adds a new option extractVersion which allows for extracting a substring of raw versions from datasources, to be used as the actual version.

Closes #7297, Closes #6793
---
 docs/usage/configuration-options.md | 45 +++++++++++++++++++++++++++++
 lib/config/definitions.ts           |  9 ++++++
 lib/datasource/common.ts            |  1 +
 lib/datasource/index.spec.ts        | 17 +++++++++++
 lib/datasource/index.ts             | 21 ++++++++++++++
 5 files changed, 93 insertions(+)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 26760b9f8f..fb7cf289bb 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -387,6 +387,51 @@ The above would mean Renovate would not include files matching the above glob pa
 
 See [shareable config presets](https://docs.renovatebot.com/config-presets) for details.
 
+## extractVersion
+
+Use this only when the raw version strings from the datasource do not match the expected format that you need in your package file. You must defined a "named capture group" called `version` as shown in the below examples.
+
+For example, to extract only the major.minor precision from a GitHub release, the following would work:
+
+```json
+{
+  "packageRules": [
+    {
+      "packageNames": ["foo"],
+      "extractVersion": "^(?<version>v\\d+\\.\\d+)"
+    }
+  ]
+}
+```
+
+The above will change a raw version of `v1.31.5` to `v1.31`, for example.
+
+Alternatively, to strip a `release-` prefix:
+
+```json
+{
+  "packageRules": [
+    {
+      "packageNames": ["bar"],
+      "extractVersion": "^release-(?<version>.*)$"
+    }
+  ]
+}
+```
+
+The above will change a raw version of `release-2.0.0` to `2.0.0`, for example. A similar one could strip leading `v` prefixes:
+
+```json
+{
+  "packageRules": [
+    {
+      "packageNames": ["baz"],
+      "extractVersion": "^v(?<version>.*)$"
+    }
+  ]
+}
+```
+
 ## fileMatch
 
 `fileMatch` is used by Renovate to know which files in a repository to parse and extract, and it is possible to override defaults values to customize for your project's needs.
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index ed3cfe4617..257d5657fb 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -704,6 +704,15 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'extractVersion',
+    description:
+      "A regex (re2) to extract a version from a datasource's raw version string",
+    type: 'string',
+    format: 'regex',
+    cli: false,
+    env: false,
+  },
   {
     name: 'versioning',
     description: 'versioning to use for filtering and comparisons',
diff --git a/lib/datasource/common.ts b/lib/datasource/common.ts
index c9355b9cbb..1ea807a037 100644
--- a/lib/datasource/common.ts
+++ b/lib/datasource/common.ts
@@ -25,6 +25,7 @@ export interface GetPkgReleasesConfig extends ReleasesConfigBase {
   depName: string;
   lookupName?: string;
   versioning?: string;
+  extractVersion?: string;
 }
 
 export function isGetPkgReleasesConfig(
diff --git a/lib/datasource/index.spec.ts b/lib/datasource/index.spec.ts
index 70e3b17251..ee1ec40b11 100644
--- a/lib/datasource/index.spec.ts
+++ b/lib/datasource/index.spec.ts
@@ -102,6 +102,23 @@ describe('datasource/index', () => {
     expect(res.changelogUrl).toBeDefined();
     expect(res.sourceUrl).toBeDefined();
   });
+  it('applies extractVersion', async () => {
+    npmDatasource.getReleases.mockResolvedValue({
+      releases: [
+        { version: 'v1.0.0' },
+        { version: 'v1.0.1' },
+        { version: 'v2' },
+      ],
+    });
+    const res = await datasource.getPkgReleases({
+      datasource: datasourceNpm.id,
+      depName: 'react-native',
+      extractVersion: '^(?<version>v\\d+\\.\\d+)',
+      versioning: 'loose',
+    });
+    expect(res.releases).toHaveLength(1);
+    expect(res.releases[0].version).toEqual('v1.0');
+  });
   it('adds sourceUrl', async () => {
     npmDatasource.getReleases.mockResolvedValue({
       releases: [{ version: '1.0.0' }],
diff --git a/lib/datasource/index.ts b/lib/datasource/index.ts
index 24fbc2d4d7..1ef1c2c7b9 100644
--- a/lib/datasource/index.ts
+++ b/lib/datasource/index.ts
@@ -5,6 +5,7 @@ import { logger } from '../logger';
 import { ExternalHostError } from '../types/errors/external-host-error';
 import * as memCache from '../util/cache/memory';
 import { clone } from '../util/clone';
+import { regEx } from '../util/regex';
 import * as allVersioning from '../versioning';
 import datasources from './api.generated';
 import {
@@ -259,6 +260,19 @@ export async function getPkgReleases(
   if (!res) {
     return res;
   }
+  if (config.extractVersion) {
+    const extractVersionRegEx = regEx(config.extractVersion);
+    res.releases = res.releases
+      .map((release) => {
+        const version = extractVersionRegEx.exec(release.version)?.groups
+          ?.version;
+        if (version) {
+          return { ...release, version }; // overwrite version
+        }
+        return null; // filter out any we can't extract
+      })
+      .filter(Boolean);
+  }
   // Filter by versioning
   const version = allVersioning.get(config.versioning);
   // Return a sorted list of valid Versions
@@ -270,6 +284,13 @@ export async function getPkgReleases(
       .filter((release) => version.isVersion(release.version))
       .sort(sortReleases);
   }
+  // Filter versions for uniqueness
+  res.releases = res.releases.filter(
+    (filterRelease, filterIndex) =>
+      res.releases.findIndex(
+        (findRelease) => findRelease.version === filterRelease.version
+      ) === filterIndex
+  );
   return res;
 }
 
-- 
GitLab