diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39299109..8931ba73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 - - uses: actions/setup-node@v2.4.1 + - uses: actions/setup-node@v2.5.0 with: node-version: 12 # https://github.com/actions/runner/issues/772 - run: npm ci diff --git a/README.md b/README.md index 509f7b26..e30937af 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This action automatically approves and merges dependabot PRs. ### `exclude` -_Optional_ An comma separated value of packages that you don't want to auto-merge and would like to manually review to decide whether to upgrade or not. +_Optional_ A comma separated value of packages that you don't want to auto-merge and would like to manually review to decide whether to upgrade or not. ### `approve-only` @@ -35,11 +35,16 @@ _Optional_ A custom url where the external API which is delegated the task of ap ### `target` -_Optional_ A flag to only auto-merge updates based on Semantic Versioning. Default to `major` merge. Possible options are: +_Optional_ A flag to only auto-merge updates based on Semantic Versioning. Defaults to `any`. -`major, premajor, minor, preminor, patch, prepatch, prerelease or any`. Defaults to `any`. +Possible options are: -For more details on how semantic version difference calculated please see [semver](https://www.npmjs.com/package/semver) package +`major, premajor, minor, preminor, patch, prepatch, prerelease, any`. + +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` diff --git a/action.yml b/action.yml index b9d3ef26..b05b1587 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: target: description: 'Auto-merge on major, minor, patch updates based on Semantic Versioning' required: false - default: 'major' + default: 'any' pr-number: description: 'A pull request number, only required if triggered from a workflow_dispatch event' required: false 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/package-lock.json b/package-lock.json index c08f0136..728b3aac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github-action-merge-dependabot", - "version": "2.4.0", + "version": "2.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -340,14 +340,14 @@ } }, "@eslint/eslintrc": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", - "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.0.0", + "espree": "^9.2.0", "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", @@ -363,9 +363,9 @@ "dev": true }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -383,12 +383,12 @@ } }, "@humanwhocodes/config-array": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", - "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.4" } @@ -557,9 +557,9 @@ "dev": true }, "@vercel/ncc": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.32.0.tgz", - "integrity": "sha512-S/SxTHHTbBQSOutpgnqEn+LyTfZcq9xMRAnzY05HpGVjxjmfmvg6SWZZkbW/GJIFznMmHGeGOrI1MEBD7efIkA==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.33.0.tgz", + "integrity": "sha512-m21HzXD+L/YHoJTwyCCyGBXD0O2L4KR/Y6SSsS4qvRlz3NAYsiiYahzOsgfWEt4PJxpPVCMGiuWzsvb2tycmyA==", "dev": true }, "acorn": { @@ -1060,13 +1060,13 @@ "dev": true }, "eslint": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", - "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-TxU/p7LB1KxQ6+7aztTnO7K0i+h0tDi81YRY9VzB6Id71kNz+fFYnf5HD5UOQmxkzcoa0TlVZf9dpMtUv0GpWg==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.0.4", - "@humanwhocodes/config-array": "^0.6.0", + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1077,7 +1077,7 @@ "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.1.0", - "espree": "^9.1.0", + "espree": "^9.2.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1112,9 +1112,9 @@ "dev": true }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -1191,9 +1191,9 @@ "dev": true }, "espree": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", - "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.2.0.tgz", + "integrity": "sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg==", "dev": true, "requires": { "acorn": "^8.6.0", @@ -2313,9 +2313,9 @@ "dev": true }, "prettier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz", - "integrity": "sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", "dev": true }, "process-on-spawn": { diff --git a/package.json b/package.json index 7aff1844..719a543e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-action-merge-dependabot", - "version": "2.4.0", + "version": "2.7.1", "description": "A GitHub action to automatically merge and approve Dependabot pull requests", "main": "src/index.js", "scripts": { @@ -32,10 +32,10 @@ "semver": "^7.3.5" }, "devDependencies": { - "@vercel/ncc": "^0.32.0", - "eslint": "^8.3.0", + "@vercel/ncc": "^0.33.0", + "eslint": "^8.4.1", "husky": "^7.0.4", - "prettier": "^2.5.0", + "prettier": "^2.5.1", "proxyquire": "^2.1.3", "sinon": "^12.0.1", "tap": "^15.1.5" 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)) + }) })