├── .env ├── .dockerignore ├── .gitignore ├── action ├── test │ ├── fixtures │ │ ├── .github │ │ │ └── auto-merge.yml │ │ └── config-valid.yml │ ├── cli │ │ ├── event.json │ │ └── early-exit.js │ └── parse │ │ ├── invalid-title.js │ │ ├── config-load.js │ │ ├── pre-release.js │ │ ├── dep-name.js │ │ ├── dependency-types.js │ │ ├── config-parse.js │ │ └── match-target.js ├── lib │ ├── api.js │ ├── dependencies.js │ ├── config.js │ ├── index.js │ └── parse.js ├── package.json └── index.js ├── .github ├── FUNDING.yml ├── linters │ ├── .commit-lint.yml │ ├── .checkov.yml │ ├── .yamllint.yml │ ├── .mega-linter.yml │ └── .markdown-lint.yml ├── dependabot.yml └── workflows │ ├── pull_request_target.yml │ └── push.yml ├── colophon.yml ├── .editorconfig ├── .pandoc.yml ├── Dockerfile ├── docs ├── README.template └── README.md ├── docker-compose.yml ├── LICENSE ├── action.yml ├── Makefile ├── .semantic.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | action/node_modules 2 | action/test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /action/test/fixtures/.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | ../config-valid.yml -------------------------------------------------------------------------------- /action/test/cli/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "sender": { 3 | "login": "foo" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /action/test/fixtures/config-valid.yml: -------------------------------------------------------------------------------- 1 | - match: 2 | dependency_type: development 3 | update_type: semver:minor 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | github: [ahmadnassri] 6 | -------------------------------------------------------------------------------- /colophon.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | id: action-dependabot-auto-merge 4 | 5 | about: 6 | title: "GitHub Action: Dependabot Auto Merge" 7 | description: Automatically merge Dependabot PRs when version comparison is within range. 8 | repository: ahmadnassri/action-dependabot-auto-merge 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.github/linters/.commit-lint.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | extends: 6 | - "@commitlint/config-conventional" 7 | rules: 8 | body-max-line-length: [2, 'always', 200] 9 | -------------------------------------------------------------------------------- /.github/linters/.checkov.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | quiet: true 6 | skip-check: 7 | - CKV_DOCKER_2 8 | - CKV_GHA_3 9 | - BC_DKR_3 10 | - CKV_GIT_1 11 | - CKV_GIT_5 12 | - CKV_GIT_6 13 | -------------------------------------------------------------------------------- /.pandoc.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | input-file: docs/README.md 6 | output-file: README.md 7 | metadata-file: colophon.yml 8 | template: docs/README.template 9 | 10 | from: gfm 11 | to: gfm 12 | 13 | wrap: preserve 14 | reference-links: true 15 | fail-if-warnings: false 16 | -------------------------------------------------------------------------------- /action/lib/api.js: -------------------------------------------------------------------------------- 1 | export async function approve (octokit, repo, { number }, body) { 2 | await octokit.rest.pulls.createReview({ 3 | ...repo, 4 | pull_number: number, 5 | event: 'APPROVE', 6 | body 7 | }) 8 | } 9 | 10 | export async function comment (octokit, repo, { number }, body) { 11 | await octokit.rest.issues.createComment({ 12 | ...repo, 13 | issue_number: number, 14 | body 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /.github/linters/.yamllint.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | extends: default 6 | 7 | rules: 8 | brackets: 9 | max-spaces-inside: 1 10 | document-start: 11 | present: false 12 | truthy: 13 | check-keys: false 14 | line-length: 15 | max: 500 16 | comments: 17 | min-spaces-from-content: 1 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | LABEL com.github.actions.name="Dependabot Auto Merge" \ 4 | com.github.actions.description="Automatically merge Dependabot PRs when version comparison is within range" \ 5 | com.github.actions.icon="git-merge" \ 6 | com.github.actions.color="blue" \ 7 | maintainer="Ahmad Nassri " 8 | 9 | RUN mkdir /action 10 | WORKDIR /action 11 | 12 | COPY action ./ 13 | 14 | RUN npm ci --omit=dev 15 | 16 | ENTRYPOINT ["node", "/action/index.js"] 17 | -------------------------------------------------------------------------------- /action/lib/dependencies.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | 4 | import core from '@actions/core' 5 | 6 | // Look at possible package files to determine the dependency type 7 | // For now, this only includes npm 8 | export default function (workspace) { 9 | const packageJsonPath = path.join(workspace, 'package.json') 10 | 11 | if (fs.existsSync(packageJsonPath)) { 12 | try { 13 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) 14 | } catch (err) { 15 | core.debug(err) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/README.template: -------------------------------------------------------------------------------- 1 | # $about.title$ 2 | 3 | $about.description$ 4 | 5 | [![license][license-img]][license-url] 6 | [![release][release-img]][release-url] 7 | 8 | $body$ 9 | 10 | ---- 11 | > Author: [Ahmad Nassri](https://www.ahmadnassri.com/) • 12 | > Twitter: [@AhmadNassri](https://twitter.com/AhmadNassri) 13 | 14 | [license-url]: LICENSE 15 | [license-img]: https://badgen.net/github/license/$about.repository$ 16 | 17 | [release-url]: https://github.com/$about.repository$/releases 18 | [release-img]: https://badgen.net/github/release/$about.repository$ 19 | -------------------------------------------------------------------------------- /action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "actions-dependabot-auto-merge", 4 | "version": "0.0.0-semantically-released", 5 | "author": { 6 | "name": "Ahmad Nassri", 7 | "email": "ahmad@ahmadnassri.com", 8 | "url": "https://ahmadnassri.com" 9 | }, 10 | "type": "module", 11 | "main": "index.js", 12 | "license": "MIT", 13 | "scripts": { 14 | "test": "tap test --no-coverage", 15 | "test:watch": "tap test --watch", 16 | "test:ci": "tap test --100 --color --coverage-report=lcov --no-browser" 17 | }, 18 | "dependencies": { 19 | "@actions/core": "^1.10.0", 20 | "@actions/github": "^5.1.1", 21 | "js-yaml": "^4.1.0", 22 | "semver": "^7.3.8" 23 | }, 24 | "devDependencies": { 25 | "sinon": "^11.1.2", 26 | "tap": "^16.3.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------- # 2 | # Note: this file originates in template-action-docker # 3 | # ---------------------------------------------------- # 4 | 5 | services: 6 | # ---- mega-linter ---- # 7 | lint: 8 | profiles: ["dev"] 9 | image: oxsecurity/megalinter-javascript:v6.14.0 10 | volumes: 11 | - ./:/tmp/lint 12 | environment: 13 | MEGALINTER_CONFIG: .github/linters/.mega-linter.yml 14 | REPORT_OUTPUT_FOLDER: none 15 | VALIDATE_ALL_CODEBASE: true 16 | 17 | # ---- readme generator ---- # 18 | readme: 19 | profiles: ["dev"] 20 | image: pandoc/minimal:2.18.0 21 | volumes: 22 | - ./:/data 23 | command: --defaults=.pandoc.yml 24 | 25 | # ---- app ---- # 26 | app: 27 | profiles: ["app"] 28 | privileged: true 29 | build: . 30 | working_dir: /github/workspace 31 | volumes: 32 | - ./:/github/workspace 33 | env_file: 34 | - .env 35 | -------------------------------------------------------------------------------- /action/lib/config.js: -------------------------------------------------------------------------------- 1 | // internals 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | // packages 6 | import core from '@actions/core' 7 | import yaml from 'js-yaml' 8 | 9 | // default value is passed from workflow 10 | export default function ({ workspace, inputs }) { 11 | const configPath = path.join(workspace || '', inputs.config || '.github/auto-merge.yml') 12 | 13 | // read auto-merge.yml to determine what should be merged 14 | if (fs.existsSync(configPath)) { 15 | // parse .github/auto-merge.yml 16 | const configYaml = fs.readFileSync(configPath, 'utf8') 17 | const config = yaml.load(configYaml) 18 | core.info('loaded merge config: \n' + configYaml) 19 | 20 | return config 21 | } 22 | 23 | // or convert the input "target" to the equivalent config 24 | const config = [{ match: { dependency_type: 'all', update_type: `semver:${inputs.target}` } }] 25 | core.info('using workflow\'s "target": \n' + yaml.dump(config)) 26 | 27 | return config 28 | } 29 | -------------------------------------------------------------------------------- /action/test/cli/early-exit.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import path from 'path' 4 | import { promisify } from 'util' 5 | import { exec } from 'child_process' 6 | 7 | const pexec = promisify(exec) 8 | 9 | tap.test('main -> wrong event', assert => { 10 | assert.plan(2) 11 | 12 | process.env.GITHUB_EVENT_NAME = 'not-a-pull_request' 13 | 14 | pexec('node index.js') 15 | .catch(({ code, stdout }) => { 16 | assert.equal(code, 1) 17 | assert.equal(stdout.trim(), '::error::action triggered outside of a pull_request') 18 | }) 19 | }) 20 | 21 | tap.test('main -> not dependabot', assert => { 22 | assert.plan(1) 23 | 24 | process.env.GITHUB_EVENT_NAME = 'pull_request' 25 | process.env.GITHUB_EVENT_PATH = path.join(path.resolve(), 'test', 'cli', 'event.json') 26 | 27 | pexec('node index.js') 28 | .then(({ code, stdout }) => { 29 | assert.equal(stdout.trim(), '::warning::exiting early - expected PR by "dependabot[bot]", found "foo" instead') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /.github/linters/.mega-linter.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | ENABLE: 6 | - ACTION 7 | - BASH 8 | - MAKEFILE 9 | - REPOSITORY 10 | - TERRAFORM 11 | - HTML 12 | - ENV 13 | - JAVASCRIPT 14 | - JSX 15 | - EDITORCONFIG 16 | - JSON 17 | - DOCKERFILE 18 | - MARKDOWN 19 | - YAML 20 | - CSS 21 | - OPENAPI 22 | - SQL 23 | 24 | DISABLE_LINTERS: 25 | - JSON_PRETTIER 26 | - JAVASCRIPT_PRETTIER 27 | - YAML_PRETTIER 28 | - REPOSITORY_TRIVY 29 | - REPOSITORY_DEVSKIM 30 | - TERRAFORM_CHECKOV 31 | 32 | CONFIG_REPORTER: false 33 | FAIL_IF_MISSING_LINTER_IN_FLAVOR: true 34 | FLAVOR_SUGGESTIONS: false 35 | LOG_LEVEL: INFO 36 | MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdown-lint.yml 37 | PRINT_ALPACA: false 38 | SHOW_ELAPSED_TIME: true 39 | VALIDATE_ALL_CODEBASE: false 40 | IGNORE_GENERATED_FILES: true 41 | FILTER_REGEX_EXCLUDE: (dist/*|README.md|test/fixtures/*|vendor/*|/schemas/*) 42 | REPOSITORY_CHECKOV_ARGUMENTS: [--skip-path, schemas] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ahmad Nassri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /action/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | // packages 4 | import github from '@actions/github' 5 | 6 | // modules 7 | import parse from './parse.js' 8 | import config from './config.js' 9 | import dependencies from './dependencies.js' 10 | import { approve, comment } from './api.js' 11 | 12 | const workspace = process.env.GITHUB_WORKSPACE || '/github/workspace' 13 | 14 | export default async function (inputs) { 15 | // extract the title 16 | const { repo, payload: { pull_request } } = github.context // eslint-disable-line camelcase 17 | 18 | // init octokit 19 | const octokit = github.getOctokit(inputs.token) 20 | 21 | // parse and determine what command to tell dependabot 22 | const proceed = parse({ 23 | title: pull_request.title, 24 | labels: pull_request.labels.map(label => label.name.toLowerCase()), 25 | config: config({ workspace, inputs }), 26 | dependencies: dependencies(workspace) 27 | }) 28 | 29 | if (proceed) { 30 | const command = inputs.approve === 'true' ? approve : comment 31 | const botName = inputs.botName || 'dependabot' 32 | 33 | await command(octokit, repo, pull_request, `@${botName} ${inputs.command}`) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge 2 | description: Automatically merge Dependabot PRs when version comparison is within range 3 | 4 | branding: 5 | color: blue 6 | icon: git-merge 7 | 8 | inputs: 9 | github-token: 10 | description: The GitHub token used to merge the pull-request 11 | required: true 12 | 13 | config: 14 | description: Path to configuration file (relative to root) 15 | default: .github/auto-merge.yml 16 | required: false 17 | 18 | command: 19 | description: The command to pass to Dependabot as a comment 20 | default: merge 21 | required: false 22 | 23 | botName: 24 | description: The bot to tag in approve/comment message. You can use this to create your own merge bot. 25 | default: dependabot 26 | required: false 27 | 28 | approve: 29 | description: Auto-approve pull-requests 30 | default: 'true' 31 | required: false 32 | 33 | target: 34 | description: The version comparison target (major, minor, patch). This is ignored if .github/auto-merge.yml exists 35 | default: patch 36 | required: false 37 | 38 | runs: 39 | using: docker 40 | image: docker://ghcr.io/ahmadnassri/action-dependabot-auto-merge:2.6.6 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make 2 | 3 | # ---------------------------------------------------- # 4 | # Note: this file originates in template-action-docker # 5 | # ---------------------------------------------------- # 6 | 7 | SHELL := /bin/bash 8 | 9 | pull: ## pull latest containers 10 | @docker compose pull 11 | 12 | lint: clean ## run mega-linter 13 | @docker compose run --rm lint 14 | 15 | readme: clean ## run readme action 16 | @docker compose run --rm readme 17 | 18 | start: ## start the project in foreground 19 | @docker compose run $(shell env | grep DOCKER | sed -E 's/DOCKER_(.*?)=(.*)/-e \1="\2"/gm;t;d') app 20 | 21 | build: clean ## start the project in background 22 | @docker compose build --no-cache app 23 | 24 | shell: ## start the container shell 25 | @docker compose run --rm --entrypoint /bin/sh app 26 | 27 | stop: ## stop all running containers 28 | @docker compose down --remove-orphans --rmi local 29 | 30 | clean: stop ## remove running containers, volumes, node_modules & anything else 31 | @docker compose rm --stop --volumes --force 32 | 33 | # Utility methods 34 | ## Help: https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 35 | 36 | help: ## display this help 37 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 38 | 39 | .DEFAULT_GOAL := help 40 | .PHONY: help all clean test 41 | -------------------------------------------------------------------------------- /action/test/parse/invalid-title.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | 5 | import core from '@actions/core' 6 | 7 | // module 8 | import parse from '../../lib/parse.js' 9 | 10 | tap.test('parse -> invalid semver', assert => { 11 | assert.plan(3) 12 | 13 | sinon.stub(core, 'info') // silence output on terminal 14 | sinon.stub(core, 'warning') 15 | sinon.stub(process, 'exit') 16 | 17 | parse({ title: 'chore(deps): bump api-problem from FOO to BAR in /path' }) 18 | 19 | assert.ok(process.exit.called) 20 | assert.equal(process.exit.getCall(0)?.firstArg, 0) 21 | assert.equal(core.warning.getCall(0)?.firstArg, 'failed to parse title: no recognizable versions') 22 | 23 | process.exit.restore() 24 | core.info.restore() 25 | core.warning.restore() 26 | }) 27 | 28 | tap.only('parse -> invalid dependency name', assert => { 29 | assert.plan(3) 30 | 31 | sinon.stub(core, 'info') // silence output on terminal 32 | sinon.stub(core, 'warning') 33 | sinon.stub(process, 'exit') 34 | 35 | parse({ title: 'from 1.0.0 to 1.0.1' }) 36 | 37 | assert.ok(process.exit.called) 38 | assert.equal(process.exit.getCall(0)?.firstArg, 0) 39 | assert.equal(core.warning.getCall(0)?.firstArg, 'failed to parse title: could not detect dependency name') 40 | 41 | process.exit.restore() 42 | core.info.restore() 43 | core.warning.restore() 44 | }) 45 | -------------------------------------------------------------------------------- /action/index.js: -------------------------------------------------------------------------------- 1 | // internals 2 | import { inspect } from 'util' 3 | 4 | // packages 5 | import core from '@actions/core' 6 | import github from '@actions/github' 7 | 8 | // modules 9 | import main from './lib/index.js' 10 | 11 | // exit early 12 | if (!['pull_request_target', 'pull_request'].includes(github.context.eventName)) { 13 | core.error('action triggered outside of a pull_request') 14 | process.exit(1) 15 | } 16 | 17 | // extract the title 18 | const { payload: { sender } } = github.context // eslint-disable-line camelcase 19 | 20 | // exit early if PR is not by dependabot 21 | if (!sender || !['dependabot[bot]', 'dependabot-preview[bot]'].includes(sender.login)) { 22 | core.warning(`exiting early - expected PR by "dependabot[bot]", found "${sender ? sender.login : 'no-sender'}" instead`) 23 | process.exit(0) 24 | } 25 | 26 | // parse inputs 27 | const inputs = { 28 | token: core.getInput('github-token', { required: true }), 29 | config: core.getInput('config', { required: false }), 30 | target: core.getInput('target', { required: false }), 31 | command: core.getInput('command', { required: false }), 32 | botName: core.getInput('botName', { required: false }), 33 | approve: core.getInput('approve', { required: false }) 34 | } 35 | 36 | // error handler 37 | function errorHandler ({ message, stack, request }) { 38 | core.error(`${message}\n${stack}`) 39 | 40 | // debugging for API calls 41 | if (request) { 42 | const { method, url, body, headers } = request 43 | core.debug(`${method} ${url}\n\n${inspect(headers)}\n\n${inspect(body)}`) 44 | } 45 | 46 | process.exit(1) 47 | } 48 | 49 | // catch errors and exit 50 | process.on('unhandledRejection', errorHandler) 51 | process.on('uncaughtException', errorHandler) 52 | 53 | await main(inputs) 54 | -------------------------------------------------------------------------------- /action/test/parse/config-load.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | 5 | import core from '@actions/core' 6 | 7 | // module 8 | import config from '../../lib/config.js' 9 | 10 | import path from 'path' 11 | const __dirname = path.resolve() 12 | 13 | const workspace = `${__dirname}/test/fixtures/` 14 | 15 | tap.test('input.config --> default', async assert => { 16 | assert.plan(2) 17 | 18 | sinon.stub(core, 'info') // silence output on terminal 19 | 20 | const expected = [{ match: { dependency_type: 'development', update_type: 'semver:minor' } }] 21 | 22 | const result = config({ workspace, inputs: { } }) 23 | 24 | assert.match(core.info.getCall(-1)?.firstArg, 'loaded merge config') 25 | assert.match(result, expected) 26 | 27 | core.info.restore() 28 | }) 29 | 30 | tap.test('input.config --> custom', async assert => { 31 | assert.plan(2) 32 | 33 | sinon.stub(core, 'info') // silence output on terminal 34 | 35 | const expected = [{ match: { dependency_type: 'development', update_type: 'semver:minor' } }] 36 | 37 | const result = config({ workspace, inputs: { config: 'config-valid.yml' } }) 38 | 39 | assert.match(core.info.getCall(-1)?.firstArg, 'loaded merge config') 40 | assert.match(result, expected) 41 | 42 | core.info.restore() 43 | }) 44 | 45 | tap.test('input.config --> no file', async assert => { 46 | assert.plan(2) 47 | 48 | sinon.stub(core, 'info') // silence output on terminal 49 | 50 | const expected = [{ match: { dependency_type: 'all', update_type: 'semver:patch' } }] 51 | 52 | const result = config({ inputs: { target: 'patch' } }) 53 | 54 | assert.match(core.info.getCall(-1)?.firstArg, 'using workflow\'s "target":') 55 | assert.match(result, expected) 56 | 57 | core.info.restore() 58 | }) 59 | -------------------------------------------------------------------------------- /.semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@semantic-release/commit-analyzer", { 4 | "preset": "conventionalcommits", 5 | "releaseRules": [ 6 | { "breaking": true, "release": "major" }, 7 | { "revert": true, "release": "patch" }, 8 | { "type": "build", "release": "patch" }, 9 | { "type": "docs", "release": "patch" }, 10 | { "type": "feat", "release": "minor" }, 11 | { "type": "fix", "release": "patch" }, 12 | { "type": "perf", "release": "patch" }, 13 | { "type": "refactor", "release": "patch" } 14 | ] 15 | }], 16 | ["@semantic-release/release-notes-generator", { 17 | "preset": "conventionalcommits", 18 | "presetConfig": { 19 | "types": [ 20 | { "type": "chore", "section": "Chores", "hidden": true }, 21 | { "type": "build", "section": "Build", "hidden": false }, 22 | { "type": "ci", "section": "CI/CD", "hidden": false }, 23 | { "type": "docs", "section": "Docs", "hidden": false }, 24 | { "type": "feat", "section": "Features", "hidden": false }, 25 | { "type": "fix", "section": "Bug Fixes", "hidden": false }, 26 | { "type": "perf", "section": "Performance", "hidden": false }, 27 | { "type": "refactor", "section": "Refactor", "hidden": false }, 28 | { "type": "style", "section": "Code Style", "hidden": false }, 29 | { "type": "test", "section": "Tests", "hidden": false } 30 | ] 31 | } 32 | }], 33 | ["@semantic-release/exec", { 34 | "prepareCmd": "sed -Ei 's/:[0-9,\\.]+/:${nextRelease.version}/g' action.yml" 35 | }], 36 | ["@semantic-release/git", { 37 | "assets": ["action.yml"], 38 | "message": "chore(release): bump to ${nextRelease.version} [skip ci]" 39 | }], 40 | ["@semantic-release/github", { 41 | "successComment": false 42 | }] 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: gitsubmodule 8 | open-pull-requests-limit: 10 9 | directory: / 10 | commit-message: 11 | prefix: build 12 | prefix-development: chore 13 | include: scope 14 | schedule: 15 | interval: daily 16 | time: "10:00" 17 | timezone: America/Toronto 18 | 19 | - package-ecosystem: github-actions 20 | open-pull-requests-limit: 10 21 | directory: / 22 | commit-message: 23 | prefix: chore 24 | prefix-development: chore 25 | include: scope 26 | schedule: 27 | interval: daily 28 | time: "10:00" 29 | timezone: America/Toronto 30 | 31 | - package-ecosystem: npm 32 | open-pull-requests-limit: 10 33 | directory: / 34 | commit-message: 35 | prefix: build 36 | prefix-development: chore 37 | include: scope 38 | schedule: 39 | interval: daily 40 | time: "10:00" 41 | timezone: America/Toronto 42 | 43 | - package-ecosystem: npm 44 | open-pull-requests-limit: 10 45 | directory: /action 46 | commit-message: 47 | prefix: build 48 | prefix-development: chore 49 | include: scope 50 | schedule: 51 | interval: daily 52 | time: "10:00" 53 | timezone: America/Toronto 54 | 55 | - package-ecosystem: bundler 56 | open-pull-requests-limit: 10 57 | directory: / 58 | commit-message: 59 | prefix: build 60 | prefix-development: chore 61 | include: scope 62 | schedule: 63 | interval: daily 64 | time: "10:00" 65 | timezone: America/Toronto 66 | 67 | - package-ecosystem: terraform 68 | open-pull-requests-limit: 10 69 | directory: / 70 | commit-message: 71 | prefix: build 72 | prefix-development: chore 73 | include: scope 74 | schedule: 75 | interval: daily 76 | time: "10:00" 77 | timezone: America/Toronto 78 | 79 | - package-ecosystem: docker 80 | open-pull-requests-limit: 10 81 | directory: / 82 | commit-message: 83 | prefix: build 84 | prefix-development: chore 85 | include: scope 86 | schedule: 87 | interval: daily 88 | time: "10:00" 89 | timezone: America/Toronto 90 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_target.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | on: pull_request_target 6 | 7 | name: pull_request_target 8 | 9 | permissions: 10 | pull-requests: write 11 | contents: write 12 | 13 | concurrency: 14 | group: ${{ github.ref }}-${{ github.workflow }} 15 | 16 | jobs: 17 | metadata: 18 | runs-on: ubuntu-latest 19 | 20 | outputs: 21 | repository_is_template: ${{ steps.metadata.outputs.repository_is_template }} 22 | 23 | steps: 24 | - uses: actions/checkout@v3.2.0 25 | 26 | - uses: ahmadnassri/action-metadata@v2.1.2 27 | id: metadata 28 | 29 | auto-merge: 30 | timeout-minutes: 5 31 | 32 | runs-on: ubuntu-latest 33 | 34 | # only run for dependabot PRs 35 | if: ${{ github.actor == 'dependabot[bot]' }} 36 | 37 | steps: 38 | - id: dependabot 39 | uses: dependabot/fetch-metadata@v1.3.6 40 | with: 41 | github-token: ${{ github.token }} 42 | 43 | - name: auto merge conditions 44 | id: auto-merge 45 | if: | 46 | ( 47 | steps.dependabot.outputs.update-type == 'version-update:semver-patch' && 48 | contains('direct:development,indirect:development,direct:production,indirect:production', steps.dependabot.outputs.dependency-type) 49 | ) || ( 50 | steps.dependabot.outputs.update-type == 'version-update:semver-minor' && 51 | contains('direct:development,indirect:development', steps.dependabot.outputs.dependency-type) 52 | ) 53 | run: echo "::notice ::auto-merge conditions satisfied" 54 | 55 | - name: auto approve pr 56 | if: ${{ steps.auto-merge.conclusion == 'success' }} 57 | env: 58 | PR_URL: ${{github.event.pull_request.html_url}} 59 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 60 | run: | 61 | gh pr review --approve "$PR_URL" 62 | gh pr merge --auto --rebase "$PR_URL" 63 | 64 | template-sync: 65 | needs: metadata 66 | 67 | timeout-minutes: 20 68 | 69 | runs-on: ubuntu-latest 70 | 71 | # only run for templates 72 | if: ${{ needs.metadata.outputs.repository_is_template == 'true' }} 73 | 74 | steps: 75 | - uses: actions/checkout@v3.2.0 76 | with: 77 | ref: ${{ github.event.pull_request.head.ref }} 78 | 79 | - uses: ahmadnassri/action-template-repository-sync@v2 80 | with: 81 | github-token: ${{ secrets.GH_TOKEN }} 82 | -------------------------------------------------------------------------------- /action/test/parse/pre-release.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | import core from '@actions/core' 5 | 6 | // module 7 | import parse from '../../lib/parse.js' 8 | 9 | function config (target) { 10 | return [{ match: { dependency_type: 'all', update_type: `semver:${target}` } }] 11 | } 12 | 13 | tap.test('parse -> pre-release -> direct match', async assert => { 14 | assert.plan(4) 15 | 16 | sinon.stub(core, 'info') 17 | 18 | const options = { 19 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4-prerelease in /path', 20 | config: config('preminor') 21 | } 22 | 23 | assert.ok(parse(options)) 24 | assert.ok(core.info.called) 25 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.4-prerelease') 26 | assert.equal(core.info.getCall(7)?.firstArg, 'all:semver:preminor detected, will auto-merge') 27 | 28 | core.info.restore() 29 | }) 30 | 31 | tap.test('parse -> pre-release -> greater match', async assert => { 32 | assert.plan(4) 33 | 34 | sinon.stub(core, 'info') 35 | 36 | const options = { 37 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4-prerelease in /path', 38 | config: config('major') 39 | } 40 | 41 | assert.ok(parse(options)) 42 | assert.ok(core.info.called) 43 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.4-prerelease') 44 | assert.equal(core.info.getCall(7)?.firstArg, 'all:semver:major detected, will auto-merge') 45 | 46 | core.info.restore() 47 | }) 48 | 49 | tap.test('parse -> pre-release -> lesser match (premajor)', async assert => { 50 | assert.plan(4) 51 | 52 | sinon.stub(core, 'info') 53 | 54 | const options = { 55 | title: 'chore(deps): bump api-problem from 6.1.2 to 7.0.0-pre.0 in /path', 56 | config: config('minor') 57 | } 58 | 59 | assert.notOk(parse(options)) 60 | assert.ok(core.info.called) 61 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 7.0.0-pre.0') 62 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 63 | 64 | core.info.restore() 65 | }) 66 | 67 | tap.test('parse -> pre-release -> lesser match (preminor)', async assert => { 68 | assert.plan(4) 69 | 70 | sinon.stub(core, 'info') 71 | 72 | const options = { 73 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.2.0-pre.1 in /path', 74 | config: config('patch') 75 | } 76 | 77 | assert.notOk(parse(options)) 78 | assert.ok(core.info.called) 79 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.2.0-pre.1') 80 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 81 | 82 | core.info.restore() 83 | }) 84 | 85 | tap.test('parse -> pre-release -> actual prerelease', async assert => { 86 | assert.plan(4) 87 | 88 | sinon.stub(core, 'info') 89 | 90 | const options = { 91 | title: 'chore(deps): bump api-problem from 6.1.2-pre.0 to 6.1.2-pre.1 in /path', 92 | config: config('patch') 93 | } 94 | 95 | assert.notOk(parse(options)) 96 | assert.ok(core.info.called) 97 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.2-pre.1') 98 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 99 | 100 | core.info.restore() 101 | }) 102 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- # 2 | # Note: this file originates in template-template # 3 | # ----------------------------------------------- # 4 | 5 | # Heading levels should only increment by one level at a time 6 | MD001: false 7 | 8 | # Heading style 9 | MD003: 10 | style: atx 11 | 12 | # Unordered list style 13 | MD004: 14 | style: dash 15 | 16 | # Inconsistent indentation for list items at the same level 17 | MD005: true 18 | 19 | # Unordered list indentation 20 | MD007: 21 | indent: 2 22 | start_indented: false 23 | 24 | # Trailing spaces 25 | MD009: 26 | br_spaces: 2 27 | list_item_empty_lines: false 28 | strict: false 29 | 30 | # Hard tabs 31 | MD010: 32 | code_blocks: false 33 | 34 | # Reversed link syntax 35 | MD011: true 36 | 37 | # Multiple consecutive blank lines 38 | MD012: 39 | maximum: 1 40 | 41 | # Line length 42 | MD013: 43 | line_length: 360 44 | strict: true 45 | stern: true 46 | 47 | # Dollar signs used before commands without showing output 48 | MD014: false 49 | 50 | # No space after hash on atx style heading 51 | MD018: true 52 | 53 | # Multiple spaces after hash on atx style heading 54 | MD019: true 55 | 56 | # No space inside hashes on closed atx style heading 57 | MD020: true 58 | 59 | # Multiple spaces inside hashes on closed atx style heading 60 | MD021: true 61 | 62 | # Headings should be surrounded by blank lines 63 | MD022: 64 | lines_above: 1 65 | lines_below: 1 66 | 67 | 68 | # Headings must start at the beginning of the line 69 | MD023: true 70 | 71 | # Multiple headings with the same content 72 | MD024: 73 | allow_different_nesting: true 74 | 75 | # Multiple top level headings in the same document 76 | MD025: true 77 | 78 | # Trailing punctuation in heading 79 | MD026: 80 | punctuation: ".,;:!?。,;:!?" 81 | 82 | # Multiple spaces after blockquote symbol 83 | MD027: true 84 | 85 | # Blank line inside blockquote 86 | MD028: true 87 | 88 | # Ordered list item prefix 89 | MD029: 90 | style: one_or_ordered 91 | 92 | # Spaces after list markers 93 | MD030: 94 | ul_single: 1 95 | ol_single: 1 96 | ul_multi: 1 97 | ol_multi: 1 98 | 99 | # Fenced code blocks should be surrounded by blank lines 100 | MD031: 101 | list_items: true 102 | 103 | # Lists should be surrounded by blank lines 104 | MD032: true 105 | 106 | # inline HTML 107 | MD033: 108 | allowed_elements: [details, summary] 109 | 110 | # Bare URL used 111 | MD034: true 112 | 113 | # Horizontal rule style 114 | MD035: 115 | style: "----" 116 | 117 | # Emphasis used instead of a heading 118 | MD036: 119 | punctuation: ".,;:!?。,;:!?" 120 | 121 | # Spaces inside emphasis markers 122 | MD037: true 123 | 124 | # Spaces inside code span elements 125 | MD038: true 126 | 127 | # Spaces inside link text 128 | MD039: true 129 | 130 | # Fenced code blocks should have a language specified 131 | MD040: true 132 | 133 | # First line in file should be a top level heading 134 | MD041: false 135 | 136 | # No empty links 137 | MD042: true 138 | 139 | # Required heading structure 140 | MD043: false 141 | 142 | # Proper names should have the correct capitalization 143 | MD044: false 144 | 145 | # Images should have alternate text (alt text) 146 | MD045: false 147 | 148 | # Code block style 149 | MD046: 150 | style: fenced 151 | 152 | # Files should end with a single newline character 153 | MD047: true 154 | 155 | # Code fence style 156 | MD048: 157 | style: backtick 158 | -------------------------------------------------------------------------------- /action/test/parse/dep-name.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | 5 | import core from '@actions/core' 6 | import fs from 'fs' 7 | 8 | // module 9 | import parse from '../../lib/parse.js' 10 | 11 | function config (dep, target) { 12 | return [{ match: { dependency_name: dep, update_type: `semver:${target}` } }] 13 | } 14 | 15 | tap.test('title -> in range', async assert => { 16 | assert.plan(8) 17 | 18 | sinon.stub(core, 'info') 19 | sinon.stub(fs, 'existsSync').returns(false) 20 | 21 | const options = { 22 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path', 23 | config: config('api-problem', 'major') 24 | } 25 | 26 | assert.ok(parse(options)) 27 | assert.ok(core.info.called) 28 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path"') 29 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: api-problem') 30 | assert.equal(core.info.getCall(2)?.firstArg, 'from: 6.1.2') 31 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.4') 32 | assert.equal(core.info.getCall(6)?.firstArg, 'config: api-problem:semver:major') 33 | assert.equal(core.info.getCall(7)?.firstArg, 'api-problem:semver:major detected, will auto-merge') 34 | 35 | core.info.restore() 36 | fs.existsSync.restore() 37 | }) 38 | 39 | tap.test('title -> wildcard / regex', async assert => { 40 | assert.plan(8) 41 | 42 | sinon.stub(core, 'info') 43 | sinon.stub(fs, 'existsSync').returns(false) 44 | 45 | const options = { 46 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path', 47 | config: config('api-prob*', 'major') 48 | } 49 | 50 | assert.ok(parse(options)) 51 | assert.ok(core.info.called) 52 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path"') 53 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: api-problem') 54 | assert.equal(core.info.getCall(2)?.firstArg, 'from: 6.1.2') 55 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.4') 56 | assert.equal(core.info.getCall(6)?.firstArg, 'config: api-prob*:semver:major') 57 | assert.equal(core.info.getCall(7)?.firstArg, 'api-prob*:semver:major detected, will auto-merge') 58 | 59 | core.info.restore() 60 | fs.existsSync.restore() 61 | }) 62 | 63 | tap.test('parse -> out of range', async assert => { 64 | assert.plan(6) 65 | 66 | sinon.stub(core, 'info') 67 | sinon.stub(fs, 'existsSync').returns(false) 68 | 69 | const options = { 70 | title: 'chore(deps): bump api-problem from 6.1.2 to 7.0.0 in /path', 71 | config: config('api-problem', 'patch') 72 | } 73 | 74 | assert.notOk(parse(options), false) 75 | assert.ok(core.info.called) 76 | assert.equal(core.info.getCall(2)?.firstArg, 'from: 6.1.2') 77 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 7.0.0') 78 | assert.equal(core.info.getCall(6)?.firstArg, 'config: api-problem:semver:patch') 79 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 80 | 81 | core.info.restore() 82 | fs.existsSync.restore() 83 | }) 84 | 85 | tap.test('parse -> edge cases', async assert => { 86 | const titles = [ 87 | { message: 'Update rake requirement from 10.4.0 to 13.0.0', name: 'rake' }, 88 | { message: 'Bump actions/cache from v2.0.0 to v2.1.2', name: 'actions/cache' }, 89 | { message: 'Update actions/setup-python requirement to v2.1.4', name: 'actions/setup-python' } 90 | ] 91 | 92 | assert.plan(titles.length * 3) 93 | 94 | sinon.stub(core, 'info') 95 | sinon.stub(fs, 'existsSync').returns(false) 96 | 97 | for (const title of titles) { 98 | assert.ok(parse({ title: title.message, config: [{ match: { dependency_name: title.name, update_type: 'all' } }] })) 99 | assert.ok(core.info.called) 100 | assert.equal(core.info.getCall(1)?.firstArg, `depName: ${title.name}`) 101 | 102 | core.info.resetHistory() 103 | } 104 | 105 | core.info.restore() 106 | fs.existsSync.restore() 107 | }) 108 | -------------------------------------------------------------------------------- /action/test/parse/dependency-types.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | 5 | import core from '@actions/core' 6 | 7 | // module 8 | import parse from '../../lib/parse.js' 9 | 10 | const config = [{ match: { dependency_type: 'all', update_type: 'semver:major' } }] 11 | 12 | const dependencies = { 13 | prod: { 14 | dependencies: { 15 | 'api-problem': '6.1.2' 16 | } 17 | }, 18 | dev: { 19 | devDependencies: { 20 | 'api-problem': '6.1.2' 21 | } 22 | } 23 | } 24 | 25 | tap.test('title -> security tag is detected', async assert => { 26 | assert.plan(3) 27 | 28 | sinon.stub(core, 'info') 29 | 30 | const options = { 31 | title: '[Security] bump api-problem from 6.1.2 to 6.1.4 in /path', 32 | dependencies: dependencies.prod, 33 | config 34 | } 35 | 36 | assert.ok(parse(options)) 37 | assert.ok(core.info.called) 38 | assert.equal(core.info.getCall(5)?.firstArg, 'security critical: true') 39 | 40 | core.info.restore() 41 | }) 42 | 43 | tap.test('title -> security tag is detected (conventional commits)', async assert => { 44 | assert.plan(3) 45 | 46 | sinon.stub(core, 'info') 47 | 48 | const options = { 49 | title: 'chore(deps): [security] bump api-problem from 6.1.2 to 6.1.4 in /path', 50 | dependencies: dependencies.prod, 51 | config 52 | } 53 | 54 | assert.ok(parse(options)) 55 | assert.ok(core.info.called) 56 | assert.equal(core.info.getCall(5)?.firstArg, 'security critical: true') 57 | 58 | core.info.restore() 59 | }) 60 | 61 | tap.test('labels -> security tag is detected', async assert => { 62 | assert.plan(3) 63 | 64 | sinon.stub(core, 'info') 65 | 66 | const options = { 67 | title: 'Bump api-problem from 6.1.2 to 6.1.4 in /path', 68 | labels: ['security'], 69 | dependencies: dependencies.prod, 70 | config 71 | } 72 | 73 | assert.ok(parse(options)) 74 | assert.ok(core.info.called) 75 | assert.equal(core.info.getCall(5)?.firstArg, 'security critical: true') 76 | 77 | core.info.restore() 78 | }) 79 | 80 | tap.test('labels -> not security-critical update is detected', async assert => { 81 | assert.plan(3) 82 | 83 | sinon.stub(core, 'info') 84 | 85 | const options = { 86 | title: 'Bump api-problem from 6.1.2 to 6.1.4 in /path', 87 | dependencies: dependencies.prod, 88 | config 89 | } 90 | 91 | assert.ok(parse(options)) 92 | assert.ok(core.info.called) 93 | assert.equal(core.info.getCall(5)?.firstArg, 'security critical: false') 94 | 95 | core.info.restore() 96 | }) 97 | 98 | tap.test('title -> dependency is detected as dev dependency (title fallback)', async assert => { 99 | assert.plan(3) 100 | 101 | sinon.stub(core, 'info') 102 | 103 | const options = { 104 | title: 'chore(deps-dev): bump api-problem from 6.1.2 to 6.1.4 in /path', 105 | dependencies: dependencies.prod, 106 | config 107 | } 108 | 109 | assert.ok(parse(options)) 110 | assert.ok(core.info.called) 111 | assert.equal(core.info.getCall(4)?.firstArg, 'dependency type: development') 112 | 113 | core.info.restore() 114 | }) 115 | 116 | tap.test('title -> dependency is detected as production dependency (title fallback)', async assert => { 117 | assert.plan(3) 118 | 119 | sinon.stub(core, 'info') 120 | 121 | const options = { 122 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path', 123 | dependencies: dependencies.prod, 124 | config 125 | } 126 | 127 | assert.ok(parse(options)) 128 | assert.ok(core.info.called) 129 | assert.equal(core.info.getCall(4)?.firstArg, 'dependency type: production') 130 | 131 | core.info.restore() 132 | }) 133 | 134 | tap.test('title -> dependency is detected as dev dependency (package.json)', async assert => { 135 | assert.plan(3) 136 | 137 | sinon.stub(core, 'info') 138 | 139 | const options = { 140 | title: 'Bump api-problem from 6.1.2 to 6.1.4 in /path', 141 | dependencies: dependencies.dev, 142 | config 143 | } 144 | 145 | assert.ok(parse(options)) 146 | assert.ok(core.info.called) 147 | assert.equal(core.info.getCall(4)?.firstArg, 'dependency type: development') 148 | 149 | core.info.restore() 150 | }) 151 | 152 | tap.test('title -> dependency is detected as production dependency (package.json)', async assert => { 153 | assert.plan(3) 154 | 155 | sinon.stub(core, 'info') 156 | 157 | const options = { 158 | title: 'Bump api-problem from 6.1.2 to 6.1.4 in /path', 159 | dependencies: dependencies.prod, 160 | config 161 | } 162 | 163 | assert.ok(parse(options)) 164 | assert.ok(core.info.called) 165 | assert.equal(core.info.getCall(4)?.firstArg, 'dependency type: production') 166 | 167 | core.info.restore() 168 | }) 169 | -------------------------------------------------------------------------------- /action/test/parse/config-parse.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import tap from 'tap' 3 | import sinon from 'sinon' 4 | 5 | import core from '@actions/core' 6 | 7 | // module 8 | import parse from '../../lib/parse.js' 9 | 10 | const configAllPatchSecMinor = [ 11 | { match: { dependency_type: 'all', update_type: 'semver:patch' } }, 12 | { match: { dependency_type: 'all', update_type: 'security:minor' } } 13 | ] 14 | 15 | const configAllPatchDevSecAll = [ 16 | { match: { dependency_type: 'all', update_type: 'semver:patch' } }, 17 | { match: { dependency_type: 'development', update_type: 'security:all' } } 18 | ] 19 | 20 | const configProdPatchDevMajor = [ 21 | { match: { dependency_type: 'production', update_type: 'semver:patch' } }, 22 | { match: { dependency_type: 'development', update_type: 'semver:major' } } 23 | ] 24 | 25 | const configInRange = [ 26 | { match: { dependency_type: 'all', update_type: 'in_range' } } 27 | ] 28 | 29 | const tests = [ 30 | { 31 | name: 'all deps patch, security minor --> patch (✓)', 32 | outcome: true, 33 | message: 'all:semver:patch detected, will auto-merge', 34 | config: configAllPatchSecMinor, 35 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4' 36 | }, 37 | { 38 | name: 'all deps patch, security minor --> minor (✗)', 39 | outcome: false, 40 | message: 'manual merging required', 41 | config: configAllPatchSecMinor, 42 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.2.0' 43 | }, 44 | { 45 | name: 'all deps patch, security minor --> security:minor (✓)', 46 | outcome: true, 47 | message: 'all:security:minor detected, will auto-merge', 48 | config: configAllPatchSecMinor, 49 | title: 'chore(deps): [security] bump api-problem from 6.1.2 to 6.2.0' 50 | }, 51 | { 52 | name: 'all deps patch, security minor --> security:major (✗)', 53 | outcome: false, 54 | message: 'manual merging required', 55 | config: configAllPatchSecMinor, 56 | title: 'chore(deps): [security] bump api-problem from 6.1.2 to 7.2.0' 57 | }, 58 | { 59 | name: 'all deps patch, dev security all --> patch (✓)', 60 | outcome: true, 61 | message: 'all:semver:patch detected, will auto-merge', 62 | config: configAllPatchDevSecAll, 63 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.3' 64 | }, 65 | { 66 | name: 'all deps patch, dev security all --> preminor (✗)', 67 | outcome: false, 68 | message: 'manual merging required', 69 | config: configAllPatchDevSecAll, 70 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.2.0-pre' 71 | }, 72 | { 73 | name: 'all deps patch, dev security all --> dev security:premajor (✓)', 74 | outcome: true, 75 | message: 'development:security:all detected, will auto-merge', 76 | config: configAllPatchDevSecAll, 77 | title: 'chore(deps-dev): [security] bump api-problem from 6.1.2 to 7.0.0-pre' 78 | }, 79 | { 80 | name: 'prod patch, dev major --> prod patch (✓)', 81 | outcome: true, 82 | message: 'production:semver:patch detected, will auto-merge', 83 | config: configProdPatchDevMajor, 84 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.3' 85 | }, 86 | { 87 | name: 'prod patch, dev major --> prod minor (✗)', 88 | outcome: false, 89 | message: 'manual merging required', 90 | config: configProdPatchDevMajor, 91 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.2.0' 92 | }, 93 | { 94 | name: 'prod patch, dev major --> dev minor (✓)', 95 | outcome: true, 96 | message: 'development:semver:major detected, will auto-merge', 97 | config: configProdPatchDevMajor, 98 | title: 'chore(deps-dev): bump api-problem from 6.1.2 to 6.2.0' 99 | }, 100 | { 101 | name: 'prod patch, dev major --> dev premajor (✗)', 102 | outcome: false, 103 | message: 'manual merging required', 104 | config: configProdPatchDevMajor, 105 | title: 'chore(deps-dev): bump api-problem from 6.1.2 to 7.0.0-pre' 106 | }, 107 | { 108 | name: 'in_range --> throws', 109 | throws: true, 110 | config: configInRange, 111 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.3' 112 | } 113 | ] 114 | 115 | for (const { name, throws, title, config, outcome, message } of tests) { 116 | tap.test(`compound merge configs in config files --> ${name}`, async assert => { 117 | assert.plan(throws ? 3 : 2) 118 | 119 | sinon.stub(core, 'info') // silence output on terminal 120 | sinon.stub(core, 'warning') 121 | sinon.stub(process, 'exit') 122 | 123 | const result = parse({ title, config }) 124 | 125 | if (throws === true) { 126 | assert.ok(process.exit.called) 127 | assert.equal(process.exit.getCall(0)?.firstArg, 0) 128 | assert.equal(core.warning.getCall(0)?.firstArg, 'in_range update type not supported yet') 129 | } else { 130 | assert.equal(core.info.getCall(-1)?.firstArg, message) 131 | assert.equal(result, outcome) 132 | } 133 | 134 | process.exit.restore() 135 | core.info.restore() 136 | core.warning.restore() 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /action/lib/parse.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import semver from 'semver' 4 | import core from '@actions/core' 5 | 6 | const regex = { 7 | // semver regex 8 | semver: /(?(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/, 9 | // detect dependency name 10 | name: /(bump|update) (?(?:@[^\s]+\/)?[^\s]+) (requirement)?/i, 11 | // detect dependency type from PR title 12 | dev: /\((deps-dev)\):/, 13 | // detect security flag 14 | security: /(^|: )\[security\]/i, 15 | // config values 16 | config: /(?security|semver):(?.+)/i 17 | } 18 | 19 | const weight = { 20 | all: 1000, 21 | premajor: 6, 22 | major: 5, 23 | preminor: 4, 24 | minor: 3, 25 | prepatch: 2, 26 | prerelease: 2, // equal to prepatch 27 | patch: 1 28 | } 29 | 30 | export default function ({ title, labels = [], config = [], dependencies = {} }) { 31 | // log 32 | core.info(`title: "${title}"`) 33 | 34 | // extract dep name from the title 35 | const depName = title.match(regex.name)?.groups.name 36 | core.info(`depName: ${depName}`) 37 | 38 | // exit early 39 | if (!depName) { 40 | core.warning('failed to parse title: could not detect dependency name') 41 | return process.exit(0) // soft exit 42 | } 43 | 44 | // extract version from the title, allowing for constraints (~,^,>=) and v prefix 45 | const from = title.match(new RegExp('from \\D*' + regex.semver.source))?.groups 46 | const to = title.match(new RegExp('to \\D*' + regex.semver.source))?.groups 47 | 48 | if (!to) { 49 | core.warning('failed to parse title: no recognizable versions') 50 | return process.exit(0) // soft exit 51 | } 52 | 53 | // exit early 54 | if (!semver.valid(to.version)) { 55 | core.warning('failed to parse title: invalid semver') 56 | return process.exit(0) // soft exit 57 | } 58 | 59 | // is this a security update? 60 | const isSecurity = regex.security.test(title) || labels.includes('security') 61 | 62 | // production dependency flag 63 | let isProd 64 | 65 | // check if this dependency is a devDependency 66 | if (dependencies.devDependencies && depName in dependencies.devDependencies) { 67 | isProd = false 68 | } 69 | 70 | // if we could not determine the dependency type from package files, fall back to title parsing 71 | if (isProd === undefined && regex.dev.test(title)) { 72 | isProd = false 73 | } 74 | 75 | // assume default to be production 76 | if (isProd === undefined) { 77 | isProd = true 78 | } 79 | 80 | // log 81 | core.info(`from: ${from ? from.version : 'unknown'}`) 82 | core.info(`to: ${to.version}`) 83 | core.info(`dependency type: ${isProd ? 'production' : 'development'}`) 84 | core.info(`security critical: ${isSecurity}`) 85 | 86 | // analyze with semver 87 | let versionChange 88 | 89 | if (from && from.version) { 90 | versionChange = semver.diff(from.version, to.version) 91 | } 92 | 93 | // check all configuration variants to see if one matches 94 | for (const { match: { dependency_name, dependency_type, update_type } } of config) { 95 | if ( 96 | // catch all 97 | dependency_type === 'all' || 98 | 99 | // evaluate prod dependencies 100 | (dependency_type === 'production' && isProd) || 101 | 102 | // evaluate dev dependencies 103 | (dependency_type === 'development' && !isProd) || 104 | 105 | // evaluate individual dependency 106 | (dependency_name && depName.match(new RegExp(dependency_name))) 107 | ) { 108 | core.info(`config: ${dependency_name || dependency_type}:${update_type}`) 109 | 110 | switch (true) { 111 | case update_type === 'in_range': 112 | core.warning('in_range update type not supported yet') 113 | return process.exit(0) // soft exit 114 | 115 | case update_type === 'all': 116 | core.info(`${dependency_name || dependency_type}:${update_type} detected, will auto-merge`) 117 | return true 118 | 119 | // security:patch, semver:minor, ... 120 | case regex.config.test(update_type): { 121 | const { type, target } = update_type.match(regex.config)?.groups 122 | 123 | // skip when config is for security update and PR is not security 124 | if (type === 'security' && !isSecurity) continue 125 | 126 | if (target === 'all') { 127 | core.info(`${dependency_name || dependency_type}:${update_type} detected, will auto-merge`) 128 | return true 129 | } 130 | 131 | // when there is no "from" version, there is no change detected 132 | if (!versionChange) { 133 | core.warning('no version range detected in PR title') 134 | continue 135 | } 136 | 137 | // evaluate weight of detected change 138 | if ((weight[target] || 0) >= (weight[versionChange] || 0)) { 139 | // tell dependabot to merge 140 | core.info(`${dependency_name || dependency_type}:${update_type} detected, will auto-merge`) 141 | return true 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | core.info('manual merging required') 149 | 150 | return false 151 | } 152 | -------------------------------------------------------------------------------- /action/test/parse/match-target.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | // packages 4 | import tap from 'tap' 5 | import sinon from 'sinon' 6 | 7 | import core from '@actions/core' 8 | import fs from 'fs' 9 | 10 | // module 11 | import parse from '../../lib/parse.js' 12 | 13 | function config (update_type) { 14 | return [{ match: { dependency_type: 'all', update_type } }] 15 | } 16 | 17 | tap.test('title -> in range', async assert => { 18 | assert.plan(8) 19 | 20 | sinon.stub(core, 'info') 21 | sinon.stub(fs, 'existsSync').returns(false) 22 | 23 | const options = { 24 | title: 'chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path', 25 | config: config('semver:major') 26 | } 27 | 28 | assert.ok(parse(options)) 29 | assert.ok(core.info.called) 30 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "chore(deps): bump api-problem from 6.1.2 to 6.1.4 in /path"') 31 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: api-problem') 32 | assert.equal(core.info.getCall(2)?.firstArg, 'from: 6.1.2') 33 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 6.1.4') 34 | assert.equal(core.info.getCall(6)?.firstArg, 'config: all:semver:major') 35 | assert.equal(core.info.getCall(7)?.firstArg, 'all:semver:major detected, will auto-merge') 36 | 37 | core.info.restore() 38 | fs.existsSync.restore() 39 | }) 40 | 41 | tap.test('title -> in range, no from', async assert => { 42 | assert.plan(8) 43 | 44 | sinon.stub(core, 'info') 45 | // sinon.stub(core, 'warning') 46 | sinon.stub(fs, 'existsSync').returns(false) 47 | 48 | const options = { 49 | title: 'Update actions/setup-python requirement to v2.1.4 in /path', 50 | config: config('all') 51 | } 52 | 53 | assert.ok(parse(options)) 54 | assert.ok(core.info.called) 55 | // assert.ok(core.warning.called) 56 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "Update actions/setup-python requirement to v2.1.4 in /path"') 57 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: actions/setup-python') 58 | assert.equal(core.info.getCall(2)?.firstArg, 'from: unknown') 59 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 2.1.4') 60 | assert.equal(core.info.getCall(6)?.firstArg, 'config: all:all') 61 | assert.equal(core.info.getCall(7)?.firstArg, 'all:all detected, will auto-merge') 62 | // assert.equal(core.warning.getCall(0)?.firstArg, 'no version range detected in PR title') 63 | 64 | core.info.restore() 65 | fs.existsSync.restore() 66 | }) 67 | 68 | tap.test('title -> in range, no from', async assert => { 69 | assert.plan(8) 70 | 71 | sinon.stub(core, 'info') 72 | // sinon.stub(core, 'warning') 73 | sinon.stub(fs, 'existsSync').returns(false) 74 | 75 | const options = { 76 | title: 'Update actions/setup-python requirement to v2.1.4 in /path', 77 | config: config('semver:all') 78 | } 79 | 80 | assert.ok(parse(options)) 81 | assert.ok(core.info.called) 82 | // assert.ok(core.warning.called) 83 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "Update actions/setup-python requirement to v2.1.4 in /path"') 84 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: actions/setup-python') 85 | assert.equal(core.info.getCall(2)?.firstArg, 'from: unknown') 86 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 2.1.4') 87 | assert.equal(core.info.getCall(6)?.firstArg, 'config: all:semver:all') 88 | assert.equal(core.info.getCall(7)?.firstArg, 'all:semver:all detected, will auto-merge') 89 | // assert.equal(core.warning.getCall(0)?.firstArg, 'no version range detected in PR title') 90 | 91 | core.info.restore() 92 | fs.existsSync.restore() 93 | }) 94 | 95 | tap.test('parse -> out of range', async assert => { 96 | assert.plan(5) 97 | 98 | sinon.stub(core, 'info') 99 | sinon.stub(fs, 'existsSync').returns(false) 100 | 101 | const options = { 102 | title: 'chore(deps): bump api-problem from 6.1.2 to 7.0.0 in /path', 103 | config: config('semver:patch') 104 | } 105 | 106 | assert.notOk(parse(options), false) 107 | assert.ok(core.info.called) 108 | assert.equal(core.info.getCall(2)?.firstArg, 'from: 6.1.2') 109 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 7.0.0') 110 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 111 | 112 | core.info.restore() 113 | fs.existsSync.restore() 114 | }) 115 | 116 | tap.test('title -> out of range, no from', async assert => { 117 | assert.plan(10) 118 | 119 | sinon.stub(core, 'info') 120 | sinon.stub(core, 'warning') 121 | sinon.stub(fs, 'existsSync').returns(false) 122 | 123 | const options = { 124 | title: 'Update actions/setup-python requirement to v2.1.4 in /path', 125 | config: config('semver:major') 126 | } 127 | 128 | assert.notOk(parse(options)) 129 | assert.ok(core.info.called) 130 | assert.ok(core.warning.called) 131 | assert.equal(core.info.getCall(0)?.firstArg, 'title: "Update actions/setup-python requirement to v2.1.4 in /path"') 132 | assert.equal(core.info.getCall(1)?.firstArg, 'depName: actions/setup-python') 133 | assert.equal(core.info.getCall(2)?.firstArg, 'from: unknown') 134 | assert.equal(core.info.getCall(3)?.firstArg, 'to: 2.1.4') 135 | assert.equal(core.info.getCall(6)?.firstArg, 'config: all:semver:major') 136 | assert.equal(core.info.getCall(7)?.firstArg, 'manual merging required') 137 | assert.equal(core.warning.getCall(0)?.firstArg, 'no version range detected in PR title') 138 | 139 | core.info.restore() 140 | core.warning.restore() 141 | fs.existsSync.restore() 142 | }) 143 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------- # 2 | # Note: this file originates in template-action-docker # 3 | # ---------------------------------------------------- # 4 | 5 | on: 6 | - push 7 | - workflow_dispatch 8 | 9 | name: push 10 | 11 | concurrency: 12 | group: ${{ github.ref }}-${{ github.workflow }} 13 | 14 | jobs: 15 | metadata: 16 | runs-on: ubuntu-latest 17 | 18 | outputs: 19 | image-name: ${{ steps.image.outputs.name }} 20 | repository_is_template: ${{ steps.metadata.outputs.repository_is_template }} 21 | repository_default_branch: ${{ steps.metadata.outputs.repository_default_branch }} 22 | 23 | steps: 24 | - uses: actions/checkout@v3.2.0 25 | 26 | - id: metadata 27 | uses: ahmadnassri/action-metadata@v2.1.2 28 | 29 | - id: image 30 | run: echo "name=$(basename "${GITHUB_REPOSITORY/docker-//}")" >> "$GITHUB_OUTPUT" 31 | 32 | commit-lint: 33 | timeout-minutes: 5 34 | 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v3.2.0 39 | 40 | - uses: ahmadnassri/action-commit-lint@v2.0.12 41 | with: 42 | config: .github/linters/.commit-lint.yml 43 | 44 | mega-linter: 45 | timeout-minutes: 5 46 | 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: actions/checkout@v3.2.0 51 | 52 | - uses: oxsecurity/megalinter/flavors/javascript@v6.15.0 53 | env: 54 | GITHUB_TOKEN: ${{ github.token }} 55 | MEGALINTER_CONFIG: .github/linters/.mega-linter.yml 56 | GITHUB_COMMENT_REPORTER: true 57 | GITHUB_STATUS_REPORTER: true 58 | 59 | - uses: actions/upload-artifact@v3 60 | if: ${{ success() }} || ${{ failure() }} 61 | with: 62 | name: mega-linter-reports 63 | path: | 64 | megalinter-reports 65 | mega-linter.log 66 | 67 | release: 68 | needs: 69 | - metadata 70 | - commit-lint 71 | - mega-linter 72 | 73 | # only runs on main branch for non template repos 74 | if: | 75 | needs.metadata.outputs.repository_is_template == 'false' && 76 | needs.metadata.outputs.repository_default_branch == github.ref_name 77 | 78 | timeout-minutes: 5 79 | 80 | runs-on: ubuntu-latest 81 | 82 | outputs: 83 | published: ${{ steps.release.outputs.published }} 84 | version: ${{ steps.release.outputs.release-version }} 85 | version-major: ${{ steps.release.outputs.release-version-major }} 86 | version-minor: ${{ steps.release.outputs.release-version-minor }} 87 | 88 | steps: 89 | - uses: actions/checkout@v3.2.0 90 | with: 91 | submodules: true 92 | 93 | - id: release 94 | uses: ahmadnassri/action-semantic-release@v2.1.13 95 | with: 96 | config: ${{ github.workspace }}/.semantic.json 97 | env: 98 | GITHUB_TOKEN: ${{ github.token }} 99 | 100 | publish-docker: 101 | needs: 102 | - release 103 | - metadata 104 | 105 | timeout-minutes: 30 106 | 107 | if: ${{ needs.release.outputs.published == 'true' }} 108 | 109 | name: publish to ghcr.io 110 | 111 | runs-on: ubuntu-latest 112 | 113 | steps: 114 | - uses: actions/checkout@v3.2.0 115 | - uses: docker/setup-qemu-action@v2 116 | - uses: docker/setup-buildx-action@v2 117 | 118 | # login to registry 119 | - uses: docker/login-action@v2 120 | with: 121 | registry: ghcr.io 122 | username: ${{ github.repository_owner }} 123 | password: ${{ secrets.GH_TOKEN }} 124 | 125 | # publish 126 | - uses: docker/build-push-action@v3 127 | with: 128 | push: true 129 | cache-from: type=gha 130 | cache-to: type=gha,mode=max 131 | platforms: linux/amd64,linux/arm64 132 | tags: | 133 | ghcr.io/${{ github.repository_owner }}/${{ needs.metadata.outputs.image-name }}:latest 134 | ghcr.io/${{ github.repository_owner }}/${{ needs.metadata.outputs.image-name }}:${{ needs.release.outputs.version-major }} 135 | ghcr.io/${{ github.repository_owner }}/${{ needs.metadata.outputs.image-name }}:${{ needs.release.outputs.version }} 136 | labels: | 137 | org.opencontainers.image.title=${{ needs.metadata.outputs.image-name }} 138 | org.opencontainers.image.url=${{ github.event.repository.html_url }} 139 | org.opencontainers.image.version=${{ needs.release.outputs.version }} 140 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 141 | org.opencontainers.image.revision=${{ github.sha }} 142 | 143 | alias: 144 | needs: release 145 | 146 | if: ${{ needs.release.outputs.published == 'true' }} 147 | 148 | runs-on: ubuntu-latest 149 | 150 | strategy: 151 | matrix: 152 | release: [ "v${{ needs.release.outputs.version }}" ] 153 | alias: 154 | - "v${{ needs.release.outputs.version-major }}" 155 | - "v${{ needs.release.outputs.version-major }}.${{ needs.release.outputs.version-minor }}" 156 | 157 | steps: 158 | - uses: actions/github-script@v6 159 | with: 160 | script: | 161 | const { data: { object: { sha } } } = await github.rest.git.getRef({ ...context.repo, ref: 'tags/${{ matrix.release }}' }) 162 | await github.rest.git.deleteRef({ ...context.repo, ref: 'tags/${{ matrix.alias }}' }).catch(() => {}) 163 | await github.rest.git.createRef({ ...context.repo, ref: 'refs/tags/${{ matrix.alias }}', sha }) 164 | 165 | template-sync: 166 | timeout-minutes: 5 167 | 168 | needs: 169 | - metadata 170 | - commit-lint 171 | - mega-linter 172 | 173 | # only runs on main branch for template repos 174 | if: | 175 | needs.metadata.outputs.repository_is_template == 'true' && 176 | needs.metadata.outputs.repository_default_branch == github.ref_name 177 | 178 | runs-on: ubuntu-latest 179 | 180 | steps: 181 | - uses: actions/checkout@v3.2.0 182 | 183 | - uses: ahmadnassri/action-template-repository-sync@v2 184 | with: 185 | github-token: ${{ secrets.GH_TOKEN }} 186 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | > **Note:** _Dependabot will wait until all your status checks pass before merging. This is a function of Dependabot itself, and not this Action._ 2 | 3 | ## Usage 4 | 5 | ```yaml 6 | name: auto-merge 7 | 8 | on: 9 | pull_request: 10 | 11 | jobs: 12 | auto-merge: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 17 | with: 18 | target: minor 19 | github-token: ${{ secrets.mytoken }} 20 | ``` 21 | 22 | The action will only merge PRs whose checks (CI/CD) pass. 23 | 24 | ### Examples 25 | 26 | Minimal setup: 27 | 28 | ```yaml 29 | steps: 30 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 31 | with: 32 | github-token: ${{ secrets.mytoken }} 33 | ``` 34 | 35 | Only merge if the changed dependency version is a `patch` _(default behavior)_: 36 | 37 | ```yaml 38 | steps: 39 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 40 | with: 41 | target: patch 42 | github-token: ${{ secrets.mytoken }} 43 | ``` 44 | 45 | Only merge if the changed dependency version is a `minor`: 46 | 47 | ```yaml 48 | steps: 49 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 50 | with: 51 | target: minor 52 | github-token: ${{ secrets.mytoken }} 53 | ``` 54 | 55 | Using a configuration file: 56 | 57 | ###### `.github/workflows/auto-merge.yml` 58 | 59 | ```yaml 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 63 | with: 64 | github-token: ${{ secrets.mytoken }} 65 | ``` 66 | 67 | ###### `.github/auto-merge.yml` 68 | 69 | ```yaml 70 | - match: 71 | dependency_type: all 72 | update_type: "semver:minor" # includes patch updates! 73 | ``` 74 | 75 | ### Inputs 76 | 77 | | input | required | default | description | 78 | |----------------|----------|--------------------------|-----------------------------------------------------| 79 | | `github-token` | ✔ | `github.token` | The GitHub token used to merge the pull-request | 80 | | `config` | ✔ | `.github/auto-merge.yml` | Path to configuration file *(relative to root)* | 81 | | `target` | ❌ | `patch` | The version comparison target (major, minor, patch) | 82 | | `command` | ❌ | `merge` | The command to pass to Dependabot | 83 | | `botName` | ❌ | `dependabot` | The bot to tag in approve/comment message. | 84 | | `approve` | ❌ | `true` | Auto-approve pull-requests | 85 | 86 | ### Token Scope 87 | 88 | The GitHub token is a [Personal Access Token][github-pat] with the following scopes: 89 | 90 | - `repo` for private repositories 91 | - `public_repo` for public repositories 92 | 93 | The token MUST be created from a user with **`push`** permission to the repository. 94 | 95 | > ℹ _see reference for [user owned repos][github-user-repos] and for [org owned repos][github-org-repos]_ 96 | 97 | ### Configuration file syntax 98 | 99 | 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 100 | 101 | - minor updates for `aws-sdk` 102 | - minor development dependency updates 103 | - patch production dependency updates 104 | - minor security-critical production dependency updates 105 | 106 | ```yaml 107 | - match: 108 | dependency_name: aws-sdk 109 | update_type: semver:minor 110 | 111 | - match: 112 | dependency_type: development 113 | update_type: semver:minor # includes patch updates! 114 | 115 | - match: 116 | dependency_type: production 117 | update_type: security:minor # includes patch updates! 118 | 119 | - match: 120 | dependency_type: production 121 | update_type: semver:patch 122 | ``` 123 | 124 | #### Match Properties 125 | 126 | | property | required | supported values | 127 | | ----------------- | -------- | ------------------------------------------ | 128 | | `dependency_name` | ❌ | full name of dependency, or a regex string | 129 | | `dependency_type` | ❌ | `all`, `production`, `development` | 130 | | `update_type` | ✔ | `all`, `security:*`, `semver:*` | 131 | 132 | > **`update_type`** can specify security match or semver match with the syntax: `${type}:${match}`, e.g. 133 | > 134 | > - **security:patch** 135 | > SemVer patch update that fixes a known security vulnerability 136 | > 137 | > - **semver:patch** 138 | > SemVer patch update, e.g. > 1.x && 1.0.1 to 1.0.3 139 | > 140 | > - **semver:minor** 141 | > SemVer minor update, e.g. > 1.x && 2.1.4 to 2.3.1 142 | > 143 | > To allow `prereleases`, the corresponding `prepatch`, `preminor` and `premajor` types are also supported 144 | 145 | ###### Defaults 146 | 147 | By default, if no configuration file is present in the repo, the action will assume the following: 148 | 149 | ```yaml 150 | - match: 151 | dependency_type: all 152 | update_type: semver:${TARGET} 153 | ``` 154 | 155 | > Where `$TARGET` is the `target` value from the action [Inputs](#inputs) 156 | 157 | The syntax is based on the [legacy dependaBot v1 config format](https://dependabot.com/docs/config-file/#automerged_updates). 158 | However, **`in_range` is not supported yet**. 159 | 160 | ## Exceptions and Edge Cases 161 | 162 | 1. Parsing of _version ranges_ is not currently supported 163 | 164 | ``` 165 | Update stone requirement from ==1.* to ==3.* 166 | requirements: update sphinx-autodoc-typehints requirement from <=1.11.0 to <1.12.0 167 | Update rake requirement from ~> 10.4 to ~> 13.0 168 | ``` 169 | 170 | 2. Parsing of non semver numbering is not currently supported 171 | 172 | ``` 173 | Bump actions/cache from v2.0 to v2.1.2 174 | chore(deps): bump docker/build-push-action from v1 to v2 175 | ``` 176 | 177 | 3. Sometimes Dependabot does not include the "from" version, so version comparison logic is impossible: 178 | 179 | ``` 180 | Update actions/setup-python requirement to v2.1.4 181 | Update actions/cache requirement to v2.1.2 182 | ``` 183 | 184 | if your config is anything other than `update_type: all`, or `update_type: semver:all` the action will fallback to manual merge, since there is no way to compare version ranges for merging. 185 | 186 | [github-pat]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token 187 | [github-user-repos]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/permission-levels-for-a-user-account-repository 188 | [github-org-repos]: https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action: Dependabot Auto Merge 2 | 3 | Automatically merge Dependabot PRs when version comparison is within range. 4 | 5 | [![license][license-img]][license-url] 6 | [![release][release-img]][release-url] 7 | 8 | > **Note:** *Dependabot will wait until all your status checks pass before merging. This is a function of Dependabot itself, and not this Action.* 9 | 10 | ## Usage 11 | 12 | ``` yaml 13 | name: auto-merge 14 | 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | auto-merge: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | target: minor 26 | github-token: ${{ secrets.mytoken }} 27 | ``` 28 | 29 | The action will only merge PRs whose checks (CI/CD) pass. 30 | 31 | ### Examples 32 | 33 | Minimal setup: 34 | 35 | ``` yaml 36 | steps: 37 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 38 | with: 39 | github-token: ${{ secrets.mytoken }} 40 | ``` 41 | 42 | Only merge if the changed dependency version is a `patch` *(default behavior)*: 43 | 44 | ``` yaml 45 | steps: 46 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 47 | with: 48 | target: patch 49 | github-token: ${{ secrets.mytoken }} 50 | ``` 51 | 52 | Only merge if the changed dependency version is a `minor`: 53 | 54 | ``` yaml 55 | steps: 56 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 57 | with: 58 | target: minor 59 | github-token: ${{ secrets.mytoken }} 60 | ``` 61 | 62 | Using a configuration file: 63 | 64 | ###### `.github/workflows/auto-merge.yml` 65 | 66 | ``` yaml 67 | steps: 68 | - uses: actions/checkout@v2 69 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 70 | with: 71 | github-token: ${{ secrets.mytoken }} 72 | ``` 73 | 74 | ###### `.github/auto-merge.yml` 75 | 76 | ``` yaml 77 | - match: 78 | dependency_type: all 79 | update_type: "semver:minor" # includes patch updates! 80 | ``` 81 | 82 | ### Inputs 83 | 84 | | input | required | default | description | 85 | |----------------|----------|--------------------------|-----------------------------------------------------| 86 | | `github-token` | ✔ | `github.token` | The GitHub token used to merge the pull-request | 87 | | `config` | ✔ | `.github/auto-merge.yml` | Path to configuration file *(relative to root)* | 88 | | `target` | ❌ | `patch` | The version comparison target (major, minor, patch) | 89 | | `command` | ❌ | `merge` | The command to pass to Dependabot | 90 | | `botName` | ❌ | `dependabot` | The bot to tag in approve/comment message. | 91 | | `approve` | ❌ | `true` | Auto-approve pull-requests | 92 | 93 | ### Token Scope 94 | 95 | The GitHub token is a [Personal Access Token][] with the following scopes: 96 | 97 | - `repo` for private repositories 98 | - `public_repo` for public repositories 99 | 100 | The token MUST be created from a user with **`push`** permission to the repository. 101 | 102 | > ℹ *see reference for [user owned repos][] and for [org owned repos][]* 103 | 104 | ### Configuration file syntax 105 | 106 | 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 107 | 108 | - minor updates for `aws-sdk` 109 | - minor development dependency updates 110 | - patch production dependency updates 111 | - minor security-critical production dependency updates 112 | 113 | ``` yaml 114 | - match: 115 | dependency_name: aws-sdk 116 | update_type: semver:minor 117 | 118 | - match: 119 | dependency_type: development 120 | update_type: semver:minor # includes patch updates! 121 | 122 | - match: 123 | dependency_type: production 124 | update_type: security:minor # includes patch updates! 125 | 126 | - match: 127 | dependency_type: production 128 | update_type: semver:patch 129 | ``` 130 | 131 | #### Match Properties 132 | 133 | | property | required | supported values | 134 | |-------------------|----------|--------------------------------------------| 135 | | `dependency_name` | ❌ | full name of dependency, or a regex string | 136 | | `dependency_type` | ❌ | `all`, `production`, `development` | 137 | | `update_type` | ✔ | `all`, `security:*`, `semver:*` | 138 | 139 | > **`update_type`** can specify security match or semver match with the syntax: `${type}:${match}`, e.g. 140 | > 141 | > - **security:patch** 142 | > SemVer patch update that fixes a known security vulnerability 143 | > 144 | > - **semver:patch** 145 | > SemVer patch update, e.g. \> 1.x && 1.0.1 to 1.0.3 146 | > 147 | > - **semver:minor** 148 | > SemVer minor update, e.g. \> 1.x && 2.1.4 to 2.3.1 149 | > 150 | > To allow `prereleases`, the corresponding `prepatch`, `preminor` and `premajor` types are also supported 151 | 152 | ###### Defaults 153 | 154 | By default, if no configuration file is present in the repo, the action will assume the following: 155 | 156 | ``` yaml 157 | - match: 158 | dependency_type: all 159 | update_type: semver:${TARGET} 160 | ``` 161 | 162 | > Where `$TARGET` is the `target` value from the action [Inputs][] 163 | 164 | The syntax is based on the [legacy dependaBot v1 config format][]. 165 | However, **`in_range` is not supported yet**. 166 | 167 | ## Exceptions and Edge Cases 168 | 169 | 1. Parsing of *version ranges* is not currently supported 170 | 171 | 172 | 173 | Update stone requirement from ==1.* to ==3.* 174 | requirements: update sphinx-autodoc-typehints requirement from <=1.11.0 to <1.12.0 175 | Update rake requirement from ~> 10.4 to ~> 13.0 176 | 177 | 2. Parsing of non semver numbering is not currently supported 178 | 179 | 180 | 181 | Bump actions/cache from v2.0 to v2.1.2 182 | chore(deps): bump docker/build-push-action from v1 to v2 183 | 184 | 3. Sometimes Dependabot does not include the "from" version, so version comparison logic is impossible: 185 | 186 | 187 | 188 | Update actions/setup-python requirement to v2.1.4 189 | Update actions/cache requirement to v2.1.2 190 | 191 | if your config is anything other than `update_type: all`, or `update_type: semver:all` the action will fallback to manual merge, since there is no way to compare version ranges for merging. 192 | 193 | [Personal Access Token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token 194 | [user owned repos]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/permission-levels-for-a-user-account-repository 195 | [org owned repos]: https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization 196 | [Inputs]: #inputs 197 | [legacy dependaBot v1 config format]: https://dependabot.com/docs/config-file/#automerged_updates 198 | 199 | ---- 200 | > Author: [Ahmad Nassri](https://www.ahmadnassri.com/) • 201 | > Twitter: [@AhmadNassri](https://twitter.com/AhmadNassri) 202 | 203 | [license-url]: LICENSE 204 | [license-img]: https://badgen.net/github/license/ahmadnassri/action-dependabot-auto-merge 205 | 206 | [release-url]: https://github.com/ahmadnassri/action-dependabot-auto-merge/releases 207 | [release-img]: https://badgen.net/github/release/ahmadnassri/action-dependabot-auto-merge 208 | --------------------------------------------------------------------------------