diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js index 8d42426..f727438 100644 --- a/__tests__/utils.test.js +++ b/__tests__/utils.test.js @@ -4,6 +4,29 @@ const { getInputs, state, validatePullRequest } = require('../src/utils') // Mock for dependencies (in this case, for the GitHub "core" module) jest.mock('@actions/core') +const basePullRequest = { + base: { + ref: '' + }, + head:{ + ref: '' + } +} + +const mockCompare = jest.fn() +mockCompare.mockReturnValue({ + data: { + status: 'before', + 'behind_by': 0 + } +}) + +const mockGetPullRequest = jest.fn() +mockGetPullRequest.mockReturnValue({ + data: { + mergeable: true, + } +}) // Mock for the object `github` that is passed to the action const github = { rest: { @@ -12,7 +35,10 @@ const github = { }, pulls: { createReview: jest.fn(), - get: jest.fn() + get: mockGetPullRequest + }, + repos: { + compare: mockCompare } } } @@ -315,9 +341,94 @@ describe('Tests for `validatePullRequest` function', () => { 'The pull-request is associated with a dependency group but the action is not configured to handle dependency groups.' ) }) + + test('should return `true` after compare commits', async () => { + mockCompare.mockReturnValueOnce({ + data: { + status: 'behind', + 'behind_by': 2 + } + }) + + const pullRequest = { + ...basePullRequest, + merged: false, + state: 'open', + draft: false, + user: { + login: 'dependabot[bot]' + }, + mergeable: true, + mergeable_state: 'behind' + } + + const config = { + inputs: { + approveOnly: false, + handleSubmodule: true, + handleDependencyGroup: true + }, + metadata: {} + } + + const result = await validatePullRequest( + github, + repository, + pullRequest, + config + ) + + expect(result.execute).toBe(true) + expect(result.body).toBe('@dependabot rebase') + expect(result.validationState).toBe(state.rebased) + expect(result.validationMessage).toBe('The pull request will be rebased.') + }) + + test('should return `true` when pull request has a mergeable state of `null`', async () => { + mockGetPullRequest.mockReturnValueOnce({ + data: { + mergeable: null, + mergeable_state: 'behind' + } + }) + + const pullRequest = { + ...basePullRequest, + merged: false, + state: 'open', + draft: false, + user: { + login: 'dependabot[bot]' + }, + mergeable: null, + mergeable_state: 'behind' + } + + const config = { + inputs: { + approveOnly: false, + handleSubmodule: true, + handleDependencyGroup: true + }, + metadata: {} + } + + const result = await validatePullRequest( + github, + repository, + pullRequest, + config + ) + + expect(result.execute).toBe(true) + expect(result.body).toBe('@dependabot rebase') + expect(result.validationState).toBe(state.rebased) + expect(result.validationMessage).toBe('The pull request will be rebased.') + }, 15000) test('should return `true` when pull request has a mergeable state of `behind`', async () => { const pullRequest = { + ...basePullRequest, merged: false, state: 'open', draft: false, @@ -354,6 +465,7 @@ describe('Tests for `validatePullRequest` function', () => { 'should return `false` when pull request has a mergeable state of `%s`', async mergeableState => { const pullRequest = { + ...basePullRequest, merged: false, state: 'open', draft: false, @@ -395,6 +507,7 @@ describe('Tests for `validatePullRequest` function', () => { 'should return `false` when pull request has a target `%s` and update type `%s`', async (target, updateType) => { const pullRequest = { + ...basePullRequest, merged: false, state: 'open', draft: false, @@ -448,6 +561,7 @@ describe('Tests for `validatePullRequest` function', () => { 'should return `true` when pull request has a target `%s` and update type `%s` and command `%s`', async (target, updateType, cmd) => { const pullRequest = { + ...basePullRequest, merged: false, state: 'open', draft: false, @@ -486,6 +600,7 @@ describe('Tests for `validatePullRequest` function', () => { test('should return `true` when pull request has approve-only enabled', async () => { const pullRequest = { + ...basePullRequest, merged: false, state: 'open', draft: false, diff --git a/badges/coverage.svg b/badges/coverage.svg index f0a3125..c37dda1 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 85.14%Coverage85.14% \ No newline at end of file +Coverage: 95.32%Coverage95.32% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index db3bdf0..29add10 100644 --- a/dist/index.js +++ b/dist/index.js @@ -24727,6 +24727,15 @@ async function approvePullRequest(github, repo, pull_request, body) { }) } +async function comparePullRequestToBase(github, repo, pull_request) { + return await github.rest.repos.compareCommits({ + owner: repo.owner.login, + repo: repo.name, + base: pull_request.base.ref, + head: pull_request.head.ref + }) +} + async function getPullRequest(github, repo, pull_request) { return await github.rest.pulls.get({ owner: repo.owner.login, @@ -24801,10 +24810,12 @@ module.exports = async function run({ github, context, inputs, metadata }) { return core.setFailed(msg) } + core.startGroup('Input Values') core.debug(`GitHub: ${JSON.stringify(github, null, 2)}`) core.debug(`Context: ${JSON.stringify(context, null, 2)}`) core.debug(`Inputs: ${JSON.stringify(inputs, null, 2)}`) core.debug(`Metadata: ${JSON.stringify(metadata, null, 2)}`) + core.endGroup() const config = { inputs: getInputs(inputs), @@ -24923,152 +24934,180 @@ function getMetadata(metadata) { } async function validatePullRequest(github, repository, pull_request, config) { - if (pull_request.state !== 'open' || pull_request.merged) { - return { - execute: false, - validationState: state.skipped, - validationMessage: 'Pull request is not open or already merged.' + core.startGroup('Validate Pull Request') + try { + if (pull_request.state !== 'open' || pull_request.merged) { + return { + execute: false, + validationState: state.skipped, + validationMessage: 'Pull request is not open or already merged.' + } } - } - if (pull_request.draft) { - return { - execute: false, - validationState: state.skipped, - validationMessage: 'Pull request is a draft.' + if (pull_request.draft) { + return { + execute: false, + validationState: state.skipped, + validationMessage: 'Pull request is a draft.' + } } - } - if ( - !config.inputs.skipVerification && - pull_request.user.login !== dependabotUser - ) { - return { - execute: false, - validationState: state.skipped, - validationMessage: `The Commit/PullRequest was not created by ${dependabotUser}.` + if ( + !config.inputs.skipVerification && + pull_request.user.login !== dependabotUser + ) { + return { + execute: false, + validationState: state.skipped, + validationMessage: `The Commit/PullRequest was not created by ${dependabotUser}.` + } } - } - let targetUpdateType = config.inputs.target - if (config.metadata.ecosystem === 'gitsubmodule') { - if (!config.inputs.handleSubmodule) { + let targetUpdateType = config.inputs.target + if (config.metadata.ecosystem === 'gitsubmodule') { + if (!config.inputs.handleSubmodule) { + return { + execute: false, + validationState: state.skipped, + validationMessage: + 'The pull-request is associated with a submodule but the action is not configured to handle submodules.' + } + } else { + targetUpdateType = updateTypes.any + } + } + + if ( + !config.inputs.handleDependencyGroup && + config.metadata.dependecyGroup !== '' + ) { return { execute: false, validationState: state.skipped, validationMessage: - 'The pull-request is associated with a submodule but the action is not configured to handle submodules.' + 'The pull-request is associated with a dependency group but the action is not configured to handle dependency groups.' } - } else { - targetUpdateType = updateTypes.any } - } - if ( - !config.inputs.handleDependencyGroup && - config.metadata.dependecyGroup !== '' - ) { - return { - execute: false, - validationState: state.skipped, - validationMessage: - 'The pull-request is associated with a dependency group but the action is not configured to handle dependency groups.' + const { data: compareData } = await cmd.comparePullRequestToBase( + github, + repository, + pull_request + ) + if (compareData.status === 'behind' && compareData.behind_by > 0) { + return { + execute: true, + body: `@dependabot ${commandText.rebase}`, + cmd: cmd.addComment, + validationState: state.rebased, + validationMessage: 'The pull request will be rebased.' + } } - } - - let retryCount = 0 - let mergeabilityResolved = pull_request.mergeable !== null - - while (!mergeabilityResolved && retryCount < 5) { - try { - core.info( - `Pull request mergeability is not resolved. Retry count: ${retryCount}` - ) - const { data } = await cmd.getPullRequest( - github, - repository, - pull_request - ) + let retryCount = 0 + let mergeabilityResolved = pull_request.mergeable !== null - if (data.mergeable === null || data.mergeable === undefined) { + while (!mergeabilityResolved && retryCount < 5) { + try { core.info( - `Pull request mergeability is not yet resolved... retrying in 5 seconds.` + `Pull request mergeability is not resolved. Retry count: ${retryCount}` ) - retryCount++ - await new Promise(resolve => setTimeout(resolve, 5000)) - } else { - mergeabilityResolved = true + + const { data: prData } = await cmd.getPullRequest( + github, + repository, + pull_request + ) + + if (prData.mergeable === null || prData.mergeable === undefined) { + core.info( + `Pull request mergeability is not yet resolved... retrying in 5 seconds.` + ) + retryCount++ + await new Promise(resolve => setTimeout(resolve, 5000)) + } else { + mergeabilityResolved = true + } + } catch (apiError) { + return { + execute: false, + validationState: state.skipped, + validationMessage: `An error occurred fetching the PR from Github: ${JSON.stringify( + apiError + )}` + } } - } catch (apiError) { + } + + if (pull_request.mergeable_state === 'behind') { + return { + execute: true, + body: `@dependabot ${commandText.rebase}`, + cmd: cmd.addComment, + validationState: state.rebased, + validationMessage: 'The pull request will be rebased.' + } + } + + if ( + pull_request.mergeable_state === 'blocked' || + pull_request.mergeable_state === 'dirty' + ) { + core.info( + `Pull request merge is blocked by conflicts. State: ${pull_request.mergeable_state}` + ) return { execute: false, validationState: state.skipped, - validationMessage: `An error occurred fetching the PR from Github: ${JSON.stringify( - apiError - )}` + validationMessage: + 'Pull request merge is blocked by conflicts, please resolve them manually.' } } - } - if (pull_request.mergeable_state === 'behind') { - return { - execute: true, - body: `@dependabot ${commandText.rebase}`, - cmd: cmd.addComment, - validationState: state.rebased, - validationMessage: 'The pull request will be rebased.' - } - } + const treatVersion = + targetUpdateType === updateTypes.any || + updateTypesPriority.indexOf(config.metadata.updateType) <= + updateTypesPriority.indexOf(targetUpdateType) - if ( - pull_request.mergeable_state === 'blocked' || - pull_request.mergeable_state === 'dirty' - ) { core.info( - `Pull request merge is blocked by conflicts. State: ${pull_request.mergeable_state}` + `Check package '${config.metadata.dependecyNames}' - Old: '${config.metadata.previousVersion}' New: '${config.metadata.newVersion}'` ) - return { - execute: false, - validationState: state.skipped, - validationMessage: - 'Pull request merge is blocked by conflicts, please resolve them manually.' + core.info(`Is the package version treated? - ${treatVersion}`) + if (!treatVersion) { + return { + execute: false, + validationState: state.skipped, + validationMessage: `The package version is not treated by the action.` + } } - } - - const treatVersion = - targetUpdateType === updateTypes.any || - updateTypesPriority.indexOf(config.metadata.updateType) <= - updateTypesPriority.indexOf(targetUpdateType) - core.info( - `Check package '${config.metadata.dependecyNames}' - Old: '${config.metadata.previousVersion}' New: '${config.metadata.newVersion}'` - ) - core.info(`Is the package version treated? - ${treatVersion}`) - if (!treatVersion) { - return { - execute: false, - validationState: state.skipped, - validationMessage: `The package version is not treated by the action.` + if (config.inputs.approveOnly) { + return { + execute: true, + body: 'Approved by DependaMerge.', + cmd: cmd.approvePullRequest, + validationState: state.approved, + validationMessage: 'The pull request will be approved.' + } } - } - if (config.inputs.approveOnly) { return { execute: true, - body: 'Approved by DependaMerge.', + body: `@dependabot ${config.inputs.commandMethod}`, cmd: cmd.approvePullRequest, - validationState: state.approved, - validationMessage: 'The pull request will be approved.' + validationState: state.merged, + validationMessage: 'The pull request will be merged.' } - } - - return { - execute: true, - body: `@dependabot ${config.inputs.commandMethod}`, - cmd: cmd.approvePullRequest, - validationState: state.merged, - validationMessage: 'The pull request will be merged.' + } catch (validationError) { + return { + execute: false, + validationState: state.failed, + validationMessage: `An error occurred validating the PR: ${JSON.stringify( + validationError + )}` + } + } finally { + core.endGroup() } } diff --git a/package-lock.json b/package-lock.json index 0e1269b..e7fb6e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dependamerge-action", - "version": "1.0.3", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dependamerge-action", - "version": "1.0.3", + "version": "1.1.1", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1" diff --git a/package.json b/package.json index 4600df5..e92bf57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dependamerge-action", "description": "GitHub action that automatically validates, approves and merges pull requests for branches created by dependabot[bot].", - "version": "1.0.3", + "version": "1.1.1", "author": "dailydevops", "private": true, "homepage": "https://github.com/dailydevops/dependamerge-action#readme", diff --git a/src/api.js b/src/api.js index 2a3821e..ef1a216 100644 --- a/src/api.js +++ b/src/api.js @@ -19,6 +19,14 @@ async function approvePullRequest(github, repo, pull_request, body) { }) } +async function comparePullRequest(github, repo, pull_request) { + return await github.rest.repos.compare({ + owner: repo.owner.login, + repo: repo.name, + basehead: `${pull_request.base.ref}...${pull_request.head.ref}` + }) +} + async function getPullRequest(github, repo, pull_request) { return await github.rest.pulls.get({ owner: repo.owner.login, @@ -30,5 +38,6 @@ async function getPullRequest(github, repo, pull_request) { module.exports = { addComment, approvePullRequest, + comparePullRequest, getPullRequest } diff --git a/src/index.js b/src/index.js index 0c2d546..59ba0ce 100644 --- a/src/index.js +++ b/src/index.js @@ -51,10 +51,12 @@ module.exports = async function run({ github, context, inputs, metadata }) { return core.setFailed(msg) } + core.startGroup('Input Values') core.debug(`GitHub: ${JSON.stringify(github, null, 2)}`) core.debug(`Context: ${JSON.stringify(context, null, 2)}`) core.debug(`Inputs: ${JSON.stringify(inputs, null, 2)}`) core.debug(`Metadata: ${JSON.stringify(metadata, null, 2)}`) + core.endGroup() const config = { inputs: getInputs(inputs), diff --git a/src/utils.js b/src/utils.js index 0b0cd1e..f8f26b6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,12 @@ 'use strict' const core = require('@actions/core') -const cmd = require('./api') +const { + addComment, + approvePullRequest, + comparePullRequest, + getPullRequest +} = require('./api') const dependabotUser = 'dependabot[bot]' // const dependabotCommitter = 'GitHub' @@ -135,6 +140,17 @@ async function validatePullRequest(github, repository, pull_request, config) { } } + const { data: compareData } = await comparePullRequest(github, repository, pull_request) + if (compareData && compareData.status === 'behind' && compareData.behind_by > 0) { + return { + execute: true, + body: `@dependabot ${commandText.rebase}`, + cmd: addComment, + validationState: state.rebased, + validationMessage: 'The pull request will be rebased.' + } + } + let retryCount = 0 let mergeabilityResolved = pull_request.mergeable !== null @@ -144,13 +160,13 @@ async function validatePullRequest(github, repository, pull_request, config) { `Pull request mergeability is not resolved. Retry count: ${retryCount}` ) - const { data } = await cmd.getPullRequest( + const { data: prData } = await getPullRequest( github, repository, pull_request ) - if (data.mergeable === null || data.mergeable === undefined) { + if (prData.mergeable === null || prData.mergeable === undefined) { core.info( `Pull request mergeability is not yet resolved... retrying in 5 seconds.` ) @@ -174,7 +190,7 @@ async function validatePullRequest(github, repository, pull_request, config) { return { execute: true, body: `@dependabot ${commandText.rebase}`, - cmd: cmd.addComment, + cmd: addComment, validationState: state.rebased, validationMessage: 'The pull request will be rebased.' } @@ -216,7 +232,7 @@ async function validatePullRequest(github, repository, pull_request, config) { return { execute: true, body: 'Approved by DependaMerge.', - cmd: cmd.approvePullRequest, + cmd: approvePullRequest, validationState: state.approved, validationMessage: 'The pull request will be approved.' } @@ -225,7 +241,7 @@ async function validatePullRequest(github, repository, pull_request, config) { return { execute: true, body: `@dependabot ${config.inputs.commandMethod}`, - cmd: cmd.approvePullRequest, + cmd: approvePullRequest, validationState: state.merged, validationMessage: 'The pull request will be merged.' }