Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check package changes via package.json diff #167

Merged
merged 2 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3,313 changes: 1,786 additions & 1,527 deletions dist/index.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@actions/core": "^1.6.0",
"@actions/github": "^5.0.0",
"actions-toolkit": "github:nearform/actions-toolkit",
"gitdiff-parser": "^0.2.2",
"semver": "^7.3.5"
},
"devDependencies": {
Expand Down
63 changes: 29 additions & 34 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

const core = require('@actions/core')
const github = require('@actions/github')
const semverMajor = require('semver/functions/major')
const semverDiff = require('semver/functions/diff')
const semverCoerce = require('semver/functions/coerce')
const toolkit = require('actions-toolkit')

const packageInfo = require('../package.json')
const { githubClient } = require('./github-client')
const checkTargetMatchToPR = require('./checkTargetMatchToPR')
const { logInfo, logWarning, logError } = require('./log')
const { getInputs } = require('./util')
const { targetOptions } = require('./getTargetInput')
const {
getModuleVersionChanges,
checkModuleVersionChanges,
} = require('./moduleVersionChanges')

const {
GITHUB_TOKEN,
Expand All @@ -22,7 +26,6 @@ const {
PR_NUMBER,
} = getInputs()


module.exports = async function run() {
try {
toolkit.logActionRefWarning()
Expand All @@ -44,25 +47,31 @@ module.exports = async function run() {
return logWarning('Not a dependabot PR, skipping.')
}

const prDiff = await client.getPullRequestDiff(pr.number)
const moduleChanges = getModuleVersionChanges(prDiff)

if (TARGET !== targetOptions.any) {
logInfo(`Checking if PR title [${pr.title}] has target ${TARGET}`)
const isTargetMatchToPR = checkTargetMatchToPR(pr.title, TARGET)
logInfo(`Checking if the changes in the PR can be merged`)

const isTargetMatchToPR = checkModuleVersionChanges(moduleChanges, TARGET)
if (!isTargetMatchToPR) {
return logWarning('Target specified does not match to PR, skipping.')
}
}

const { name: pkgName, version } = getPackageDetails(pr)
const upgradeMessage = `Cannot automerge github-action-merge-dependabot ${version} major release.
Read how to upgrade it manually:
https://github.com/fastify/github-action-merge-dependabot#how-to-upgrade-from-2x-to-new-3x`

if (EXCLUDE_PKGS.includes(pkgName)) {
return logInfo(`${pkgName} is excluded, skipping.`)
const changedExcludedPackages = EXCLUDE_PKGS.filter((pkg) => pkg in moduleChanges)
if (changedExcludedPackages.length > 0) {
return logInfo(`${changedExcludedPackages.length} package(s) excluded: \
${changedExcludedPackages.join(', ')}. Skipping.`)
}

if (pkgName === 'github-action-merge-dependabot' && isMajorRelease(pr)) {
const thisModuleChanges = moduleChanges[packageInfo.name]
if (thisModuleChanges && isAMajorReleaseBump(thisModuleChanges)) {
const version = moduleChanges[packageInfo.name].insert
const upgradeMessage = `Cannot automerge ${packageInfo.name} ${version} major release.
Read how to upgrade it manually:
https://github.com/fastify/${packageInfo.name}#how-to-upgrade-from-2x-to-new-3x`

core.setFailed(upgradeMessage)
return
}
Expand All @@ -79,27 +88,13 @@ module.exports = async function run() {
}
}

function getPackageDetails(pullRequest) {
// dependabot branch names are in format "dependabot/npm_and_yarn/pkg-0.0.1"
// or "dependabot/github_actions/fastify/github-action-merge-dependabot-2.6.0"
const nameAndVersion = pullRequest.head.ref.split('/').pop().split('-')
const version = nameAndVersion.pop() // remove the version
return {
name: nameAndVersion.join('-'),
version
function isAMajorReleaseBump(change) {
const from = change.delete
const to = change.insert
if (!from || !to) {
return false
}
}

function isMajorRelease(pullRequest) {
const expression = /bump \S+ from (\S+) to (\S+)/i
const match = expression.exec(pullRequest.title)
if (match) {
const [, oldVersion, newVersion] = match
const oldVersionSemver = semverCoerce(oldVersion)
const newVersionSemver = semverCoerce(newVersion)
if (semverMajor(oldVersionSemver) !== semverMajor(newVersionSemver)) {
return true
}
}
return false
const diff = semverDiff(semverCoerce(from), semverCoerce(to))
return diff === targetOptions.major
}
38 changes: 0 additions & 38 deletions src/checkTargetMatchToPR.js

This file was deleted.

15 changes: 13 additions & 2 deletions src/github-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,20 @@ function githubClient(githubToken) {
})
// todo assert
return data
}
}
},

async getPullRequestDiff(pullRequestNumber) {
const { data: pullRequest } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: pullRequestNumber,
mediaType: {
format: 'diff',
},
})
return pullRequest
},
}
}

module.exports = { githubClient }
80 changes: 80 additions & 0 deletions src/moduleVersionChanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict'

const semverDiff = require('semver/functions/diff')
const semverCoerce = require('semver/functions/coerce')
const semverValid = require('semver/functions/valid')
const { parse } = require('gitdiff-parser')

const { semanticVersionOrder } = require('./getTargetInput')
const { logWarning } = require('./log')

const expression = /"([^\s]+)":\s*"([^\s]+)"/

function hasBadChars(version) {
// recognize submodules title likes 'Bump dotbot from `aa93350` to `acaaaac`'
return /^[^^~*-0-9+x]/.test(version)
}

const checkModuleVersionChanges = (moduleChanges, target) => {
for (const module in moduleChanges) {
const from = moduleChanges[module].delete
const to = moduleChanges[module].insert

if (!from || !to) {
return false
}

if ((!semverValid(from) && hasBadChars(from)) || (!semverValid(to) && hasBadChars(to))) {
logWarning(`Module "${module}" contains invalid semver versions from: ${from} to: ${to}`)
return false
}

const diff = semverDiff(semverCoerce(from), semverCoerce(to))
const isDiffBeyondTarget =
semanticVersionOrder.indexOf(diff) > semanticVersionOrder.indexOf(target)

if (diff && isDiffBeyondTarget) {
return false
}
}

return true
}

const getModuleVersionChanges = (prDiff) => {
const parsedDiffFiles = parse(prDiff)
const packageJsonChanges = parsedDiffFiles.find((file) => file.newPath === 'package.json')
if (!packageJsonChanges) {
return false
}

const moduleChanges = {}
for (const idx in packageJsonChanges.hunks) {
const changes = packageJsonChanges.hunks[idx].changes.filter(
(c) => c.type === 'delete' || c.type === 'insert'
)

for (const changeIdx in changes) {
const change = changes[changeIdx]

const match = expression.exec(change.content)
if (!match) {
continue
}

const [, module, version] = match
if (module in moduleChanges) {
moduleChanges[module][change.type] = version
} else {
moduleChanges[module] = { [change.type]: version }
}
}
}

return moduleChanges
}

module.exports = {
getModuleVersionChanges,
checkModuleVersionChanges,
}