From f71123a6f703377ddcfc8b5936ac224202592fc0 Mon Sep 17 00:00:00 2001 From: Geoffrey Testelin Date: Tue, 19 Jan 2021 11:54:16 +0100 Subject: [PATCH] feat(exempt): add new options to exempt the milestones (#279) * feat(exempt): add new options to exempt the milestones closes #270 * test(milestones): add coverage * test(issue): add coverage * chore(rebase): fix all errors due to the rebase also made some changes regarding the change I made with the lint scripts and prettier. I did not saw that some scripts were already here and I created to more to keep the old ones as well * test(milestone): add coverage * chore(index): update index * fix(checks): remove checks over optional number options the code was actually handling the case where the values are NaN so it's fine --- README.md | 21 + __tests__/main.test.ts | 1033 +++++++++++++---- action.yml | 20 +- dist/index.js | 251 ++-- package.json | 10 +- src/IssueProcessor.ts | 122 +- src/classes/issue.spec.ts | 208 ++++ src/classes/issue.ts | 48 + .../{ => loggers}/issue-logger.spec.ts | 2 +- src/classes/{ => loggers}/issue-logger.ts | 4 +- src/classes/{ => loggers}/logger.spec.ts | 0 src/classes/{ => loggers}/logger.ts | 0 src/classes/milestones.spec.ts | 596 ++++++++++ src/classes/milestones.ts | 59 + src/functions/is-labeled.spec.ts | 2 +- src/functions/is-labeled.ts | 7 +- src/functions/is-pull-request.spec.ts | 2 +- src/functions/is-pull-request.ts | 2 +- src/functions/labels-to-list.spec.ts | 141 --- src/functions/labels-to-list.ts | 23 - src/functions/words-to-list.spec.ts | 137 +++ src/functions/words-to-list.ts | 23 + src/interfaces/issue.ts | 15 + src/interfaces/milestone.ts | 3 + src/main.ts | 9 +- 25 files changed, 2163 insertions(+), 575 deletions(-) create mode 100644 src/classes/issue.spec.ts create mode 100644 src/classes/issue.ts rename src/classes/{ => loggers}/issue-logger.spec.ts (97%) rename src/classes/{ => loggers}/issue-logger.ts (89%) rename src/classes/{ => loggers}/logger.spec.ts (100%) rename src/classes/{ => loggers}/logger.ts (100%) create mode 100644 src/classes/milestones.spec.ts create mode 100644 src/classes/milestones.ts delete mode 100644 src/functions/labels-to-list.spec.ts delete mode 100644 src/functions/labels-to-list.ts create mode 100644 src/functions/words-to-list.spec.ts create mode 100644 src/functions/words-to-list.ts create mode 100644 src/interfaces/issue.ts create mode 100644 src/interfaces/milestone.ts diff --git a/README.md b/README.md index dd19ebf45..3100c8bf3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ $ npm test | `close-pr-label` | Label to apply on the closing pr. | Optional | | `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional | | `exempt-pr-labels` | Labels on the pr exempted from being marked as stale. | Optional | +| `exempt-milestones` | Milestones on an issue or a pr exempted from being marked as stale. | Optional | +| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional | +| `exempt-pr-milestones` | Milestones on the pr exempted from being marked as stale (override `exempt-milestones`). | Optional | | `only-labels` | Only labels checked for stale issue/pr. | Optional | | `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional | | `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. _Defaults to **true**_ | Optional | @@ -181,6 +184,24 @@ jobs: start-date: '2020-18-04T00:00:00Z' // ISO 8601 or RFC 2822 ``` +Avoid stale for specific milestones: + +```yaml +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + exempt-issue-milestones: 'future,alpha,beta' + exempt-pr-milestones: 'bugfix,improvement' +``` + ### Debugging To see debug output from this action, you must set the secret `ACTIONS_STEP_DEBUG` to `true` in your repository. You can run this action in debug only mode (no actions will be taken on your issues) by passing `debug-only` `true` as an argument to the action. diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 55bdb163d..aad95e563 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,12 +1,11 @@ import * as github from '@actions/github'; -import { - Issue, - IssueProcessor, - IssueProcessorOptions -} from '../src/IssueProcessor'; +import {Issue} from '../src/classes/issue'; + +import {IssueProcessor, IssueProcessorOptions} from '../src/IssueProcessor'; import {IsoDateString} from '../src/types/iso-date-string'; function generateIssue( + options: IssueProcessorOptions, id: number, title: string, updatedAt: IsoDateString, @@ -14,9 +13,10 @@ function generateIssue( isPullRequest: boolean = false, labels: string[] = [], isClosed: boolean = false, - isLocked: boolean = false + isLocked: boolean = false, + milestone = '' ): Issue { - return { + return new Issue(options, { number: id, labels: labels.map(l => { return {name: l}; @@ -26,8 +26,11 @@ function generateIssue( updated_at: updatedAt, pull_request: isPullRequest ? {} : null, state: isClosed ? 'closed' : 'open', - locked: isLocked - }; + locked: isLocked, + milestone: { + title: milestone + } + }); } const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ @@ -56,7 +59,10 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ skipStaleIssueMessage: false, skipStalePrMessage: false, deleteBranch: false, - startDate: '' + startDate: '', + exemptMilestones: '', + exemptIssueMilestones: '', + exemptPrMilestones: '' }); test('empty issue list results in 1 operation', async () => { @@ -76,15 +82,13 @@ test('empty issue list results in 1 operation', async () => { }); test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to 0', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, daysBeforeClose: 0 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -102,20 +106,21 @@ test('processing an issue with no label will make it stale and close it, if it i test('processing an issue with no label and a start date as ECMAScript epoch in seconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2000 = 946681200000; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2000.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2000 = 946681200000; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2000.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -133,20 +138,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in test('processing an issue with no label and a start date as ECMAScript epoch in seconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2021 = 1609455600000; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2021.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2021 = 1609455600000; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2021.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -164,20 +170,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2000 = 946681200000000; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2000.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2000 = 946681200000000; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2000.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -195,20 +202,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2021 = 1609455600000; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2021.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2021 = 1609455600000; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2021.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -226,20 +234,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in test('processing an issue with no label and a start date as ISO 8601 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2000 = '2000-01-01T00:00:00Z'; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2000.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2000 = '2000-01-01T00:00:00Z'; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2000.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -257,20 +266,21 @@ test('processing an issue with no label and a start date as ISO 8601 being befor test('processing an issue with no label and a start date as ISO 8601 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2021 = '2021-01-01T00:00:00Z'; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2021.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2021 = '2021-01-01T00:00:00Z'; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2021.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -288,20 +298,21 @@ test('processing an issue with no label and a start date as ISO 8601 being after test('processing an issue with no label and a start date as RFC 2822 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2000 = 'January 1, 2000 00:00:00'; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2000.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2000 = 'January 1, 2000 00:00:00'; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2000.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -319,20 +330,21 @@ test('processing an issue with no label and a start date as RFC 2822 being befor test('processing an issue with no label and a start date as RFC 2822 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { expect.assertions(2); + const january2021 = 'January 1, 2021 00:00:00'; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0, + startDate: january2021.toString() + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z' ) ]; - const january2021 = 'January 1, 2021 00:00:00'; - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 0, - startDate: january2021.toString() - }; const processor = new IssueProcessor( opts, async () => 'abot', @@ -349,16 +361,14 @@ test('processing an issue with no label and a start date as RFC 2822 being after }); test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to 0', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, daysBeforeClose: 1, daysBeforeIssueClose: 0 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -376,16 +386,14 @@ test('processing an issue with no label will make it stale and close it, if it i }); test('processing an issue with no label will make it stale and not close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to > 0', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, daysBeforeClose: 1, daysBeforeIssueClose: 1 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -402,15 +410,13 @@ test('processing an issue with no label will make it stale and not close it, if }); test('processing an issue with no label will make it stale and not close it if days-before-close is set to > 0', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, daysBeforeClose: 15 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -427,16 +433,14 @@ test('processing an issue with no label will make it stale and not close it if d }); test('processing an issue with no label will make it stale and not close it if days-before-close is set to -1 and days-before-issue-close is set to > 0', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, daysBeforeClose: -1, daysBeforeIssueClose: 15 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -453,16 +457,14 @@ test('processing an issue with no label will make it stale and not close it if d }); test('processing an issue with no label will not make it stale if days-before-stale is set to -1', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, staleIssueMessage: '', daysBeforeStale: -1 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -479,17 +481,15 @@ test('processing an issue with no label will not make it stale if days-before-st }); test('processing an issue with no label will not make it stale if days-before-stale and days-before-issue-stale are set to -1', async () => { - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') - ]; - const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, staleIssueMessage: '', daysBeforeStale: -1, daysBeforeIssueStale: -1 }; - + const TestIssueList: Issue[] = [ + generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z') + ]; const processor = new IssueProcessor( opts, async () => 'abot', @@ -510,11 +510,14 @@ test('processing an issue with no label will make it stale but not close it', as // stale but not close-able, based on default settings let issueDate = new Date(); issueDate.setDate(issueDate.getDate() - 2); - const TestIssueList: Issue[] = [ - generateIssue(1, 'An issue with no label', issueDate.toDateString()) + generateIssue( + DefaultProcessorOptions, + 1, + 'An issue with no label', + issueDate.toDateString() + ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -531,8 +534,13 @@ test('processing an issue with no label will make it stale but not close it', as }); test('processing a stale issue will close it', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30 + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale issue that should be closed', '2020-01-01T17:00:00Z', @@ -541,12 +549,6 @@ test('processing a stale issue will close it', async () => { ['Stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 30 - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -563,8 +565,13 @@ test('processing a stale issue will close it', async () => { }); test('processing a stale issue containing a space in the label will close it', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + staleIssueLabel: 'state: stale' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale issue that should be closed', '2020-01-01T17:00:00Z', @@ -573,12 +580,6 @@ test('processing a stale issue containing a space in the label will close it', a ['state: stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - staleIssueLabel: 'state: stale' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -595,8 +596,13 @@ test('processing a stale issue containing a space in the label will close it', a }); test('processing a stale issue containing a slash in the label will close it', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + staleIssueLabel: 'lifecycle/stale' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale issue that should be closed', '2020-01-01T17:00:00Z', @@ -605,12 +611,6 @@ test('processing a stale issue containing a slash in the label will close it', a ['lifecycle/stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - staleIssueLabel: 'lifecycle/stale' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -627,8 +627,14 @@ test('processing a stale issue containing a slash in the label will close it', a }); test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30, + daysBeforeIssueStale: 30 + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale issue that should be closed', '2020-01-01T17:00:00Z', @@ -637,13 +643,6 @@ test('processing a stale issue will close it when days-before-issue-stale overri ['Stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 30, - daysBeforeIssueStale: 30 - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -660,8 +659,13 @@ test('processing a stale issue will close it when days-before-issue-stale overri }); test('processing a stale PR will close it', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30 + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale PR that should be closed', '2020-01-01T17:00:00Z', @@ -670,12 +674,6 @@ test('processing a stale PR will close it', async () => { ['Stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 30 - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -692,8 +690,14 @@ test('processing a stale PR will close it', async () => { }); test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30, + daysBeforePrClose: 30 + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'A stale PR that should be closed', '2020-01-01T17:00:00Z', @@ -702,13 +706,6 @@ test('processing a stale PR will close it when days-before-pr-stale override day ['Stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - daysBeforeClose: 30, - daysBeforePrClose: 30 - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -725,8 +722,14 @@ test('processing a stale PR will close it when days-before-pr-stale override day }); test('processing a stale issue will close it even if configured not to mark as stale', async () => { + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: -1, + staleIssueMessage: '' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', @@ -735,13 +738,6 @@ test('processing a stale issue will close it even if configured not to mark as s ['Stale'] ) ]; - - const opts = { - ...DefaultProcessorOptions, - daysBeforeStale: -1, - staleIssueMessage: '' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -758,8 +754,15 @@ test('processing a stale issue will close it even if configured not to mark as s }); test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => { + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: 0, + daysBeforeIssueStale: -1, + staleIssueMessage: '' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', @@ -768,14 +771,6 @@ test('processing a stale issue will close it even if configured not to mark as s ['Stale'] ) ]; - - const opts = { - ...DefaultProcessorOptions, - daysBeforeStale: 0, - daysBeforeIssueStale: -1, - staleIssueMessage: '' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -792,8 +787,14 @@ test('processing a stale issue will close it even if configured not to mark as s }); test('processing a stale PR will close it even if configured not to mark as stale', async () => { + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: -1, + stalePrMessage: '' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', @@ -802,13 +803,6 @@ test('processing a stale PR will close it even if configured not to mark as stal ['Stale'] ) ]; - - const opts = { - ...DefaultProcessorOptions, - daysBeforeStale: -1, - stalePrMessage: '' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -825,8 +819,15 @@ test('processing a stale PR will close it even if configured not to mark as stal }); test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => { + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: 0, + daysBeforePrStale: -1, + stalePrMessage: '' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z', @@ -835,14 +836,6 @@ test('processing a stale PR will close it even if configured not to mark as stal ['Stale'] ) ]; - - const opts = { - ...DefaultProcessorOptions, - daysBeforeStale: 0, - daysBeforePrStale: -1, - stalePrMessage: '' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -861,6 +854,7 @@ test('processing a stale PR will close it even if configured not to mark as stal test('closed issues will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A closed issue that will not be marked', '2020-01-01T17:00:00Z', @@ -870,7 +864,6 @@ test('closed issues will not be marked stale', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -888,6 +881,7 @@ test('closed issues will not be marked stale', async () => { test('stale closed issues will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A stale closed issue', '2020-01-01T17:00:00Z', @@ -897,7 +891,6 @@ test('stale closed issues will not be closed', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -916,6 +909,7 @@ test('stale closed issues will not be closed', async () => { test('closed prs will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A closed PR that will not be marked', '2020-01-01T17:00:00Z', @@ -925,7 +919,6 @@ test('closed prs will not be marked stale', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -944,6 +937,7 @@ test('closed prs will not be marked stale', async () => { test('stale closed prs will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A stale closed PR that will not be closed again', '2020-01-01T17:00:00Z', @@ -953,7 +947,6 @@ test('stale closed prs will not be closed', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -972,6 +965,7 @@ test('stale closed prs will not be closed', async () => { test('locked issues will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A locked issue that will not be stale', '2020-01-01T17:00:00Z', @@ -982,7 +976,6 @@ test('locked issues will not be marked stale', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -999,6 +992,7 @@ test('locked issues will not be marked stale', async () => { test('stale locked issues will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A stale locked issue that will not be closed', '2020-01-01T17:00:00Z', @@ -1009,7 +1003,6 @@ test('stale locked issues will not be closed', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -1028,6 +1021,7 @@ test('stale locked issues will not be closed', async () => { test('locked prs will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A locked PR that will not be marked stale', '2020-01-01T17:00:00Z', @@ -1038,7 +1032,6 @@ test('locked prs will not be marked stale', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -1055,6 +1048,7 @@ test('locked prs will not be marked stale', async () => { test('stale locked prs will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( + DefaultProcessorOptions, 1, 'A stale locked PR that will not be closed', '2020-01-01T17:00:00Z', @@ -1065,7 +1059,6 @@ test('stale locked prs will not be closed', async () => { true ) ]; - const processor = new IssueProcessor( DefaultProcessorOptions, async () => 'abot', @@ -1083,8 +1076,11 @@ test('stale locked prs will not be closed', async () => { test('exempt issue labels will not be marked stale', async () => { expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptIssueLabels = 'Exempt'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1093,10 +1089,6 @@ test('exempt issue labels will not be marked stale', async () => { ['Exempt'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.exemptIssueLabels = 'Exempt'; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1114,8 +1106,11 @@ test('exempt issue labels will not be marked stale', async () => { }); test('exempt issue labels will not be marked stale (multi issue label with spaces)', async () => { + const opts = {...DefaultProcessorOptions}; + opts.exemptIssueLabels = 'Exempt, Cool, None'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1124,10 +1119,6 @@ test('exempt issue labels will not be marked stale (multi issue label with space ['Cool'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.exemptIssueLabels = 'Exempt, Cool, None'; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1144,8 +1135,11 @@ test('exempt issue labels will not be marked stale (multi issue label with space }); test('exempt issue labels will not be marked stale (multi issue label)', async () => { + const opts = {...DefaultProcessorOptions}; + opts.exemptIssueLabels = 'Exempt,Cool,None'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1154,10 +1148,6 @@ test('exempt issue labels will not be marked stale (multi issue label)', async ( ['Cool'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.exemptIssueLabels = 'Exempt,Cool,None'; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1175,8 +1165,11 @@ test('exempt issue labels will not be marked stale (multi issue label)', async ( }); test('exempt pr labels will not be marked stale', async () => { + const opts = {...DefaultProcessorOptions}; + opts.exemptIssueLabels = 'Cool'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1185,6 +1178,7 @@ test('exempt pr labels will not be marked stale', async () => { ['Cool'] ), generateIssue( + opts, 2, 'My first PR', '2020-01-01T17:00:00Z', @@ -1193,6 +1187,7 @@ test('exempt pr labels will not be marked stale', async () => { ['Cool'] ), generateIssue( + opts, 3, 'Another issue', '2020-01-01T17:00:00Z', @@ -1200,10 +1195,6 @@ test('exempt pr labels will not be marked stale', async () => { false ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.exemptIssueLabels = 'Cool'; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1220,8 +1211,11 @@ test('exempt pr labels will not be marked stale', async () => { test('exempt issue labels will not be marked stale and will remove the existing stale label', async () => { expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptIssueLabels = 'Exempt'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1230,8 +1224,6 @@ test('exempt issue labels will not be marked stale and will remove the existing ['Exempt', 'Stale'] ) ]; - const opts = {...DefaultProcessorOptions}; - opts.exemptIssueLabels = 'Exempt'; const processor = new IssueProcessor( opts, async () => 'abot', @@ -1256,8 +1248,11 @@ test('exempt issue labels will not be marked stale and will remove the existing }); test('stale issues should not be closed if days is set to -1', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeClose = -1; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'My first issue', '2020-01-01T17:00:00Z', @@ -1266,6 +1261,7 @@ test('stale issues should not be closed if days is set to -1', async () => { ['Stale'] ), generateIssue( + opts, 2, 'My first PR', '2020-01-01T17:00:00Z', @@ -1274,6 +1270,7 @@ test('stale issues should not be closed if days is set to -1', async () => { ['Stale'] ), generateIssue( + opts, 3, 'Another issue', '2020-01-01T17:00:00Z', @@ -1282,10 +1279,6 @@ test('stale issues should not be closed if days is set to -1', async () => { ['Stale'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeClose = -1; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1302,8 +1295,11 @@ test('stale issues should not be closed if days is set to -1', async () => { }); test('stale label should be removed if a comment was added to a stale issue', async () => { + const opts = {...DefaultProcessorOptions}; + opts.removeStaleWhenUpdated = true; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should un-stale', '2020-01-01T17:00:00Z', @@ -1312,10 +1308,6 @@ test('stale label should be removed if a comment was added to a stale issue', as ['Stale'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.removeStaleWhenUpdated = true; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1340,9 +1332,12 @@ test('stale label should be removed if a comment was added to a stale issue', as }); test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => { + const opts = {...DefaultProcessorOptions}; + opts.removeStaleWhenUpdated = true; github.context.actor = 'abot'; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should stay stale', '2020-01-01T17:00:00Z', @@ -1351,10 +1346,6 @@ test('stale label should not be removed if a comment was added by the bot (and t ['Stale'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.removeStaleWhenUpdated = true; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1379,8 +1370,14 @@ test('stale label should not be removed if a comment was added by the bot (and t }); test('stale label containing a space should be removed if a comment was added to a stale issue', async () => { + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + removeStaleWhenUpdated: true, + staleIssueLabel: 'stat: stale' + }; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should un-stale', '2020-01-01T17:00:00Z', @@ -1389,13 +1386,6 @@ test('stale label containing a space should be removed if a comment was added to ['stat: stale'] ) ]; - - const opts: IssueProcessorOptions = { - ...DefaultProcessorOptions, - removeStaleWhenUpdated: true, - staleIssueLabel: 'stat: stale' - }; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1413,10 +1403,14 @@ test('stale label containing a space should be removed if a comment was added to }); test('stale issues should not be closed until after the closed number of days', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 1; // closes after 6 days let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 5); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1424,11 +1418,6 @@ test('stale issues should not be closed until after the closed number of days', false ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 1; // closes after 6 days - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1446,10 +1435,14 @@ test('stale issues should not be closed until after the closed number of days', }); test('stale issues should be closed if the closed nubmer of days (additive) is also passed', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 1; // closes after 6 days let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 7); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be stale and closed', lastUpdate.toString(), @@ -1458,11 +1451,6 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a ['Stale'] ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 1; // closes after 6 days - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1480,10 +1468,14 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a }); test('stale issues should not be closed until after the closed number of days (long)', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 10); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1491,11 +1483,6 @@ test('stale issues should not be closed until after the closed number of days (l false ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 20; // closes after 25 days - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1513,10 +1500,15 @@ test('stale issues should not be closed until after the closed number of days (l }); test('skips stale message on issues when skip-stale-issue-message is set', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStaleIssueMessage = true; let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 10); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1524,12 +1516,6 @@ test('skips stale message on issues when skip-stale-issue-message is set', async false ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 20; // closes after 25 days - opts.skipStaleIssueMessage = true; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1559,10 +1545,15 @@ test('skips stale message on issues when skip-stale-issue-message is set', async }); test('skips stale message on prs when skip-stale-pr-message is set', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 10); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1570,12 +1561,6 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () => true ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 20; // closes after 25 days - opts.skipStalePrMessage = true; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1605,10 +1590,16 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () => }); test('not providing state takes precedence over skipStaleIssueMessage', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; + opts.staleIssueMessage = ''; let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 10); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1616,13 +1607,6 @@ test('not providing state takes precedence over skipStaleIssueMessage', async () false ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 20; // closes after 25 days - opts.skipStalePrMessage = true; - opts.staleIssueMessage = ''; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1640,10 +1624,16 @@ test('not providing state takes precedence over skipStaleIssueMessage', async () }); test('not providing stalePrMessage takes precedence over skipStalePrMessage', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; + opts.stalePrMessage = ''; let lastUpdate = new Date(); lastUpdate.setDate(lastUpdate.getDate() - 10); const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should be marked stale but not closed', lastUpdate.toString(), @@ -1651,13 +1641,6 @@ test('not providing stalePrMessage takes precedence over skipStalePrMessage', as true ) ]; - - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeStale = 5; // stale after 5 days - opts.daysBeforeClose = 20; // closes after 25 days - opts.skipStalePrMessage = true; - opts.stalePrMessage = ''; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1679,6 +1662,7 @@ test('git branch is deleted when option is enabled', async () => { const isPullRequest = true; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should have its branch deleted', '2020-01-01T17:00:00Z', @@ -1687,7 +1671,6 @@ test('git branch is deleted when option is enabled', async () => { ['Stale'] ) ]; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1709,6 +1692,7 @@ test('git branch is not deleted when issue is not pull request', async () => { const isPullRequest = false; const TestIssueList: Issue[] = [ generateIssue( + opts, 1, 'An issue that should not have its branch deleted', '2020-01-01T17:00:00Z', @@ -1717,7 +1701,6 @@ test('git branch is not deleted when issue is not pull request', async () => { ['Stale'] ) ]; - const processor = new IssueProcessor( opts, async () => 'abot', @@ -1733,3 +1716,549 @@ test('git branch is not deleted when issue is not pull request', async () => { expect(processor.staleIssues.length).toEqual(0); expect(processor.deletedBranchIssues.length).toEqual(0); }); + +test('an issue without a milestone will be marked as stale', async () => { + expect.assertions(3); + const TestIssueList: Issue[] = [ + generateIssue( + DefaultProcessorOptions, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + '' + ) + ]; + const processor = new IssueProcessor( + DefaultProcessorOptions, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue without an exempted milestone will be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone will not be marked as stale (multi milestones with spaces)', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1, Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone2' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone will not be marked as stale (multi milestones without spaces)', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1,Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone2' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone but without an exempted issue milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptIssueMilestones = ''; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone but with another exempted issue milestone will be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptIssueMilestones = 'Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('an issue with an exempted milestone and with an exempted issue milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptIssueMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR without a milestone will be marked as stale', async () => { + expect.assertions(3); + const TestIssueList: Issue[] = [ + generateIssue( + DefaultProcessorOptions, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + '' + ) + ]; + const processor = new IssueProcessor( + DefaultProcessorOptions, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR without an exempted milestone will be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone will not be marked as stale (multi milestones with spaces)', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1, Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone2' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone will not be marked as stale (multi milestones without spaces)', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1,Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone2' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone but without an exempted issue milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptPrMilestones = ''; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone but with another exempted issue milestone will be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptPrMilestones = 'Milestone2'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(1); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); + +test('a PR with an exempted milestone and with an exempted issue milestone will not be marked as stale', async () => { + expect.assertions(3); + const opts = {...DefaultProcessorOptions}; + opts.exemptMilestones = 'Milestone1'; + opts.exemptPrMilestones = 'Milestone1'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'My first issue', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + true, + undefined, + undefined, + undefined, + 'Milestone1' + ) + ]; + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num: number, dt: string) => [], + async (issue: Issue, label: string) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toStrictEqual(0); + expect(processor.closedIssues.length).toStrictEqual(0); + expect(processor.removedLabelIssues.length).toStrictEqual(0); +}); diff --git a/action.yml b/action.yml index 1c90b6191..41f667a7f 100644 --- a/action.yml +++ b/action.yml @@ -23,20 +23,20 @@ inputs: required: false default: '60' days-before-issue-stale: - description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding the issues only.' + description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding only the issues.' required: false days-before-pr-stale: - description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding the pull requests only.' + description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding only the pull requests.' required: false days-before-close: description: 'The number of days to wait to close an issue or a pull request after it being marked stale. Set to -1 to never close stale issues or pull requests.' required: false default: '7' days-before-issue-close: - description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding the issues only.' + description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding only the issues.' required: false days-before-pr-close: - description: 'The number of days to wait to close a pull request after it being marked stale. Set to -1 to never close stale pull requests. Override "days-before-close" option regarding the pull requests only.' + description: 'The number of days to wait to close a pull request after it being marked stale. Set to -1 to never close stale pull requests. Override "days-before-close" option regarding only the pull requests.' required: false stale-issue-label: description: 'The label to apply when an issue is stale.' @@ -60,6 +60,18 @@ inputs: description: 'The labels that mean a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")' default: '' required: false + exempt-milestones: + description: 'The milestones that mean an issue or a pr is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2")' + default: '' + required: false + exempt-issue-milestones: + description: 'The milestones that mean an issue is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2"). Override "exempt-milestones" option regarding only the issue.' + default: '' + required: false + exempt-pr-milestones: + description: 'The milestones that mean a pull request is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2"). Override "exempt-milestones" option regarding only the pull requests.' + default: '' + required: false only-labels: description: 'Only issues or pull requests with all of these labels are checked if stale. Defaults to `[]` (disabled) and can be a comma-separated list of labels.' default: '' diff --git a/dist/index.js b/dist/index.js index 19c002276..341822c75 100644 --- a/dist/index.js +++ b/dist/index.js @@ -19,29 +19,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", ({ value: true })); exports.IssueProcessor = void 0; const github_1 = __nccwpck_require__(5438); +const issue_1 = __nccwpck_require__(4783); +const issue_logger_1 = __nccwpck_require__(2984); +const logger_1 = __nccwpck_require__(6212); +const milestones_1 = __nccwpck_require__(4601); const get_humanized_date_1 = __nccwpck_require__(965); const is_date_more_recent_than_1 = __nccwpck_require__(1473); const is_valid_date_1 = __nccwpck_require__(891); const get_issue_type_1 = __nccwpck_require__(5153); -const issue_logger_1 = __nccwpck_require__(1699); -const logger_1 = __nccwpck_require__(8236); const is_labeled_1 = __nccwpck_require__(6792); const is_pull_request_1 = __nccwpck_require__(5400); -const labels_to_list_1 = __nccwpck_require__(9107); const should_mark_when_stale_1 = __nccwpck_require__(2461); -const logger = new logger_1.Logger(); +const words_to_list_1 = __nccwpck_require__(1883); /*** * Handle processing of issues for staleness/closure. */ class IssueProcessor { constructor(options, getActor, getIssues, listIssueComments, getLabelCreationDate) { + this._logger = new logger_1.Logger(); + this._operationsLeft = 0; this.staleIssues = []; this.closedIssues = []; this.deletedBranchIssues = []; this.removedLabelIssues = []; - this.operationsLeft = 0; this.options = options; - this.operationsLeft = options.operationsPerRun; + this._operationsLeft = options.operationsPerRun; this.client = github_1.getOctokit(options.repoToken); if (getActor) { this._getActor = getActor; @@ -56,7 +58,7 @@ class IssueProcessor { this._getLabelCreationDate = getLabelCreationDate; } if (this.options.debugOnly) { - logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.'); + this._logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.'); } } static _updatedSince(timestamp, num_days) { @@ -68,39 +70,37 @@ class IssueProcessor { return __awaiter(this, void 0, void 0, function* () { // get the next batch of issues const issues = yield this._getIssues(page); - this.operationsLeft -= 1; + this._operationsLeft -= 1; const actor = yield this._getActor(); if (issues.length <= 0) { - logger.info('---'); - logger.info('No more issues found to process. Exiting.'); - return this.operationsLeft; + this._logger.info('---'); + this._logger.info('No more issues found to process. Exiting.'); + return this._operationsLeft; } for (const issue of issues.values()) { const issueLogger = new issue_logger_1.IssueLogger(issue); - const isPr = is_pull_request_1.isPullRequest(issue); - issueLogger.info(`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})`); + issueLogger.info(`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${issue.isPullRequest})`); // calculate string based messages for this issue - const staleMessage = isPr + const staleMessage = issue.isPullRequest ? this.options.stalePrMessage : this.options.staleIssueMessage; - const closeMessage = isPr + const closeMessage = issue.isPullRequest ? this.options.closePrMessage : this.options.closeIssueMessage; - const staleLabel = isPr + const staleLabel = issue.isPullRequest ? this.options.stalePrLabel : this.options.staleIssueLabel; - const closeLabel = isPr + const closeLabel = issue.isPullRequest ? this.options.closePrLabel : this.options.closeIssueLabel; - const exemptLabels = labels_to_list_1.labelsToList(isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels); - const skipMessage = isPr + const skipMessage = issue.isPullRequest ? this.options.skipStalePrMessage : this.options.skipStaleIssueMessage; - const issueType = get_issue_type_1.getIssueType(isPr); - const daysBeforeStale = isPr + const issueType = get_issue_type_1.getIssueType(issue.isPullRequest); + const daysBeforeStale = issue.isPullRequest ? this._getDaysBeforePrStale() : this._getDaysBeforeIssueStale(); - if (isPr) { + if (issue.isPullRequest) { issueLogger.info(`Days before pull request stale: ${daysBeforeStale}`); } else { @@ -134,38 +134,44 @@ class IssueProcessor { continue; // don't process issues which were created before the start date } } - // Does this issue have a stale label? - let isStale = is_labeled_1.isLabeled(issue, staleLabel); - if (isStale) { + if (issue.isStale) { issueLogger.info(`This issue has a stale label`); } else { issueLogger.info(`This issue hasn't a stale label`); } + const exemptLabels = words_to_list_1.wordsToList(issue.isPullRequest + ? this.options.exemptPrLabels + : this.options.exemptIssueLabels); if (exemptLabels.some((exemptLabel) => is_labeled_1.isLabeled(issue, exemptLabel))) { - if (isStale) { + if (issue.isStale) { issueLogger.info(`An exempt label was added after the stale label.`); yield this._removeStaleLabel(issue, staleLabel); } issueLogger.info(`Skipping ${issueType} because it has an exempt label`); continue; // don't process exempt issues } + const milestones = new milestones_1.Milestones(this.options, issue); + if (milestones.shouldExemptMilestones()) { + issueLogger.info(`Skipping ${issueType} because it has an exempt milestone`); + continue; // don't process exempt milestones + } // should this issue be marked stale? const shouldBeStale = !IssueProcessor._updatedSince(issue.updated_at, this.options.daysBeforeStale); // determine if this issue needs to be marked stale first - if (!isStale && shouldBeStale && shouldMarkAsStale) { + if (!issue.isStale && shouldBeStale && shouldMarkAsStale) { issueLogger.info(`Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label`); yield this._markStale(issue, staleMessage, staleLabel, skipMessage); - isStale = true; // this issue is now considered stale + issue.isStale = true; // this issue is now considered stale } // process the issue if it was marked stale - if (isStale) { + if (issue.isStale) { issueLogger.info(`Found a stale ${issueType}`); yield this._processStaleIssue(issue, issueType, staleLabel, actor, closeMessage, closeLabel); } } - if (this.operationsLeft <= 0) { - logger.warning('Reached max number of operations to process. Exiting.'); + if (this._operationsLeft <= 0) { + this._logger.warning('Reached max number of operations to process. Exiting.'); return 0; } // do the next batch @@ -244,7 +250,7 @@ class IssueProcessor { return comments.data; } catch (error) { - logger.error(`List issue comments error: ${error.message}`); + this._logger.error(`List issue comments error: ${error.message}`); return Promise.resolve([]); } }); @@ -262,7 +268,7 @@ class IssueProcessor { return actor.data.login; }); } - // grab issues from github in baches of 100 + // grab issues from github in batches of 100 _getIssues(page) { return __awaiter(this, void 0, void 0, function* () { // generate type for response @@ -277,10 +283,10 @@ class IssueProcessor { direction: this.options.ascending ? 'asc' : 'desc', page }); - return issueResult.data; + return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue)); } catch (error) { - logger.error(`Get issues for repo error: ${error.message}`); + this._logger.error(`Get issues for repo error: ${error.message}`); return Promise.resolve([]); } }); @@ -291,7 +297,7 @@ class IssueProcessor { const issueLogger = new issue_logger_1.IssueLogger(issue); issueLogger.info(`Marking issue #${issue.number} as stale`); this.staleIssues.push(issue); - this.operationsLeft -= 2; + this._operationsLeft -= 2; // if the issue is being marked stale, the updated date should be changed to right now // so that close calculations work correctly const newUpdatedAtDate = new Date(); @@ -331,7 +337,7 @@ class IssueProcessor { const issueLogger = new issue_logger_1.IssueLogger(issue); issueLogger.info(`Closing issue #${issue.number} for being stale`); this.closedIssues.push(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; if (this.options.debugOnly) { return; } @@ -377,7 +383,7 @@ class IssueProcessor { _getPullRequest(issue) { return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; try { const pullRequest = yield this.client.pulls.get({ owner: github_1.context.repo.owner, @@ -406,7 +412,7 @@ class IssueProcessor { } const branch = pullRequest.head.ref; issueLogger.info(`Deleting branch ${branch} from closed issue #${issue.number}`); - this.operationsLeft -= 1; + this._operationsLeft -= 1; try { yield this.client.git.deleteRef({ owner: github_1.context.repo.owner, @@ -425,7 +431,7 @@ class IssueProcessor { const issueLogger = new issue_logger_1.IssueLogger(issue); issueLogger.info(`Removing label "${label}" from issue #${issue.number}`); this.removedLabelIssues.push(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; // @todo remove the debug only to be able to test the code below if (this.options.debugOnly) { return; @@ -449,7 +455,7 @@ class IssueProcessor { return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); issueLogger.info(`Checking for label on issue #${issue.number}`); - this.operationsLeft -= 1; + this._operationsLeft -= 1; const options = this.client.issues.listEvents.endpoint.merge({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -499,7 +505,43 @@ exports.IssueProcessor = IssueProcessor; /***/ }), -/***/ 1699: +/***/ 4783: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Issue = void 0; +const is_labeled_1 = __nccwpck_require__(6792); +const is_pull_request_1 = __nccwpck_require__(5400); +class Issue { + constructor(options, issue) { + this._options = options; + this.title = issue.title; + this.number = issue.number; + this.created_at = issue.created_at; + this.updated_at = issue.updated_at; + this.labels = issue.labels; + this.pull_request = issue.pull_request; + this.state = issue.state; + this.locked = issue.locked; + this.milestone = issue.milestone; + this.isPullRequest = is_pull_request_1.isPullRequest(this); + this.staleLabel = this._getStaleLabel(); + this.isStale = is_labeled_1.isLabeled(this, this.staleLabel); + } + _getStaleLabel() { + return this.isPullRequest + ? this._options.stalePrLabel + : this._options.staleIssueLabel; + } +} +exports.Issue = Issue; + + +/***/ }), + +/***/ 2984: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -551,7 +593,7 @@ exports.IssueLogger = IssueLogger; /***/ }), -/***/ 8236: +/***/ 6212: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -592,6 +634,58 @@ class Logger { exports.Logger = Logger; +/***/ }), + +/***/ 4601: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Milestones = void 0; +const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601)); +const words_to_list_1 = __nccwpck_require__(1883); +class Milestones { + constructor(options, issue) { + this._options = options; + this._issue = issue; + } + static _cleanMilestone(label) { + return lodash_deburr_1.default(label.toLowerCase()); + } + shouldExemptMilestones() { + const exemptMilestones = this._getExemptMilestones(); + return exemptMilestones.some((exemptMilestone) => this._hasMilestone(exemptMilestone)); + } + _getExemptMilestones() { + return words_to_list_1.wordsToList(this._issue.isPullRequest + ? this._getExemptPullRequestMilestones() + : this._getExemptIssueMilestones()); + } + _getExemptIssueMilestones() { + return this._options.exemptIssueMilestones !== '' + ? this._options.exemptIssueMilestones + : this._options.exemptMilestones; + } + _getExemptPullRequestMilestones() { + return this._options.exemptPrMilestones !== '' + ? this._options.exemptPrMilestones + : this._options.exemptMilestones; + } + _hasMilestone(milestone) { + if (!this._issue.milestone) { + return false; + } + return (Milestones._cleanMilestone(milestone) === + Milestones._cleanMilestone(this._issue.milestone.title)); + } +} +exports.Milestones = Milestones; + + /***/ }), /***/ 9639: @@ -707,12 +801,12 @@ exports.isLabeled = void 0; const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601)); /** * @description - * Check if the label is listed as a label of the issue + * Check if the given label is listed as a label of the given issue * * @param {Readonly} issue A GitHub issue containing some labels * @param {Readonly} label The label to check the presence with * - * @return {boolean} Return true when the given label is also in the issue labels + * @return {boolean} Return true when the given label is also in the given issue labels */ function isLabeled(issue, label) { return !!issue.labels.find((issueLabel) => { @@ -742,51 +836,51 @@ exports.isPullRequest = isPullRequest; /***/ }), -/***/ 9107: +/***/ 2461: /***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.labelsToList = void 0; -/** - * @description - * Transform a string of comma separated labels - * to an array of labels - * - * @example - * labelsToList('label') => ['label'] - * labelsToList('label,label') => ['label', 'label'] - * labelsToList('kebab-label') => ['kebab-label'] - * labelsToList('kebab%20label') => ['kebab%20label'] - * labelsToList('label with words') => ['label with words'] - * - * @param {Readonly} labels A string of comma separated labels - * - * @return {string[]} A list of labels - */ -function labelsToList(labels) { - if (!labels.length) { - return []; - } - return labels.split(',').map(l => l.trim()); +exports.shouldMarkWhenStale = void 0; +function shouldMarkWhenStale(daysBeforeStale) { + return daysBeforeStale >= 0; } -exports.labelsToList = labelsToList; +exports.shouldMarkWhenStale = shouldMarkWhenStale; /***/ }), -/***/ 2461: +/***/ 1883: /***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.shouldMarkWhenStale = void 0; -function shouldMarkWhenStale(daysBeforeStale) { - return daysBeforeStale >= 0; +exports.wordsToList = void 0; +/** + * @description + * Transform a string of comma separated words + * to an array of words + * + * @example + * wordsToList('label') => ['label'] + * wordsToList('label,label') => ['label', 'label'] + * wordsToList('kebab-label') => ['kebab-label'] + * wordsToList('kebab%20label') => ['kebab%20label'] + * wordsToList('label with words') => ['label with words'] + * + * @param {Readonly} words A string of comma separated words + * + * @return {string[]} A list of words + */ +function wordsToList(words) { + if (!words.length) { + return []; + } + return words.split(',').map((word) => word.trim()); } -exports.shouldMarkWhenStale = shouldMarkWhenStale; +exports.wordsToList = wordsToList; /***/ }), @@ -870,15 +964,14 @@ function getAndValidateArgs() { deleteBranch: core.getInput('delete-branch') === 'true', startDate: core.getInput('start-date') !== '' ? core.getInput('start-date') - : undefined + : undefined, + exemptMilestones: core.getInput('exempt-milestones'), + exemptIssueMilestones: core.getInput('exempt-issue-milestones'), + exemptPrMilestones: core.getInput('exempt-pr-milestones') }; for (const numberInput of [ 'days-before-stale', - 'days-before-issue-stale', - 'days-before-pr-stale', 'days-before-close', - 'days-before-issue-close', - 'days-before-pr-close', 'operations-per-run' ]) { if (isNaN(parseInt(core.getInput(numberInput)))) { diff --git a/package.json b/package.json index ef50666a1..7d8592870 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "main": "lib/main.js", "scripts": { "build": "tsc", - "format": "prettier --write **/*.ts", - "format-check": "prettier --check **/*.ts", - "lint": "prettier --check --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts", - "lint:fix": "prettier --write --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts --fix", + "format": "prettier --write --ignore-unknown **/*.{md,json,yml,ts}", + "format-check": "prettier --check --ignore-unknown **/*.{md,json,yml,ts}", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "lint:all": "npm run format-check && npm run lint", + "lint:all:fix": "npm run format && npm run lint:fix", "pack": "ncc build", "test": "jest", "all": "npm run build && npm run format && npm run lint && npm run pack && npm test" diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts index e8a3a0e21..f1c2d93db 100644 --- a/src/IssueProcessor.ts +++ b/src/IssueProcessor.ts @@ -1,30 +1,21 @@ import {context, getOctokit} from '@actions/github'; import {GitHub} from '@actions/github/lib/utils'; import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; +import {Issue} from './classes/issue'; +import {IssueLogger} from './classes/loggers/issue-logger'; +import {Logger} from './classes/loggers/logger'; +import {Milestones} from './classes/milestones'; import {IssueType} from './enums/issue-type'; import {getHumanizedDate} from './functions/dates/get-humanized-date'; import {isDateMoreRecentThan} from './functions/dates/is-date-more-recent-than'; import {isValidDate} from './functions/dates/is-valid-date'; import {getIssueType} from './functions/get-issue-type'; -import {IssueLogger} from './classes/issue-logger'; -import {Logger} from './classes/logger'; import {isLabeled} from './functions/is-labeled'; import {isPullRequest} from './functions/is-pull-request'; -import {labelsToList} from './functions/labels-to-list'; import {shouldMarkWhenStale} from './functions/should-mark-when-stale'; -import {IsoDateString} from './types/iso-date-string'; import {IsoOrRfcDateString} from './types/iso-or-rfc-date-string'; - -export interface Issue { - title: string; - number: number; - created_at: IsoDateString; - updated_at: IsoDateString; - labels: Label[]; - pull_request: any; - state: string; - locked: boolean; -} +import {wordsToList} from './functions/words-to-list'; +import {IIssue} from './interfaces/issue'; export interface PullRequest { number: number; @@ -79,10 +70,11 @@ export interface IssueProcessorOptions { skipStalePrMessage: boolean; deleteBranch: boolean; startDate: IsoOrRfcDateString | undefined; // Should be ISO 8601 or RFC 2822 + exemptMilestones: string; + exemptIssueMilestones: string; + exemptPrMilestones: string; } -const logger: Logger = new Logger(); - /*** * Handle processing of issues for staleness/closure. */ @@ -95,13 +87,14 @@ export class IssueProcessor { return millisSinceLastUpdated <= daysInMillis; } + private readonly _logger: Logger = new Logger(); + private _operationsLeft = 0; readonly client: InstanceType; readonly options: IssueProcessorOptions; readonly staleIssues: Issue[] = []; readonly closedIssues: Issue[] = []; readonly deletedBranchIssues: Issue[] = []; readonly removedLabelIssues: Issue[] = []; - private operationsLeft = 0; constructor( options: IssueProcessorOptions, @@ -117,7 +110,7 @@ export class IssueProcessor { ) => Promise ) { this.options = options; - this.operationsLeft = options.operationsPerRun; + this._operationsLeft = options.operationsPerRun; this.client = getOctokit(options.repoToken); if (getActor) { @@ -137,7 +130,7 @@ export class IssueProcessor { } if (this.options.debugOnly) { - logger.warning( + this._logger.warning( 'Executing in debug mode. Debug output will be written but no issues will be processed.' ); } @@ -146,49 +139,45 @@ export class IssueProcessor { async processIssues(page = 1): Promise { // get the next batch of issues const issues: Issue[] = await this._getIssues(page); - this.operationsLeft -= 1; + this._operationsLeft -= 1; const actor: string = await this._getActor(); if (issues.length <= 0) { - logger.info('---'); - logger.info('No more issues found to process. Exiting.'); - return this.operationsLeft; + this._logger.info('---'); + this._logger.info('No more issues found to process. Exiting.'); + return this._operationsLeft; } for (const issue of issues.values()) { const issueLogger: IssueLogger = new IssueLogger(issue); - const isPr = isPullRequest(issue); issueLogger.info( - `Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})` + `Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${issue.isPullRequest})` ); // calculate string based messages for this issue - const staleMessage: string = isPr + const staleMessage: string = issue.isPullRequest ? this.options.stalePrMessage : this.options.staleIssueMessage; - const closeMessage: string = isPr + const closeMessage: string = issue.isPullRequest ? this.options.closePrMessage : this.options.closeIssueMessage; - const staleLabel: string = isPr + const staleLabel: string = issue.isPullRequest ? this.options.stalePrLabel : this.options.staleIssueLabel; - const closeLabel: string = isPr + const closeLabel: string = issue.isPullRequest ? this.options.closePrLabel : this.options.closeIssueLabel; - const exemptLabels: string[] = labelsToList( - isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels - ); - const skipMessage = isPr + const skipMessage = issue.isPullRequest ? this.options.skipStalePrMessage : this.options.skipStaleIssueMessage; - const issueType: IssueType = getIssueType(isPr); - const daysBeforeStale: number = isPr + const issueType: IssueType = getIssueType(issue.isPullRequest); + const daysBeforeStale: number = issue.isPullRequest ? this._getDaysBeforePrStale() : this._getDaysBeforeIssueStale(); - if (isPr) { + if (issue.isPullRequest) { issueLogger.info(`Days before pull request stale: ${daysBeforeStale}`); } else { issueLogger.info(`Days before issue stale: ${daysBeforeStale}`); @@ -244,21 +233,24 @@ export class IssueProcessor { } } - // Does this issue have a stale label? - let isStale: boolean = isLabeled(issue, staleLabel); - - if (isStale) { + if (issue.isStale) { issueLogger.info(`This issue has a stale label`); } else { issueLogger.info(`This issue hasn't a stale label`); } + const exemptLabels: string[] = wordsToList( + issue.isPullRequest + ? this.options.exemptPrLabels + : this.options.exemptIssueLabels + ); + if ( exemptLabels.some((exemptLabel: Readonly): boolean => isLabeled(issue, exemptLabel) ) ) { - if (isStale) { + if (issue.isStale) { issueLogger.info(`An exempt label was added after the stale label.`); await this._removeStaleLabel(issue, staleLabel); } @@ -269,6 +261,15 @@ export class IssueProcessor { continue; // don't process exempt issues } + const milestones: Milestones = new Milestones(this.options, issue); + + if (milestones.shouldExemptMilestones()) { + issueLogger.info( + `Skipping ${issueType} because it has an exempt milestone` + ); + continue; // don't process exempt milestones + } + // should this issue be marked stale? const shouldBeStale = !IssueProcessor._updatedSince( issue.updated_at, @@ -276,16 +277,16 @@ export class IssueProcessor { ); // determine if this issue needs to be marked stale first - if (!isStale && shouldBeStale && shouldMarkAsStale) { + if (!issue.isStale && shouldBeStale && shouldMarkAsStale) { issueLogger.info( `Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label` ); await this._markStale(issue, staleMessage, staleLabel, skipMessage); - isStale = true; // this issue is now considered stale + issue.isStale = true; // this issue is now considered stale } // process the issue if it was marked stale - if (isStale) { + if (issue.isStale) { issueLogger.info(`Found a stale ${issueType}`); await this._processStaleIssue( issue, @@ -298,8 +299,10 @@ export class IssueProcessor { } } - if (this.operationsLeft <= 0) { - logger.warning('Reached max number of operations to process. Exiting.'); + if (this._operationsLeft <= 0) { + this._logger.warning( + 'Reached max number of operations to process. Exiting.' + ); return 0; } @@ -427,7 +430,7 @@ export class IssueProcessor { }); return comments.data; } catch (error) { - logger.error(`List issue comments error: ${error.message}`); + this._logger.error(`List issue comments error: ${error.message}`); return Promise.resolve([]); } } @@ -444,7 +447,7 @@ export class IssueProcessor { return actor.data.login; } - // grab issues from github in baches of 100 + // grab issues from github in batches of 100 private async _getIssues(page: number): Promise { // generate type for response const endpoint = this.client.issues.listForRepo; @@ -462,9 +465,12 @@ export class IssueProcessor { page } ); - return issueResult.data; + + return issueResult.data.map( + (issue: Readonly): Issue => new Issue(this.options, issue) + ); } catch (error) { - logger.error(`Get issues for repo error: ${error.message}`); + this._logger.error(`Get issues for repo error: ${error.message}`); return Promise.resolve([]); } } @@ -482,7 +488,7 @@ export class IssueProcessor { this.staleIssues.push(issue); - this.operationsLeft -= 2; + this._operationsLeft -= 2; // if the issue is being marked stale, the updated date should be changed to right now // so that close calculations work correctly @@ -530,7 +536,7 @@ export class IssueProcessor { this.closedIssues.push(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; if (this.options.debugOnly) { return; @@ -578,7 +584,7 @@ export class IssueProcessor { issue: Issue ): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; try { const pullRequest = await this.client.pulls.get({ @@ -621,7 +627,7 @@ export class IssueProcessor { `Deleting branch ${branch} from closed issue #${issue.number}` ); - this.operationsLeft -= 1; + this._operationsLeft -= 1; try { await this.client.git.deleteRef({ @@ -644,7 +650,7 @@ export class IssueProcessor { this.removedLabelIssues.push(issue); - this.operationsLeft -= 1; + this._operationsLeft -= 1; // @todo remove the debug only to be able to test the code below if (this.options.debugOnly) { @@ -673,7 +679,7 @@ export class IssueProcessor { issueLogger.info(`Checking for label on issue #${issue.number}`); - this.operationsLeft -= 1; + this._operationsLeft -= 1; const options = this.client.issues.listEvents.endpoint.merge({ owner: context.repo.owner, @@ -722,7 +728,7 @@ export class IssueProcessor { } private async _removeStaleLabel( - issue: Readonly, + issue: Issue, staleLabel: Readonly ): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts new file mode 100644 index 000000000..0b395cf86 --- /dev/null +++ b/src/classes/issue.spec.ts @@ -0,0 +1,208 @@ +import {IIssue} from '../interfaces/issue'; +import {IMilestone} from '../interfaces/milestone'; +import {IssueProcessorOptions, Label} from '../IssueProcessor'; +import {Issue} from './issue'; + +describe('Issue', (): void => { + let issue: Issue; + let optionsInterface: IssueProcessorOptions; + let issueInterface: IIssue; + + beforeEach((): void => { + optionsInterface = { + ascending: false, + closeIssueLabel: '', + closeIssueMessage: '', + closePrLabel: '', + closePrMessage: '', + daysBeforeClose: 0, + daysBeforeIssueClose: 0, + daysBeforeIssueStale: 0, + daysBeforePrClose: 0, + daysBeforePrStale: 0, + daysBeforeStale: 0, + debugOnly: false, + deleteBranch: false, + exemptIssueLabels: '', + exemptIssueMilestones: '', + exemptMilestones: '', + exemptPrLabels: '', + exemptPrMilestones: '', + onlyLabels: '', + operationsPerRun: 0, + removeStaleWhenUpdated: false, + repoToken: '', + skipStaleIssueMessage: false, + skipStalePrMessage: false, + staleIssueMessage: '', + stalePrMessage: '', + startDate: undefined, + stalePrLabel: 'dummy-stale-pr-label', + staleIssueLabel: 'dummy-stale-issue-label' + }; + issueInterface = { + title: 'dummy-title', + number: 8, + created_at: 'dummy-created-at', + updated_at: 'dummy-updated-at', + labels: [ + { + name: 'dummy-name' + } + ], + pull_request: {}, + state: 'dummy-state', + locked: false, + milestone: { + title: 'dummy-milestone' + } + }; + issue = new Issue(optionsInterface, issueInterface); + }); + + describe('constructor()', (): void => { + it('should set the title with the given issue title', (): void => { + expect.assertions(1); + + expect(issue.title).toStrictEqual('dummy-title'); + }); + + it('should set the number with the given issue number', (): void => { + expect.assertions(1); + + expect(issue.number).toStrictEqual(8); + }); + + it('should set the created_at with the given issue created_at', (): void => { + expect.assertions(1); + + expect(issue.created_at).toStrictEqual('dummy-created-at'); + }); + + it('should set the updated_at with the given issue updated_at', (): void => { + expect.assertions(1); + + expect(issue.updated_at).toStrictEqual('dummy-updated-at'); + }); + + it('should set the labels with the given issue labels', (): void => { + expect.assertions(1); + + expect(issue.labels).toStrictEqual([ + { + name: 'dummy-name' + } as Label + ]); + }); + + it('should set the pull_request with the given issue pull_request', (): void => { + expect.assertions(1); + + expect(issue.pull_request).toStrictEqual({}); + }); + + it('should set the state with the given issue state', (): void => { + expect.assertions(1); + + expect(issue.state).toStrictEqual('dummy-state'); + }); + + it('should set the locked with the given issue locked', (): void => { + expect.assertions(1); + + expect(issue.locked).toStrictEqual(false); + }); + + it('should set the milestone with the given issue milestone', (): void => { + expect.assertions(1); + + expect(issue.milestone).toStrictEqual({ + title: 'dummy-milestone' + } as IMilestone); + }); + + describe('when the given issue pull_request is not set', (): void => { + beforeEach((): void => { + issueInterface.pull_request = undefined; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the isPullRequest to false', (): void => { + expect.assertions(1); + + expect(issue.isPullRequest).toStrictEqual(false); + }); + }); + + describe('when the given issue pull_request is set', (): void => { + beforeEach((): void => { + issueInterface.pull_request = {}; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the isPullRequest to true', (): void => { + expect.assertions(1); + + expect(issue.isPullRequest).toStrictEqual(true); + }); + }); + + describe('when the given issue is not a pull request', (): void => { + beforeEach((): void => { + issueInterface.pull_request = undefined; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the staleLabel with the given option staleIssueLabel', (): void => { + expect.assertions(1); + + expect(issue.staleLabel).toStrictEqual('dummy-stale-issue-label'); + }); + }); + + describe('when the given issue is a pull request', (): void => { + beforeEach((): void => { + issueInterface.pull_request = {}; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the staleLabel with the given option stalePrLabel', (): void => { + expect.assertions(1); + + expect(issue.staleLabel).toStrictEqual('dummy-stale-pr-label'); + }); + }); + + describe('when the given issue does not contains the stale label', (): void => { + beforeEach((): void => { + issueInterface.pull_request = undefined; + issueInterface.labels = []; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the isStale to false', (): void => { + expect.assertions(1); + + expect(issue.isStale).toStrictEqual(false); + }); + }); + + describe('when the given issue contains the stale label', (): void => { + beforeEach((): void => { + issueInterface.pull_request = undefined; + issueInterface.labels = [ + { + name: 'dummy-stale-issue-label' + } as Label + ]; + issue = new Issue(optionsInterface, issueInterface); + }); + + it('should set the isStale to true', (): void => { + expect.assertions(1); + + expect(issue.isStale).toStrictEqual(true); + }); + }); + }); +}); diff --git a/src/classes/issue.ts b/src/classes/issue.ts new file mode 100644 index 000000000..31a40c7c7 --- /dev/null +++ b/src/classes/issue.ts @@ -0,0 +1,48 @@ +import {isLabeled} from '../functions/is-labeled'; +import {isPullRequest} from '../functions/is-pull-request'; +import {IIssue} from '../interfaces/issue'; +import {IMilestone} from '../interfaces/milestone'; +import {IssueProcessorOptions, Label} from '../IssueProcessor'; +import {IsoDateString} from '../types/iso-date-string'; + +export class Issue implements IIssue { + private readonly _options: IssueProcessorOptions; + readonly title: string; + readonly number: number; + created_at: IsoDateString; + updated_at: IsoDateString; + readonly labels: Label[]; + readonly pull_request: Object | null | undefined; + readonly state: string; + readonly locked: boolean; + readonly milestone: IMilestone | undefined; + readonly isPullRequest: boolean; + readonly staleLabel: string; + isStale: boolean; + + constructor( + options: Readonly, + issue: Readonly + ) { + this._options = options; + this.title = issue.title; + this.number = issue.number; + this.created_at = issue.created_at; + this.updated_at = issue.updated_at; + this.labels = issue.labels; + this.pull_request = issue.pull_request; + this.state = issue.state; + this.locked = issue.locked; + this.milestone = issue.milestone; + + this.isPullRequest = isPullRequest(this); + this.staleLabel = this._getStaleLabel(); + this.isStale = isLabeled(this, this.staleLabel); + } + + private _getStaleLabel(): string { + return this.isPullRequest + ? this._options.stalePrLabel + : this._options.staleIssueLabel; + } +} diff --git a/src/classes/issue-logger.spec.ts b/src/classes/loggers/issue-logger.spec.ts similarity index 97% rename from src/classes/issue-logger.spec.ts rename to src/classes/loggers/issue-logger.spec.ts index 23d4b9a1d..27e7f90b0 100644 --- a/src/classes/issue-logger.spec.ts +++ b/src/classes/loggers/issue-logger.spec.ts @@ -1,4 +1,4 @@ -import {Issue} from '../IssueProcessor'; +import {Issue} from '../issue'; import {IssueLogger} from './issue-logger'; import * as core from '@actions/core'; diff --git a/src/classes/issue-logger.ts b/src/classes/loggers/issue-logger.ts similarity index 89% rename from src/classes/issue-logger.ts rename to src/classes/loggers/issue-logger.ts index bbdbae776..979c231fa 100644 --- a/src/classes/issue-logger.ts +++ b/src/classes/loggers/issue-logger.ts @@ -1,11 +1,11 @@ import * as core from '@actions/core'; -import {Issue} from '../IssueProcessor'; +import {Issue} from '../issue'; import {Logger} from './logger'; export class IssueLogger implements Logger { private readonly _issue: Issue; - constructor(issue: Readonly) { + constructor(issue: Issue) { this._issue = issue; } diff --git a/src/classes/logger.spec.ts b/src/classes/loggers/logger.spec.ts similarity index 100% rename from src/classes/logger.spec.ts rename to src/classes/loggers/logger.spec.ts diff --git a/src/classes/logger.ts b/src/classes/loggers/logger.ts similarity index 100% rename from src/classes/logger.ts rename to src/classes/loggers/logger.ts diff --git a/src/classes/milestones.spec.ts b/src/classes/milestones.spec.ts new file mode 100644 index 000000000..032c1610e --- /dev/null +++ b/src/classes/milestones.spec.ts @@ -0,0 +1,596 @@ +import {IIssue} from '../interfaces/issue'; +import {IssueProcessorOptions} from '../IssueProcessor'; +import {Issue} from './issue'; +import {Milestones} from './milestones'; + +describe('Milestones', (): void => { + let milestones: Milestones; + let optionsInterface: IssueProcessorOptions; + let issue: Issue; + let issueInterface: IIssue; + + beforeEach((): void => { + optionsInterface = { + ascending: false, + closeIssueLabel: '', + closeIssueMessage: '', + closePrLabel: '', + closePrMessage: '', + daysBeforeClose: 0, + daysBeforeIssueClose: 0, + daysBeforeIssueStale: 0, + daysBeforePrClose: 0, + daysBeforePrStale: 0, + daysBeforeStale: 0, + debugOnly: false, + deleteBranch: false, + exemptIssueLabels: '', + exemptPrLabels: '', + onlyLabels: '', + operationsPerRun: 0, + removeStaleWhenUpdated: false, + repoToken: '', + skipStaleIssueMessage: false, + skipStalePrMessage: false, + staleIssueLabel: '', + staleIssueMessage: '', + stalePrLabel: '', + stalePrMessage: '', + startDate: undefined, + exemptIssueMilestones: '', + exemptPrMilestones: '', + exemptMilestones: '' + }; + issueInterface = { + created_at: '', + locked: false, + milestone: undefined, + number: 0, + pull_request: undefined, + state: '', + title: '', + updated_at: '', + labels: [] + }; + }); + + describe('shouldExemptMilestones()', (): void => { + describe('when the given issue is not a pull request', (): void => { + beforeEach((): void => { + issueInterface.pull_request = undefined; + }); + + describe('when the given options are not configured to exempt a milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptMilestones = ''; + }); + + describe('when the given options are not configured to exempt an issue milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptIssueMilestones = ''; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + }); + + describe('when the given options are configured to exempt an issue milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptIssueMilestones = + 'dummy-exempt-issue-milestone'; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt issue milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt issue milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-issue-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + }); + }); + + describe('when the given options are configured to exempt a milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptMilestones = 'dummy-exempt-milestone'; + }); + + describe('when the given options are not configured to exempt an issue milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptIssueMilestones = ''; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + }); + + describe('when the given options are configured to exempt an issue milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptIssueMilestones = + 'dummy-exempt-issue-milestone'; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt issue milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt issue milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-issue-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + + describe('when the given issue does have a milestone different than the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-milestone' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + }); + }); + }); + + describe('when the given issue is a pull request', (): void => { + beforeEach((): void => { + issueInterface.pull_request = {}; + }); + + describe('when the given options are not configured to exempt a milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptMilestones = ''; + }); + + describe('when the given options are not configured to exempt a pull request milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptPrMilestones = ''; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + }); + + describe('when the given options are configured to exempt a pull request milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptPrMilestones = 'dummy-exempt-pr-milestone'; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt pull request milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt pull request milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-pr-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + }); + }); + + describe('when the given options are configured to exempt a milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptMilestones = 'dummy-exempt-milestone'; + }); + + describe('when the given options are not configured to exempt a pull request milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptPrMilestones = ''; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + }); + + describe('when the given options are configured to exempt a pull request milestone', (): void => { + beforeEach((): void => { + optionsInterface.exemptPrMilestones = 'dummy-exempt-pr-milestone'; + }); + + describe('when the given issue does not have a milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = undefined; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone different than the exempt pull request milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt pull request milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-pr-milestone' + }; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(true); + }); + }); + + describe('when the given issue does have a milestone different than the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-title' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have a milestone equaling the exempt milestone', (): void => { + beforeEach((): void => { + issueInterface.milestone = { + title: 'dummy-exempt-milestone' + }; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + milestones = new Milestones(optionsInterface, issue); + + const result = milestones.shouldExemptMilestones(); + + expect(result).toStrictEqual(false); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/classes/milestones.ts b/src/classes/milestones.ts new file mode 100644 index 000000000..99b2cce65 --- /dev/null +++ b/src/classes/milestones.ts @@ -0,0 +1,59 @@ +import deburr from 'lodash.deburr'; +import {wordsToList} from '../functions/words-to-list'; +import {IssueProcessorOptions} from '../IssueProcessor'; +import {Issue} from './issue'; + +type CleanMilestone = string; + +export class Milestones { + private static _cleanMilestone(label: Readonly): CleanMilestone { + return deburr(label.toLowerCase()); + } + + private readonly _options: IssueProcessorOptions; + private readonly _issue: Issue; + + constructor(options: Readonly, issue: Issue) { + this._options = options; + this._issue = issue; + } + + shouldExemptMilestones(): boolean { + const exemptMilestones: string[] = this._getExemptMilestones(); + + return exemptMilestones.some((exemptMilestone: Readonly): boolean => + this._hasMilestone(exemptMilestone) + ); + } + + private _getExemptMilestones(): string[] { + return wordsToList( + this._issue.isPullRequest + ? this._getExemptPullRequestMilestones() + : this._getExemptIssueMilestones() + ); + } + + private _getExemptIssueMilestones(): string { + return this._options.exemptIssueMilestones !== '' + ? this._options.exemptIssueMilestones + : this._options.exemptMilestones; + } + + private _getExemptPullRequestMilestones(): string { + return this._options.exemptPrMilestones !== '' + ? this._options.exemptPrMilestones + : this._options.exemptMilestones; + } + + private _hasMilestone(milestone: Readonly): boolean { + if (!this._issue.milestone) { + return false; + } + + return ( + Milestones._cleanMilestone(milestone) === + Milestones._cleanMilestone(this._issue.milestone.title) + ); + } +} diff --git a/src/functions/is-labeled.spec.ts b/src/functions/is-labeled.spec.ts index fbabb3dbf..249fcd08a 100644 --- a/src/functions/is-labeled.spec.ts +++ b/src/functions/is-labeled.spec.ts @@ -1,4 +1,4 @@ -import {Issue} from '../IssueProcessor'; +import {Issue} from '../classes/issue'; import {isLabeled} from './is-labeled'; describe('isLabeled()', (): void => { diff --git a/src/functions/is-labeled.ts b/src/functions/is-labeled.ts index 751117546..791cd0a82 100644 --- a/src/functions/is-labeled.ts +++ b/src/functions/is-labeled.ts @@ -1,15 +1,16 @@ import deburr from 'lodash.deburr'; -import {Issue, Label} from '../IssueProcessor'; +import {Issue} from '../classes/issue'; +import {Label} from '../IssueProcessor'; import {CleanLabel} from '../types/clean-label'; /** * @description - * Check if the label is listed as a label of the issue + * Check if the given label is listed as a label of the given issue * * @param {Readonly} issue A GitHub issue containing some labels * @param {Readonly} label The label to check the presence with * - * @return {boolean} Return true when the given label is also in the issue labels + * @return {boolean} Return true when the given label is also in the given issue labels */ export function isLabeled( issue: Readonly, diff --git a/src/functions/is-pull-request.spec.ts b/src/functions/is-pull-request.spec.ts index cfa20d604..b0851f1d1 100644 --- a/src/functions/is-pull-request.spec.ts +++ b/src/functions/is-pull-request.spec.ts @@ -1,4 +1,4 @@ -import {Issue} from '../IssueProcessor'; +import {Issue} from '../classes/issue'; import {isPullRequest} from './is-pull-request'; describe('isPullRequest()', (): void => { diff --git a/src/functions/is-pull-request.ts b/src/functions/is-pull-request.ts index 4c3ae99de..1ba0017c4 100644 --- a/src/functions/is-pull-request.ts +++ b/src/functions/is-pull-request.ts @@ -1,4 +1,4 @@ -import {Issue} from '../IssueProcessor'; +import {Issue} from '../classes/issue'; export function isPullRequest(issue: Readonly): boolean { return !!issue.pull_request; diff --git a/src/functions/labels-to-list.spec.ts b/src/functions/labels-to-list.spec.ts deleted file mode 100644 index f70c28140..000000000 --- a/src/functions/labels-to-list.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {labelsToList} from './labels-to-list'; - -describe('labelsToList()', (): void => { - let labels: string; - - describe('when the given labels is empty', (): void => { - beforeEach((): void => { - labels = ''; - }); - - it('should return an empty list of labels', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual([]); - }); - }); - - describe('when the given labels is a simple label', (): void => { - beforeEach((): void => { - labels = 'label'; - }); - - it('should return a list of one label', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual(['label']); - }); - }); - - describe('when the given labels is a label with extra spaces before and after', (): void => { - beforeEach((): void => { - labels = ' label '; - }); - - it('should return a list of one label and remove all spaces before and after', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual(['label']); - }); - }); - - describe('when the given labels is a kebab case label', (): void => { - beforeEach((): void => { - labels = 'kebab-case-label'; - }); - - it('should return a list of one label', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual(['kebab-case-label']); - }); - }); - - describe('when the given labels is two kebab case labels separated with a comma', (): void => { - beforeEach((): void => { - labels = 'kebab-case-label-1,kebab-case-label-2'; - }); - - it('should return a list of two labels', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual([ - 'kebab-case-label-1', - 'kebab-case-label-2' - ]); - }); - }); - - describe('when the given labels is a multiple word label', (): void => { - beforeEach((): void => { - labels = 'label like a sentence'; - }); - - it('should return a list of one label', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual(['label like a sentence']); - }); - }); - - describe('when the given labels is two multiple word labels separated with a comma', (): void => { - beforeEach((): void => { - labels = 'label like a sentence, another label like a sentence'; - }); - - it('should return a list of two labels', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual([ - 'label like a sentence', - 'another label like a sentence' - ]); - }); - }); - - describe('when the given labels is a multiple word label with %20 spaces', (): void => { - beforeEach((): void => { - labels = 'label%20like%20a%20sentence'; - }); - - it('should return a list of one label', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual(['label%20like%20a%20sentence']); - }); - }); - - describe('when the given labels is two multiple word labels with %20 spaces separated with a comma', (): void => { - beforeEach((): void => { - labels = - 'label%20like%20a%20sentence,another%20label%20like%20a%20sentence'; - }); - - it('should return a list of two labels', (): void => { - expect.assertions(1); - - const result = labelsToList(labels); - - expect(result).toStrictEqual([ - 'label%20like%20a%20sentence', - 'another%20label%20like%20a%20sentence' - ]); - }); - }); -}); diff --git a/src/functions/labels-to-list.ts b/src/functions/labels-to-list.ts deleted file mode 100644 index 59ac5de62..000000000 --- a/src/functions/labels-to-list.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @description - * Transform a string of comma separated labels - * to an array of labels - * - * @example - * labelsToList('label') => ['label'] - * labelsToList('label,label') => ['label', 'label'] - * labelsToList('kebab-label') => ['kebab-label'] - * labelsToList('kebab%20label') => ['kebab%20label'] - * labelsToList('label with words') => ['label with words'] - * - * @param {Readonly} labels A string of comma separated labels - * - * @return {string[]} A list of labels - */ -export function labelsToList(labels: Readonly): string[] { - if (!labels.length) { - return []; - } - - return labels.split(',').map(l => l.trim()); -} diff --git a/src/functions/words-to-list.spec.ts b/src/functions/words-to-list.spec.ts new file mode 100644 index 000000000..06f75768e --- /dev/null +++ b/src/functions/words-to-list.spec.ts @@ -0,0 +1,137 @@ +import {wordsToList} from './words-to-list'; + +describe('wordsToList()', (): void => { + let words: string; + + describe('when the given words is empty', (): void => { + beforeEach((): void => { + words = ''; + }); + + it('should return an empty list of words', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual([]); + }); + }); + + describe('when the given words is a simple word', (): void => { + beforeEach((): void => { + words = 'word'; + }); + + it('should return a list of one word', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['word']); + }); + }); + + describe('when the given words is a word with extra spaces before and after', (): void => { + beforeEach((): void => { + words = ' word '; + }); + + it('should return a list of one word and remove all spaces before and after', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['word']); + }); + }); + + describe('when the given words is a kebab case word', (): void => { + beforeEach((): void => { + words = 'kebab-case-word'; + }); + + it('should return a list of one word', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['kebab-case-word']); + }); + }); + + describe('when the given words is two kebab case words separated with a comma', (): void => { + beforeEach((): void => { + words = 'kebab-case-word-1,kebab-case-word-2'; + }); + + it('should return a list of two words', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['kebab-case-word-1', 'kebab-case-word-2']); + }); + }); + + describe('when the given words is a multiple word word', (): void => { + beforeEach((): void => { + words = 'word like a sentence'; + }); + + it('should return a list of one word', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['word like a sentence']); + }); + }); + + describe('when the given words is two multiple word words separated with a comma', (): void => { + beforeEach((): void => { + words = 'word like a sentence, another word like a sentence'; + }); + + it('should return a list of two words', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual([ + 'word like a sentence', + 'another word like a sentence' + ]); + }); + }); + + describe('when the given words is a multiple word word with %20 spaces', (): void => { + beforeEach((): void => { + words = 'word%20like%20a%20sentence'; + }); + + it('should return a list of one word', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual(['word%20like%20a%20sentence']); + }); + }); + + describe('when the given words is two multiple word words with %20 spaces separated with a comma', (): void => { + beforeEach((): void => { + words = 'word%20like%20a%20sentence,another%20word%20like%20a%20sentence'; + }); + + it('should return a list of two words', (): void => { + expect.assertions(1); + + const result = wordsToList(words); + + expect(result).toStrictEqual([ + 'word%20like%20a%20sentence', + 'another%20word%20like%20a%20sentence' + ]); + }); + }); +}); diff --git a/src/functions/words-to-list.ts b/src/functions/words-to-list.ts new file mode 100644 index 000000000..8eb3701d3 --- /dev/null +++ b/src/functions/words-to-list.ts @@ -0,0 +1,23 @@ +/** + * @description + * Transform a string of comma separated words + * to an array of words + * + * @example + * wordsToList('label') => ['label'] + * wordsToList('label,label') => ['label', 'label'] + * wordsToList('kebab-label') => ['kebab-label'] + * wordsToList('kebab%20label') => ['kebab%20label'] + * wordsToList('label with words') => ['label with words'] + * + * @param {Readonly} words A string of comma separated words + * + * @return {string[]} A list of words + */ +export function wordsToList(words: Readonly): string[] { + if (!words.length) { + return []; + } + + return words.split(',').map((word: Readonly): string => word.trim()); +} diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts new file mode 100644 index 000000000..9ee7ce8ec --- /dev/null +++ b/src/interfaces/issue.ts @@ -0,0 +1,15 @@ +import {Label} from '../IssueProcessor'; +import {IsoDateString} from '../types/iso-date-string'; +import {IMilestone} from './milestone'; + +export interface IIssue { + title: string; + number: number; + created_at: IsoDateString; + updated_at: IsoDateString; + labels: Label[]; + pull_request: Object | null | undefined; + state: string; + locked: boolean; + milestone: IMilestone | undefined; +} diff --git a/src/interfaces/milestone.ts b/src/interfaces/milestone.ts new file mode 100644 index 000000000..d0ff24251 --- /dev/null +++ b/src/interfaces/milestone.ts @@ -0,0 +1,3 @@ +export interface IMilestone { + title: string; +} diff --git a/src/main.ts b/src/main.ts index 2e44657da..31a492ea5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -52,16 +52,15 @@ function getAndValidateArgs(): IssueProcessorOptions { startDate: core.getInput('start-date') !== '' ? core.getInput('start-date') - : undefined + : undefined, + exemptMilestones: core.getInput('exempt-milestones'), + exemptIssueMilestones: core.getInput('exempt-issue-milestones'), + exemptPrMilestones: core.getInput('exempt-pr-milestones') }; for (const numberInput of [ 'days-before-stale', - 'days-before-issue-stale', - 'days-before-pr-stale', 'days-before-close', - 'days-before-issue-close', - 'days-before-pr-close', 'operations-per-run' ]) { if (isNaN(parseInt(core.getInput(numberInput)))) {