From 12ae0d1ee9990d07a01db986a0e3aa77bc252859 Mon Sep 17 00:00:00 2001
From: Sebastian Poxhofer <secustor@users.noreply.github.com>
Date: Wed, 15 Sep 2021 09:20:31 +0200
Subject: [PATCH] feat(manager/terraform): support range strategy update
 lockfile (#11720)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 docs/usage/configuration-options.md           |  2 +-
 .../terraform/__fixtures__/lockedVersion.tf   | 15 +++++
 .../terraform/__fixtures__/rangeStrategy.hcl  | 32 +++++++++++
 .../__snapshots__/extract.spec.ts.snap        | 57 ++++++++++++++++++-
 lib/manager/terraform/extract.spec.ts         | 54 ++++++++++++++----
 lib/manager/terraform/extract.ts              | 23 +++++++-
 lib/manager/terraform/lockfile/index.ts       | 26 ++++-----
 lib/manager/terraform/providers.ts            | 38 ++++++++-----
 lib/manager/terraform/required-providers.ts   |  8 ++-
 lib/manager/terraform/util.ts                 | 34 +++++++++++
 lib/versioning/hashicorp/index.spec.ts        | 22 ++++---
 lib/versioning/hashicorp/index.ts             |  2 +-
 12 files changed, 257 insertions(+), 56 deletions(-)
 create mode 100644 lib/manager/terraform/__fixtures__/lockedVersion.tf
 create mode 100644 lib/manager/terraform/__fixtures__/rangeStrategy.hcl

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 8abfac7a13..8cfcb0a98a 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1860,7 +1860,7 @@ Behavior:
 - `bump` = e.g. bump the range even if the new version satisfies the existing range, e.g. `^1.0.0` -> `^1.1.0`
 - `replace` = Replace the range with a newer one if the new version falls outside it, e.g. `^1.0.0` -> `^2.0.0`
 - `widen` = Widen the range with newer one, e.g. `^1.0.0` -> `^1.0.0 || ^2.0.0`
-- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, `yarn` and `poetry` so far
+- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, `yarn`, `terraform` and `poetry` so far
 
 Renovate's `"auto"` strategy works like this for npm:
 
diff --git a/lib/manager/terraform/__fixtures__/lockedVersion.tf b/lib/manager/terraform/__fixtures__/lockedVersion.tf
new file mode 100644
index 0000000000..fc354ff15d
--- /dev/null
+++ b/lib/manager/terraform/__fixtures__/lockedVersion.tf
@@ -0,0 +1,15 @@
+terraform {
+  required_providers {
+    aws = {
+      source  = "aws"
+      version = "~> 3.0"
+    }
+    azurerm = {
+      version = "~> 2.50.0"
+    }
+    kubernetes = {
+      source  = "terraform.example.com/example/kubernetes"
+      version = ">= 1.0"
+    }
+  }
+}
diff --git a/lib/manager/terraform/__fixtures__/rangeStrategy.hcl b/lib/manager/terraform/__fixtures__/rangeStrategy.hcl
new file mode 100644
index 0000000000..5bcdf22bcb
--- /dev/null
+++ b/lib/manager/terraform/__fixtures__/rangeStrategy.hcl
@@ -0,0 +1,32 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+  version     = "3.1.0"
+  constraints = "~> 3.0"
+  hashes = [
+    "h1:ULKfwySvQ4pDhy027ryRhLxDhg640wsojYc+7NHMFBU=",
+    "zh:df568a69087831c1780fac4395630a2cfb3cdf67b7dffbfe16bd78c64770bb75",
+    "zh:fce1b69dd673aace19508640b0b9b7eb1ef7e746d76cb846b49e7d52e0f5fb7e",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/azurerm" {
+  version     = "2.50.0"
+  constraints = "~> 2.50.0"
+  hashes = [
+    "h1:Vr6WUm88s9hXGkyVjHtHsP2Jmc2ypQXn6ww7dXtvk1M=",
+    "zh:e98f1d178d1e111b3f3449e27d305ce263071226fad3d86272e1bd161c26fd43",
+    "zh:eb76ec000c9c49a0bf730370c8880f671597bc01f7b7401ab301df7124c049ec",
+  ]
+}
+
+provider "https://terraform.example.com/example/kubernetes" {
+  version     = "1.5.0"
+  constraints = ">= 1.0"
+  hashes = [
+    "h1:Vr6WUm88s9hXGkyVjHtHsP2Jmc2ypQXn6ww7dXtvk1M=",
+    "zh:e98f1d178d1e111b3f3449e27d305ce263071226fad3d86272e1bd161c26fd43",
+    "zh:eb76ec000c9c49a0bf730370c8880f671597bc01f7b7401ab301df7124c049ec",
+  ]
+}
diff --git a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
index 1fba3faa42..edc6ea6fdc 100644
--- a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
@@ -187,29 +187,39 @@ Object {
       "datasource": "terraform-provider",
       "depName": "azurerm",
       "depType": "provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/azurerm",
     },
     Object {
       "currentValue": "=2.4",
       "datasource": "terraform-provider",
       "depName": "gitlab",
       "depType": "provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/gitlab",
     },
     Object {
       "currentValue": "=1.3",
       "datasource": "terraform-provider",
       "depName": "gitlab",
       "depType": "provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/gitlab",
     },
     Object {
       "datasource": "terraform-provider",
       "depName": "helm",
       "depType": "provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/helm",
     },
     Object {
       "currentValue": "V1.9",
       "datasource": "terraform-provider",
       "depName": "newrelic",
       "depType": "provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/newrelic",
     },
     Object {
       "currentValue": "v1.0.0",
@@ -258,12 +268,16 @@ Object {
       "datasource": "terraform-provider",
       "depName": "aws",
       "depType": "required_provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/aws",
     },
     Object {
       "currentValue": ">= 2.0.0",
       "datasource": "terraform-provider",
       "depName": "azurerm",
       "depType": "required_provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/azurerm",
     },
     Object {
       "currentValue": ">= 0.13",
@@ -278,6 +292,8 @@ Object {
       "datasource": "terraform-provider",
       "depName": "docker",
       "depType": "required_provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/docker",
       "registryUrls": Array [
         "https://releases.hashicorp.com",
       ],
@@ -287,13 +303,16 @@ Object {
       "datasource": "terraform-provider",
       "depName": "aws",
       "depType": "required_provider",
-      "lookupName": "aws",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/aws",
     },
     Object {
       "currentValue": "=2.27.0",
       "datasource": "terraform-provider",
       "depName": "azurerm",
       "depType": "required_provider",
+      "lockedVersion": undefined,
+      "lookupName": "hashicorp/azurerm",
     },
     Object {
       "currentValue": "1.2.4",
@@ -307,6 +326,7 @@ Object {
       "datasource": "terraform-provider",
       "depName": "helm",
       "depType": "required_provider",
+      "lockedVersion": undefined,
       "lookupName": "hashicorp/helm",
     },
     Object {
@@ -314,6 +334,7 @@ Object {
       "datasource": "terraform-provider",
       "depName": "kubernetes",
       "depType": "required_provider",
+      "lockedVersion": undefined,
       "lookupName": "hashicorp/kubernetes",
       "registryUrls": Array [
         "https://terraform.example.com",
@@ -368,3 +389,37 @@ Object {
   ],
 }
 `;
+
+exports[`manager/terraform/extract extractPackageFile() update lockfile constraints with range strategy update-lockfile 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentValue": "~> 3.0",
+      "datasource": "terraform-provider",
+      "depName": "aws",
+      "depType": "required_provider",
+      "lockedVersion": "3.1.0",
+      "lookupName": "hashicorp/aws",
+    },
+    Object {
+      "currentValue": "~> 2.50.0",
+      "datasource": "terraform-provider",
+      "depName": "azurerm",
+      "depType": "required_provider",
+      "lockedVersion": "2.50.0",
+      "lookupName": "hashicorp/azurerm",
+    },
+    Object {
+      "currentValue": ">= 1.0",
+      "datasource": "terraform-provider",
+      "depName": "kubernetes",
+      "depType": "required_provider",
+      "lockedVersion": undefined,
+      "lookupName": "example/kubernetes",
+      "registryUrls": Array [
+        "https://terraform.example.com",
+      ],
+    },
+  ],
+}
+`;
diff --git a/lib/manager/terraform/extract.spec.ts b/lib/manager/terraform/extract.spec.ts
index 90f9abc5bd..88f53e9ffb 100644
--- a/lib/manager/terraform/extract.spec.ts
+++ b/lib/manager/terraform/extract.spec.ts
@@ -1,5 +1,8 @@
-import { loadFixture } from '../../../test/util';
-import { extractPackageFile } from './extract';
+import { join } from 'upath';
+import { fs, loadFixture } from '../../../test/util';
+import { setGlobalConfig } from '../../config/global';
+import type { RepoGlobalConfig } from '../../config/types';
+import { extractPackageFile } from '.';
 
 const tf1 = loadFixture('1.tf');
 const tf2 = `module "relative" {
@@ -7,26 +10,57 @@ const tf2 = `module "relative" {
 }
 `;
 const helm = loadFixture('helm.tf');
+const lockedVersion = loadFixture('lockedVersion.tf');
+const lockedVersionLockfile = loadFixture('rangeStrategy.hcl');
+
+const adminConfig: RepoGlobalConfig = {
+  // `join` fixes Windows CI
+  localDir: join('/tmp/github/some/repo'),
+  cacheDir: join('/tmp/cache'),
+};
+
+// auto-mock fs
+jest.mock('../../util/fs');
 
 describe('manager/terraform/extract', () => {
+  beforeEach(() => {
+    setGlobalConfig(adminConfig);
+  });
   describe('extractPackageFile()', () => {
-    it('returns null for empty', () => {
-      expect(extractPackageFile('nothing here')).toBeNull();
+    it('returns null for empty', async () => {
+      expect(await extractPackageFile('nothing here', '1.tf', {})).toBeNull();
     });
-    it('extracts', () => {
-      const res = extractPackageFile(tf1);
+
+    it('extracts', async () => {
+      const res = await extractPackageFile(tf1, '1.tf', {});
       expect(res).toMatchSnapshot();
       expect(res.deps).toHaveLength(46);
       expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(8);
     });
-    it('returns null if only local deps', () => {
-      expect(extractPackageFile(tf2)).toBeNull();
+
+    it('returns null if only local deps', async () => {
+      expect(await extractPackageFile(tf2, '2.tf', {})).toBeNull();
     });
-    it('extract helm releases', () => {
-      const res = extractPackageFile(helm);
+
+    it('extract helm releases', async () => {
+      const res = await extractPackageFile(helm, 'helm.tf', {});
       expect(res).toMatchSnapshot();
       expect(res.deps).toHaveLength(6);
       expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(2);
     });
+
+    it('update lockfile constraints with range strategy update-lockfile', async () => {
+      fs.readLocalFile.mockResolvedValueOnce(lockedVersionLockfile);
+      fs.getSiblingFileName.mockReturnValueOnce('aLockFile.hcl');
+
+      const res = await extractPackageFile(
+        lockedVersion,
+        'lockedVersion.tf',
+        {}
+      );
+      expect(res).toMatchSnapshot();
+      expect(res.deps).toHaveLength(3);
+      expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(0);
+    });
   });
 });
diff --git a/lib/manager/terraform/extract.ts b/lib/manager/terraform/extract.ts
index 2d94fb9820..952bf0fab8 100644
--- a/lib/manager/terraform/extract.ts
+++ b/lib/manager/terraform/extract.ts
@@ -1,6 +1,8 @@
 import { logger } from '../../logger';
+import type { ExtractConfig } from '../types';
 import type { PackageDependency, PackageFile } from '../types';
 import { TerraformDependencyTypes } from './common';
+import { extractLocks, findLockFile, readLockFile } from './lockfile/util';
 import { analyseTerraformModule, extractTerraformModule } from './modules';
 import {
   analyzeTerraformProvider,
@@ -34,7 +36,11 @@ const contentCheckList = [
   ' "docker_image" ',
 ];
 
-export function extractPackageFile(content: string): PackageFile | null {
+export async function extractPackageFile(
+  content: string,
+  fileName: string,
+  config: ExtractConfig
+): Promise<PackageFile | null> {
   logger.trace({ content }, 'terraform.extractPackageFile()');
   if (!checkFileContainsDependency(content, contentCheckList)) {
     return null;
@@ -99,13 +105,24 @@ export function extractPackageFile(content: string): PackageFile | null {
   } catch (err) /* istanbul ignore next */ {
     logger.warn({ err }, 'Error extracting terraform plugins');
   }
+
+  const locks = [];
+  const lockFilePath = findLockFile(fileName);
+  if (lockFilePath) {
+    const lockFileContent = await readLockFile(lockFilePath);
+    if (lockFileContent) {
+      const extractedLocks = extractLocks(lockFileContent);
+      locks.push(...extractedLocks);
+    }
+  }
+
   deps.forEach((dep) => {
     switch (dep.managerData.terraformDependencyType) {
       case TerraformDependencyTypes.required_providers:
-        analyzeTerraformRequiredProvider(dep);
+        analyzeTerraformRequiredProvider(dep, locks);
         break;
       case TerraformDependencyTypes.provider:
-        analyzeTerraformProvider(dep);
+        analyzeTerraformProvider(dep, locks);
         break;
       case TerraformDependencyTypes.module:
         analyseTerraformModule(dep);
diff --git a/lib/manager/terraform/lockfile/index.ts b/lib/manager/terraform/lockfile/index.ts
index 6322f3e6fe..4fe381265a 100644
--- a/lib/manager/terraform/lockfile/index.ts
+++ b/lib/manager/terraform/lockfile/index.ts
@@ -4,6 +4,7 @@ import { TerraformProviderDatasource } from '../../../datasource/terraform-provi
 import { logger } from '../../../logger';
 import { get as getVersioning } from '../../../versioning';
 import type { UpdateArtifact, UpdateArtifactsResult } from '../../types';
+import { massageProviderLookupName } from '../util';
 import { TerraformProviderHash } from './hash';
 import type { ProviderLock, ProviderLockUpdate } from './types';
 import {
@@ -85,30 +86,23 @@ export async function updateArtifacts({
         ['provider', 'required_provider'].includes(dep.depType)
       );
       for (const dep of providerDeps) {
-        const lookupName = dep.lookupName ?? dep.depName;
+        massageProviderLookupName(dep);
+        const { registryUrls, newVersion, newValue, lookupName } = dep;
 
-        // handle cases like `Telmate/proxmox`
-        const massagedLookupName = lookupName.toLowerCase();
-
-        const repository = massagedLookupName.includes('/')
-          ? massagedLookupName
-          : `hashicorp/${massagedLookupName}`;
-        const registryUrl = dep.registryUrls
-          ? dep.registryUrls[0]
+        const registryUrl = registryUrls
+          ? registryUrls[0]
           : TerraformProviderDatasource.defaultRegistryUrls[0];
-        const newConstraint = isPinnedVersion(dep.newValue)
-          ? dep.newVersion
-          : dep.newValue;
+        const newConstraint = isPinnedVersion(newValue) ? newVersion : newValue;
         const updateLock = locks.find(
-          (value) => value.lookupName === repository
+          (value) => value.lookupName === lookupName
         );
         const update: ProviderLockUpdate = {
-          newVersion: dep.newVersion,
+          newVersion,
           newConstraint,
           newHashes: await TerraformProviderHash.createHashes(
             registryUrl,
-            repository,
-            dep.newVersion
+            lookupName,
+            newVersion
           ),
           ...updateLock,
         };
diff --git a/lib/manager/terraform/providers.ts b/lib/manager/terraform/providers.ts
index 6feb0f9209..553fcacbe8 100644
--- a/lib/manager/terraform/providers.ts
+++ b/lib/manager/terraform/providers.ts
@@ -4,8 +4,13 @@ import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import type { PackageDependency } from '../types';
 import { TerraformDependencyTypes } from './common';
+import type { ProviderLock } from './lockfile/types';
 import type { ExtractionResult } from './types';
-import { keyValueExtractionRegex } from './util';
+import {
+  getLockedVersion,
+  keyValueExtractionRegex,
+  massageProviderLookupName,
+} from './util';
 
 export const sourceExtractionRegex =
   /^(?:(?<hostname>(?:[a-zA-Z0-9]+\.+)+[a-zA-Z0-9]+)\/)?(?:(?<namespace>[^/]+)\/)?(?<type>[^/]+)/;
@@ -58,7 +63,10 @@ export function extractTerraformProvider(
   return { lineNumber, dependencies: deps };
 }
 
-export function analyzeTerraformProvider(dep: PackageDependency): void {
+export function analyzeTerraformProvider(
+  dep: PackageDependency,
+  locks: ProviderLock[]
+): void {
   /* eslint-disable no-param-reassign */
   dep.depType = 'provider';
   dep.depName = dep.managerData.moduleName;
@@ -66,19 +74,23 @@ export function analyzeTerraformProvider(dep: PackageDependency): void {
 
   if (is.nonEmptyString(dep.managerData.source)) {
     const source = sourceExtractionRegex.exec(dep.managerData.source);
-    if (source) {
-      // buildin providers https://github.com/terraform-providers
-      if (source.groups.namespace === 'terraform-providers') {
-        dep.registryUrls = [`https://releases.hashicorp.com`];
-      } else if (source.groups.hostname) {
-        dep.registryUrls = [`https://${source.groups.hostname}`];
-        dep.lookupName = `${source.groups.namespace}/${source.groups.type}`;
-      } else {
-        dep.lookupName = dep.managerData.source;
-      }
-    } else {
+    if (!source) {
       dep.skipReason = SkipReason.UnsupportedUrl;
+      return;
+    }
+
+    // buildin providers https://github.com/terraform-providers
+    if (source.groups.namespace === 'terraform-providers') {
+      dep.registryUrls = [`https://releases.hashicorp.com`];
+    } else if (source.groups.hostname) {
+      dep.registryUrls = [`https://${source.groups.hostname}`];
+      dep.lookupName = `${source.groups.namespace}/${source.groups.type}`;
+    } else {
+      dep.lookupName = dep.managerData.source;
     }
   }
+  massageProviderLookupName(dep);
+
+  dep.lockedVersion = getLockedVersion(dep, locks);
   /* eslint-enable no-param-reassign */
 }
diff --git a/lib/manager/terraform/required-providers.ts b/lib/manager/terraform/required-providers.ts
index 7d519ed74a..95c7f0d25f 100644
--- a/lib/manager/terraform/required-providers.ts
+++ b/lib/manager/terraform/required-providers.ts
@@ -1,5 +1,6 @@
 import type { PackageDependency } from '../types';
 import { TerraformDependencyTypes } from './common';
+import type { ProviderLock } from './lockfile/types';
 import { analyzeTerraformProvider } from './providers';
 import type { ExtractionResult } from './types';
 import { keyValueExtractionRegex } from './util';
@@ -72,9 +73,12 @@ export function extractTerraformRequiredProviders(
   return { lineNumber, dependencies: deps };
 }
 
-export function analyzeTerraformRequiredProvider(dep: PackageDependency): void {
+export function analyzeTerraformRequiredProvider(
+  dep: PackageDependency,
+  locks: ProviderLock[]
+): void {
   /* eslint-disable no-param-reassign */
-  analyzeTerraformProvider(dep);
+  analyzeTerraformProvider(dep, locks);
   dep.depType = `required_provider`;
   /* eslint-enable no-param-reassign */
 }
diff --git a/lib/manager/terraform/util.ts b/lib/manager/terraform/util.ts
index 9e42e144c6..10082d7d55 100644
--- a/lib/manager/terraform/util.ts
+++ b/lib/manager/terraform/util.ts
@@ -1,4 +1,7 @@
+import { TerraformProviderDatasource } from '../../datasource/terraform-provider';
+import type { PackageDependency } from '../types';
 import { TerraformDependencyTypes } from './common';
+import type { ProviderLock } from './lockfile/types';
 
 export const keyValueExtractionRegex =
   /^\s*(?<key>[^\s]+)\s+=\s+"(?<value>[^"]+)"\s*$/;
@@ -42,3 +45,34 @@ export function checkIfStringIsPath(path: string): boolean {
   const match = pathStringRegex.exec(path);
   return !!match;
 }
+
+export function massageProviderLookupName(dep: PackageDependency): void {
+  /* eslint-disable no-param-reassign */
+  if (!dep.lookupName) {
+    dep.lookupName = dep.depName;
+  }
+  if (!dep.lookupName.includes('/')) {
+    dep.lookupName = `hashicorp/${dep.lookupName}`;
+  }
+
+  // handle cases like `Telmate/proxmox`
+  dep.lookupName = dep.lookupName.toLowerCase();
+  /* eslint-enable no-param-reassign */
+}
+
+export function getLockedVersion(
+  dep: PackageDependency,
+  locks: ProviderLock[]
+): string {
+  const depRegistryUrl = dep.registryUrls
+    ? dep.registryUrls[0]
+    : TerraformProviderDatasource.defaultRegistryUrls[0];
+  const foundLock = locks.find(
+    (lock) =>
+      lock.lookupName === dep.lookupName && lock.registryUrl === depRegistryUrl
+  );
+  if (foundLock) {
+    return foundLock.version;
+  }
+  return undefined;
+}
diff --git a/lib/versioning/hashicorp/index.spec.ts b/lib/versioning/hashicorp/index.spec.ts
index e05f9ae507..63f6831883 100644
--- a/lib/versioning/hashicorp/index.spec.ts
+++ b/lib/versioning/hashicorp/index.spec.ts
@@ -53,15 +53,19 @@ describe('versioning/hashicorp/index', () => {
   );
 
   test.each`
-    currentValue            | rangeStrategy | currentVersion | newVersion  | expected
-    ${'~> 1.2'}             | ${'replace'}  | ${'1.2.3'}     | ${'2.0.7'}  | ${'~> 2.0'}
-    ${'~> 1.2.0'}           | ${'replace'}  | ${'1.2.3'}     | ${'2.0.7'}  | ${'~> 2.0.0'}
-    ${'~> 0.14.0'}          | ${'replace'}  | ${'0.14.1'}    | ${'0.15.0'} | ${'~> 0.15.0'}
-    ${'~> 0.14.0'}          | ${'replace'}  | ${'0.14.1'}    | ${'0.15.1'} | ${'~> 0.15.0'}
-    ${'~> 0.14.6'}          | ${'replace'}  | ${'0.14.6'}    | ${'0.15.0'} | ${'~> 0.15.0'}
-    ${'>= 1.0.0, <= 2.0.0'} | ${'widen'}    | ${'1.2.3'}     | ${'2.0.7'}  | ${'>= 1.0.0, <= 2.0.7'}
-    ${'0.14'}               | ${'replace'}  | ${'0.14.2'}    | ${'0.15.0'} | ${'0.15'}
-    ${'~> 0.14'}            | ${'replace'}  | ${'0.14.2'}    | ${'0.15.0'} | ${'~> 0.15'}
+    currentValue            | rangeStrategy        | currentVersion | newVersion  | expected
+    ${'~> 1.2'}             | ${'replace'}         | ${'1.2.3'}     | ${'2.0.7'}  | ${'~> 2.0'}
+    ${'~> 1.2.0'}           | ${'replace'}         | ${'1.2.3'}     | ${'2.0.7'}  | ${'~> 2.0.0'}
+    ${'~> 0.14.0'}          | ${'replace'}         | ${'0.14.1'}    | ${'0.15.0'} | ${'~> 0.15.0'}
+    ${'~> 0.14.0'}          | ${'replace'}         | ${'0.14.1'}    | ${'0.15.1'} | ${'~> 0.15.0'}
+    ${'~> 0.14.6'}          | ${'replace'}         | ${'0.14.6'}    | ${'0.15.0'} | ${'~> 0.15.0'}
+    ${'>= 1.0.0, <= 2.0.0'} | ${'widen'}           | ${'1.2.3'}     | ${'2.0.7'}  | ${'>= 1.0.0, <= 2.0.7'}
+    ${'0.14'}               | ${'replace'}         | ${'0.14.2'}    | ${'0.15.0'} | ${'0.15'}
+    ${'~> 0.14'}            | ${'replace'}         | ${'0.14.2'}    | ${'0.15.0'} | ${'~> 0.15'}
+    ${'~> 0.14'}            | ${'update-lockfile'} | ${'0.14.2'}    | ${'0.14.6'} | ${'~> 0.14'}
+    ${'~> 0.14'}            | ${'update-lockfile'} | ${'0.14.2'}    | ${'0.15.0'} | ${'~> 0.15'}
+    ${'~> 2.62.0'}          | ${'update-lockfile'} | ${'2.62.0'}    | ${'2.62.1'} | ${'~> 2.62.0'}
+    ${'~> 2.62.0'}          | ${'update-lockfile'} | ${'2.62.0'}    | ${'2.67.0'} | ${'~> 2.67.0'}
   `(
     'getNewValue("$currentValue", "$rangeStrategy", "$currentVersion", "$newVersion") === "$expected"',
     ({ currentValue, rangeStrategy, currentVersion, newVersion, expected }) => {
diff --git a/lib/versioning/hashicorp/index.ts b/lib/versioning/hashicorp/index.ts
index 288c0a210b..7fccf31e6a 100644
--- a/lib/versioning/hashicorp/index.ts
+++ b/lib/versioning/hashicorp/index.ts
@@ -35,7 +35,7 @@ function getNewValue({
   currentVersion,
   newVersion,
 }: NewValueConfig): string {
-  if (rangeStrategy === 'replace') {
+  if (['replace', 'update-lockfile'].includes(rangeStrategy)) {
     if (/~>\s*0\.\d+/.test(currentValue) && npm.getMajor(newVersion) === 0) {
       const testFullVersion = /(~>\s*0\.)(\d+)\.\d$/;
       let replaceValue = '';
-- 
GitLab