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(config): support for rules & ignore sections in config #43

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 37 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,74 +25,20 @@ jobs:
- uses: actions/checkout@v2
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: minor
config: .github/auto-merge.yml
github-token: ${{ secrets.mytoken }}
```

The action will only merge PRs whose checks (CI/CD) pass.

### Examples

Minimal setup:

``` yaml
steps:
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
github-token: ${{ secrets.mytoken }}
```

Only merge if the changed dependency version is a `patch` *(default behavior)*:

``` yaml
steps:
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: patch
github-token: ${{ secrets.mytoken }}
```

Only merge if the changed dependency version is a `minor`:

``` yaml
steps:
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: minor
github-token: ${{ secrets.mytoken }}
```

Using a configuration file:

###### `.github/workflows/auto-merge.yml`

``` yaml
steps:
- uses: actions/checkout@v2
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
github-token: ${{ secrets.mytoken }}
```

###### `.github/auto-merge.yml`

``` yaml
- match:
dependency_type: all
update_type: "semver:minor" # includes patch updates!
```
> **Note**: The action will only merge PRs whose checks (CI/CD) pass.

### Inputs

| input | required | default | description |
|----------------|----------|--------------------------|-----------------------------------------------------|
| `github-token` | ✔ | `github.token` | The GitHub token used to merge the pull-request |
| `config` | ✔ | `.github/auto-merge.yml` | Path to configuration file *(relative to root)* |
| `target` | ❌ | `patch` | The version comparison target (major, minor, patch) |
| `command` | ❌ | `merge` | The command to pass to Dependabot |
| `approve` | ❌ | `true` | Auto-approve pull-requests |
Comment on lines -92 to -93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these two are missing from the docs now.

| input | required | default | description |
|----------------|----------|--------------------------|-------------------------------------------------|
| `github-token` | ✔ | `github.token` | The GitHub token used to merge the pull-request |
| `config` | ✔ | `.github/auto-merge.yml` | Path to configuration file *(relative to root)* |

### Token Scope
#### Token Scope

The GitHub token is a [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with the following scopes:

Expand All @@ -103,34 +49,46 @@ The token MUST be created from a user with **`push`** permission to the reposito

> ℹ *see reference for [user owned repos](https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/permission-levels-for-a-user-account-repository) and for [org owned repos](https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization)*

### Configuration file syntax
### Configuration File

A configuration file is **REQUIRED** to successfully determine the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete sentence?


Using the configuration file *(specified with `config` input)*, you have the option to provide a more fine-grained configuration. The following example configuration file merges
The syntax is loosely based on the [legacy dependaBot v1 config format](https://dependabot.com/docs/config-file/), with `ignore` and `rules` resembling [`ignored_updates`](https://dependabot.com/docs/config-file/#ignored_updates) and [`automerged_updates`](https://dependabot.com/docs/config-file/#automerged_updates) respectively.

- minor updates for `aws-sdk`
- minor development dependency updates
- patch production dependency updates
- minor security-critical production dependency updates
###### Example

minimal configuration:

``` yaml
- match:
dependency_name: aws-sdk
rules:
- dependency_type: all
update_type: semver:minor
```

- match:
dependency_type: development
update_type: semver:minor # includes patch updates!
advanced configuration:

``` yaml
# ignore certain dependencies
ignore:
- dependency_name: react-router
- dependency_name: react-*

# auto merge rules
rules:
# rule for specific dependency
- dependency_name: aws-sdk
update_type: semver:minor

- match:
dependency_type: production
update_type: security:minor # includes patch updates!
# rule per dependency type
- dependency_type: development
update_type: semver:minor

