diff --git a/README.md b/README.md index ac9f508a..e30937af 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ Possible options are: For more details on how semantic version difference is calculated please see [semver](https://www.npmjs.com/package/semver) package. +If you set a value other than `any`, PRs that are not semantic version compliant are skipped. +An example of a non-semantic version is a commit hash when using git submodules. + ### `pr-number` _Optional_ A pull request number, only required if triggered from a workflow_dispatch event. Typically this would be triggered by a script running in a seperate CI provider. See [Trigger action from workflow_dispatch event](#trigger-action-from-workflow_dispatch-event) diff --git a/dist/index.js b/dist/index.js index f65eabfb..7bfcf575 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6142,6 +6142,64 @@ class SemVer { module.exports = SemVer +/***/ }), + +/***/ 3466: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const SemVer = __nccwpck_require__(8088) +const parse = __nccwpck_require__(5925) +const {re, t} = __nccwpck_require__(9523) + +const coerce = (version, options) => { + if (version instanceof SemVer) { + return version + } + + if (typeof version === 'number') { + version = String(version) + } + + if (typeof version !== 'string') { + return null + } + + options = options || {} + + let match = null + if (!options.rtl) { + match = version.match(re[t.COERCE]) + } else { + // Find the right-most coercible string that does not share + // a terminus with a more left-ward coercible string. + // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4' + // + // Walk through the string checking with a /g regexp + // Manually set the index so as to pick up overlapping matches. + // Stop when we get a match that ends at the string end, since no + // coercible string can be more right-ward without the same terminus. + let next + while ((next = re[t.COERCERTL].exec(version)) && + (!match || match.index + match[0].length !== version.length) + ) { + if (!match || + next.index + next[0].length !== match.index + match[0].length) { + match = next + } + re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length + } + // leave it in a clean state + re[t.COERCERTL].lastIndex = -1 + } + + if (match === null) + return null + + return parse(`${match[2]}.${match[3] || '0'}.${match[4] || '0'}`, options) +} +module.exports = coerce + + /***/ }), /***/ 4309: @@ -6244,6 +6302,19 @@ const parse = (version, options) => { module.exports = parse +/***/ }), + +/***/ 9601: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const parse = __nccwpck_require__(5925) +const valid = (version, options) => { + const v = parse(version, options) + return v ? v.version : null +} +module.exports = valid + + /***/ }), /***/ 2293: @@ -9179,8 +9250,11 @@ function isMajorRelease(pullRequest) { "use strict"; const semverDiff = __nccwpck_require__(4297) +const semverCoerce = __nccwpck_require__(3466) +const semverValid = __nccwpck_require__(9601) const { semanticVersionOrder } = __nccwpck_require__(5013) +const { logWarning } = __nccwpck_require__(653) const expression = /from ([^\s]+) to ([^\s]+)/ @@ -9190,13 +9264,27 @@ const checkTargetMatchToPR = (prTitle, target) => { if (!match) { return true } - const diff = semverDiff(match[1], match[2]) + + const [, from, to] = match + + if ((!semverValid(from) && hasBadChars(from)) || (!semverValid(to) && hasBadChars(to))) { + logWarning(`PR title contains invalid semver versions from: ${from} to: ${to}`) + return false + } + + const diff = semverDiff(semverCoerce(from), semverCoerce(to)) return !( diff && semanticVersionOrder.indexOf(diff) > semanticVersionOrder.indexOf(target) ) } + +function hasBadChars(version) { + // recognize submodules title likes 'Bump dotbot from `aa93350` to `acaaaac`' + return /`/.test(version) +} + module.exports = checkTargetMatchToPR diff --git a/src/checkTargetMatchToPR.js b/src/checkTargetMatchToPR.js index e8db5353..0130b8d5 100644 --- a/src/checkTargetMatchToPR.js +++ b/src/checkTargetMatchToPR.js @@ -1,7 +1,10 @@ 'use strict' const semverDiff = require('semver/functions/diff') +const semverCoerce = require('semver/functions/coerce') +const semverValid = require('semver/functions/valid') const { semanticVersionOrder } = require('./getTargetInput') +const { logWarning } = require('./log') const expression = /from ([^\s]+) to ([^\s]+)/ @@ -11,11 +14,25 @@ const checkTargetMatchToPR = (prTitle, target) => { if (!match) { return true } - const diff = semverDiff(match[1], match[2]) + + const [, from, to] = match + + if ((!semverValid(from) && hasBadChars(from)) || (!semverValid(to) && hasBadChars(to))) { + logWarning(`PR title contains invalid semver versions from: ${from} to: ${to}`) + return false + } + + const diff = semverDiff(semverCoerce(from), semverCoerce(to)) return !( diff && semanticVersionOrder.indexOf(diff) > semanticVersionOrder.indexOf(target) ) } + +function hasBadChars(version) { + // recognize submodules title likes 'Bump dotbot from `aa93350` to `acaaaac`' + return /`/.test(version) +} + module.exports = checkTargetMatchToPR diff --git a/test/action.test.js b/test/action.test.js index 728095e2..7f756ad5 100644 --- a/test/action.test.js +++ b/test/action.test.js @@ -263,3 +263,29 @@ tap.test('should call external api for github-action-merge-dependabot major rele t.ok(stubs.logStub.logWarning.calledOnce) t.ok(stubs.fetchStub.calledOnce) }) + +tap.test('should check submodules semver when target is set', async t => { + const PR_NUMBER = Math.random() + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + number: PR_NUMBER, + title: 'Bump dotbot from `aa93350` to `ac5793c`', + user: { login: BOT_NAME }, + head: { ref: 'dependabot/submodules/dotbot-ac5793c' }, + } + }, + inputs: { + PR_NUMBER, + TARGET: 'minor', + EXCLUDE_PKGS: [], + API_URL: 'custom one', + DEFAULT_API_URL, + } + }) + + await action() + + t.ok(stubs.logStub.logWarning.calledOnceWith('Target specified does not match to PR, skipping.')) + t.ok(stubs.fetchStub.notCalled) +}) diff --git a/test/checkTargetMatchToPR.test.js b/test/checkTargetMatchToPR.test.js index 66cc0d81..f5d51a2a 100644 --- a/test/checkTargetMatchToPR.test.js +++ b/test/checkTargetMatchToPR.test.js @@ -18,6 +18,11 @@ const preReleaseToPathUpgradePRTitle = 'chore(deps-dev): bump fastify from 3.18.0-alpha to 3.18.2' const sameVersion = 'chore(deps-dev): bump fastify from 3.18.0 to 3.18.0' const patchPRTitleInSubDirectory = 'chore(deps-dev): bump fastify from 3.18.0 to 3.18.1 in /packages/a' +const semverLikeMinor = 'chore(deps): bump nearform/optic-release-automation-action from 2.2.0 to 2.3' +const semverLikeMajor = 'chore(deps): bump nearform/optic-release-automation-action from 2.2.0 to 3' +const semverLikeBothWay = 'chore(deps): bump nearform/optic-release-automation-action from 2 to 3' +const submodules = 'Bump dotbot from `aa93350` to `ac5793c`' +const submodulesAlpha = 'Bump dotbot from `aa93350` to `acaaaac`' tap.test('checkTargetMatchToPR', async t => { t.test('should return true when target is major', async t => { @@ -140,4 +145,35 @@ tap.test('checkTargetMatchToPR', async t => { t.notOk(checkTargetMatchToPR(preMajorPRTitle, targetOptions.minor)) }) }) + + t.test('semver-like PR titles', async t => { + t.test('semver to minor semver-like', async t => { + t.notOk(checkTargetMatchToPR(semverLikeMinor, targetOptions.prepatch)) + t.notOk(checkTargetMatchToPR(semverLikeMinor, targetOptions.patch)) + t.ok(checkTargetMatchToPR(semverLikeMinor, targetOptions.minor)) + t.ok(checkTargetMatchToPR(semverLikeMinor, targetOptions.major)) + }) + + t.test('semver to major semver-like', async t => { + t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.prepatch)) + t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.patch)) + t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.minor)) + t.ok(checkTargetMatchToPR(semverLikeMajor, targetOptions.major)) + }) + + t.test('semver-like to semver-like', async t => { + t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.prepatch)) + t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.patch)) + t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.minor)) + t.ok(checkTargetMatchToPR(semverLikeBothWay, targetOptions.major)) + }) + }) + + t.test('submodules', async t => { + t.notOk(checkTargetMatchToPR(submodules, targetOptions.prepatch)) + t.notOk(checkTargetMatchToPR(submodules, targetOptions.patch)) + t.notOk(checkTargetMatchToPR(submodules, targetOptions.minor)) + t.notOk(checkTargetMatchToPR(submodules, targetOptions.major)) + t.notOk(checkTargetMatchToPR(submodulesAlpha, targetOptions.major)) + }) })