Skip to content

Commit 8695e84

Browse files
authored
fix: semver like titles (#125)
1 parent c8f3391 commit 8695e84

File tree

5 files changed

+172
-2
lines changed

5 files changed

+172
-2
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Possible options are:
4343

4444
For more details on how semantic version difference is calculated please see [semver](https://www.npmjs.com/package/semver) package.
4545

46+
If you set a value other than `any`, PRs that are not semantic version compliant are skipped.
47+
An example of a non-semantic version is a commit hash when using git submodules.
48+
4649
### `pr-number`
4750

4851
_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)

dist/index.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6142,6 +6142,64 @@ class SemVer {
61426142
module.exports = SemVer
61436143

61446144

6145+
/***/ }),
6146+
6147+
/***/ 3466:
6148+
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
6149+
6150+
const SemVer = __nccwpck_require__(8088)
6151+
const parse = __nccwpck_require__(5925)
6152+
const {re, t} = __nccwpck_require__(9523)
6153+
6154+
const coerce = (version, options) => {
6155+
if (version instanceof SemVer) {
6156+
return version
6157+
}
6158+
6159+
if (typeof version === 'number') {
6160+
version = String(version)
6161+
}
6162+
6163+
if (typeof version !== 'string') {
6164+
return null
6165+
}
6166+
6167+
options = options || {}
6168+
6169+
let match = null
6170+
if (!options.rtl) {
6171+
match = version.match(re[t.COERCE])
6172+
} else {
6173+
// Find the right-most coercible string that does not share
6174+
// a terminus with a more left-ward coercible string.
6175+
// Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
6176+
//
6177+
// Walk through the string checking with a /g regexp
6178+
// Manually set the index so as to pick up overlapping matches.
6179+
// Stop when we get a match that ends at the string end, since no
6180+
// coercible string can be more right-ward without the same terminus.
6181+
let next
6182+
while ((next = re[t.COERCERTL].exec(version)) &&
6183+
(!match || match.index + match[0].length !== version.length)
6184+
) {
6185+
if (!match ||
6186+
next.index + next[0].length !== match.index + match[0].length) {
6187+
match = next
6188+
}
6189+
re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length
6190+
}
6191+
// leave it in a clean state
6192+
re[t.COERCERTL].lastIndex = -1
6193+
}
6194+
6195+
if (match === null)
6196+
return null
6197+
6198+
return parse(`${match[2]}.${match[3] || '0'}.${match[4] || '0'}`, options)
6199+
}
6200+
module.exports = coerce
6201+
6202+
61456203
/***/ }),
61466204

61476205
/***/ 4309:
@@ -6244,6 +6302,19 @@ const parse = (version, options) => {
62446302
module.exports = parse
62456303

62466304

6305+
/***/ }),
6306+
6307+
/***/ 9601:
6308+
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
6309+
6310+
const parse = __nccwpck_require__(5925)
6311+
const valid = (version, options) => {
6312+
const v = parse(version, options)
6313+
return v ? v.version : null
6314+
}
6315+
module.exports = valid
6316+
6317+
62476318
/***/ }),
62486319

62496320
/***/ 2293:
@@ -9179,8 +9250,11 @@ function isMajorRelease(pullRequest) {
91799250
"use strict";
91809251

91819252
const semverDiff = __nccwpck_require__(4297)
9253+
const semverCoerce = __nccwpck_require__(3466)
9254+
const semverValid = __nccwpck_require__(9601)
91829255

91839256
const { semanticVersionOrder } = __nccwpck_require__(5013)
9257+
const { logWarning } = __nccwpck_require__(653)
91849258

91859259
const expression = /from ([^\s]+) to ([^\s]+)/
91869260

@@ -9190,13 +9264,27 @@ const checkTargetMatchToPR = (prTitle, target) => {
91909264
if (!match) {
91919265
return true
91929266
}
9193-
const diff = semverDiff(match[1], match[2])
9267+
9268+
const [, from, to] = match
9269+
9270+
if ((!semverValid(from) && hasBadChars(from)) || (!semverValid(to) && hasBadChars(to))) {
9271+
logWarning(`PR title contains invalid semver versions from: ${from} to: ${to}`)
9272+
return false
9273+
}
9274+
9275+
const diff = semverDiff(semverCoerce(from), semverCoerce(to))
91949276

91959277
return !(
91969278
diff &&
91979279
semanticVersionOrder.indexOf(diff) > semanticVersionOrder.indexOf(target)
91989280
)
91999281
}
9282+
9283+
function hasBadChars(version) {
9284+
// recognize submodules title likes 'Bump dotbot from `aa93350` to `acaaaac`'
9285+
return /`/.test(version)
9286+
}
9287+
92009288
module.exports = checkTargetMatchToPR
92019289

92029290

src/checkTargetMatchToPR.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
'use strict'
22
const semverDiff = require('semver/functions/diff')
3+
const semverCoerce = require('semver/functions/coerce')
4+
const semverValid = require('semver/functions/valid')
35

46
const { semanticVersionOrder } = require('./getTargetInput')
7+
const { logWarning } = require('./log')
58

69
const expression = /from ([^\s]+) to ([^\s]+)/
710

@@ -11,11 +14,25 @@ const checkTargetMatchToPR = (prTitle, target) => {
1114
if (!match) {
1215
return true
1316
}
14-
const diff = semverDiff(match[1], match[2])
17+
18+
const [, from, to] = match
19+
20+
if ((!semverValid(from) && hasBadChars(from)) || (!semverValid(to) && hasBadChars(to))) {
21+
logWarning(`PR title contains invalid semver versions from: ${from} to: ${to}`)
22+
return false
23+
}
24+
25+
const diff = semverDiff(semverCoerce(from), semverCoerce(to))
1526

1627
return !(
1728
diff &&
1829
semanticVersionOrder.indexOf(diff) > semanticVersionOrder.indexOf(target)
1930
)
2031
}
32+
33+
function hasBadChars(version) {
34+
// recognize submodules title likes 'Bump dotbot from `aa93350` to `acaaaac`'
35+
return /`/.test(version)
36+
}
37+
2138
module.exports = checkTargetMatchToPR

test/action.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,29 @@ tap.test('should call external api for github-action-merge-dependabot major rele
263263
t.ok(stubs.logStub.logWarning.calledOnce)
264264
t.ok(stubs.fetchStub.calledOnce)
265265
})
266+
267+
tap.test('should check submodules semver when target is set', async t => {
268+
const PR_NUMBER = Math.random()
269+
const { action, stubs } = buildStubbedAction({
270+
payload: {
271+
pull_request: {
272+
number: PR_NUMBER,
273+
title: 'Bump dotbot from `aa93350` to `ac5793c`',
274+
user: { login: BOT_NAME },
275+
head: { ref: 'dependabot/submodules/dotbot-ac5793c' },
276+
}
277+
},
278+
inputs: {
279+
PR_NUMBER,
280+
TARGET: 'minor',
281+
EXCLUDE_PKGS: [],
282+
API_URL: 'custom one',
283+
DEFAULT_API_URL,
284+
}
285+
})
286+
287+
await action()
288+
289+
t.ok(stubs.logStub.logWarning.calledOnceWith('Target specified does not match to PR, skipping.'))
290+
t.ok(stubs.fetchStub.notCalled)
291+
})

