Skip to content

Commit

Permalink
feat: distinguish dep type, security fixes and add config file
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Aug 30, 2020
1 parent 6760397 commit b47b86d
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 17 deletions.
37 changes: 37 additions & 0 deletions README.md
Expand Up @@ -85,3 +85,40 @@ steps:
| `github-token` || `github.token` | The GitHub token used to merge the pull-request |
| `command` || `merge` | The command to pass to Dependabot |
| `approve` || `true` | Auto-approve pull-requests |

### Configuration file syntax

Using the configuration file `.github/auto-merge.yml`, you have the option to provide a more fine-grained configuration. The following example configuration file merges

* minor development dependency updates
* patch production dependency updates
* minor security-critical production dependency updates

```yml
automerged_updates:
- match:
dependency_type: "development"
# Supported dependency types:
# - "development"
# - "production"
# - "all"
update_type: "semver:minor" # includes patch updates!
# Supported updates to automerge:
# - "security:patch"
# SemVer patch update that fixes a known security vulnerability
# - "semver:patch"
# SemVer patch update, e.g. > 1.x && 1.0.1 to 1.0.3
# - "semver:minor"
# SemVer minor update, e.g. > 1.x && 2.1.4 to 2.3.1
# - "in_range" (NOT SUPPORTED YET)
# matching the version requirement in your package manifest
# - "all"
- match:
dependency_type: "production"
update_type: "security:minor" # includes patch updates!
- match:
dependency_type: "production"
update_type: "semver:patch"
```

The syntax is based on https://dependabot.com/docs/config-file/#automerged_updates, but does not support `dependency_name` and `in_range` yet.
4 changes: 2 additions & 2 deletions action.yml
Expand Up @@ -19,10 +19,10 @@ inputs:
default: true

target:
description: The version comparison target (major, minor, patch)
description: The version comparison target (major, minor, patch). This is ignored if .github/auto-merge.yml exists
default: patch
required: false

runs:
using: docker
image: docker://ahmadnassri/action-dependabot-auto-merge:v1
image: Dockerfile
2 changes: 1 addition & 1 deletion action/index.js
Expand Up @@ -10,7 +10,7 @@ import main from './lib/index.js'
// parse inputs
const inputs = {
token: core.getInput('github-token', { required: true }),
target: core.getInput('target', { required: true }),
target: core.getInput('target', { required: false }),
command: core.getInput('command', { required: false }),
approve: core.getInput('approve', { required: false })
}
Expand Down
6 changes: 5 additions & 1 deletion action/lib/index.js
Expand Up @@ -25,7 +25,11 @@ export default async function (inputs) {
const octokit = github.getOctokit(inputs.token)

// parse and determine what command to tell dependabot
const proceed = parse(pull_request.title, inputs.target || 'patch')
const proceed = parse(
pull_request.title,
pull_request.labels.map(l => l.name),
inputs.target
)

if (proceed) {
const command = inputs.approve === 'true' ? approve : comment
Expand Down
93 changes: 86 additions & 7 deletions action/lib/parse.js
@@ -1,19 +1,38 @@
import semver from 'semver'
import core from '@actions/core'
import path from 'path'
import fs from 'fs'
import yaml from 'js-yaml';

// semver regex
const semverRegEx = /(?<version>(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/
// regex to detect dependency name
const depNameRegex = /((?:@[^\s]+\/)?[^\s]+) from/
// regexes to detect dependency type from PR title
const devDependencyRegEx = /\((deps-dev)\):/
const dependencyRegEx = /\((deps)\):/
const securityRegEx = /(^|: )\[Security\]/i

const weight = {
major: 3,
minor: 2,
patch: 1
}

export default function (title, target) {
export default function (title, labels, target) {
// log
core.info(`title: "${title}"`)

// extract dep name from the title
const depName = title.match(depNameRegex)?.[1];
core.info(`depName: ${depName}`)

// exit early
if (!depName) {
core.error('failed to parse title: could not detect dependency name')
return process.exit(0) // soft exit
}

// extract version from the title
const from = title.match(new RegExp('from ' + semverRegEx.source))?.groups
const to = title.match(new RegExp('to ' + semverRegEx.source))?.groups
Expand All @@ -24,18 +43,78 @@ export default function (title, target) {
return process.exit(0) // soft exit
}

let isDev = devDependencyRegEx.test(title);
let isProd = dependencyRegEx.test(title);
const isSecurity = securityRegEx.test(title) || labels.includes("security") || labels.includes("Security");

if (!isDev && !isProd) {
// couldn't extract the dependency type from the title, try to read package.json
try {
const packageJsonPath = path.join(process.env.GITHUB_WORKSPACE, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
isDev = depName in packageJson.devDependencies
isProd = depName in packageJson.dependencies
} catch (e) {
console.dir(e);
}
}
if (!isDev && !isProd) {
// we failed again, assume its a production dependency
core.info(`failed to parse dependency type, assuming it is a production dependency`)
isProd = true
}

// log
core.info(`from: ${from.version}`)
core.info(`to: ${to.version}`)
core.info(`dependency type: ${isDev ? "development" : isProd ? "production" : "unknown"}`)
core.info(`security critical: ${isSecurity}`)

// convert target to the automerged_updates syntax
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github/auto-merge.yml')
let mergeConfig;
if (fs.existsSync(configPath)) {
// parse .github/auto-merge.yml
mergeConfig = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
core.info('loaded merge config: ' + JSON.stringify(mergeConfig, undefined, 4));
} else {
mergeConfig = {
automerged_updates: [
{ match: { dependency_type: "all", update_type: `semver:${target}` } },
],
};
core.info('target converted to equivalent config: ' + JSON.stringify(mergeConfig, undefined, 4));
}

// analyze with semver
const result = semver.diff(from.version, to.version)
const updateType = semver.diff(from.version, to.version)

// compare weight to target
if ((weight[target] || 0) >= (weight[result] || 0)) {
// tell dependabot to merge
core.info(`dependency update target is "${target}", found "${result}", will auto-merge`)
return true
// Check all defined automerge configs to see if one matches
for (const {
match: { dependency_type, update_type },
} of mergeConfig.automerged_updates) {
if (
dependency_type === "all" ||
(dependency_type === "production" && isProd) ||
(dependency_type === "development" && isDev)
) {
if (update_type === "all") {
core.info(`all ${dependency_type} updates allowed, will auto-merge`);
return true;
} else if (update_type === "in_range") {
throw new Error("in_range update type not supported yet");
} else if (update_type.includes(":")) {
// security:patch, semver:minor, ...
const [secOrSemver, maxType] = update_type.split(":", 2);
console.log(maxType);
if (secOrSemver === "security" && !isSecurity) continue;
if ((weight[maxType] || 0) >= (weight[updateType] || 0)) {
// tell dependabot to merge
core.info(`${dependency_type} dependency update ${update_type} allowed, got ${isSecurity ? "security" : "semver"}:${updateType}, will auto-merge`);
return true;
}
}
}
}

core.info('manual merging required')
Expand Down
8 changes: 2 additions & 6 deletions action/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 action/package.json
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@actions/core": "^1.2.4",
"@actions/github": "^4.0.0",
"js-yaml": "^3.14.0",
"semver": "^7.3.2"
},
"devDependencies": {
Expand Down

0 comments on commit b47b86d

Please sign in to comment.