├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── check-linked-issues.yml
│ ├── ci.yml
│ ├── notify-release.yml
│ └── release.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .nvmrc
├── LICENSE
├── README.md
├── action.yml
├── dist
└── index.js
├── eslint.config.js
├── package-lock.json
├── package.json
├── src
├── action.js
├── getDependabotDetails.js
├── github-client.js
├── index.js
├── log.js
├── mapUpdateType.js
├── util.js
└── verifyCommitSignatures.js
└── test
├── action.test.js
├── github-client.test.js
├── log.test.js
├── mapUpdateType.test.js
├── util.test.js
└── verifyCommitSignatures.test.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behavior to automatically convert line endings
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | open-pull-requests-limit: 10
8 |
9 | - package-ecosystem: "npm"
10 | directory: "/"
11 | schedule:
12 | interval: "monthly"
13 | open-pull-requests-limit: 10
14 |
--------------------------------------------------------------------------------
/.github/workflows/check-linked-issues.yml:
--------------------------------------------------------------------------------
1 | name: Check linked issues
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, edited, reopened, synchronize]
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | check_pull_requests:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | issues: read
15 | pull-requests: write
16 | name: Check linked issues
17 | steps:
18 | - uses: nearform-actions/github-action-check-linked-issues@7140e2e01aa0c18b8ac61bddf5e89abde1ca760e # v1.8.3
19 | id: check-linked-issues
20 | with:
21 | github-token: ${{ github.token }}
22 | exclude-branches: "release/**, dependabot/**"
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v5
18 | with:
19 | persist-credentials: false
20 | - uses: actions/setup-node@v5
21 | with:
22 | check-latest: true
23 | node-version-file: '.nvmrc'
24 | - run: npm ci --ignore-scripts
25 | - run: npm run lint
26 | - run: npm run test
27 |
28 | automerge:
29 | needs: build
30 | runs-on: ubuntu-latest
31 | permissions:
32 | pull-requests: write
33 | contents: write
34 | steps:
35 | - uses: fastify/github-action-merge-dependabot@main
36 | with:
37 | github-token: ${{ github.token }}
38 | target: minor
39 |
--------------------------------------------------------------------------------
/.github/workflows/notify-release.yml:
--------------------------------------------------------------------------------
1 | name: notify-release
2 | on:
3 | workflow_dispatch:
4 | release:
5 | types: [published]
6 | schedule:
7 | - cron: '30 8 * * *'
8 | permissions:
9 | contents: read
10 | jobs:
11 | setup:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | issues: write
15 | contents: read
16 | steps:
17 | - name: Notify release
18 | uses: nearform-actions/github-action-notify-release@1a1c03a2b8c9a41a7a88f9a8de05b2b6582f0966 # v1.12.2
19 | with:
20 | github-token: ${{ github.token }}
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | semver:
7 | description: "The semver to use"
8 | required: true
9 | default: "patch"
10 | type: choice
11 | options:
12 | - patch
13 | - minor
14 | - major
15 | pull_request:
16 | types: [closed]
17 |
18 | permissions:
19 | contents: read
20 |
21 | jobs:
22 | release:
23 | runs-on: ubuntu-latest
24 | permissions:
25 | contents: write
26 | issues: write
27 | pull-requests: write
28 | steps:
29 | - name: Setting action build runtime
30 | uses: actions/setup-node@v5
31 | with:
32 | check-latest: true
33 | node-version: 20
34 | - uses: nearform-actions/optic-release-automation-action@a2785548e8bdafab7e628c2317b604278e460434 # v4.12.2
35 | with:
36 | github-token: ${{ github.token }}
37 | semver: ${{ github.event.inputs.semver }}
38 | sync-semver-tags: true
39 | build-command: |
40 | npm ci
41 | npm run build
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules/
3 | .idea
4 | .tap/
5 | coverage
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run build && git add dist
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-scripts=true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016-present The Fastify team
4 |
5 | The Fastify team members are listed at https://github.com/fastify/fastify#team.
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Github Action Merge Dependabot
2 |
3 | This action automatically approves and merges dependabot PRs.
4 |
5 | ## Usage
6 |
7 | Configure this action in your workflows providing the inputs described below.
8 | Note that this action requires a GitHub token with additional permissions. You must use the [`permissions`](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions) tag to specify the required rules or configure your [GitHub account](https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/).
9 |
10 | The permissions required are:
11 |
12 | - [`pull-requests`](https://docs.github.com/en/rest/overview/permissions-required-for-github-apps?apiVersion=2022-11-28#pull-requests): it is needed to approve PRs.
13 | - [`contents`](https://docs.github.com/en/rest/overview/permissions-required-for-github-apps?apiVersion=2022-11-28#contents): it is necessary to merge the pull request. You don't need it if you set `approve-only: true`, see [Approving without merging](#approving-without-merging) example below.
14 |
15 | If some of the required permissions are missing, the action will fail with the error message:
16 |
17 | ```
18 | Error: Resource not accessible by integration
19 | ```
20 |
21 | ## Inputs
22 |
23 | | input | required | default | description |
24 | | -------------------------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25 | | `github-token` | No | `${{github.token}}` | A GitHub token. |
26 | | `exclude` | No | | A comma or semicolon separated value of packages that you don't want to auto-merge and would like to manually review to decide whether to upgrade or not. |
27 | | `approve-only` | No | `false` | If `true`, the PR is only approved but not merged. |
28 | | `merge-method` | No | `squash` | The merge method you would like to use (squash, merge, rebase). |
29 | | `merge-comment` | No | `''` | An arbitrary message that you'd like to comment on the PR after it gets auto-merged. This is only useful when you're receiving too much of noise in email and would like to filter mails for PRs that got automatically merged. |
30 | | `use-github-auto-merge` | No | `false` | If `true`, the PR is marked as auto-merge and will be merged by GitHub when status checks are satisfied.
_NOTE_: This feature only works when all of the following conditions are met.
- The repository enables auto-merge.
- The pull request base must have a branch protection rule.
- The pull request's status checks are not yet satisfied.
Refer to [the official document](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request) about GitHub auto-merge. |
31 | | `target` | No | `any` | A flag to only auto-merge updates based on Semantic Versioning.
Possible options are: `major, premajor, minor, preminor, patch, prepatch, prerelease, any`.
The value of this flag allows for updates for all the matching versions **and lower** with the respect for priority. This means, for example, if the `target` is set to `major` and the update is made to `minor` version the auto-merge will be triggered.
For more details on how semantic version difference is calculated please see [semver](https://www.npmjs.com/package/semver) package.
If you set a value other than `any`, PRs that are not semantic version compliant are skipped. An example of a non-semantic version is a commit hash when using git submodules. |
32 | | `target-development` | No | | Same as `target` but specifies semver for `development` dependencies only. If present, then it overrides the value in `target` for `development` dependencies. |
33 | | `target-production` | No | | Same as `target` but specifies semver for `production` dependencies only. If present, then it overrides the value in `target` for `production` dependencies. |
34 | | `target-indirect` | No | | Same as `target` but specifies semver for indirect dependency updates only. If present, then it overrides the value in `target` for indirect dependency updates. |
35 | | `pr-number` | No | | A pull request number, only required if triggered from a workflow_dispatch event. Typically this would be triggered by a script running in a separate CI provider. See [Trigger action from workflow_dispatch event](#trigger-action-from-workflow_dispatch-event) example. |
36 | | `skip-commit-verification` | No | `false` | If `true`, then the action will not expect the commits to have a verification signature. It is required to set this to `true` in GitHub Enterprise Server. |
37 | | `skip-verification` | No | `false` | If true, the action will not validate the user or the commit verification status |
38 |
39 | ## Output
40 |
41 | | outputs | Description |
42 | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43 | | merge_status | The result status of the merge. It can be one of the following: `approved`, `merged`, `auto_merge`, `merge_failed`, `skipped:commit_verification_failed`, `skipped:not_a_dependabot_pr`, `skipped:cannot_update_major`, `skipped:bump_higher_than_target`, `skipped:packaged_excluded` |
44 |
45 | ## Examples
46 |
47 | ### Basic example
48 |
49 | ```yml
50 | name: CI
51 | on:
52 | push:
53 | branches:
54 | - main
55 | pull_request:
56 |
57 | jobs:
58 | build:
59 | runs-on: ubuntu-latest
60 | steps:
61 | # ...
62 |
63 | automerge:
64 | needs: build
65 | runs-on: ubuntu-latest
66 |
67 | permissions:
68 | pull-requests: write
69 | contents: write
70 |
71 | steps:
72 | - uses: fastify/github-action-merge-dependabot@v3
73 | ```
74 |
75 | ### Excluding packages
76 |
77 | ```yml
78 | permissions:
79 | pull-requests: write
80 | contents: write
81 |
82 | steps:
83 | - uses: fastify/github-action-merge-dependabot@v3
84 | with:
85 | exclude: 'react,fastify'
86 | ```
87 |
88 | ### Approving without merging
89 |
90 | ```yml
91 | permissions:
92 | pull-requests: write
93 | steps:
94 | - uses: fastify/github-action-merge-dependabot@v3
95 | with:
96 | approve-only: true
97 | ```
98 |
99 | ### Specifying target versions
100 |
101 | #### Specifying target for all packages
102 |
103 | ```yml
104 | steps:
105 | - uses: fastify/github-action-merge-dependabot@v3
106 | with:
107 | target: 'minor'
108 | ```
109 |
110 | #### Specifying target for development and production packages separately
111 |
112 | ```yml
113 | steps:
114 | - uses: fastify/github-action-merge-dependabot@v3
115 | with:
116 | target-development: 'major'
117 | target-production: 'minor'
118 | ```
119 |
120 | ### Trigger action from workflow_dispatch event
121 |
122 | If you need to trigger this action manually, you can use the [`workflow_dispatch`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch) event. A use case might be that your CI runs on a seperate provider, so you would like to run this action as a result of a successful CI run.
123 |
124 | When using the `workflow_dispatch` approach, you will need to send the PR number as part of the input for this action:
125 |
126 | ```yml
127 | name: automerge
128 |
129 | on:
130 | workflow_dispatch:
131 | inputs:
132 | pr-number:
133 | required: true
134 |
135 | jobs:
136 | automerge:
137 | runs-on: ubuntu-latest
138 | permissions:
139 | pull-requests: write
140 | contents: write
141 | steps:
142 | - uses: fastify/github-action-merge-dependabot@v3
143 | with:
144 | pr-number: ${{ github.event.inputs.pr-number }}
145 | ```
146 |
147 | You can initiate a call to trigger this event via [API](https://docs.github.com/en/rest/reference/actions/#create-a-workflow-dispatch-event):
148 |
149 | ```bash
150 | # Note: replace dynamic values with your relevant data
151 | curl -X POST \
152 | -H "Accept: application/vnd.github.v3+json" \
153 | -H "Authorization: token {token}" \
154 | https://api.github.com/repos/{owner}/{reponame}/actions/workflows/{workflow}/dispatches \
155 | -d '{"ref":"{ref}", "inputs":{ "pr-number": "{number}"}}'
156 | ```
157 |
158 | ## How to upgrade from `2.x` to new `3.x`
159 |
160 | - Update the action version.
161 | - Add the new `permissions` configuration into your workflow or, instead, you can set the permissions rules on [the repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#setting-the-permissions-of-the-github_token-for-your-repository) or on [the organization](https://docs.github.com/en/enterprise-server@3.3/admin/policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise#enforcing-a-policy-for-workflow-permissions-in-your-enterprise).
162 | - Uninstall the [dependabot-merge-action](https://github.com/apps/dependabot-merge-action) GitHub App from your repos/orgs.
163 | - If you have customized the `api-url` you can:
164 | - Remove the `api-url` option from your workflow.
165 | - Turn off the [`dependabot-merge-action-app`](https://github.com/fastify/dependabot-merge-action-app/) application.
166 |
167 | Migration example:
168 |
169 | ```diff
170 | jobs:
171 | build:
172 | runs-on: ubuntu-latest
173 | steps:
174 | # ...
175 |
176 | automerge:
177 | needs: build
178 | runs-on: ubuntu-latest
179 | + permissions:
180 | + pull-requests: write
181 | + contents: write
182 | steps:
183 | - - uses: fastify/github-action-merge-dependabot@v2.1.1
184 | + - uses: fastify/github-action-merge-dependabot@v3
185 | ```
186 |
187 | ## Notes
188 |
189 | - A GitHub token is automatically provided by Github Actions, which can be accessed using `github.token`. If you want to provide a token that's not the default one you can used the `github-token` input.
190 | - Make sure to use `needs: ` to delay the auto-merging until CI checks (test/build) are passed.
191 | - If you want to use GitHub's [auto-merge](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/automatically-merging-a-pull-request) feature but still use this action to approve Pull Requests without merging, use `approve-only: true`.
192 |
193 | ## Acknowledgements
194 |
195 | This project is kindly sponsored by [NearForm](https://nearform.com)
196 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Github Action Merge Dependabot'
2 | description: 'Automatically approve and merge dependabot PRs'
3 |
4 | inputs:
5 | github-token:
6 | description: 'A GitHub token'
7 | required: false
8 | default: ${{ github.token }}
9 | exclude:
10 | description: 'Packages that you want to manually review before upgrading'
11 | required: false
12 | approve-only:
13 | description: 'If true, the PR is only approved but not merged'
14 | required: false
15 | default: false
16 | merge-method:
17 | description: 'The merge method you would like to use (squash, merge, rebase)'
18 | required: false
19 | default: 'squash'
20 | merge-comment:
21 | description: "An arbitrary message that you'd like to comment on the PR after it gets auto-merged"
22 | required: false
23 | default: ''
24 | use-github-auto-merge:
25 | description: 'If true, the PR is only marked as auto-merge enabled'
26 | required: false
27 | default: false
28 | target:
29 | description: 'Auto-merge on updates based on Semantic Versioning'
30 | required: false
31 | default: 'any'
32 | target-development:
33 | description: 'Auto-merge on updates based on Semantic Versioning for development dependencies'
34 | required: false
35 | target-production:
36 | description: 'Auto-merge on updates based on Semantic Versioning for production dependencies'
37 | required: false
38 | target-indirect:
39 | description: 'Auto-merge on updates based on Semantic Versioning for indirect updates'
40 | required: false
41 | pr-number:
42 | description: 'A pull request number, only required if triggered from a workflow_dispatch event'
43 | required: false
44 | skip-commit-verification:
45 | description: 'If true, then the action will not expect the commits to have a verification signature. It is required to set this to true in GitHub Enterprise Server'
46 | required: false
47 | default: false
48 | skip-verification:
49 | type: boolean
50 | description: 'If true, the action will not validate the user or the commit verification status'
51 | default: false
52 |
53 | outputs:
54 | merge_status:
55 | description: 'Merge status'
56 | value: ${{ steps.approver.outputs.merge_status }}
57 |
58 | runs:
59 | using: 'composite'
60 | steps:
61 | - name: Fetch metadata
62 | id: dependabot-metadata
63 | uses: dependabot/fetch-metadata@v2
64 | if: github.event_name == 'pull_request' && (github.actor == 'dependabot[bot]' || inputs.skip-verification == 'true')
65 | with:
66 | skip-commit-verification: ${{ inputs.skip-commit-verification }}
67 | skip-verification: ${{ inputs.skip-verification }}
68 | - name: Merge/approve PR
69 | uses: actions/github-script@v8
70 | id: approver
71 | env:
72 | ACTION_PATH: ${{ github.action_path }}
73 | UPDATE_TYPE: ${{ steps.dependabot-metadata.outputs.update-type }}
74 | DEPENDENCY_TYPE: ${{ steps.dependabot-metadata.outputs.dependency-type }}
75 | DEPENDENCY_NAMES: ${{ steps.dependabot-metadata.outputs.dependency-names }}
76 | with:
77 | github-token: ${{ inputs.github-token }}
78 | script: |
79 | const { ACTION_PATH, UPDATE_TYPE, DEPENDENCY_TYPE, DEPENDENCY_NAMES } = process.env
80 | const script = require(ACTION_PATH + '/dist/index.js')
81 | await script({
82 | github,
83 | context,
84 | inputs: ${{ toJSON(inputs) }},
85 | dependabotMetadata: {
86 | updateType: UPDATE_TYPE,
87 | dependencyType: DEPENDENCY_TYPE,
88 | dependencyNames: DEPENDENCY_NAMES,
89 | }
90 | })
91 |
92 | branding:
93 | icon: 'git-pull-request'
94 | color: 'gray-dark'
95 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = require('neostandard')({
4 | semi: false,
5 | ignores: ['dist', 'node_modules'],
6 | env: ['es2015', 'node'],
7 | })
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-action-merge-dependabot",
3 | "version": "3.11.2",
4 | "description": "A GitHub action to automatically merge and approve Dependabot pull requests",
5 | "main": "src/index.js",
6 | "type": "commonjs",
7 | "scripts": {
8 | "build": "ncc build src/index.js",
9 | "lint": "eslint .",
10 | "lint:fix": "eslint . --fix",
11 | "test": "c8 --100 node --test",
12 | "prepare": "husky install"
13 | },
14 | "author": {
15 | "name": "Salman Mitha",
16 | "email": "SalmanMitha@gmail.com"
17 | },
18 | "contributors": [
19 | "Simone Busoli "
20 | ],
21 | "license": "MIT",
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/fastify/github-action-merge-dependabot.git"
25 | },
26 | "bugs": {
27 | "url": "https://github.com/fastify/github-action-merge-dependabot/issues"
28 | },
29 | "homepage": "https://github.com/fastify/github-action-merge-dependabot#readme",
30 | "dependencies": {
31 | "@actions/core": "^1.11.1",
32 | "actions-toolkit": "github:nearform/actions-toolkit"
33 | },
34 | "devDependencies": {
35 | "@vercel/ncc": "^0.38.4",
36 | "c8": "^10.1.3",
37 | "eslint": "^9.36.0",
38 | "husky": "^9.1.7",
39 | "neostandard": "^0.12.2",
40 | "proxyquire": "^2.1.3",
41 | "sinon": "^21.0.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/action.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const core = require('@actions/core')
4 | const toolkit = require('actions-toolkit')
5 |
6 | const packageInfo = require('../package.json')
7 | const { githubClient } = require('./github-client')
8 | const { logInfo, logWarning, logError } = require('./log')
9 | const {
10 | MERGE_STATUS,
11 | MERGE_STATUS_KEY,
12 | getInputs,
13 | parseCommaOrSemicolonSeparatedValue,
14 | getTarget,
15 | } = require('./util')
16 | const { verifyCommits } = require('./verifyCommitSignatures')
17 | const { dependabotAuthor } = require('./getDependabotDetails')
18 | const { updateTypes } = require('./mapUpdateType')
19 | const { updateTypesPriority } = require('./mapUpdateType')
20 |
21 | module.exports = async function run ({
22 | github,
23 | context,
24 | inputs,
25 | dependabotMetadata,
26 | }) {
27 | const { updateType } = dependabotMetadata
28 | const dependencyNames = parseCommaOrSemicolonSeparatedValue(
29 | dependabotMetadata.dependencyNames
30 | )
31 |
32 | const {
33 | MERGE_METHOD,
34 | EXCLUDE_PKGS,
35 | MERGE_COMMENT,
36 | APPROVE_ONLY,
37 | USE_GITHUB_AUTO_MERGE,
38 | TARGET,
39 | TARGET_DEV,
40 | TARGET_PROD,
41 | TARGET_INDIRECT,
42 | PR_NUMBER,
43 | SKIP_COMMIT_VERIFICATION,
44 | SKIP_VERIFICATION,
45 | } = getInputs(inputs)
46 |
47 | try {
48 | toolkit.logActionRefWarning()
49 |
50 | const PULLREQUEST = context.payload.pull_request
51 | if (!PULLREQUEST && !PR_NUMBER) {
52 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedNotADependabotPr)
53 | return logError(
54 | 'This action must be used in the context of a Pull Request or with a Pull Request number'
55 | )
56 | }
57 |
58 | const client = githubClient(github, context)
59 | const pr = PULLREQUEST || (await client.getPullRequest(PR_NUMBER))
60 |
61 | const isDependabotPR = pr.user.login === dependabotAuthor
62 | if (!SKIP_VERIFICATION && !isDependabotPR) {
63 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedNotADependabotPr)
64 | return logWarning('Not a dependabot PR, skipping.')
65 | }
66 |
67 | const commits = await client.getPullRequestCommits(pr.number)
68 | if (
69 | !SKIP_VERIFICATION &&
70 | !commits.every(commit => commit.author?.login === dependabotAuthor)
71 | ) {
72 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedNotADependabotPr)
73 | return logWarning('PR contains non dependabot commits, skipping.')
74 | }
75 |
76 | if (!SKIP_COMMIT_VERIFICATION && !SKIP_VERIFICATION) {
77 | try {
78 | verifyCommits(commits)
79 | } catch {
80 | core.setOutput(
81 | MERGE_STATUS_KEY,
82 | MERGE_STATUS.skippedCommitVerificationFailed
83 | )
84 | return logWarning(
85 | 'PR contains invalid dependabot commit signatures, skipping.'
86 | )
87 | }
88 | }
89 |
90 | const target = getTarget(
91 | { TARGET, TARGET_DEV, TARGET_PROD, TARGET_INDIRECT },
92 | dependabotMetadata
93 | )
94 |
95 | if (
96 | target !== updateTypes.any &&
97 | updateTypesPriority.indexOf(updateType) < 0
98 | ) {
99 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedInvalidVersion)
100 | logWarning(`Semver bump '${updateType}' is invalid!`)
101 | return
102 | }
103 |
104 | if (
105 | target !== updateTypes.any &&
106 | updateTypesPriority.indexOf(updateType) >
107 | updateTypesPriority.indexOf(target)
108 | ) {
109 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedBumpHigherThanTarget)
110 | logWarning(`Semver bump is higher than allowed in TARGET.
111 | Tried to do a '${updateType}' update but the max allowed is '${target}'`)
112 | return
113 | }
114 |
115 | const changedExcludedPackages = EXCLUDE_PKGS.filter(
116 | pkg => dependencyNames.indexOf(pkg) > -1
117 | )
118 |
119 | // TODO: Improve error message for excluded packages?
120 | if (changedExcludedPackages.length > 0) {
121 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedPackageExcluded)
122 | return logInfo(`${changedExcludedPackages.length} package(s) excluded: \
123 | ${changedExcludedPackages.join(', ')}. Skipping.`)
124 | }
125 |
126 | if (
127 | dependencyNames.indexOf(packageInfo.name) > -1 &&
128 | updateType === updateTypes.major
129 | ) {
130 | const upgradeMessage = `Cannot automerge ${packageInfo.name} major release.
131 | Read how to upgrade it manually:
132 | https://github.com/fastify/${packageInfo.name}#how-to-upgrade-from-2x-to-new-3x`
133 |
134 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.skippedCannotUpdateMajor)
135 | core.setFailed(upgradeMessage)
136 | return
137 | }
138 |
139 | await client.approvePullRequest(pr.number, MERGE_COMMENT)
140 | if (APPROVE_ONLY) {
141 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.approved)
142 | return logInfo(
143 | 'APPROVE_ONLY set, PR was approved but it will not be merged'
144 | )
145 | }
146 |
147 | if (USE_GITHUB_AUTO_MERGE) {
148 | await client.enableAutoMergePullRequest(pr.node_id, MERGE_METHOD)
149 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.autoMerge)
150 | return logInfo('USE_GITHUB_AUTO_MERGE set, PR was marked as auto-merge')
151 | }
152 |
153 | await client.mergePullRequest(pr.number, MERGE_METHOD)
154 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.merged)
155 | logInfo('Dependabot merge completed')
156 | } catch (error) {
157 | core.setFailed(error.message)
158 | core.setOutput(MERGE_STATUS_KEY, MERGE_STATUS.mergeFailed)
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/getDependabotDetails.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const dependabotAuthor = 'dependabot[bot]'
3 |
4 | const dependabotCommitter = 'GitHub'
5 |
6 | module.exports = {
7 | dependabotAuthor,
8 | dependabotCommitter,
9 | }
10 |
--------------------------------------------------------------------------------
/src/github-client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function githubClient (github, context) {
4 | const payload = context.payload
5 |
6 | const repo = payload.repository
7 | const owner = repo.owner.login
8 | const repoName = repo.name
9 |
10 | return {
11 | async getPullRequest (pullRequestNumber) {
12 | const { data: pullRequest } = await github.rest.pulls.get({
13 | owner,
14 | repo: repoName,
15 | pull_number: pullRequestNumber,
16 | })
17 | return pullRequest
18 | },
19 |
20 | async approvePullRequest (pullRequestNumber, approveComment) {
21 | const { data } = await github.rest.pulls.createReview({
22 | owner,
23 | repo: repoName,
24 | pull_number: pullRequestNumber,
25 | event: 'APPROVE',
26 | body: approveComment,
27 | })
28 | // todo assert
29 | return data
30 | },
31 |
32 | async mergePullRequest (pullRequestNumber, mergeMethod) {
33 | const { data } = await github.rest.pulls.merge({
34 | owner,
35 | repo: repoName,
36 | pull_number: pullRequestNumber,
37 | merge_method: mergeMethod,
38 | })
39 | // todo assert
40 | return data
41 | },
42 |
43 | async enableAutoMergePullRequest (pullRequestId, mergeMethod) {
44 | const query = `
45 | mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
46 | enablePullRequestAutoMerge(
47 | input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }
48 | ) {
49 | pullRequest {
50 | autoMergeRequest {
51 | enabledAt
52 | enabledBy {
53 | login
54 | }
55 | }
56 | }
57 | }
58 | }
59 | `
60 | const { data } = await github.graphql(query, {
61 | pullRequestId,
62 | mergeMethod: mergeMethod.toUpperCase(),
63 | })
64 | return data
65 | },
66 |
67 | async getPullRequestDiff (pullRequestNumber) {
68 | const { data: pullRequest } = await github.rest.pulls.get({
69 | owner,
70 | repo: repoName,
71 | pull_number: pullRequestNumber,
72 | mediaType: {
73 | format: 'diff',
74 | },
75 | })
76 | return pullRequest
77 | },
78 |
79 | async getPullRequestCommits (pullRequestNumber) {
80 | const { data } = await github.rest.pulls.listCommits({
81 | owner,
82 | repo: repoName,
83 | pull_number: pullRequestNumber,
84 | })
85 |
86 | return data
87 | },
88 | }
89 | }
90 |
91 | module.exports = { githubClient }
92 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const run = require('./action')
4 |
5 | module.exports = run
6 |
--------------------------------------------------------------------------------
/src/log.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { debug, error, info, warning } = require('@actions/core')
4 |
5 | const stringify = msg =>
6 | typeof msg === 'string' ? msg : msg.stack || msg.toString()
7 |
8 | const log = logger => message => logger(stringify(message))
9 |
10 | exports.logDebug = log(debug)
11 | exports.logError = log(error)
12 | exports.logInfo = log(info)
13 | exports.logWarning = log(warning)
14 |
--------------------------------------------------------------------------------
/src/mapUpdateType.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // NOTE: fetch-metadata only support the major, minor and patch update types for now, so I removed the `pre` types
4 | const updateTypes = {
5 | major: 'version-update:semver-major',
6 | minor: 'version-update:semver-minor',
7 | patch: 'version-update:semver-patch',
8 | any: 'version-update:semver-any',
9 | }
10 |
11 | const updateTypesPriority = [
12 | updateTypes.patch,
13 | updateTypes.minor,
14 | updateTypes.major,
15 | updateTypes.any,
16 | ]
17 |
18 | const mapUpdateType = input => {
19 | return updateTypes[input] || updateTypes.any
20 | }
21 |
22 | module.exports = {
23 | mapUpdateType,
24 | updateTypes,
25 | updateTypesPriority,
26 | }
27 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { mapUpdateType } = require('./mapUpdateType')
4 | const { logWarning } = require('./log')
5 |
6 | const mergeMethods = {
7 | merge: 'merge',
8 | squash: 'squash',
9 | rebase: 'rebase',
10 | }
11 |
12 | const getMergeMethod = inputs => {
13 | const input = inputs['merge-method']
14 |
15 | if (!input) {
16 | return mergeMethods.squash
17 | }
18 |
19 | if (!mergeMethods[input]) {
20 | logWarning(
21 | 'merge-method input is ignored because it is malformed, defaulting to `squash`.'
22 | )
23 | return mergeMethods.squash
24 | }
25 |
26 | return mergeMethods[input]
27 | }
28 |
29 | const parseCommaOrSemicolonSeparatedValue = value => {
30 | return value ? value.split(/[;,]/).map(el => el.trim()) : []
31 | }
32 |
33 | exports.parseCommaOrSemicolonSeparatedValue =
34 | parseCommaOrSemicolonSeparatedValue
35 |
36 | exports.getInputs = inputs => {
37 | if (!inputs) {
38 | throw new Error('Invalid inputs object passed to getInputs')
39 | }
40 |
41 | return {
42 | MERGE_METHOD: getMergeMethod(inputs),
43 | EXCLUDE_PKGS: parseCommaOrSemicolonSeparatedValue(inputs['exclude']),
44 | MERGE_COMMENT: inputs['merge-comment'] || '',
45 | APPROVE_ONLY: /true/i.test(inputs['approve-only']),
46 | USE_GITHUB_AUTO_MERGE: /true/i.test(inputs['use-github-auto-merge']),
47 | TARGET: mapUpdateType(inputs['target']),
48 | TARGET_DEV:
49 | inputs['target-development'] &&
50 | mapUpdateType(inputs['target-development']),
51 | TARGET_PROD:
52 | inputs['target-production'] && mapUpdateType(inputs['target-production']),
53 | TARGET_INDIRECT:
54 | inputs['target-indirect'] && mapUpdateType(inputs['target-indirect']),
55 | PR_NUMBER: inputs['pr-number'],
56 | SKIP_COMMIT_VERIFICATION: /true/i.test(inputs['skip-commit-verification']),
57 | SKIP_VERIFICATION: /true/i.test(inputs['skip-verification']),
58 | }
59 | }
60 |
61 | exports.getTarget = (
62 | { TARGET, TARGET_DEV, TARGET_PROD, TARGET_INDIRECT },
63 | { dependencyType }
64 | ) => {
65 | if (dependencyType === 'direct:development' && TARGET_DEV) {
66 | return TARGET_DEV
67 | }
68 | if (dependencyType === 'direct:production' && TARGET_PROD) {
69 | return TARGET_PROD
70 | }
71 | if (dependencyType === 'indirect' && TARGET_INDIRECT) {
72 | return TARGET_INDIRECT
73 | }
74 | return TARGET
75 | }
76 |
77 | exports.MERGE_STATUS = {
78 | approved: 'approved',
79 | merged: 'merged',
80 | autoMerge: 'auto_merge',
81 | mergeFailed: 'merge_failed',
82 | skippedCommitVerificationFailed: 'skipped:commit_verification_failed',
83 | skippedNotADependabotPr: 'skipped:not_a_dependabot_pr',
84 | skippedCannotUpdateMajor: 'skipped:cannot_update_major',
85 | skippedBumpHigherThanTarget: 'skipped:bump_higher_than_target',
86 | skippedPackageExcluded: 'skipped:packaged_excluded',
87 | skippedInvalidVersion: 'skipped:invalid_semver',
88 | }
89 |
90 | exports.MERGE_STATUS_KEY = 'merge_status'
91 |
--------------------------------------------------------------------------------
/src/verifyCommitSignatures.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const {
4 | dependabotAuthor,
5 | dependabotCommitter,
6 | } = require('./getDependabotDetails')
7 |
8 | function verifyCommits (commits) {
9 | commits.forEach(function (commit) {
10 | const {
11 | commit: {
12 | verification: { verified },
13 | committer,
14 | author,
15 | },
16 | sha,
17 | } = commit
18 | verifyCommitSignatureCommitterAndAuthor(sha, author, committer, verified)
19 | })
20 | }
21 |
22 | function verifyCommitSignatureCommitterAndAuthor (
23 | sha,
24 | author,
25 | committer,
26 | verified
27 | ) {
28 | if (
29 | !verified ||
30 | committer.name !== dependabotCommitter ||
31 | author.name !== dependabotAuthor
32 | ) {
33 | throw new Error(
34 | `Signature for commit ${sha} could not be verified - Not a dependabot commit`
35 | )
36 | }
37 | }
38 |
39 | module.exports = {
40 | verifyCommits,
41 | verifyCommitSignatureCommitterAndAuthor,
42 | }
43 |
--------------------------------------------------------------------------------
/test/action.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test, afterEach } = require('node:test')
4 | const proxyquire = require('proxyquire')
5 | const sinon = require('sinon')
6 |
7 | // Stubbing modules
8 | const core = require('@actions/core')
9 | const toolkit = require('actions-toolkit')
10 |
11 | const actionLog = require('../src/log')
12 | const actionGithubClient = require('../src/github-client')
13 | const verifyCommits = require('../src/verifyCommitSignatures')
14 | const { updateTypes } = require('../src/mapUpdateType')
15 | const { MERGE_STATUS, MERGE_STATUS_KEY } = require('../src/util')
16 |
17 | const BOT_NAME = 'dependabot[bot]'
18 |
19 | // TODO: Share this stubs with github-client test
20 | const data = 'octokit-result'
21 | const octokitStubs = {
22 | get: sinon.stub().returns(Promise.resolve({ data })),
23 | createReview: sinon.stub().returns(Promise.resolve({ data })),
24 | merge: sinon.stub().returns(Promise.resolve({ data })),
25 | listCommits: sinon.stub().returns(Promise.resolve({ data })),
26 | }
27 |
28 | const githubStub = {
29 | getOctokit: () => ({
30 | rest: {
31 | pulls: octokitStubs,
32 | },
33 | }),
34 | }
35 |
36 | const createDependabotMetadata = (props = {}) => ({
37 | updateType: 'update-version:semver-minor',
38 | dependencyType: 'direct:development',
39 | dependencyNames: 'react',
40 | ...props,
41 | })
42 |
43 | function buildStubbedAction ({ payload, inputs, dependabotMetadata }) {
44 | const coreStub = sinon.stub(core)
45 | const toolkitStub = sinon
46 | .stub(toolkit, 'logActionRefWarning')
47 | .get(() => sinon.stub())
48 |
49 | const contextStub = {
50 | payload: {
51 | pull_request: {
52 | user: {
53 | login: 'pr-user-login',
54 | },
55 | },
56 | repository: {
57 | owner: {
58 | login: 'owner-login.',
59 | },
60 | name: 'repository-name',
61 | },
62 | },
63 | ...{ payload },
64 | }
65 |
66 | const logStub = sinon.stub(actionLog)
67 | const prStub = sinon.stub()
68 | const prDiffStub = sinon.stub()
69 | const prCommitsStub = sinon.stub()
70 | const approveStub = sinon.stub()
71 | const mergeStub = sinon.stub()
72 | const enableAutoMergeStub = sinon.stub()
73 |
74 | const clientStub = sinon.stub(actionGithubClient, 'githubClient').returns({
75 | getPullRequest: prStub.resolves(),
76 | approvePullRequest: approveStub.resolves(),
77 | mergePullRequest: mergeStub.resolves(),
78 | enableAutoMergePullRequest: enableAutoMergeStub.resolves(),
79 | getPullRequestDiff: prDiffStub.resolves(),
80 | getPullRequestCommits: prCommitsStub.resolves([]),
81 | })
82 |
83 | const verifyCommitsStub = sinon.stub(verifyCommits, 'verifyCommits')
84 |
85 | const action = proxyquire('../src/action', {
86 | '@actions/core': coreStub,
87 | 'actions-toolkit': toolkitStub,
88 | './log': logStub,
89 | './github-client': clientStub,
90 | })
91 | return {
92 | action: props =>
93 | action({
94 | github: githubStub,
95 | context: contextStub,
96 | inputs: inputs || {},
97 | dependabotMetadata: createDependabotMetadata(dependabotMetadata),
98 | ...props,
99 | }),
100 | stubs: {
101 | coreStub,
102 | githubStub,
103 | logStub,
104 | prStub,
105 | approveStub,
106 | mergeStub,
107 | enableAutoMergeStub,
108 | prCommitsStub,
109 | verifyCommitsStub,
110 | },
111 | }
112 | }
113 |
114 | afterEach(() => {
115 | sinon.restore()
116 | })
117 |
118 | test('should not run if a pull request number is missing', async () => {
119 | const { action, stubs } = buildStubbedAction({
120 | payload: { issue: {} },
121 | })
122 | await action()
123 | sinon.assert.notCalled(stubs.approveStub)
124 | sinon.assert.notCalled(stubs.mergeStub)
125 |
126 | sinon.assert.calledWith(
127 | stubs.coreStub.setOutput,
128 | MERGE_STATUS_KEY,
129 | MERGE_STATUS.skippedNotADependabotPr
130 | )
131 | })
132 |
133 | test(
134 | 'should retrieve PR info when trigger by non pull_request events',
135 | async () => {
136 | const PR_NUMBER = Math.random()
137 | const { action, stubs } = buildStubbedAction({
138 | payload: { 'not a pull_request': {} },
139 | inputs: { 'pr-number': PR_NUMBER },
140 | })
141 |
142 | await action()
143 |
144 | sinon.assert.calledOnce(stubs.prStub)
145 | }
146 | )
147 |
148 | test('should skip non-dependabot PR', async () => {
149 | const PR_NUMBER = Math.random()
150 | const { action, stubs } = buildStubbedAction({
151 | payload: { issue: {} },
152 | inputs: { 'pr-number': PR_NUMBER },
153 | })
154 |
155 | stubs.prStub.resolves({
156 | user: {
157 | login: 'not dependabot',
158 | },
159 | })
160 |
161 | await action()
162 |
163 | sinon.assert.calledOnce(stubs.prStub)
164 | sinon.assert.calledWithExactly(
165 | stubs.logStub.logWarning,
166 | 'Not a dependabot PR, skipping.'
167 | )
168 | sinon.assert.notCalled(stubs.approveStub)
169 | sinon.assert.notCalled(stubs.mergeStub)
170 | sinon.assert.calledWith(
171 | stubs.coreStub.setOutput,
172 | MERGE_STATUS_KEY,
173 | MERGE_STATUS.skippedNotADependabotPr
174 | )
175 | })
176 |
177 | const prCommitsStubs = [
178 | {
179 | author: {
180 | login: 'not dependabot',
181 | },
182 | },
183 | {
184 | author: undefined,
185 | },
186 | ]
187 |
188 | for (const prCommitsStub of prCommitsStubs) {
189 | test('should skip PR with non dependabot commit', async () => {
190 | const PR_NUMBER = Math.random()
191 | const { action, stubs } = buildStubbedAction({
192 | payload: {
193 | pull_request: {
194 | user: {
195 | login: BOT_NAME,
196 | },
197 | number: PR_NUMBER,
198 | },
199 | },
200 | })
201 |
202 | stubs.prCommitsStub.resolves([prCommitsStub])
203 |
204 | await action()
205 |
206 | sinon.assert.calledOnce(stubs.prCommitsStub)
207 | sinon.assert.calledWithExactly(
208 | stubs.logStub.logWarning,
209 | 'PR contains non dependabot commits, skipping.'
210 | )
211 | sinon.assert.notCalled(stubs.approveStub)
212 | sinon.assert.notCalled(stubs.mergeStub)
213 |
214 | sinon.assert.calledWith(
215 | stubs.coreStub.setOutput,
216 | MERGE_STATUS_KEY,
217 | MERGE_STATUS.skippedNotADependabotPr
218 | )
219 | })
220 | }
221 |
222 | for (const prCommitsStub of prCommitsStubs) {
223 | test(
224 | 'should NOT skip PR with non dependabot commit when skip-verification is enabled',
225 | async () => {
226 | const PR_NUMBER = Math.random()
227 | const { action, stubs } = buildStubbedAction({
228 | payload: {
229 | pull_request: {
230 | user: {
231 | login: BOT_NAME,
232 | },
233 | number: PR_NUMBER,
234 | },
235 | },
236 | inputs: {
237 | 'skip-verification': true,
238 | },
239 | })
240 | stubs.prCommitsStub.resolves([prCommitsStub])
241 |
242 | await action()
243 |
244 | sinon.assert.calledWithExactly(
245 | stubs.logStub.logInfo,
246 | 'Dependabot merge completed'
247 | )
248 | sinon.assert.calledOnce(stubs.prCommitsStub)
249 | sinon.assert.calledOnce(stubs.approveStub)
250 | sinon.assert.calledOnce(stubs.mergeStub)
251 |
252 | sinon.assert.calledWith(
253 | stubs.coreStub.setOutput,
254 | MERGE_STATUS_KEY,
255 | MERGE_STATUS.merged
256 | )
257 | }
258 | )
259 | }
260 |
261 | test(
262 | 'should skip PR if dependabot commit signatures cannot be verified',
263 | async () => {
264 | const PR_NUMBER = Math.random()
265 | const { action, stubs } = buildStubbedAction({
266 | payload: {
267 | pull_request: {
268 | user: {
269 | login: BOT_NAME,
270 | },
271 | number: PR_NUMBER,
272 | },
273 | },
274 | })
275 |
276 | stubs.prCommitsStub.resolves([
277 | {
278 | author: {
279 | login: 'dependabot[bot]',
280 | },
281 | },
282 | ])
283 |
284 | stubs.verifyCommitsStub.throws()
285 |
286 | await action()
287 |
288 | sinon.assert.calledWithExactly(
289 | stubs.logStub.logWarning,
290 | 'PR contains invalid dependabot commit signatures, skipping.'
291 | )
292 | sinon.assert.notCalled(stubs.approveStub)
293 | sinon.assert.notCalled(stubs.mergeStub)
294 | sinon.assert.calledWith(
295 | stubs.coreStub.setOutput,
296 | MERGE_STATUS_KEY,
297 | MERGE_STATUS.skippedCommitVerificationFailed
298 | )
299 | }
300 | )
301 |
302 | test(
303 | 'should review and merge even if commit signatures cannot be verified with skip-commit-verification',
304 | async () => {
305 | const PR_NUMBER = Math.random()
306 | const { action, stubs } = buildStubbedAction({
307 | payload: {
308 | pull_request: {
309 | user: {
310 | login: BOT_NAME,
311 | },
312 | number: PR_NUMBER,
313 | },
314 | },
315 | inputs: {
316 | 'skip-commit-verification': true,
317 | },
318 | })
319 |
320 | stubs.prCommitsStub.resolves([
321 | {
322 | author: {
323 | login: 'dependabot[bot]',
324 | },
325 | },
326 | ])
327 |
328 | await action()
329 |
330 | sinon.assert.calledWithExactly(
331 | stubs.logStub.logInfo,
332 | 'Dependabot merge completed'
333 | )
334 | sinon.assert.notCalled(stubs.coreStub.setFailed)
335 | sinon.assert.calledOnce(stubs.approveStub)
336 | sinon.assert.calledOnce(stubs.mergeStub)
337 | sinon.assert.calledWith(
338 | stubs.coreStub.setOutput,
339 | MERGE_STATUS_KEY,
340 | MERGE_STATUS.merged
341 | )
342 | }
343 | )
344 |
345 | test(
346 | 'should review and merge even if commit signatures cannot be verified when skip-verification is enabled',
347 | async () => {
348 | const PR_NUMBER = Math.random()
349 | const { action, stubs } = buildStubbedAction({
350 | payload: {
351 | pull_request: {
352 | user: {
353 | login: BOT_NAME,
354 | },
355 | number: PR_NUMBER,
356 | },
357 | },
358 | inputs: {
359 | 'skip-verification': true,
360 | },
361 | })
362 |
363 | stubs.prCommitsStub.resolves([
364 | {
365 | author: {
366 | login: 'dependabot[bot]',
367 | },
368 | },
369 | ])
370 |
371 | await action()
372 |
373 | sinon.assert.calledWithExactly(
374 | stubs.logStub.logInfo,
375 | 'Dependabot merge completed'
376 | )
377 | sinon.assert.notCalled(stubs.coreStub.setFailed)
378 | sinon.assert.calledOnce(stubs.approveStub)
379 | sinon.assert.calledOnce(stubs.mergeStub)
380 |
381 | sinon.assert.calledWith(
382 | stubs.coreStub.setOutput,
383 | MERGE_STATUS_KEY,
384 | MERGE_STATUS.merged
385 | )
386 | }
387 | )
388 |
389 | test(
390 | 'should review and merge even the user is not dependabot when skip-verification is enabled',
391 | async () => {
392 | const PR_NUMBER = Math.random()
393 | const { action, stubs } = buildStubbedAction({
394 | payload: {
395 | pull_request: {
396 | user: {
397 | login: BOT_NAME,
398 | },
399 | number: PR_NUMBER,
400 | },
401 | },
402 | inputs: {
403 | 'skip-verification': true,
404 | },
405 | })
406 |
407 | stubs.prCommitsStub.resolves([
408 | {
409 | author: {
410 | login: 'myCustomUser',
411 | },
412 | },
413 | ])
414 |
415 | await action()
416 |
417 | sinon.assert.calledWithExactly(
418 | stubs.logStub.logInfo,
419 | 'Dependabot merge completed'
420 | )
421 | sinon.assert.notCalled(stubs.coreStub.setFailed)
422 | sinon.assert.calledOnce(stubs.approveStub)
423 | sinon.assert.calledOnce(stubs.mergeStub)
424 | sinon.assert.calledWith(
425 | stubs.coreStub.setOutput,
426 | MERGE_STATUS_KEY,
427 | MERGE_STATUS.merged
428 | )
429 | }
430 | )
431 |
432 | test('should ignore excluded package', async () => {
433 | const PR_NUMBER = Math.random()
434 | const { action, stubs } = buildStubbedAction({
435 | payload: {
436 | pull_request: {
437 | number: PR_NUMBER,
438 | user: { login: BOT_NAME },
439 | },
440 | },
441 | inputs: { 'pr-number': PR_NUMBER, target: 'any', exclude: 'react' },
442 | })
443 |
444 | await action()
445 |
446 | sinon.assert.calledWithExactly(
447 | stubs.logStub.logInfo,
448 | '1 package(s) excluded: react. Skipping.'
449 | )
450 | sinon.assert.notCalled(stubs.approveStub)
451 | sinon.assert.notCalled(stubs.mergeStub)
452 | sinon.assert.calledWith(
453 | stubs.coreStub.setOutput,
454 | MERGE_STATUS_KEY,
455 | MERGE_STATUS.skippedPackageExcluded
456 | )
457 | })
458 |
459 | test('approve only should not merge', async () => {
460 | const PR_NUMBER = Math.random()
461 | const { action, stubs } = buildStubbedAction({
462 | payload: { issue: {} },
463 | inputs: {
464 | 'pr-number': PR_NUMBER,
465 | target: 'any',
466 | 'approve-only': true,
467 | },
468 | })
469 |
470 | stubs.prStub.resolves({
471 | number: PR_NUMBER,
472 | user: { login: BOT_NAME },
473 | })
474 |
475 | stubs.approveStub.resolves({ data: true })
476 |
477 | await action()
478 |
479 | sinon.assert.calledWithExactly(
480 | stubs.logStub.logInfo,
481 | 'APPROVE_ONLY set, PR was approved but it will not be merged'
482 | )
483 | sinon.assert.notCalled(stubs.mergeStub)
484 | sinon.assert.calledWith(
485 | stubs.coreStub.setOutput,
486 | MERGE_STATUS_KEY,
487 | MERGE_STATUS.approved
488 | )
489 | })
490 |
491 | test('should review and merge', async () => {
492 | const PR_NUMBER = Math.random()
493 | const { action, stubs } = buildStubbedAction({
494 | payload: {
495 | pull_request: {
496 | number: PR_NUMBER,
497 | user: { login: BOT_NAME },
498 | },
499 | },
500 | inputs: { 'pr-number': PR_NUMBER, target: 'any' },
501 | })
502 |
503 | await action()
504 |
505 | sinon.assert.calledWithExactly(
506 | stubs.logStub.logInfo,
507 | 'Dependabot merge completed'
508 | )
509 | sinon.assert.calledOnce(stubs.approveStub)
510 | sinon.assert.calledOnce(stubs.mergeStub)
511 | sinon.assert.calledWith(
512 | stubs.coreStub.setOutput,
513 | MERGE_STATUS_KEY,
514 | MERGE_STATUS.merged
515 | )
516 | })
517 |
518 | test(
519 | 'should merge github-action-merge-dependabot minor release',
520 | async () => {
521 | const PR_NUMBER = Math.random()
522 | const { action, stubs } = buildStubbedAction({
523 | payload: {
524 | pull_request: {
525 | number: PR_NUMBER,
526 | user: { login: BOT_NAME },
527 | },
528 | },
529 | inputs: { 'pr-number': PR_NUMBER, target: 'any' },
530 | })
531 |
532 | await action()
533 |
534 | sinon.assert.calledWithExactly(
535 | stubs.logStub.logInfo,
536 | 'Dependabot merge completed'
537 | )
538 | sinon.assert.calledOnce(stubs.approveStub)
539 | sinon.assert.calledOnce(stubs.mergeStub)
540 | sinon.assert.calledWith(
541 | stubs.coreStub.setOutput,
542 | MERGE_STATUS_KEY,
543 | MERGE_STATUS.merged
544 | )
545 | }
546 | )
547 |
548 | test(
549 | 'should not merge github-action-merge-dependabot major release',
550 | async () => {
551 | const PR_NUMBER = Math.random()
552 | const { action, stubs } = buildStubbedAction({
553 | payload: {
554 | pull_request: {
555 | number: PR_NUMBER,
556 | user: { login: BOT_NAME },
557 | },
558 | },
559 | inputs: { 'pr-number': PR_NUMBER, target: 'any' },
560 | dependabotMetadata: {
561 | updateType: updateTypes.major,
562 | dependencyNames: 'github-action-merge-dependabot',
563 | },
564 | })
565 |
566 | await action()
567 |
568 | sinon.assert.calledOnce(stubs.coreStub.setFailed)
569 | sinon.assert.notCalled(stubs.approveStub)
570 | sinon.assert.notCalled(stubs.mergeStub)
571 | sinon.assert.calledWith(
572 | stubs.coreStub.setOutput,
573 | MERGE_STATUS_KEY,
574 | MERGE_STATUS.skippedCannotUpdateMajor
575 | )
576 | }
577 | )
578 |
579 | test('should review and merge', async () => {
580 | const PR_NUMBER = Math.random()
581 | const { action, stubs } = buildStubbedAction({
582 | payload: {
583 | pull_request: {
584 | number: PR_NUMBER,
585 | user: { login: BOT_NAME },
586 | },
587 | },
588 | inputs: { 'pr-number': PR_NUMBER, target: 'any' },
589 | })
590 |
591 | await action()
592 |
593 | sinon.assert.calledWithExactly(
594 | stubs.logStub.logInfo,
595 | 'Dependabot merge completed'
596 | )
597 | sinon.assert.notCalled(stubs.coreStub.setFailed)
598 | sinon.assert.calledOnce(stubs.approveStub)
599 | sinon.assert.calledOnce(stubs.mergeStub)
600 | sinon.assert.calledWith(
601 | stubs.coreStub.setOutput,
602 | MERGE_STATUS_KEY,
603 | MERGE_STATUS.merged
604 | )
605 | })
606 |
607 | test('should review and enable github auto-merge', async () => {
608 | const PR_NUMBER = Math.random()
609 | const PR_NODE_ID = Math.random()
610 | const { action, stubs } = buildStubbedAction({
611 | payload: {
612 | pull_request: {
613 | number: PR_NUMBER,
614 | node_id: PR_NODE_ID,
615 | user: { login: BOT_NAME },
616 | },
617 | },
618 | inputs: {
619 | 'pr-number': PR_NUMBER,
620 | target: 'any',
621 | 'use-github-auto-merge': true,
622 | },
623 | })
624 |
625 | await action()
626 |
627 | sinon.assert.calledWithExactly(
628 | stubs.logStub.logInfo,
629 | 'USE_GITHUB_AUTO_MERGE set, PR was marked as auto-merge'
630 | )
631 | sinon.assert.notCalled(stubs.coreStub.setFailed)
632 | sinon.assert.calledOnce(stubs.approveStub)
633 | sinon.assert.calledOnce(stubs.enableAutoMergeStub)
634 | sinon.assert.calledWith(
635 | stubs.coreStub.setOutput,
636 | MERGE_STATUS_KEY,
637 | MERGE_STATUS.autoMerge
638 | )
639 | })
640 |
641 | test('should forbid major when target is minor', async () => {
642 | const PR_NUMBER = Math.random()
643 |
644 | const { action, stubs } = buildStubbedAction({
645 | payload: {
646 | pull_request: {
647 | number: PR_NUMBER,
648 | user: { login: BOT_NAME },
649 | },
650 | },
651 | inputs: {
652 | PR_NUMBER,
653 | target: 'minor',
654 | exclude: 'react',
655 | },
656 | dependabotMetadata: createDependabotMetadata({
657 | updateType: updateTypes.major,
658 | }),
659 | })
660 |
661 | await action()
662 |
663 | sinon.assert.calledWithExactly(
664 | stubs.logStub.logWarning,
665 | `Semver bump is higher than allowed in TARGET.
666 | Tried to do a '${updateTypes.major}' update but the max allowed is '${updateTypes.minor}'`
667 | )
668 | sinon.assert.notCalled(stubs.approveStub)
669 | sinon.assert.notCalled(stubs.mergeStub)
670 | sinon.assert.calledWith(
671 | stubs.coreStub.setOutput,
672 | MERGE_STATUS_KEY,
673 | MERGE_STATUS.skippedBumpHigherThanTarget
674 | )
675 | })
676 |
677 | test('should forbid minor when target is patch', async () => {
678 | const PR_NUMBER = Math.random()
679 |
680 | const { action, stubs } = buildStubbedAction({
681 | payload: {
682 | pull_request: {
683 | number: PR_NUMBER,
684 | user: { login: BOT_NAME },
685 | },
686 | },
687 | inputs: {
688 | PR_NUMBER,
689 | target: 'patch',
690 | exclude: 'react',
691 | },
692 | dependabotMetadata: createDependabotMetadata({
693 | updateType: updateTypes.minor,
694 | }),
695 | })
696 |
697 | await action()
698 |
699 | sinon.assert.calledWithExactly(
700 | stubs.logStub.logWarning,
701 | `Semver bump is higher than allowed in TARGET.
702 | Tried to do a '${updateTypes.minor}' update but the max allowed is '${updateTypes.patch}'`
703 | )
704 | sinon.assert.notCalled(stubs.approveStub)
705 | sinon.assert.notCalled(stubs.mergeStub)
706 | sinon.assert.calledWith(
707 | stubs.coreStub.setOutput,
708 | MERGE_STATUS_KEY,
709 | MERGE_STATUS.skippedBumpHigherThanTarget
710 | )
711 | })
712 |
713 | test('should forbid when update type is missing', async () => {
714 | const PR_NUMBER = Math.random()
715 |
716 | const { action, stubs } = buildStubbedAction({
717 | payload: {
718 | pull_request: {
719 | number: PR_NUMBER,
720 | user: { login: BOT_NAME },
721 | },
722 | },
723 | inputs: {
724 | PR_NUMBER,
725 | target: 'minor',
726 | exclude: 'react',
727 | },
728 | dependabotMetadata: createDependabotMetadata({
729 | updateType: null,
730 | }),
731 | })
732 |
733 | await action()
734 |
735 | sinon.assert.calledWithExactly(
736 | stubs.logStub.logWarning,
737 | 'Semver bump \'null\' is invalid!'
738 | )
739 | sinon.assert.notCalled(stubs.approveStub)
740 | sinon.assert.notCalled(stubs.mergeStub)
741 | sinon.assert.calledWith(
742 | stubs.coreStub.setOutput,
743 | MERGE_STATUS_KEY,
744 | MERGE_STATUS.skippedInvalidVersion
745 | )
746 | })
747 |
748 | test('should forbid when update type is not valid', async () => {
749 | const PR_NUMBER = Math.random()
750 |
751 | const { action, stubs } = buildStubbedAction({
752 | payload: {
753 | pull_request: {
754 | number: PR_NUMBER,
755 | user: { login: BOT_NAME },
756 | },
757 | },
758 | inputs: {
759 | PR_NUMBER,
760 | target: 'minor',
761 | exclude: 'react',
762 | },
763 | dependabotMetadata: createDependabotMetadata({
764 | updateType: 'semver:invalid',
765 | }),
766 | })
767 |
768 | await action()
769 |
770 | sinon.assert.calledWithExactly(
771 | stubs.logStub.logWarning,
772 | 'Semver bump \'semver:invalid\' is invalid!'
773 | )
774 | sinon.assert.notCalled(stubs.approveStub)
775 | sinon.assert.notCalled(stubs.mergeStub)
776 | sinon.assert.calledWith(
777 | stubs.coreStub.setOutput,
778 | MERGE_STATUS_KEY,
779 | MERGE_STATUS.skippedInvalidVersion
780 | )
781 | })
782 |
783 | test('should allow minor when target is major', async () => {
784 | const PR_NUMBER = Math.random()
785 |
786 | const { action, stubs } = buildStubbedAction({
787 | payload: {
788 | pull_request: {
789 | number: PR_NUMBER,
790 | user: { login: BOT_NAME },
791 | },
792 | },
793 | inputs: {
794 | PR_NUMBER,
795 | target: 'major',
796 | },
797 | dependabotMetadata: createDependabotMetadata({
798 | updateType: updateTypes.minor,
799 | }),
800 | })
801 |
802 | await action()
803 |
804 | sinon.assert.calledWithExactly(
805 | stubs.logStub.logInfo,
806 | 'Dependabot merge completed'
807 | )
808 | sinon.assert.notCalled(stubs.coreStub.setFailed)
809 | sinon.assert.calledOnce(stubs.approveStub)
810 | sinon.assert.calledOnce(stubs.mergeStub)
811 | sinon.assert.calledWith(
812 | stubs.coreStub.setOutput,
813 | MERGE_STATUS_KEY,
814 | MERGE_STATUS.merged
815 | )
816 | })
817 |
818 | test(
819 | 'should pick target-development version for dev package update',
820 | async () => {
821 | const PR_NUMBER = Math.random()
822 |
823 | const { action, stubs } = buildStubbedAction({
824 | payload: {
825 | pull_request: {
826 | number: PR_NUMBER,
827 | user: { login: BOT_NAME },
828 | },
829 | },
830 | inputs: {
831 | PR_NUMBER,
832 | target: 'minor',
833 | 'target-development': 'major',
834 | },
835 | dependabotMetadata: createDependabotMetadata({
836 | updateType: updateTypes.major,
837 | dependencyType: 'direct:development',
838 | }),
839 | })
840 |
841 | await action()
842 |
843 | sinon.assert.calledWithExactly(
844 | stubs.logStub.logInfo,
845 | 'Dependabot merge completed'
846 | )
847 | sinon.assert.notCalled(stubs.coreStub.setFailed)
848 | sinon.assert.calledOnce(stubs.approveStub)
849 | sinon.assert.calledOnce(stubs.mergeStub)
850 | sinon.assert.calledWith(
851 | stubs.coreStub.setOutput,
852 | MERGE_STATUS_KEY,
853 | MERGE_STATUS.merged
854 | )
855 | }
856 | )
857 |
858 | test(
859 | 'should pick target-production version for production package update',
860 | async () => {
861 | const PR_NUMBER = Math.random()
862 |
863 | const { action, stubs } = buildStubbedAction({
864 | payload: {
865 | pull_request: {
866 | number: PR_NUMBER,
867 | user: { login: BOT_NAME },
868 | },
869 | },
870 | inputs: {
871 | PR_NUMBER,
872 | 'target-development': 'major',
873 | 'target-production': 'minor',
874 | },
875 | dependabotMetadata: createDependabotMetadata({
876 | updateType: updateTypes.major,
877 | dependencyType: 'direct:production',
878 | }),
879 | })
880 |
881 | await action()
882 |
883 | sinon.assert.calledWithExactly(
884 | stubs.logStub.logWarning,
885 | `Semver bump is higher than allowed in TARGET.
886 | Tried to do a '${updateTypes.major}' update but the max allowed is '${updateTypes.minor}'`
887 | )
888 | sinon.assert.notCalled(stubs.approveStub)
889 | sinon.assert.notCalled(stubs.mergeStub)
890 | sinon.assert.calledWith(
891 | stubs.coreStub.setOutput,
892 | MERGE_STATUS_KEY,
893 | MERGE_STATUS.skippedBumpHigherThanTarget
894 | )
895 | }
896 | )
897 |
898 | test(
899 | 'should pick target-indirect version for indirect package update',
900 | async () => {
901 | const PR_NUMBER = Math.random()
902 |
903 | const { action, stubs } = buildStubbedAction({
904 | payload: {
905 | pull_request: {
906 | number: PR_NUMBER,
907 | user: { login: BOT_NAME },
908 | },
909 | },
910 | inputs: {
911 | 'pr-number': PR_NUMBER,
912 | target: 'patch',
913 | 'target-indirect': 'major',
914 | },
915 | dependabotMetadata: createDependabotMetadata({
916 | updateType: updateTypes.major,
917 | dependencyType: 'indirect',
918 | }),
919 | })
920 |
921 | await action()
922 |
923 | sinon.assert.calledWithExactly(
924 | stubs.logStub.logInfo,
925 | 'Dependabot merge completed'
926 | )
927 | sinon.assert.notCalled(stubs.coreStub.setFailed)
928 | sinon.assert.calledOnce(stubs.approveStub)
929 | sinon.assert.calledOnce(stubs.mergeStub)
930 | sinon.assert.calledWith(
931 | stubs.coreStub.setOutput,
932 | MERGE_STATUS_KEY,
933 | MERGE_STATUS.merged
934 | )
935 | }
936 | )
937 |
938 | // https://github.com/dependabot/dependabot-core/issues/4893
939 | test(
940 | 'should merge if target-indirect is any and update-type metadata is not set',
941 | async () => {
942 | const PR_NUMBER = Math.random()
943 |
944 | const { action, stubs } = buildStubbedAction({
945 | payload: {
946 | pull_request: {
947 | number: PR_NUMBER,
948 | user: { login: BOT_NAME },
949 | },
950 | },
951 | inputs: {
952 | 'pr-number': PR_NUMBER,
953 | target: 'patch',
954 | 'target-indirect': 'any',
955 | },
956 | dependabotMetadata: createDependabotMetadata({
957 | updateType: '',
958 | dependencyType: 'indirect',
959 | }),
960 | })
961 |
962 | await action()
963 |
964 | sinon.assert.calledWithExactly(
965 | stubs.logStub.logInfo,
966 | 'Dependabot merge completed'
967 | )
968 | sinon.assert.notCalled(stubs.coreStub.setFailed)
969 | sinon.assert.calledOnce(stubs.approveStub)
970 | sinon.assert.calledOnce(stubs.mergeStub)
971 | sinon.assert.calledWith(
972 | stubs.coreStub.setOutput,
973 | MERGE_STATUS_KEY,
974 | MERGE_STATUS.merged
975 | )
976 | }
977 | )
978 |
979 | test(
980 | 'should pick target version for production package update if target-production is not set',
981 | async () => {
982 | const PR_NUMBER = Math.random()
983 |
984 | const { action, stubs } = buildStubbedAction({
985 | payload: {
986 | pull_request: {
987 | number: PR_NUMBER,
988 | user: { login: BOT_NAME },
989 | },
990 | },
991 | inputs: {
992 | PR_NUMBER,
993 | 'target-development': 'major',
994 | target: 'patch',
995 | },
996 | dependabotMetadata: createDependabotMetadata({
997 | updateType: updateTypes.major,
998 | dependencyType: 'direct:production',
999 | }),
1000 | })
1001 |
1002 | await action()
1003 |
1004 | sinon.assert.calledWithExactly(
1005 | stubs.logStub.logWarning,
1006 | `Semver bump is higher than allowed in TARGET.
1007 | Tried to do a '${updateTypes.major}' update but the max allowed is '${updateTypes.patch}'`
1008 | )
1009 | sinon.assert.notCalled(stubs.approveStub)
1010 | sinon.assert.notCalled(stubs.mergeStub)
1011 | sinon.assert.calledWith(
1012 | stubs.coreStub.setOutput,
1013 | MERGE_STATUS_KEY,
1014 | MERGE_STATUS.skippedBumpHigherThanTarget
1015 | )
1016 | }
1017 | )
1018 |
1019 | test(
1020 | 'should pick target version for development package update if target-development is not set',
1021 | async () => {
1022 | const PR_NUMBER = Math.random()
1023 |
1024 | const { action, stubs } = buildStubbedAction({
1025 | payload: {
1026 | pull_request: {
1027 | number: PR_NUMBER,
1028 | user: { login: BOT_NAME },
1029 | },
1030 | },
1031 | inputs: {
1032 | PR_NUMBER,
1033 | 'target-production': 'major',
1034 | target: 'patch',
1035 | },
1036 | dependabotMetadata: createDependabotMetadata({
1037 | updateType: updateTypes.major,
1038 | dependencyType: 'direct:development',
1039 | }),
1040 | })
1041 |
1042 | await action()
1043 |
1044 | sinon.assert.calledWithExactly(
1045 | stubs.logStub.logWarning,
1046 | `Semver bump is higher than allowed in TARGET.
1047 | Tried to do a '${updateTypes.major}' update but the max allowed is '${updateTypes.patch}'`
1048 | )
1049 | sinon.assert.notCalled(stubs.approveStub)
1050 | sinon.assert.notCalled(stubs.mergeStub)
1051 | sinon.assert.calledWith(
1052 | stubs.coreStub.setOutput,
1053 | MERGE_STATUS_KEY,
1054 | MERGE_STATUS.skippedBumpHigherThanTarget
1055 | )
1056 | }
1057 | )
1058 |
1059 | test(
1060 | 'should pick target version for development package update if target-indirect is not set',
1061 | async () => {
1062 | const PR_NUMBER = Math.random()
1063 |
1064 | const { action, stubs } = buildStubbedAction({
1065 | payload: {
1066 | pull_request: {
1067 | number: PR_NUMBER,
1068 | user: { login: BOT_NAME },
1069 | },
1070 | },
1071 | inputs: {
1072 | PR_NUMBER,
1073 | target: 'patch',
1074 | },
1075 | dependabotMetadata: createDependabotMetadata({
1076 | updateType: updateTypes.major,
1077 | dependencyType: 'direct:development',
1078 | }),
1079 | })
1080 |
1081 | await action()
1082 |
1083 | sinon.assert.calledWithExactly(
1084 | stubs.logStub.logWarning,
1085 | `Semver bump is higher than allowed in TARGET.
1086 | Tried to do a '${updateTypes.major}' update but the max allowed is '${updateTypes.patch}'`
1087 | )
1088 | sinon.assert.notCalled(stubs.approveStub)
1089 | sinon.assert.notCalled(stubs.mergeStub)
1090 | sinon.assert.calledWith(
1091 | stubs.coreStub.setOutput,
1092 | MERGE_STATUS_KEY,
1093 | MERGE_STATUS.skippedBumpHigherThanTarget
1094 | )
1095 | }
1096 | )
1097 |
--------------------------------------------------------------------------------
/test/github-client.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test, afterEach } = require('node:test')
4 | const sinon = require('sinon')
5 | const { githubClient } = require('../src/github-client')
6 |
7 | const githubContext = {
8 | repository: {
9 | owner: {
10 | login: 'owner-login.',
11 | },
12 | name: 'repository-name',
13 | },
14 | }
15 |
16 | const data = 'octokit-result'
17 |
18 | const octokitStubs = {
19 | get: sinon.stub().returns(Promise.resolve({ data })),
20 | createReview: sinon.stub().returns(Promise.resolve({ data })),
21 | merge: sinon.stub().returns(Promise.resolve({ data })),
22 | listCommits: sinon.stub().returns(Promise.resolve({ data })),
23 | }
24 | const contextStub = { payload: githubContext }
25 | const githubStub = {
26 | rest: {
27 | pulls: octokitStubs,
28 | },
29 | graphql: sinon.stub().returns(Promise.resolve({ data })),
30 | }
31 |
32 | const PR_NUMBER = Math.floor(Math.random() * 10)
33 | const PR_NODE_ID = Math.floor(Math.random() * 10)
34 |
35 | afterEach(() => {
36 | sinon.resetHistory()
37 | })
38 |
39 | test('githubClient', async t => {
40 | await t.test('getPullRequest', async (t) => {
41 | const result = await githubClient(githubStub, contextStub).getPullRequest(
42 | PR_NUMBER
43 | )
44 | t.assert.deepStrictEqual(result, data)
45 |
46 | sinon.assert.calledWith(octokitStubs.get, {
47 | owner: githubContext.repository.owner.login,
48 | repo: githubContext.repository.name,
49 | pull_number: PR_NUMBER,
50 | })
51 | })
52 |
53 | await t.test('approvePullRequest', async (t) => {
54 | const comment = 'Test pull request comment'
55 | const result = await githubClient(
56 | githubStub,
57 | contextStub
58 | ).approvePullRequest(PR_NUMBER, comment)
59 | t.assert.deepStrictEqual(result, data)
60 |
61 | sinon.assert.calledWith(octokitStubs.createReview, {
62 | owner: githubContext.repository.owner.login,
63 | repo: githubContext.repository.name,
64 | pull_number: PR_NUMBER,
65 | event: 'APPROVE',
66 | body: comment,
67 | })
68 | })
69 |
70 | await t.test('mergePullRequest', async (t) => {
71 | const method = 'squash'
72 | const result = await githubClient(githubStub, contextStub).mergePullRequest(
73 | PR_NUMBER,
74 | method
75 | )
76 | t.assert.deepStrictEqual(result, data)
77 |
78 | sinon.assert.calledWith(octokitStubs.merge, {
79 | owner: githubContext.repository.owner.login,
80 | repo: githubContext.repository.name,
81 | pull_number: PR_NUMBER,
82 | merge_method: method,
83 | })
84 | })
85 |
86 | await t.test('enableAutoMergePullRequest', async (t) => {
87 | const method = 'squash'
88 | const result = await githubClient(
89 | githubStub,
90 | contextStub
91 | ).enableAutoMergePullRequest(PR_NODE_ID, method)
92 | t.assert.deepStrictEqual(result, data)
93 |
94 | const query = `
95 | mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) {
96 | enablePullRequestAutoMerge(
97 | input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }
98 | ) {
99 | pullRequest {
100 | autoMergeRequest {
101 | enabledAt
102 | enabledBy {
103 | login
104 | }
105 | }
106 | }
107 | }
108 | }
109 | `
110 |
111 | sinon.assert.calledWith(githubStub.graphql, query, {
112 | pullRequestId: PR_NODE_ID,
113 | mergeMethod: method.toUpperCase(),
114 | })
115 | })
116 |
117 | await t.test('getPullRequestDiff', async (t) => {
118 | const result = await githubClient(
119 | githubStub,
120 | contextStub
121 | ).getPullRequestDiff(PR_NUMBER)
122 | t.assert.deepStrictEqual(result, data)
123 |
124 | sinon.assert.calledWith(octokitStubs.get, {
125 | owner: githubContext.repository.owner.login,
126 | repo: githubContext.repository.name,
127 | pull_number: PR_NUMBER,
128 | mediaType: {
129 | format: 'diff',
130 | },
131 | })
132 | })
133 |
134 | await t.test('getPullRequestCommits', async (t) => {
135 | const result = await githubClient(
136 | githubStub,
137 | contextStub
138 | ).getPullRequestCommits(PR_NUMBER)
139 | t.assert.deepStrictEqual(result, data)
140 |
141 | sinon.assert.calledWith(octokitStubs.listCommits, {
142 | owner: githubContext.repository.owner.login,
143 | repo: githubContext.repository.name,
144 | pull_number: PR_NUMBER,
145 | })
146 | })
147 | })
148 |
--------------------------------------------------------------------------------
/test/log.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test, afterEach } = require('node:test')
4 | const sinon = require('sinon')
5 | const proxyquire = require('proxyquire')
6 |
7 | const coreStubs = {
8 | debug: sinon.stub(),
9 | error: sinon.stub(),
10 | info: sinon.stub(),
11 | warning: sinon.stub(),
12 | }
13 |
14 | const log = proxyquire('../src/log', {
15 | '@actions/core': coreStubs,
16 | })
17 |
18 | afterEach(() => {
19 | sinon.resetHistory()
20 | })
21 |
22 | test('logError should work with numbers', async () => {
23 | log.logError(100)
24 | sinon.assert.calledWith(coreStubs.error, '100')
25 | })
26 |
27 | test('logError should work with strings', async () => {
28 | log.logError('100')
29 | sinon.assert.calledWith(coreStubs.error, '100')
30 | })
31 |
32 | test('log should call aproppriate core function', async () => {
33 | const msg = 'log message'
34 | log.logError(msg)
35 | sinon.assert.calledWith(coreStubs.error, msg)
36 | log.logWarning(msg)
37 | sinon.assert.calledWith(coreStubs.warning, msg)
38 | log.logInfo(msg)
39 | sinon.assert.calledWith(coreStubs.info, msg)
40 | log.logDebug(msg)
41 | sinon.assert.calledWith(coreStubs.debug, msg)
42 | })
43 |
--------------------------------------------------------------------------------
/test/mapUpdateType.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const { test } = require('node:test')
3 |
4 | const { updateTypes, mapUpdateType } = require('../src/mapUpdateType')
5 |
6 | test('mapUpdateType', async t => {
7 | t.test(
8 | 'should return the updateType or any if invalid or missing',
9 | async t => {
10 | t.assert.deepEqual(mapUpdateType('major'), updateTypes.major)
11 | t.assert.deepEqual(mapUpdateType('minor'), updateTypes.minor)
12 | t.assert.deepEqual(mapUpdateType('patch'), updateTypes.patch)
13 | t.assert.deepEqual(mapUpdateType('bad_input'), updateTypes.any)
14 | t.assert.deepEqual(mapUpdateType(), updateTypes.any)
15 | t.assert.deepEqual(mapUpdateType('any'), updateTypes.any)
16 | }
17 | )
18 | })
19 |
--------------------------------------------------------------------------------
/test/util.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const { test } = require('node:test')
3 | const sinon = require('sinon')
4 | const proxyquire = require('proxyquire')
5 |
6 | const logWarningStub = sinon.stub()
7 | const { getInputs, parseCommaOrSemicolonSeparatedValue } = proxyquire(
8 | '../src/util',
9 | {
10 | '../src/log.js': {
11 | logWarning: logWarningStub,
12 | },
13 | }
14 | )
15 |
16 | test('parseCommaOrSemicolonSeparatedValue', async t => {
17 | await t.test('should split semicolon separated values correctly', async t => {
18 | t.assert.deepStrictEqual(parseCommaOrSemicolonSeparatedValue('test1;test2;test3'), [
19 | 'test1',
20 | 'test2',
21 | 'test3',
22 | ])
23 | t.assert.deepStrictEqual(parseCommaOrSemicolonSeparatedValue(' test1; test2; test3'), [
24 | 'test1',
25 | 'test2',
26 | 'test3',
27 | ])
28 | })
29 | await t.test('should split comma separated values correctly', async t => {
30 | t.assert.deepStrictEqual(parseCommaOrSemicolonSeparatedValue('test1,test2,test3'), [
31 | 'test1',
32 | 'test2',
33 | 'test3',
34 | ])
35 | t.assert.deepStrictEqual(parseCommaOrSemicolonSeparatedValue(' test1, test2, test3'), [
36 | 'test1',
37 | 'test2',
38 | 'test3',
39 | ])
40 | })
41 | })
42 |
43 | const BOOLEAN_INPUTS = [
44 | { input: 'approve-only', key: 'APPROVE_ONLY' },
45 | { input: 'use-github-auto-merge', key: 'USE_GITHUB_AUTO_MERGE' },
46 | {
47 | input: 'skip-commit-verification',
48 | key: 'SKIP_COMMIT_VERIFICATION',
49 | },
50 | {
51 | input: 'skip-verification',
52 | key: 'SKIP_VERIFICATION',
53 | },
54 | ]
55 |
56 | test('getInputs', async t => {
57 | await t.test('should fail if no inputs object is provided', async t => {
58 | t.assert.throws(() => getInputs())
59 | })
60 | await t.test(
61 | 'should return the correct inputs with default value if needed',
62 | async t => {
63 | await t.test('MERGE_METHOD', async t => {
64 | t.assert.deepEqual(getInputs({}).MERGE_METHOD, 'squash')
65 | t.assert.deepEqual(getInputs({ 'merge-method': 'merge' }).MERGE_METHOD, 'merge')
66 | t.assert.deepEqual(logWarningStub.callCount, 0)
67 | t.assert.deepEqual(
68 | getInputs({ 'merge-method': 'invalid-merge-method' }).MERGE_METHOD,
69 | 'squash'
70 | )
71 | t.assert.deepEqual(logWarningStub.callCount, 1)
72 | t.assert.deepEqual(
73 | logWarningStub.firstCall.args[0],
74 | 'merge-method input is ignored because it is malformed, defaulting to `squash`.'
75 | )
76 | })
77 | await t.test('EXCLUDE_PKGS', async t => {
78 | t.assert.deepStrictEqual(getInputs({ exclude: 'react,vue' }).EXCLUDE_PKGS, [
79 | 'react',
80 | 'vue',
81 | ])
82 | })
83 | await t.test('MERGE_COMMENT', async t => {
84 | t.assert.deepEqual(getInputs({}).MERGE_COMMENT, '')
85 | t.assert.deepStrictEqual(
86 | getInputs({ 'merge-comment': 'test-merge-comment' }).MERGE_COMMENT,
87 | 'test-merge-comment'
88 | )
89 | })
90 | await t.test('BOOLEAN INPUTS', async t => {
91 | BOOLEAN_INPUTS.forEach(({ input, key }) => {
92 | t.assert.deepEqual(getInputs({})[key], false)
93 | t.assert.deepEqual(getInputs({ [input]: 'false' })[key], false)
94 | t.assert.deepEqual(getInputs({ [input]: 'False' })[key], false)
95 | t.assert.deepEqual(getInputs({ [input]: 'FALSE' })[key], false)
96 | t.assert.deepEqual(getInputs({ [input]: 'true' })[key], true)
97 | t.assert.deepEqual(getInputs({ [input]: 'True' })[key], true)
98 | t.assert.deepEqual(getInputs({ [input]: 'TRUE' })[key], true)
99 | })
100 | })
101 | await t.test('TARGET', async t => {
102 | t.assert.deepEqual(
103 | getInputs({ target: 'major' }).TARGET,
104 | 'version-update:semver-major'
105 | )
106 | t.assert.deepEqual(
107 | getInputs({ target: 'minor' }).TARGET,
108 | 'version-update:semver-minor'
109 | )
110 | t.assert.deepEqual(
111 | getInputs({ target: 'patch' }).TARGET,
112 | 'version-update:semver-patch'
113 | )
114 | t.assert.deepEqual(getInputs({ target: '' }).TARGET, 'version-update:semver-any')
115 | t.assert.deepEqual(
116 | getInputs({ target: 'any' }).TARGET,
117 | 'version-update:semver-any'
118 | )
119 | })
120 | await t.test('PR_NUMBER', async t => {
121 | t.assert.deepEqual(getInputs({ 'pr-number': '10' }).PR_NUMBER, '10')
122 | })
123 | }
124 | )
125 | })
126 |
--------------------------------------------------------------------------------
/test/verifyCommitSignatures.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test } = require('node:test')
4 |
5 | const {
6 | dependabotAuthor,
7 | dependabotCommitter,
8 | } = require('../src/getDependabotDetails')
9 | const {
10 | verifyCommits,
11 | verifyCommitSignatureCommitterAndAuthor,
12 | } = require('../src/verifyCommitSignatures')
13 |
14 | const ValidAuthorValidCommitterVerifiedCommitMock = {
15 | commit: {
16 | author: { name: dependabotAuthor },
17 | committer: { name: dependabotCommitter },
18 | verification: { verified: true },
19 | },
20 | sha: 'sha',
21 | }
22 | const ValidAuthorValidCommitterUnverifiedCommitMock = {
23 | commit: {
24 | author: { name: dependabotAuthor },
25 | committer: { name: dependabotCommitter },
26 | verification: { verified: false },
27 | },
28 | sha: 'sha',
29 | }
30 | const InvalidAuthorValidCommitterVerifiedCommitMock = {
31 | commit: {
32 | author: { name: 'testUser' },
33 | committer: { name: dependabotCommitter },
34 | verification: { verified: true },
35 | },
36 | sha: 'sha',
37 | }
38 | const ValidAuthorInvalidCommitterVerifiedCommitMock = {
39 | commit: {
40 | author: { name: dependabotAuthor },
41 | committer: { name: 'testUser' },
42 | verification: { verified: true },
43 | },
44 | sha: 'sha',
45 | }
46 | const InvalidAuthorInvalidCommitterUnverifiedCommitMock = {
47 | commit: {
48 | author: { name: 'testUser' },
49 | committer: { name: 'testUser' },
50 | verification: { verified: false },
51 | },
52 | sha: 'sha',
53 | }
54 |
55 | const prCommitsValid = [
56 | ValidAuthorValidCommitterVerifiedCommitMock,
57 | ValidAuthorValidCommitterVerifiedCommitMock,
58 | ]
59 | const prCommitsInvalid = [
60 | InvalidAuthorValidCommitterVerifiedCommitMock,
61 | InvalidAuthorValidCommitterVerifiedCommitMock,
62 | ]
63 | const prCommitsValidAndInvalid = [
64 | ValidAuthorValidCommitterVerifiedCommitMock,
65 | InvalidAuthorValidCommitterVerifiedCommitMock,
66 | ]
67 |
68 | test('verifyCommitSignatureCommitterAndAuthor', async t => {
69 | await t.test('shoud throw error when', async t => {
70 | await t.test('the verified flag is set to false', async t => {
71 | const {
72 | commit: {
73 | author,
74 | committer,
75 | verification: { verified },
76 | },
77 | sha,
78 | } = ValidAuthorValidCommitterUnverifiedCommitMock
79 | t.assert.throws(
80 | () =>
81 | verifyCommitSignatureCommitterAndAuthor(
82 | sha,
83 | author,
84 | committer,
85 | verified
86 | ),
87 | new Error(
88 | `Signature for commit ${sha} could not be verified - Not a dependabot commit`
89 | )
90 | )
91 | })
92 |
93 | await t.test('the committer name is not GitHub', async t => {
94 | const {
95 | commit: {
96 | author,
97 | committer,
98 | verification: { verified },
99 | },
100 | sha,
101 | } = ValidAuthorInvalidCommitterVerifiedCommitMock
102 | t.assert.throws(
103 | () =>
104 | verifyCommitSignatureCommitterAndAuthor(
105 | sha,
106 | author,
107 | committer,
108 | verified
109 | ),
110 | new Error(
111 | `Signature for commit ${sha} could not be verified - Not a dependabot commit`
112 | )
113 | )
114 | })
115 |
116 | await t.test('the authot name is not dependabot[bot]', async t => {
117 | const {
118 | commit: {
119 | author,
120 | committer,
121 | verification: { verified },
122 | },
123 | sha,
124 | } = InvalidAuthorValidCommitterVerifiedCommitMock
125 | t.assert.throws(
126 | () =>
127 | verifyCommitSignatureCommitterAndAuthor(
128 | sha,
129 | author,
130 | committer,
131 | verified
132 | ),
133 | new Error(
134 | `Signature for commit ${sha} could not be verified - Not a dependabot commit`
135 | )
136 | )
137 | })
138 |
139 | await t.test(
140 | 'the committer name is not Github, the author is not dependabot[bot] and is not verified',
141 | async t => {
142 | const {
143 | commit: {
144 | author,
145 | committer,
146 | verification: { verified },
147 | },
148 | sha,
149 | } = InvalidAuthorInvalidCommitterUnverifiedCommitMock
150 | t.assert.throws(
151 | () =>
152 | verifyCommitSignatureCommitterAndAuthor(
153 | sha,
154 | author,
155 | committer,
156 | verified
157 | ),
158 | new Error(
159 | `Signature for commit ${sha} could not be verified - Not a dependabot commit`
160 | )
161 | )
162 | }
163 | )
164 | })
165 |
166 | await t.test('should not throw an error when', async t => {
167 | await t.test(
168 | 'the committer name is Github, the author is dependabot[bot] and is verified',
169 | async t => {
170 | const {
171 | commit: {
172 | author,
173 | committer,
174 | verification: { verified },
175 | },
176 | sha,
177 | } = ValidAuthorValidCommitterVerifiedCommitMock
178 | t.assert.doesNotThrow(
179 | () =>
180 | verifyCommitSignatureCommitterAndAuthor(
181 | sha,
182 | author,
183 | committer,
184 | verified
185 | ),
186 | {}
187 | )
188 | }
189 | )
190 | })
191 | })
192 |
193 | test('VerifyCommits', async t => {
194 | await t.test('Should throw error when', async t => {
195 | await t.test('At least one commit does not match the requirements', async t => {
196 | t.assert.throws(
197 | () => verifyCommits(prCommitsValidAndInvalid),
198 | new Error(
199 | 'Signature for commit sha could not be verified - Not a dependabot commit'
200 | )
201 | )
202 | })
203 |
204 | await t.test('At least one commit does not match the requirements', async t => {
205 | t.assert.throws(
206 | () => verifyCommits(prCommitsInvalid),
207 | new Error(
208 | 'Signature for commit sha could not be verified - Not a dependabot commit'
209 | )
210 | )
211 | })
212 | })
213 | await t.test('Should not throw error when', async t => {
214 | await t.test('All commits match the requirements', async t => {
215 | t.assert.doesNotThrow(() => verifyCommits(prCommitsValid), {})
216 | })
217 | })
218 | })
219 |
--------------------------------------------------------------------------------