test/checkTargetMatchToPR.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const preReleaseToPathUpgradePRTitle =
1818
'chore(deps-dev): bump fastify from 3.18.0-alpha to 3.18.2'
1919
const sameVersion = 'chore(deps-dev): bump fastify from 3.18.0 to 3.18.0'
2020
const patchPRTitleInSubDirectory = 'chore(deps-dev): bump fastify from 3.18.0 to 3.18.1 in /packages/a'
21+
const semverLikeMinor = 'chore(deps): bump nearform/optic-release-automation-action from 2.2.0 to 2.3'
22+
const semverLikeMajor = 'chore(deps): bump nearform/optic-release-automation-action from 2.2.0 to 3'
23+
const semverLikeBothWay = 'chore(deps): bump nearform/optic-release-automation-action from 2 to 3'
24+
const submodules = 'Bump dotbot from `aa93350` to `ac5793c`'
25+
const submodulesAlpha = 'Bump dotbot from `aa93350` to `acaaaac`'
2126

2227
tap.test('checkTargetMatchToPR', async t => {
2328
t.test('should return true when target is major', async t => {
@@ -140,4 +145,35 @@ tap.test('checkTargetMatchToPR', async t => {
140145
t.notOk(checkTargetMatchToPR(preMajorPRTitle, targetOptions.minor))
141146
})
142147
})
148+
149+
t.test('semver-like PR titles', async t => {
150+
t.test('semver to minor semver-like', async t => {
151+
t.notOk(checkTargetMatchToPR(semverLikeMinor, targetOptions.prepatch))
152+
t.notOk(checkTargetMatchToPR(semverLikeMinor, targetOptions.patch))
153+
t.ok(checkTargetMatchToPR(semverLikeMinor, targetOptions.minor))
154+
t.ok(checkTargetMatchToPR(semverLikeMinor, targetOptions.major))
155+
})
156+
157+
t.test('semver to major semver-like', async t => {
158+
t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.prepatch))
159+
t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.patch))
160+
t.notOk(checkTargetMatchToPR(semverLikeMajor, targetOptions.minor))
161+
t.ok(checkTargetMatchToPR(semverLikeMajor, targetOptions.major))
162+
})
163+
164+
t.test('semver-like to semver-like', async t => {
165+
t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.prepatch))
166+
t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.patch))
167+
t.notOk(checkTargetMatchToPR(semverLikeBothWay, targetOptions.minor))
168+
t.ok(checkTargetMatchToPR(semverLikeBothWay, targetOptions.major))
169+
})
170+
})
171+
172+
t.test('submodules', async t => {
173+
t.notOk(checkTargetMatchToPR(submodules, targetOptions.prepatch))
174+
t.notOk(checkTargetMatchToPR(submodules, targetOptions.patch))
175+
t.notOk(checkTargetMatchToPR(submodules, targetOptions.minor))
176+
t.notOk(checkTargetMatchToPR(submodules, targetOptions.major))
177+
t.notOk(checkTargetMatchToPR(submodulesAlpha, targetOptions.major))
178+
})
143179
})

0 commit comments

Comments
 (0)