From b1724b67455e3dc16dccde4027c5fda354e65b97 Mon Sep 17 00:00:00 2001
From: Yanis Benson <yanis.benson@gmail.com>
Date: Mon, 9 Aug 2021 13:14:50 +0300
Subject: [PATCH] feat: add updatePinnedDependencies option (#11087)

---
 docs/usage/configuration-options.md           |  5 ++++
 lib/config/definitions.ts                     |  7 +++++
 lib/types/skip-reason.ts                      |  1 +
 .../lookup/__snapshots__/index.spec.ts.snap   | 14 +++++++++
 .../repository/process/lookup/index.spec.ts   | 29 +++++++++++++++++++
 .../repository/process/lookup/index.ts        |  7 +++++
 .../repository/process/lookup/types.ts        |  1 +
 7 files changed, 64 insertions(+)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index d889328df0..5c29c045fe 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -2324,6 +2324,11 @@ However there are cases where updates might be desirable - e.g. if you have conf
 This defaults to `true`, meaning that Renovate will perform certain "desirable" updates to _existing_ PRs even when outside of schedule.
 If you wish to disable all updates outside of scheduled hours then configure this field to `false`.
 
+## updatePinnedDependencies
+
+By default, Renovate will attempt to update all detected dependencies, regardless of whether they are defined using pinned single versions (e.g. `1.2.3`) or constraints/ranges (e.g. (`^1.2.3`).
+You can set this option to `false` if you wish to disable updating for pinned (single version) dependencies specifically.
+
 ## versioning
 
 Usually, each language or package manager has a specific type of "versioning":
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index 014877a1e1..99def5503d 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -2030,6 +2030,13 @@ const options: RenovateOptions[] = [
     stage: 'global',
     admin: true,
   },
+  {
+    name: 'updatePinnedDependencies',
+    description:
+      'Whether to update pinned (single version) dependencies or not.',
+    type: 'boolean',
+    default: true,
+  },
 ];
 
 export function getOptions(): RenovateOptions[] {
diff --git a/lib/types/skip-reason.ts b/lib/types/skip-reason.ts
index d6d0baee5c..501cce0a5c 100644
--- a/lib/types/skip-reason.ts
+++ b/lib/types/skip-reason.ts
@@ -40,4 +40,5 @@ export enum SkipReason {
   Unsupported = 'unsupported',
   UnversionedReference = 'unversioned-reference',
   VersionPlaceholder = 'version-placeholder',
+  IsPinned = 'is-pinned',
 }
diff --git a/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap b/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
index ae7f6b7755..85185e8157 100644
--- a/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
+++ b/lib/workers/repository/process/lookup/__snapshots__/index.spec.ts.snap
@@ -754,6 +754,20 @@ Array [
 ]
 `;
 
+exports[`workers/repository/process/lookup/index .lookupUpdates() should update pinned versions if updatePinnedDependencies=true 1`] = `
+Array [
+  Object {
+    "bucket": "non-major",
+    "newMajor": 0,
+    "newMinor": 0,
+    "newValue": "0.0.35",
+    "newVersion": "0.0.35",
+    "releaseTimestamp": "2017-04-27T16:59:06.479Z",
+    "updateType": "patch",
+  },
+]
+`;
+
 exports[`workers/repository/process/lookup/index .lookupUpdates() should warn if no version matches dist-tag 1`] = `Array []`;
 
 exports[`workers/repository/process/lookup/index .lookupUpdates() skips uncompatible versions for 8 1`] = `
diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts
index bf16bda5f5..add2ba3e2d 100644
--- a/lib/workers/repository/process/lookup/index.spec.ts
+++ b/lib/workers/repository/process/lookup/index.spec.ts
@@ -825,6 +825,35 @@ describe(getName(), () => {
       expect(res.updates).toHaveLength(1);
       expect(res.updates[0].newValue).toEqual('3.0.1');
     });
+
+    it('should update pinned versions if updatePinnedDependencies=true', async () => {
+      config.currentValue = '0.0.34';
+      config.updatePinnedDependencies = true;
+      config.depName = '@types/helmet';
+      config.datasource = datasourceNpmId;
+      httpMock
+        .scope('https://registry.npmjs.org')
+        .get('/@types%2Fhelmet')
+        .reply(200, helmetJson);
+      const res = await lookup.lookupUpdates(config);
+      expect(res.updates).toMatchSnapshot();
+      expect(res.updates).toHaveLength(1);
+      expect(res.updates[0].newValue).toEqual('0.0.35');
+    });
+
+    it('should not update pinned versions if updatePinnedDependencies=false', async () => {
+      config.currentValue = '0.0.34';
+      config.updatePinnedDependencies = false;
+      config.depName = '@types/helmet';
+      config.datasource = datasourceNpmId;
+      httpMock
+        .scope('https://registry.npmjs.org')
+        .get('/@types%2Fhelmet')
+        .reply(200, helmetJson);
+      const res = await lookup.lookupUpdates(config);
+      expect(res.updates).toHaveLength(0);
+    });
+
     it('should follow dist-tag even if newer version exists', async () => {
       config.currentValue = '3.0.1-insiders.20180713';
       config.depName = 'typescript';
diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts
index e520d15dbc..cf3143ade0 100644
--- a/lib/workers/repository/process/lookup/index.ts
+++ b/lib/workers/repository/process/lookup/index.ts
@@ -39,6 +39,7 @@ export async function lookupUpdates(
     pinDigests,
     rollbackPrs,
     isVulnerabilityAlert,
+    updatePinnedDependencies,
   } = config;
   logger.trace({ dependency: depName, currentValue }, 'lookupUpdates');
   // Use the datasource's default versioning if none is configured
@@ -59,6 +60,11 @@ export async function lookupUpdates(
   }
   const isValid = currentValue && versioning.isValid(currentValue);
   if (isValid) {
+    if (!updatePinnedDependencies && versioning.isSingleVersion(currentValue)) {
+      res.skipReason = SkipReason.IsPinned;
+      return res;
+    }
+
     const dependency = clone(await getPkgReleases(config));
     if (!dependency) {
       // If dependency lookup fails then warn and return
@@ -82,6 +88,7 @@ export async function lookupUpdates(
     res.homepage = dependency.homepage;
     res.changelogUrl = dependency.changelogUrl;
     res.dependencyUrl = dependency?.dependencyUrl;
+
     const latestVersion = dependency.tags?.latest;
     // Filter out any results from datasource that don't comply with our versioning
     let allVersions = dependency.releases.filter((release) =>
diff --git a/lib/workers/repository/process/lookup/types.ts b/lib/workers/repository/process/lookup/types.ts
index 104ce7fbea..2c44ff188e 100644
--- a/lib/workers/repository/process/lookup/types.ts
+++ b/lib/workers/repository/process/lookup/types.ts
@@ -13,6 +13,7 @@ export interface FilterConfig {
   ignoreDeprecated?: boolean;
   ignoreUnstable?: boolean;
   respectLatest?: boolean;
+  updatePinnedDependencies?: boolean;
   versioning: string;
 }
 
-- 
GitLab