diff --git a/__tests__/unit/validators/baseRef.test.js b/__tests__/unit/validators/baseRef.test.js index ae15ed69..d409bb60 100644 --- a/__tests__/unit/validators/baseRef.test.js +++ b/__tests__/unit/validators/baseRef.test.js @@ -1,6 +1,63 @@ const BaseRef = require('../../../lib/validators/baseRef') const Helper = require('../../../__fixtures__/unit/helper') +test('validateCheckSuite called for check_suite events', async () => { + // GIVEN + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef' + } + + const returnValue = {} + baseRef.validateCheckSuite = jest.fn() + baseRef.validateCheckSuite.mockReturnValueOnce(returnValue) + + // WHEN + const output = await baseRef.processValidate(mockCheckSuiteContext(['foo']), settings) + + // THEN + expect(output).toBe(returnValue) +}) + +test('validateStatus called for status events', async () => { + // GIVEN + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef' + } + + const returnValue = {} + baseRef.validateStatus = jest.fn() + baseRef.validateStatus.mockReturnValueOnce(returnValue) + + // WHEN + const output = await baseRef.processValidate(mockStatusContext(['foo']), settings) + + // THEN + expect(output).toBe(returnValue) +}) + +test('processOptions called for non-check-suite non-status events', async () => { + // GIVEN + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef' + } + + const returnValue = {} + baseRef.processOptions = jest.fn() + baseRef.processOptions.mockReturnValueOnce(returnValue) + + // WHEN + const output = await baseRef.processValidate(mockContext('foo'), settings) + + // THEN + expect(output).toBe(returnValue) +}) + test('fail gracefully if invalid regex', async () => { const baseRef = new BaseRef() @@ -158,8 +215,129 @@ const mockContext = baseRef => { return context } +test('fail when exclude regex is in baseRef of single pull request related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_exclude: { + regex: 'wip' + } + } + + const context = mockStatusContext(['WIP foo']) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('fail') +}) + +test('fail when exclude regex is in one baseRef of multiple pull requests related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_exclude: { + regex: 'wip' + } + } + + const context = mockStatusContext(['foo', 'WIP bar', 'baz']) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('fail') +}) + +test('pass when exclude regex is not in any baseRef of multiple pull requests related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_exclude: { + regex: 'wip' + } + } + + const context = mockStatusContext(['foo', 'bar', 'baz']) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('pass') +}) + +test('fail when include regex exists and there are no pull requests related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_include: { + regex: 'foo' + } + } + + const context = mockStatusContext([]) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('fail') +}) + +test('pass when exclude regex is only in baseRef of a closed pull request related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_exclude: { + regex: 'wip' + } + } + + const context = Helper.mockContext({ eventName: 'status' }) + const pulls = { + data: [ + { state: 'open', base: { ref: 'foo' } }, + { state: 'closed', base: { ref: 'wip bar' } } + ] + } + context.octokit.request = jest.fn() + context.octokit.request.mockReturnValueOnce(pulls) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('pass') +}) + +test('fail with mediaType when exclude regex is in baseRef of single pull request related to status', async () => { + const baseRef = new BaseRef() + + const settings = { + do: 'baseRef', + must_exclude: { + regex: 'wip' + }, + mediaType: { + previews: ['groot'] + } + } + + const context = mockStatusContext(['WIP foo']) + + const baseRefValidation = await baseRef.processValidate(context, settings) + expect(baseRefValidation.status).toBe('fail') + + expect(context.octokit.request.mock.calls[0][1].mediaType.previews[0]).toBe('groot') +}) + const mockCheckSuiteContext = baseRefs => { const context = Helper.mockContext({ eventName: 'check_suite' }) context.payload.check_suite.pull_requests = baseRefs.map(baseRef => ({ base: { ref: baseRef } })) return context } + +const mockStatusContext = baseRefs => { + const context = Helper.mockContext({ eventName: 'status' }) + const pulls = { + data: baseRefs.map(baseRef => ({ state: 'open', base: { ref: baseRef } })) + } + + context.octokit.request = jest.fn() + context.octokit.request.mockReturnValueOnce(pulls) + + return context +} diff --git a/docs/changelog.rst b/docs/changelog.rst index c0946123..bfcf7dc8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,6 @@ CHANGELOG ===================================== +| December 12, 2021: feat: Add support for status events to baseRef validator `#395 ` _ | November 25, 2021: feat: Add more supported events to baseRef validator `#395 ` _ | November 12, 2021 : feat: Add baseRef filter `#596 `_ | October 19, 2021 : feat: Add validator approval option to exclude users `#594 `_ diff --git a/docs/validators/baseRef.rst b/docs/validators/baseRef.rst index 7a674baa..9265f93a 100644 --- a/docs/validators/baseRef.rst +++ b/docs/validators/baseRef.rst @@ -12,6 +12,8 @@ BaseRef regex: 'feature-branch2' regex_flag: 'none' # Optional. Specify the flag for Regex. default is 'i', to disable default use 'none' message: 'Custom message...' + mediaType: # Optional. Required by status.* events to enable the groot preview on some Github Enterprise servers + previews: 'array' Simple example: @@ -23,7 +25,19 @@ Simple example: message: 'Merging into repo:master is forbidden' +Example with groot preview enabled (for status.* events on some older Github Enterprise servers) +:: + + - do: baseRef + must_include: + regex: 'master|main' + message: 'Auto-merging is only enabled for default branch' + mediaType: + previews: + - groot + + Supported Events: :: - 'pull_request.*', 'pull_request_review.*', 'check_suite.*' + 'pull_request.*', 'pull_request_review.*', 'check_suite.*', status.* diff --git a/lib/validators/baseRef.js b/lib/validators/baseRef.js index f88c0785..86ef513a 100644 --- a/lib/validators/baseRef.js +++ b/lib/validators/baseRef.js @@ -8,7 +8,8 @@ class BaseRef extends Validator { this.supportedEvents = [ 'pull_request.*', 'pull_request_review.*', - 'check_suite.*' + 'check_suite.*', + 'status.*' ] this.supportedSettings = { must_include: { @@ -20,6 +21,9 @@ class BaseRef extends Validator { regex: ['string', 'array'], regex_flag: 'string', message: 'string' + }, + mediaType: { + previews: 'array' } } } @@ -27,23 +31,57 @@ class BaseRef extends Validator { async validate (context, validationSettings) { const payload = this.getPayload(context) + const mediaType = validationSettings.mediaType + delete validationSettings.mediaType + if (context.eventName === 'check_suite') { return this.validateCheckSuite(payload, validationSettings) } + if (context.eventName === 'status') { + return this.validateStatus(context, validationSettings, mediaType) + } + return this.processOptions(validationSettings, payload.base.ref) } async validateCheckSuite (payload, validationSettings) { // A check_suite's payload contains multiple pull_requests // Need to make sure that each pull_request's base ref is valid + return this.validatePullRequests(payload.pull_requests, validationSettings) + } + + async validateStatus (context, validationSettings, mediaType) { + // The commit associated with a status can belong to multiple pull_requests + // Need to make sure that each "open" pull_request's base ref is valid + const request = { + owner: context.payload.repository.owner.login, + repo: context.payload.repository.name, + commit_sha: context.payload.sha + } + + if (mediaType) { + request.mediaType = mediaType + } + + const pulls = await context.octokit.request( + 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls', + request + ) + + const openPullRequests = pulls.data.filter(pullRequest => pullRequest.state === 'open') + + return this.validatePullRequests(openPullRequests, validationSettings) + } + + async validatePullRequests (pullRequests, validationSettings) { const validatorContext = { name: 'baseRef' } - const baseRefs = payload.pull_requests.map(pullRequest => pullRequest.base.ref) + const baseRefs = pullRequests.map(pullRequest => pullRequest.base.ref) - // If a check_suite has NO associated pull requests it is considered + // If an event has NO associated pull requests it is considered // a failed validation since there is no baseRef to validate if (baseRefs.length === 0) { - return constructOutput({ name: 'baseRef' }, undefined, validationSettings, { status: 'fail', description: 'No pull requests associated with check_suite' }) + return constructOutput({ name: 'baseRef' }, undefined, validationSettings, { status: 'fail', description: 'No pull requests associated with event' }) } const results = await Promise.all(baseRefs.map(