From 4cebc7ad6411a84bedba48e4df657aecc05ee354 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Wed, 8 Nov 2017 21:57:34 +0100
Subject: [PATCH] feat: unstablePattern (#1125)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a configuration option unstablePattern - used only by Docker currently - that can be used to define a regex patternt to identify “unstable” versions.

Closes #1035
---
 docs/configuration.md                         | 17 +++++++---
 lib/config/definitions.js                     |  8 +++++
 lib/manager/docker/package.js                 | 15 +++++++++
 .../__snapshots__/resolve.spec.js.snap        |  6 ++++
 .../docker/__snapshots__/package.spec.js.snap | 14 ++++++++
 test/manager/docker/package.spec.js           | 33 +++++++++++++++++++
 .../__snapshots__/branchify.spec.js.snap      |  5 +++
 7 files changed, 94 insertions(+), 4 deletions(-)

diff --git a/docs/configuration.md b/docs/configuration.md
index d837f7fdbe..9e76e1011b 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -441,6 +441,14 @@ Obviously, you can't set repository or package file location with this method.
   <td>`RENOVATE_IGNORE_UNSTABLE`</td>
   <td>`--ignore-unstable`<td>
 </tr>
+<tr>
+  <td>`unstablePattern`</td>
+  <td>Regex for identifying unstable versions (docker only)</td>
+  <td>string</td>
+  <td><pre>null</pre></td>
+  <td></td>
+  <td><td>
+</tr>
 <tr>
   <td>`respectLatest`</td>
   <td>Ignore versions newer than npm "latest" version</td>
@@ -490,6 +498,7 @@ Obviously, you can't set repository or package file location with this method.
   <td><pre>{
   "unpublishSafe": false,
   "recreateClosed": true,
+  "rebaseStalePrs": true,
   "groupName": "Pin Dependencies",
   "group": {
     "commitMessage": "Pin Dependencies",
@@ -703,7 +712,7 @@ Obviously, you can't set repository or package file location with this method.
   <td>`npm`</td>
   <td>Configuration object for npm package.json renovation</td>
   <td>json</td>
-  <td><pre>{"enabled": true}</pre></td>
+  <td><pre>{"enabled": true, "pin": {"automerge": true}}</pre></td>
   <td>`RENOVATE_NPM`</td>
   <td>`--npm`<td>
 </tr>
@@ -731,13 +740,13 @@ Obviously, you can't set repository or package file location with this method.
   "digest": {
     "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
     "commitMessage": "Update {{depName}}:{{currentTag}} digest",
-    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image `{{depName}}@{{currentTag}}` to the latest digest (`{{newDigest}}`).\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n-   `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n-   `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image `{{depName}}:{{currentTag}}` to the latest digest (`{{newDigest}}`).\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n-   `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n-   `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
     "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})"
   },
   "pin": {
     "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
-    "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
-    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image `{{depName}}@{{currentTag}}` to use a digest (`{{newDigest}}`).\nThis digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n-   `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n-   `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Pin Dockerfile {{depName}}:{{currentTag}} image digest",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image `{{depName}}:{{currentTag}}` to use a digest (`{{newDigest}}`).\nThis digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n-   `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n-   `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
     "groupName": "Pin Docker Digests",
     "group": {
       "prTitle": "Pin Docker digests",
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index f2ee95c608..12068a7936 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -351,6 +351,14 @@ const options = [
     stage: 'package',
     type: 'boolean',
   },
+  {
+    name: 'unstablePattern',
+    description: 'Regex for identifying unstable versions (docker only)',
+    stage: 'package',
+    type: 'string',
+    cli: false,
+    env: false,
+  },
   {
     name: 'respectLatest',
     description: 'Ignore versions newer than npm "latest" version',
diff --git a/lib/manager/docker/package.js b/lib/manager/docker/package.js
index 911605fa9f..3d6171680f 100644
--- a/lib/manager/docker/package.js
+++ b/lib/manager/docker/package.js
@@ -4,6 +4,7 @@ const versions = require('../../workers/package/versions');
 const compareVersions = require('compare-versions');
 
 module.exports = {
+  isStable,
   getPackageUpdates,
 };
 
@@ -15,6 +16,7 @@ async function getPackageUpdates(config) {
     currentDepTag,
     currentTag,
     currentDigest,
+    unstablePattern,
   } = config;
   if (dockerRegistry) {
     logger.info({ currentFrom }, 'Skipping Dockerfile image with custom host');
@@ -53,6 +55,7 @@ async function getPackageUpdates(config) {
       );
       return upgrades;
     }
+    const currentlyStable = isStable(tagVersion, unstablePattern);
     let versionList = [];
     const allTags = await dockerApi.getTags(config.depName);
     if (allTags) {
@@ -60,6 +63,12 @@ async function getPackageUpdates(config) {
         .filter(tag => getSuffix(tag) === tagSuffix)
         .map(getVersion)
         .filter(versions.isValidVersion)
+        .filter(
+          version =>
+            isStable(version, unstablePattern) ||
+            !currentlyStable ||
+            !config.ignoreUnstable
+        )
         .filter(
           prefix => prefix.split('.').length === tagVersion.split('.').length
         )
@@ -116,6 +125,12 @@ async function getPackageUpdates(config) {
   return upgrades;
 }
 
+function isStable(tag, unstablePattern) {
+  return unstablePattern
+    ? tag.match(new RegExp(unstablePattern)) === null
+    : true;
+}
+
 function getVersion(tag) {
   const split = tag.indexOf('-');
   return split > 0 ? tag.substring(0, split) : tag;
diff --git a/test/manager/__snapshots__/resolve.spec.js.snap b/test/manager/__snapshots__/resolve.spec.js.snap
index edab1ee93c..76f78390d2 100644
--- a/test/manager/__snapshots__/resolve.spec.js.snap
+++ b/test/manager/__snapshots__/resolve.spec.js.snap
@@ -482,6 +482,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [
     Object {
@@ -1201,6 +1202,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [],
   "workspaceDir": undefined,
@@ -1700,6 +1702,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [],
   "workspaceDir": undefined,
@@ -2200,6 +2203,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [],
   "workspaceDir": undefined,
@@ -2689,6 +2693,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [],
   "workspaceDir": undefined,
@@ -3187,6 +3192,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "warnings": Array [],
   "workspaceDir": undefined,
diff --git a/test/manager/docker/__snapshots__/package.spec.js.snap b/test/manager/docker/__snapshots__/package.spec.js.snap
index 55e0b1559d..6c1456ba18 100644
--- a/test/manager/docker/__snapshots__/package.spec.js.snap
+++ b/test/manager/docker/__snapshots__/package.spec.js.snap
@@ -23,6 +23,20 @@ Array [
 ]
 `;
 
+exports[`lib/workers/package/docker getPackageUpdates ignores unstable upgrades 1`] = `
+Array [
+  Object {
+    "isMajor": true,
+    "newDepTag": "node:8",
+    "newFrom": "node:8",
+    "newTag": "8",
+    "newVersion": "8",
+    "newVersionMajor": "8",
+    "type": "major",
+  },
+]
+`;
+
 exports[`lib/workers/package/docker getPackageUpdates returns major and minor upgrades 1`] = `
 Array [
   Object {
diff --git a/test/manager/docker/package.spec.js b/test/manager/docker/package.spec.js
index 0998214fd2..8d3c32ab3e 100644
--- a/test/manager/docker/package.spec.js
+++ b/test/manager/docker/package.spec.js
@@ -7,6 +7,21 @@ dockerApi.getDigest = jest.fn();
 dockerApi.getTags = jest.fn();
 
 describe('lib/workers/package/docker', () => {
+  describe('isStable', () => {
+    it('returns true if no pattern', () => {
+      expect(docker.isStable('8', null)).toBe(true);
+    });
+    it('returns true if no match', () => {
+      const unstablePattern = '^\\d*[13579]($|.)';
+      expect(docker.isStable('8', unstablePattern)).toBe(true);
+      expect(docker.isStable('8.9.1', unstablePattern)).toBe(true);
+    });
+    it('returns false if match', () => {
+      const unstablePattern = '^\\d*[13579]($|.)';
+      expect(docker.isStable('9.0', unstablePattern)).toBe(false);
+      expect(docker.isStable('15.04', unstablePattern)).toBe(false);
+    });
+  });
   describe('getPackageUpdates', () => {
     let config;
     beforeEach(() => {
@@ -70,6 +85,24 @@ describe('lib/workers/package/docker', () => {
       expect(res[1].type).toEqual('major');
       expect(res[2].newVersionMajor).toEqual('3');
     });
+    it('ignores unstable upgrades', async () => {
+      config = {
+        ...defaultConfig,
+        depName: 'node',
+        currentFrom: 'node:6',
+        currentDepTag: 'node:6',
+        currentTag: '6',
+        currentDigest: undefined,
+        pinDigests: false,
+        unstablePattern: '^\\d*[13579]($|.)',
+      };
+      dockerApi.getTags.mockReturnValueOnce(['4', '6', '6.1', '7', '8', '9']);
+      const res = await docker.getPackageUpdates(config);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+      expect(res[0].type).toEqual('major');
+      expect(res[0].newVersion).toEqual('8');
+    });
     it('adds digest', async () => {
       delete config.currentDigest;
       config.currentTag = '1.0.0-something';
diff --git a/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
index ddd01de643..e819153a8f 100644
--- a/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
@@ -526,6 +526,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "upgrades": null,
   "warnings": Array [],
@@ -1051,6 +1052,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "upgrades": null,
   "warnings": Array [],
@@ -1582,6 +1584,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "upgrades": null,
   "warnings": Array [],
@@ -2101,6 +2104,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "upgrades": null,
   "warnings": Array [
@@ -2616,6 +2620,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht
   "timezone": null,
   "token": null,
   "unpublishSafe": false,
+  "unstablePattern": null,
   "updateNotScheduled": true,
   "upgrades": null,
   "warnings": Array [],
-- 
GitLab