- match:
dependency_type: production
update_type: semver:patch
# rule per update type
- dependency_type: production
update_type: security:minor
```

#### Match Properties
#### Properties

| property | required | supported values |
|-------------------|----------|--------------------------------------------|
Expand All @@ -151,21 +109,6 @@ Using the configuration file *(specified with `config` input)*, you have the opt
>
> To allow `prereleases`, the corresponding `prepatch`, `preminor` and `premajor` types are also supported

###### Defaults

By default, if no configuration file is present in the repo, the action will assume the following:

``` yaml
- match:
dependency_type: all
update_type: semver:${TARGET}
```

> Where `$TARGET` is the `target` value from the action [Inputs](#inputs)

The syntax is based on the [legacy dependaBot v1 config format](https://dependabot.com/docs/config-file/#automerged_updates).
However, **`in_range` is not supported yet**.

## Exceptions and Edge Cases

1. Parsing of *version ranges* is not currently supported
Expand Down
15 changes: 0 additions & 15 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,6 @@ inputs:
default: .github/auto-merge.yml
required: false

command:
description: The command to pass to Dependabot as a comment
default: merge
required: false

approve:
description: Auto-approve pull-requests
default: 'true'
required: false

target:
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://ghcr.io/ahmadnassri/action-dependabot-auto-merge:v2
5 changes: 1 addition & 4 deletions action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ if (!sender || !['dependabot[bot]', 'dependabot-preview[bot]'].includes(sender.l
// parse inputs
const inputs = {
token: core.getInput('github-token', { required: true }),
config: core.getInput('config', { required: false }),
target: core.getInput('target', { required: false }),
command: core.getInput('command', { required: false }),
approve: core.getInput('approve', { required: false })
config: core.getInput('config', { required: false })
}

// error handler
Expand Down
4 changes: 2 additions & 2 deletions action/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export async function approve (octokit, repo, { number }, body) {
export async function approve (octokit, repo, number, body) {
await octokit.pulls.createReview({
...repo,
pull_number: number,
Expand All @@ -7,7 +7,7 @@ export async function approve (octokit, repo, { number }, body) {
})
}

export async function comment (octokit, repo, { number }, body) {
export async function comment (octokit, repo, number, body) {
await octokit.issues.createComment({
...repo,
issue_number: number,
Expand Down
30 changes: 24 additions & 6 deletions action/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
// internals
import fs from 'fs'
import path from 'path'
import Ajv from 'ajv'

// packages
import core from '@actions/core'
import yaml from 'js-yaml'

// require for json loading
import { createRequire } from 'module'

const require = createRequire(import.meta.url)
const schema = require('./config.schema.json')

const ajv = new Ajv()
const validate = ajv.compile(schema)

// default value is passed from workflow
export default function ({ workspace, inputs }) {
const configPath = path.join(workspace || '', inputs.config || '.github/auto-merge.yml')
export default function ({ workspace = '', filename = '.github/auto-merge.yml' }) {
const configPath = path.join(workspace, filename)

// read auto-merge.yml to determine what should be merged
if (fs.existsSync(configPath)) {
// parse .github/auto-merge.yml
const configYaml = fs.readFileSync(configPath, 'utf8')
const config = yaml.safeLoad(configYaml)

core.info('loaded merge config: \n' + configYaml)

// validate config schema
const valid = validate(config)

if (!valid) {
core.error('invalid config file format')
return process.exit(1)
}

return config
}

// or convert the input "target" to the equivalent config
const config = [{ match: { dependency_type: 'all', update_type: `semver:${inputs.target}` } }]
core.info('using workflow\'s "target": \n' + yaml.safeDump(config))
// create default config structure

return config
core.error('missing config file')
return process.exit(1)
}
62 changes: 62 additions & 0 deletions action/lib/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",

"definitions": {
"update_type": {
"type": "string",
"enum": ["security:patch", "semver:patch", "semver:minor", "all"],
"default": "semver:patch"
},

"dependency_type": {
"type": "string",
"enum": ["development", "production", "all"],
"default": "all"
},

"dependency_name": {
"type": "string"
},

"match": {
"type": "object",
"properties": {
"dependency_name": { "$ref": "#/definitions/dependency_name" },
"dependency_type": { "$ref": "#/definitions/dependency_type" },
"update_type": { "$ref": "#/definitions/update_type" }
}
}
},

"type": "object",
"properties": {
"command": {
"description": "The command to pass to Dependabot as a comment",
"type": "string",
"enum": ["merge", "squash and merge"],
"default": "merge"
},

"approve": {
"description": "Auto-approve pull-requests",
"type": "boolean",
"default": true
},

"rules": {
"type": "array",
"default": [],
"items": { "$ref": "#/definitions/match" }
},
"ignore": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"dependency_name": { "$ref": "#/definitions/dependency_name" }
}
}
}
}
}
14 changes: 8 additions & 6 deletions action/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,32 @@ import github from '@actions/github'

// modules
import parse from './parse.js'
import config from './config.js'
import loadConfig from './config.js'
import dependencies from './dependencies.js'
import { approve, comment } from './api.js'

const workspace = process.env.GITHUB_WORKSPACE || '/github/workspace'

export default async function (inputs) {
export default async function ({ token, config: filename }) {
// extract the title
const { repo, payload: { pull_request } } = github.context // eslint-disable-line camelcase

// init octokit
const octokit = github.getOctokit(inputs.token)
const octokit = github.getOctokit(token)

const config = loadConfig({ workspace, filename })

// parse and determine what command to tell dependabot
const proceed = parse({
config,
title: pull_request.title,
labels: pull_request.labels.map(label => label.name.toLowerCase()),
config: config({ workspace, inputs }),
dependencies: dependencies(workspace)
})

if (proceed) {
const command = inputs.approve === 'true' ? approve : comment
const command = config.approve ? approve : comment

await command(octokit, repo, pull_request, `@dependabot ${inputs.command}`)
await command(octokit, repo, pull_request.number, `@dependabot ${config.command}`)
}
}
Loading