diff --git a/README.md b/README.md index 4dc4ca96..0f895b5b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ All configuration values, except `GITHUB_TOKEN`, are optional. - `PR_LABELS`: Controls which labels _autoupdate_ will look for when monitoring PRs. Only used if `PR_FILTER="labelled"`. This can be either a single label or a comma-separated list of labels. +- `PR_READY_STATE`: Controls how _autoupdate_ monitors pull requests based on their current [draft / ready for review](https://help.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request) state. Possible values are: + + - `"all"`: (default): No filter, _autoupdate_ will monitor and update pull requests regardless of ready state. + - `"ready_for_review"`: Only monitor PRs that are not currently in the draft state. + - `"draft"`: Only monitor PRs that are currently in the draft state. + - `EXCLUDED_LABELS`: Controls which labels _autoupdate_ will ignore when evaluating otherwise-included PRs. This option works with all `PR_FILTER` options and can be either a single label or a comma-separated list of labels. - `MERGE_MSG`: A custom message to use when creating the merge commit from the destination branch to your pull request's branch. diff --git a/src/autoupdater.ts b/src/autoupdater.ts index 03ed161f..d7a96e79 100644 --- a/src/autoupdater.ts +++ b/src/autoupdater.ts @@ -265,6 +265,21 @@ export class AutoUpdater { } } + const readyStateFilter = this.config.pullRequestReadyState(); + if (readyStateFilter !== 'all') { + ghCore.info('Checking PR ready state'); + + if (readyStateFilter === 'draft' && !pull.draft) { + ghCore.info('PR_READY_STATE=draft and pull request is not draft, skipping update.'); + return false; + } + + if (readyStateFilter === 'ready_for_review' && pull.draft) { + ghCore.info('PR_READY_STATE=ready_for_review and pull request is draft, skipping update.'); + return false; + } + } + const prFilter = this.config.pullRequestFilter(); ghCore.info( diff --git a/src/config-loader.ts b/src/config-loader.ts index dd2319cb..be5ed4ff 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -69,6 +69,10 @@ export class ConfigLoader { return this.getValue('GITHUB_REPOSITORY', true, ''); } + pullRequestReadyState(): string { + return this.getValue('PR_READY_STATE', false, 'all'); + } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types getValue(key: string, required = false, defaultVal?: any): any { if ( diff --git a/test/autoupdate.test.ts b/test/autoupdate.test.ts index 6512f068..2f597e86 100644 --- a/test/autoupdate.test.ts +++ b/test/autoupdate.test.ts @@ -120,6 +120,7 @@ const validPull = { }, }, }, + draft: false, }; const clonePull = () => JSON.parse(JSON.stringify(validPull)); @@ -452,6 +453,77 @@ describe('test `prNeedsUpdate`', () => { expect(config.pullRequestFilter).toHaveBeenCalled(); expect(config.excludedLabels).toHaveBeenCalled(); }); + + describe('pull request ready state filtering', () => { + const readyPull = clonePull(); + const draftPull = Object.assign(clonePull(), { draft: true }); + + const nockCompareRequest = () => + nock('https://api.github.com:443') + .get(`/repos/${owner}/${repo}/compare/${head}...${base}`) + .reply(200, { + behind_by: 1, + }); + + beforeEach(() => { + (config.excludedLabels as jest.Mock).mockReturnValue([]); + }); + + test('pull request ready state is not filtered', async () => { + (config.pullRequestReadyState as jest.Mock).mockReturnValue('all'); + + const readyScope = nockCompareRequest(); + const draftScope = nockCompareRequest(); + + const updater = new AutoUpdater(config, emptyEvent); + + const readyPullNeedsUpdate = await updater.prNeedsUpdate(readyPull); + const draftPullNeedsUpdate = await updater.prNeedsUpdate(draftPull); + + expect(readyPullNeedsUpdate).toEqual(true); + expect(draftPullNeedsUpdate).toEqual(true); + expect(config.pullRequestReadyState).toHaveBeenCalled(); + expect(readyScope.isDone()).toEqual(true); + expect(draftScope.isDone()).toEqual(true); + }); + + test('pull request is filtered to drafts only', async () => { + (config.pullRequestReadyState as jest.Mock).mockReturnValue('draft'); + + const readyScope = nockCompareRequest(); + const draftScope = nockCompareRequest(); + + const updater = new AutoUpdater(config, emptyEvent); + + const readyPullNeedsUpdate = await updater.prNeedsUpdate(readyPull); + const draftPullNeedsUpdate = await updater.prNeedsUpdate(draftPull); + + expect(readyPullNeedsUpdate).toEqual(false); + expect(draftPullNeedsUpdate).toEqual(true); + expect(config.pullRequestReadyState).toHaveBeenCalled(); + expect(readyScope.isDone()).toEqual(true); + expect(draftScope.isDone()).toEqual(true); + }); + + test('pull request ready state is filtered to ready PRs only', async () => { + (config.pullRequestReadyState as jest.Mock).mockReturnValue( + 'ready_for_review', + ); + + const readyScope = nockCompareRequest(); + const draftScope = nockCompareRequest(); + + const updater = new AutoUpdater(config, emptyEvent); + const readyPullNeedsUpdate = await updater.prNeedsUpdate(readyPull); + const draftPullNeedsUpdate = await updater.prNeedsUpdate(draftPull); + + expect(readyPullNeedsUpdate).toEqual(true); + expect(draftPullNeedsUpdate).toEqual(false); + expect(config.pullRequestReadyState).toHaveBeenCalled(); + expect(readyScope.isDone()).toEqual(true); + expect(draftScope.isDone()).toEqual(true); + }); + }); }); describe('test `handlePush`', () => { diff --git a/test/config-loader.test.ts b/test/config-loader.test.ts index 03a5a88a..1185e3af 100644 --- a/test/config-loader.test.ts +++ b/test/config-loader.test.ts @@ -85,6 +85,13 @@ const tests = [ default: '', type: 'string', }, + { + name: 'pullRequestReadyState', + envVar: 'PR_READY_STATE', + required: false, + default: 'all', + type: 'string', + }, ]; for (const testDef of tests) {