diff --git a/lib/modules/versioning/hashicorp/convertor.spec.ts b/lib/modules/versioning/hashicorp/convertor.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..29e682fcc7bad5c501e96558ea88a218ffe1affc --- /dev/null +++ b/lib/modules/versioning/hashicorp/convertor.spec.ts @@ -0,0 +1,68 @@ +import { hashicorp2npm, npm2hashicorp } from './convertor'; + +describe('modules/versioning/hashicorp/convertor', () => { + test.each` + hashicorp | npm + ${'4.2.0'} | ${'4.2.0'} + ${'4.2.0-alpha'} | ${'4.2.0-alpha'} + ${'~> 4.0'} | ${'^4.0'} + ${'~> 4.1'} | ${'^4.1'} + ${'~> 4.0.0'} | ${'~4.0.0'} + ${'~> 4.0.1'} | ${'~4.0.1'} + ${'~> 4.1.0'} | ${'~4.1.0'} + ${'~> 4.1.1'} | ${'~4.1.1'} + ${'~> 4.0.0-alpha'} | ${'~4.0.0-alpha'} + ${'>= 4.0'} | ${'>=4.0'} + ${'<= 4.0'} | ${'<=4.0'} + ${'> 4.0'} | ${'>4.0'} + ${'< 4.0'} | ${'<4.0'} + ${'> 4.0, < 5.0'} | ${'>4.0 <5.0'} + ${'~> 2.3.4'} | ${'~2.3.4'} + `( + 'hashicorp2npm("$hashicorp") === $npm && npm2hashicorp("$npm") === $hashicorp', + ({ hashicorp, npm }) => { + expect(hashicorp2npm(hashicorp)).toBe(npm); + expect(npm2hashicorp(npm)).toBe(hashicorp); + } + ); + + // These are non-reflective cases for hashicorp2npm + test.each` + hashicorp | npm + ${'~> 4'} | ${'>=4'} + ${'~> v4'} | ${'>=4'} + ${'>= v4.0'} | ${'>=4.0'} + ${'>=4.0'} | ${'>=4.0'} + ${'<=4.0'} | ${'<=4.0'} + ${'= 4.0'} | ${'4.0'} + ${'> 4.0,< 5.0'} | ${'>4.0 <5.0'} + `('hashicorp2npm("$hashicorp") === $npm', ({ hashicorp, npm }) => { + expect(hashicorp2npm(hashicorp)).toBe(npm); + }); + + // These are non-reflective cases for npm2hashicorp + test.each` + hashicorp | npm + ${'~> 4.0'} | ${'^4'} + ${'~> 4.0'} | ${'^4.0.0'} + ${'~> 4.1'} | ${'^4.1.0'} + ${'~> 4.1'} | ${'^4.1.1'} + ${'~> 4.0'} | ${'~4'} + ${'~> 4.0.0'} | ${'~4.0'} + ${'~> 4.1.0'} | ${'~4.1'} + `('npm2hashicorp("$npm") === $hashicorp', ({ hashicorp, npm }) => { + expect(npm2hashicorp(npm)).toBe(hashicorp); + }); + + test('hashicorp2npm doesnt support !=', () => { + expect(() => hashicorp2npm('!= 4')).toThrow(); + }); + + test('hashicorp2npm throws on invalid', () => { + expect(() => hashicorp2npm('^4')).toThrow(); + }); + + test('npm2hashicorp throws on unsupported', () => { + expect(() => npm2hashicorp('4.x.x')).toThrow(); + }); +}); diff --git a/lib/modules/versioning/hashicorp/convertor.ts b/lib/modules/versioning/hashicorp/convertor.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dabfb54146d616b5bf538b55137c39d5e69ef7f --- /dev/null +++ b/lib/modules/versioning/hashicorp/convertor.ts @@ -0,0 +1,101 @@ +import { regEx } from '../../../util/regex'; + +/** + * This can convert most hashicorp ranges to valid npm syntax + * The `!=` syntax is currently unsupported as there is no direct + * equivalent in npm and isn't widely used + * Also prerelease syntax is less well-defined for hashicorp and will + * cause issues if it is not semvar compatible as no attempts to convert it + * are made + */ +export function hashicorp2npm(input: string): string { + return input + .split(',') + .map((single) => { + const r = single.match( + regEx(/^\s*(|=|!=|>|<|>=|<=|~>)\s*v?((\d+)(\.\d+){0,2}[\w-+]*)\s*$/) + ); + if (!r) { + throw new Error('invalid hashicorp constraint'); + } + if (r[1] === '!=') { + throw new Error('unsupported != in hashicorp constraint'); + } + return { + operator: r[1], + version: r[2], + }; + }) + .map(({ operator, version }) => { + switch (operator) { + case '=': + return version; + case '~>': + if (version.match(regEx(/^\d+$/))) { + return `>=${version}`; + } + if (version.match(regEx(/^\d+\.\d+$/))) { + return `^${version}`; + } + return `~${version}`; + default: + return `${operator}${version}`; + } + }) + .join(' '); +} + +/** + * This can convert a limited set of npm range syntax to hashicorp, + * it supports all the syntax that hashicorp2npm can output + * It cannot handle `*`, `1.x.x`, range with `-`, `||` + */ +export function npm2hashicorp(input: string): string { + return input + .split(' ') + .map((single) => { + const r = single.match( + regEx(/^(|>|<|>=|<=|~|\^)((\d+)(\.\d+){0,2}[\w-]*)$/) + ); + if (!r) { + throw new Error('invalid npm constraint'); + } + return { + operator: r[1], + version: r[2], + }; + }) + .map(({ operator, version }) => { + switch (operator) { + case '^': { + if (version.match(regEx(/^\d+$/))) { + return `~> ${version}.0`; + } + const withZero = version.match(regEx(/^(\d+\.\d+)\.0$/)); + if (withZero) { + return `~> ${withZero[1]}`; + } + const nonZero = version.match(regEx(/^(\d+\.\d+)\.\d+$/)); + if (nonZero) { + // not including`>= ${version}`, which makes this less accurate + // but makes the results cleaner + return `~> ${nonZero[1]}`; + } + return `~> ${version}`; + } + case '~': + if (version.match(regEx(/^\d+$/))) { + return `~> ${version}.0`; + } + if (version.match(regEx(/^\d+\.\d+$/))) { + return `~> ${version}.0`; + } + return `~> ${version}`; + case '': + return `${version}`; + default: + return `${operator} ${version}`; + } + }) + .join(', '); +} diff --git a/lib/modules/versioning/hashicorp/index.spec.ts b/lib/modules/versioning/hashicorp/index.spec.ts index 7eb2795c5b92ff70d4a00e40f82b43ee436443e7..cc6bcede70c377b0e5b3bf8be7826febd9dc3e5f 100644 --- a/lib/modules/versioning/hashicorp/index.spec.ts +++ b/lib/modules/versioning/hashicorp/index.spec.ts @@ -26,6 +26,16 @@ describe('modules/versioning/hashicorp/index', () => { test.each` input | expected ${'>= 1.0.0, <= 2.0.0'} | ${true} + ${'~> 4'} | ${true} + ${'~> 4.0'} | ${true} + ${'~> 4.1'} | ${true} + ${'~> 4.1.2'} | ${true} + ${'=4'} | ${true} + ${'=4.0'} | ${true} + ${'!=4.0'} | ${false} + ${'>=4.1'} | ${true} + ${'<=4.1.2'} | ${true} + ${''} | ${false} `('isValid("$input") === $expected', ({ input, expected }) => { const res = !!semver.isValid(input); expect(res).toBe(expected); @@ -54,35 +64,30 @@ describe('modules/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'} - ${'~> 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 }) => { - const res = semver.getNewValue({ - currentValue, - rangeStrategy, - currentVersion, - newVersion, - }); - expect(res).toEqual(expected); - } - ); - - test.each` - currentValue | rangeStrategy | currentVersion | newVersion | expected - ${'v0.14'} | ${'replace'} | ${'v0.14.2'} | ${'v0.15.0'} | ${'v0.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'} + ${'~> 1.2'} | ${'replace'} | ${'1.2.3'} | ${'1.2.3'} | ${'~> 1.2'} + ${'~> 1.2'} | ${'replace'} | ${'1.2.3'} | ${'1.2.4'} | ${'~> 1.2'} + ${'~> 1.2.0'} | ${'replace'} | ${'1.2.3'} | ${'1.2.3'} | ${'~> 1.2.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'} + ${'~> 0.14.0'} | ${'replace'} | ${'0.14.1'} | ${'0.14.2'} | ${'~> 0.14.0'} + ${'~> 0.14.6'} | ${'replace'} | ${'0.14.6'} | ${'0.14.7'} | ${'~> 0.14.0'} + ${'~> 2.3.4'} | ${'replace'} | ${'2.3.4'} | ${'2.3.5'} | ${'~> 2.3.0'} + ${'~> 0.14.0'} | ${'bump'} | ${'0.14.1'} | ${'0.14.2'} | ${'~> 0.14.2'} + ${'~> 0.14.6'} | ${'bump'} | ${'0.14.6'} | ${'0.14.7'} | ${'~> 0.14.7'} + ${'~> 0.14.6'} | ${'bump'} | ${'0.14.6'} | ${'0.15.1'} | ${'~> 0.15.1'} + ${'~> 0.14.6'} | ${'bump'} | ${'0.14.6'} | ${'2.0.7'} | ${'~> 2.0.7'} + ${'>= 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'} + ${'v0.14'} | ${'replace'} | ${'v0.14.2'} | ${'v0.15.0'} | ${'v0.15'} `( 'getNewValue("$currentValue", "$rangeStrategy", "$currentVersion", "$newVersion") === "$expected"', ({ currentValue, rangeStrategy, currentVersion, newVersion, expected }) => { diff --git a/lib/modules/versioning/hashicorp/index.ts b/lib/modules/versioning/hashicorp/index.ts index 9ad144ce73776b37a26cf69e22a343eda8edac4b..c736d22b42fa25fe0a8f7c50f2a399e48c4c53c2 100644 --- a/lib/modules/versioning/hashicorp/index.ts +++ b/lib/modules/versioning/hashicorp/index.ts @@ -1,7 +1,7 @@ import type { RangeStrategy } from '../../../types/versioning'; -import { regEx } from '../../../util/regex'; import { api as npm } from '../npm'; import type { NewValueConfig, VersioningApi } from '../types'; +import { hashicorp2npm, npm2hashicorp } from './convertor'; export const id = 'hashicorp'; export const displayName = 'Hashicorp'; @@ -16,39 +16,37 @@ export const supportedRangeStrategies: RangeStrategy[] = [ 'replace', ]; -function hashicorp2npm(input: string): string { - // The only case incompatible with semver is a "short" ~>, e.g. ~> 1.2 - return input.replace(regEx(/~>(\s*\d+\.\d+$)/), '^$1').replace(',', ''); -} - function isLessThanRange(version: string, range: string): boolean { - return !!npm.isLessThanRange?.(hashicorp2npm(version), hashicorp2npm(range)); + return !!npm.isLessThanRange?.(version, hashicorp2npm(range)); } -export const isValid = (input: string): boolean => - !!input && npm.isValid(hashicorp2npm(input)); +export function isValid(input: string): boolean { + if (input) { + try { + return npm.isValid(hashicorp2npm(input)); + } catch (err) { + return false; + } + } + return false; +} -const matches = (version: string, range: string): boolean => - npm.matches(hashicorp2npm(version), hashicorp2npm(range)); +function matches(version: string, range: string): boolean { + return npm.matches(version, hashicorp2npm(range)); +} function getSatisfyingVersion( versions: string[], range: string ): string | null { - return npm.getSatisfyingVersion( - versions.map(hashicorp2npm), - hashicorp2npm(range) - ); + return npm.getSatisfyingVersion(versions, hashicorp2npm(range)); } function minSatisfyingVersion( versions: string[], range: string ): string | null { - return npm.minSatisfyingVersion( - versions.map(hashicorp2npm), - hashicorp2npm(range) - ); + return npm.minSatisfyingVersion(versions, hashicorp2npm(range)); } function getNewValue({ @@ -57,42 +55,17 @@ function getNewValue({ currentVersion, newVersion, }: NewValueConfig): string | null { - if (['replace', 'update-lockfile'].includes(rangeStrategy)) { - const minor = npm.getMinor(newVersion); - const major = npm.getMajor(newVersion); - if (regEx(/~>\s*0\.\d+/).test(currentValue) && major === 0 && minor) { - const testFullVersion = regEx(/(~>\s*0\.)(\d+)\.\d$/); - let replaceValue = ''; - if (testFullVersion.test(currentValue)) { - replaceValue = `$<prefix>${minor}.0`; - } else { - replaceValue = `$<prefix>${minor}$<suffix>`; - } - return currentValue.replace( - regEx(`(?<prefix>~>\\s*0\\.)\\d+(?<suffix>.*)$`), - replaceValue - ); - } - // handle special ~> 1.2 case - if (major && regEx(/(~>\s*)\d+\.\d+$/).test(currentValue)) { - return currentValue.replace( - regEx(`(?<prefix>~>\\s*)\\d+\\.\\d+$`), - `$<prefix>${major}.0` - ); - } - } let npmNewVersion = npm.getNewValue({ - currentValue, + currentValue: hashicorp2npm(currentValue), rangeStrategy, currentVersion, newVersion, }); - if ( - npmNewVersion && - currentValue.startsWith('v') && - !npmNewVersion.startsWith('v') - ) { - npmNewVersion = `v${npmNewVersion}`; + if (npmNewVersion) { + npmNewVersion = npm2hashicorp(npmNewVersion); + if (currentValue.startsWith('v') && !npmNewVersion.startsWith('v')) { + npmNewVersion = `v${npmNewVersion}`; + } } return npmNewVersion; } diff --git a/lib/modules/versioning/npm/index.spec.ts b/lib/modules/versioning/npm/index.spec.ts index 53183b4135e00968bcd2604eb46b938852463500..8330064d653422bdc1433f521e1bd4f1fac31bad 100644 --- a/lib/modules/versioning/npm/index.spec.ts +++ b/lib/modules/versioning/npm/index.spec.ts @@ -108,6 +108,8 @@ describe('modules/versioning/npm/index', () => { ${'^0.2.3'} | ${'replace'} | ${'0.2.3'} | ${'0.2.4'} | ${'^0.2.3'} ${'^2.3.0'} | ${'replace'} | ${'2.3.0'} | ${'2.4.0'} | ${'^2.3.0'} ${'^2.3.4'} | ${'replace'} | ${'2.3.4'} | ${'2.4.5'} | ${'^2.3.4'} + ${'^2.3.4'} | ${'replace'} | ${'2.3.4'} | ${'2.3.5'} | ${'^2.3.4'} + ${'~2.3.4'} | ${'replace'} | ${'2.3.4'} | ${'2.3.5'} | ${'~2.3.0'} ${'^0.0.1'} | ${'replace'} | ${'0.0.1'} | ${'0.0.2'} | ${'^0.0.2'} ${'^1.0.1'} | ${'replace'} | ${'1.0.1'} | ${'2.0.2'} | ${'^2.0.0'} ${'^1.2.3'} | ${'replace'} | ${'1.2.3'} | ${'1.2.3'} | ${'^1.2.3'}