diff --git a/.eslintrc.json b/.eslintrc.json index 57346b613..5f707a9a1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,52 +1,57 @@ { - "plugins": ["jest", "@typescript-eslint"], - "extends": ["plugin:github/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "rules": { - "eslint-comments/no-use": "off", - "import/no-namespace": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], - "@typescript-eslint/no-require-imports": "error", - "@typescript-eslint/array-type": "error", - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/ban-ts-comment": "error", - "camelcase": "off", - "@typescript-eslint/consistent-type-assertions": "error", - "@typescript-eslint/func-call-spacing": ["error", "never"], - "@typescript-eslint/no-array-constructor": "error", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-extraneous-class": "error", - "@typescript-eslint/no-for-in-array": "error", - "@typescript-eslint/no-inferrable-types": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-namespace": "error", - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-unnecessary-qualifier": "error", - "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-for-of": "warn", - "@typescript-eslint/prefer-function-type": "warn", - "@typescript-eslint/prefer-includes": "error", - "@typescript-eslint/prefer-string-starts-ends-with": "error", - "@typescript-eslint/promise-function-async": "error", - "@typescript-eslint/require-array-sort-compare": "error", - "@typescript-eslint/restrict-plus-operands": "error", - "semi": "off", - "@typescript-eslint/type-annotation-spacing": "error", - "@typescript-eslint/unbound-method": "off" - }, - "env": { - "node": true, - "es6": true, - "jest/globals": true - } - } \ No newline at end of file + "plugins": ["jest", "@typescript-eslint"], + "extends": ["plugin:github/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "rules": { + "eslint-comments/no-use": "off", + "import/no-namespace": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + {"accessibility": "no-public"} + ], + "@typescript-eslint/no-require-imports": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/ban-ts-comment": "error", + "camelcase": "off", + "@typescript-eslint/consistent-type-assertions": "error", + "@typescript-eslint/func-call-spacing": ["error", "never"], + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extraneous-class": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "semi": "off", + "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/unbound-method": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error" + }, + "env": { + "node": true, + "es6": true, + "jest/globals": true + } +} diff --git a/.licensed.yml b/.licensed.yml index 15f619829..4e473ae21 100644 --- a/.licensed.yml +++ b/.licensed.yml @@ -11,4 +11,4 @@ allowed: - unlicense reviewed: - npm: \ No newline at end of file + npm: diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..91c870c48 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +.idea +.licenses +.vscode +dist +node_modules +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json index f6736bc76..b243c2ff0 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,11 +1,10 @@ { - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": true, - "trailingComma": "none", - "bracketSpacing": false, - "arrowParens": "avoid", - "parser": "typescript" - } \ No newline at end of file + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "bracketSpacing": false, + "arrowParens": "avoid" +} diff --git a/README.md b/README.md index d9d45bd76..5d84ebfdd 100644 --- a/README.md +++ b/README.md @@ -24,28 +24,32 @@ $ npm test ### Arguments -| Input | Description | Usage | -| :-------------------------: | :-------------------------------------------------------------------------------: | :------: | -| `repo-token` | PAT(Personal Access Token) for authorizing repository. | Optional | -| `days-before-stale` | Idle number of days before marking an issue/pr as stale. \*Defaults to **60\*** | Optional | -| `days-before-close` | Idle number of days before closing an stale issue/pr. \*Defaults to **7\*** | Optional | -| `stale-issue-message` | Message to post on the stale issue. | Optional | -| `stale-pr-message` | Message to post on the stale pr. | Optional | -| `close-issue-message` | Message to post on the stale issue while closing it. | Optional | -| `close-pr-message` | Message to post on the stale pr while closing it. | Optional | -| `stale-issue-label` | Label to apply on the stale issue. \*Defaults to **stale\*** | Optional | -| `close-issue-label` | Label to apply on closing issue. | Optional | -| `stale-pr-label` | Label to apply on the stale pr. | Optional | -| `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 | -| `only-labels` | Only labels checked for stale issue/pr. | Optional | -| `operations-per-run` | Maximum number of operations per run. \*Defaults to **30\*** | Optional | -| `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. \*Defaults to **true\*** | Optional | -| `debug-only` | Dry-run on action. \*Defaults to **false\*** | Optional | -| `ascending` | Order to get issues/pr. \*Defaults to **false\*** | Optional | -| `skip-stale-issue-message` | Skip adding stale message on stale issue. \*Defaults to **false\*** | Optional | -| `skip-stale-pr-message` | Skip adding stale message on stale pr. \*Defaults to **false\*** | Optional | +| Input | Description | Usage | +| --------------------------- | ------------------------------------------------------------------------------------ | -------- | +| `repo-token` | PAT(Personal Access Token) for authorizing repository. | Optional | +| `days-before-stale` | Idle number of days before marking an issue/pr as stale. \*Defaults to **60** | Optional | +| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional | +| `days-before-pr-stale` | Idle number of days before marking an pr as stale (override `days-before-stale`). | Optional | +| `days-before-close` | Idle number of days before closing an stale issue/pr. \*Defaults to **7\*** | Optional | +| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional | +| `days-before-pr-close` | Idle number of days before closing an stale pr (override `days-before-close`). | Optional | +| `stale-issue-message` | Message to post on the stale issue. | Optional | +| `stale-pr-message` | Message to post on the stale pr. | Optional | +| `close-issue-message` | Message to post on the stale issue while closing it. | Optional | +| `close-pr-message` | Message to post on the stale pr while closing it. | Optional | +| `stale-issue-label` | Label to apply on the stale issue. \*Defaults to **stale\*** | Optional | +| `close-issue-label` | Label to apply on closing issue. | Optional | +| `stale-pr-label` | Label to apply on the stale pr. | Optional | +| `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 | +| `only-labels` | Only labels checked for stale issue/pr. | Optional | +| `operations-per-run` | Maximum number of operations per run. \*Defaults to **30\*** | Optional | +| `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. \*Defaults to **true\*** | Optional | +| `debug-only` | Dry-run on action. \*Defaults to **false\*** | Optional | +| `ascending` | Order to get issues/pr. \*Defaults to **false\*** | Optional | +| `skip-stale-issue-message` | Skip adding stale message on stale issue. \*Defaults to **false\*** | Optional | +| `skip-stale-pr-message` | Skip adding stale message on stale pr. \*Defaults to **false\*** | Optional | ### Usage @@ -54,7 +58,7 @@ See [action.yml](./action.yml) For comprehensive list of options. Basic: ```yaml -name: 'Close stale issues' +name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' @@ -72,7 +76,7 @@ jobs: Configure stale timeouts: ```yaml -name: 'Close stale issues' +name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' @@ -83,15 +87,63 @@ jobs: steps: - uses: actions/stale@v3 with: - stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' days-before-stale: 30 days-before-close: 5 ``` +Configure different stale timeouts but never close a pr: + +```yaml +name: 'Close stale issues and PR' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' + close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' + days-before-stale: 30 + days-before-close: 5 + days-before-pr-close: -1 +``` + +Configure different stale timeouts: + +```yaml +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' + close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' + close-pr-message: 'This pr was closed because it has been stalled for 10 days with no activity.' + days-before-issue-stale: 30 + days-before-pr-stale: 45 + days-before-issue-close: 5 + days-before-pr-close: 10 +``` + Configure labels: ```yaml -name: 'Close stale issues' +name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 6b8f3da23..3de60e560 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,8 +1,8 @@ import * as github from '@actions/github'; import { - IssueProcessor, Issue, + IssueProcessor, IssueProcessorOptions } from '../src/IssueProcessor'; @@ -35,7 +35,11 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ closeIssueMessage: 'This issue is being closed', closePrMessage: 'This PR is being closed', daysBeforeStale: 1, + daysBeforeIssueStale: NaN, + daysBeforePrStale: NaN, daysBeforeClose: 30, + daysBeforeIssueClose: NaN, + daysBeforePrClose: NaN, staleIssueLabel: 'Stale', closeIssueLabel: '', exemptIssueLabels: '', @@ -73,8 +77,36 @@ test('processing an issue with no label will make it stale and close it, if it i generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') ]; - const opts = {...DefaultProcessorOptions}; - opts.daysBeforeClose = 0; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0 + }; + + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues.length).toEqual(1); +}); + +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 processor = new IssueProcessor( opts, @@ -92,16 +124,70 @@ test('processing an issue with no label will make it stale and close it, if it i expect(processor.deletedBranchIssues.length).toEqual(0); }); +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 processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues.length).toEqual(0); +}); + 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 = {...DefaultProcessorOptions}; - opts.daysBeforeClose = 15; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 15 + }; const processor = new IssueProcessor( - DefaultProcessorOptions, + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues.length).toEqual(0); +}); + +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 processor = new IssueProcessor( + opts, async () => 'abot', async p => (p == 1 ? TestIssueList : []), async (num, dt) => [], @@ -120,7 +206,7 @@ test('processing an issue with no label will not make it stale if days-before-st generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') ]; - const opts = { + const opts: IssueProcessorOptions = { ...DefaultProcessorOptions, staleIssueMessage: '', daysBeforeStale: -1 @@ -141,6 +227,33 @@ test('processing an issue with no label will not make it stale if days-before-st expect(processor.closedIssues.length).toEqual(0); }); +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 processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues.length).toEqual(0); +}); + test('processing an issue with no label will make it stale but not close it', async () => { // issue should be from 2 days ago so it will be // stale but not close-able, based on default settings @@ -177,8 +290,13 @@ test('processing a stale issue will close it', async () => { ) ]; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30 + }; + const processor = new IssueProcessor( - DefaultProcessorOptions, + opts, async () => 'abot', async p => (p == 1 ? TestIssueList : []), async (num, dt) => [], @@ -254,6 +372,38 @@ test('processing a stale issue containing a slash in the label will close it', a expect(processor.closedIssues.length).toEqual(1); }); +test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => { + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'A stale issue that should be closed', + '2020-01-01T17:00:00Z', + false, + ['Stale'] + ) + ]; + + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30, + daysBeforeIssueStale: 30 + }; + + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues.length).toEqual(1); +}); + test('processing a stale PR will close it', async () => { const TestIssueList: Issue[] = [ generateIssue( @@ -265,8 +415,45 @@ test('processing a stale PR will close it', async () => { ) ]; + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30 + }; + const processor = new IssueProcessor( - DefaultProcessorOptions, + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues.length).toEqual(1); +}); + +test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => { + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'A stale PR that should be closed', + '2020-01-01T17:00:00Z', + true, + ['Stale'] + ) + ]; + + const opts: IssueProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 30, + daysBeforePrClose: 30 + }; + + const processor = new IssueProcessor( + opts, async () => 'abot', async p => (p == 1 ? TestIssueList : []), async (num, dt) => [], @@ -308,6 +495,35 @@ test('processing a stale issue will close it even if configured not to mark as s expect(processor.closedIssues.length).toEqual(1); }); +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 TestIssueList: Issue[] = [ + generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', false, [ + 'Stale' + ]) + ]; + + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: 0, + daysBeforeIssueStale: -1, + staleIssueMessage: '' + }; + + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues.length).toEqual(1); +}); + test('processing a stale PR will close it even if configured not to mark as stale', async () => { const TestIssueList: Issue[] = [ generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [ @@ -336,6 +552,35 @@ test('processing a stale PR will close it even if configured not to mark as stal expect(processor.closedIssues.length).toEqual(1); }); +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 TestIssueList: Issue[] = [ + generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [ + 'Stale' + ]) + ]; + + const opts = { + ...DefaultProcessorOptions, + daysBeforeStale: 0, + daysBeforePrStale: -1, + stalePrMessage: '' + }; + + const processor = new IssueProcessor( + opts, + async () => 'abot', + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues.length).toEqual(1); +}); + test('closed issues will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( @@ -692,7 +937,14 @@ test('stale label should be removed if a comment was added to a stale issue', as opts, async () => 'abot', async p => (p == 1 ? TestIssueList : []), - async (num: number, dt: string) => [{user: {login: 'notme', type: 'User'}}], // return a fake comment to indicate there was an update + async (num: number, dt: string) => [ + { + user: { + login: 'notme', + type: 'User' + } + } + ], // return a fake comment to indicate there was an update async (issue: Issue, label: string) => new Date().toDateString() ); @@ -723,7 +975,14 @@ test('stale label should not be removed if a comment was added by the bot (and t opts, async () => 'abot', async p => (p == 1 ? TestIssueList : []), - async (num: number, dt: string) => [{user: {login: 'abot', type: 'User'}}], // return a fake comment to indicate there was an update by the bot + async (num: number, dt: string) => [ + { + user: { + login: 'abot', + type: 'User' + } + } + ], // return a fake comment to indicate there was an update by the bot async (issue: Issue, label: string) => new Date().toDateString() ); diff --git a/action.yml b/action.yml index b561962fc..6fd705494 100644 --- a/action.yml +++ b/action.yml @@ -7,58 +7,90 @@ inputs: default: ${{ github.token }} stale-issue-message: description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.' + required: false stale-pr-message: description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.' + required: false close-issue-message: description: 'The message to post on the issue when closing it. If none provided, will not comment when closing an issue.' + required: false close-pr-message: description: 'The message to post on the pr when closing it. If none provided, will not comment when closing a pull requests.' + required: false days-before-stale: - description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.' - default: 60 + description: 'The number of days old an issue or a pull request can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.' + 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.' + 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.' + required: false days-before-close: - description: 'The number of days to wait to close an issue or pull request after it being marked stale. Set to -1 to never close stale issues.' - default: 7 + 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.' + 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.' + required: false stale-issue-label: description: 'The label to apply when an issue is stale.' + required: false default: 'Stale' close-issue-label: description: 'The label to apply when an issue is closed.' + required: false exempt-issue-labels: description: 'The labels that mean an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")' default: '' + required: false stale-pr-label: description: 'The label to apply when a pull request is stale.' default: 'Stale' + required: false close-pr-label: description: 'The label to apply when a pull request is closed.' + required: false exempt-pr-labels: 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 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: '' + required: false operations-per-run: description: 'The maximum number of operations per run, used to control rate limiting.' - default: 30 + default: '30' + required: false remove-stale-when-updated: description: 'Remove stale labels from issues when they are updated or commented on.' - default: true + default: 'true' + required: false debug-only: description: 'Run the processor in debug mode without actually performing any operations on live issues.' - default: false + default: 'false' + required: false ascending: description: 'The order to get issues or pull requests. Defaults to false, which is descending' - default: false + default: 'false' + required: false skip-stale-pr-message: description: 'Skip adding stale message when marking a pull request as stale.' - default: false + default: 'false' + required: false skip-stale-issue-message: description: 'Skip adding stale message when marking an issue as stale.' - default: false + default: 'false' + required: false delete-branch: description: 'Delete the git branch after closing a stale pull request.' - default: false + default: 'false' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/package-lock.json b/package-lock.json index c6007af7b..acfe5ed4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1912,53 +1912,60 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.0.tgz", - "integrity": "sha512-4OEcPON3QIx0ntsuiuFP/TkldmBGXf0uKxPQlGtS/W2F3ndYm8Vgdpj/woPJkzUc65gd3iR+qi3K8SDQP/obFg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz", + "integrity": "sha512-ygqDUm+BUPvrr0jrXqoteMqmIaZ/bixYOc3A4BRwzEPTZPi6E+n44rzNZWaB0YvtukgP+aoj0i/fyx7FkM2p1w==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.7.0", + "@typescript-eslint/experimental-utils": "4.13.0", + "@typescript-eslint/scope-manager": "4.13.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" }, "dependencies": { "@typescript-eslint/experimental-utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.0.tgz", - "integrity": "sha512-xpfXXAfZqhhqs5RPQBfAFrWDHoNxD5+sVB5A46TF58Bq1hRfVROrWHcQHHUM9aCBdy9+cwATcvCbRg8aIRbaHQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz", + "integrity": "sha512-/ZsuWmqagOzNkx30VWYV3MNB/Re/CGv/7EzlqZo5RegBN8tMuPaBgNK6vPBCQA8tcYrbsrTdbx3ixMRRKEEGVw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.7.0", - "@typescript-eslint/typescript-estree": "3.7.0", + "@typescript-eslint/scope-manager": "4.13.0", + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/typescript-estree": "4.13.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, + "@typescript-eslint/scope-manager": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz", + "integrity": "sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/visitor-keys": "4.13.0" + } + }, "@typescript-eslint/typescript-estree": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.0.tgz", - "integrity": "sha512-xr5oobkYRebejlACGr1TJ0Z/r0a2/HUf0SXqPvlgUMwiMqOCu/J+/Dr9U3T0IxpE5oLFSkqMx1FE/dKaZ8KsOQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz", + "integrity": "sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg==", "dev": true, "requires": { - "@typescript-eslint/types": "3.7.0", - "@typescript-eslint/visitor-keys": "3.7.0", + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/visitor-keys": "4.13.0", "debug": "^4.1.1", - "glob": "^7.1.6", + "globby": "^11.0.1", "is-glob": "^4.0.1", "lodash": "^4.17.15", "semver": "^7.3.2", "tsutils": "^3.17.1" } - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true } } }, @@ -1985,41 +1992,35 @@ } }, "@typescript-eslint/parser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.8.1.tgz", - "integrity": "sha512-QND8XSVetATHK9y2Ltc/XBl5Ro7Y62YuZKnPEwnNPB8E379fDsvzJ1dMJ46fg/VOmk0hXhatc+GXs5MaXuL5Uw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.13.0.tgz", + "integrity": "sha512-KO0J5SRF08pMXzq9+abyHnaGQgUJZ3Z3ax+pmqz9vl81JxmTTOUfQmq7/4awVfq09b6C4owNlOgOwp61pYRBSg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.8.1", - "@typescript-eslint/types": "4.8.1", - "@typescript-eslint/typescript-estree": "4.8.1", + "@typescript-eslint/scope-manager": "4.13.0", + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/typescript-estree": "4.13.0", "debug": "^4.1.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.8.1.tgz", - "integrity": "sha512-r0iUOc41KFFbZdPAdCS4K1mXivnSZqXS5D9oW+iykQsRlTbQRfuFRSW20xKDdYiaCoH+SkSLeIF484g3kWzwOQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz", + "integrity": "sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.8.1", - "@typescript-eslint/visitor-keys": "4.8.1" + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/visitor-keys": "4.13.0" } }, - "@typescript-eslint/types": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.8.1.tgz", - "integrity": "sha512-ave2a18x2Y25q5K05K/U3JQIe2Av4+TNi/2YuzyaXLAsDx6UZkz1boZ7nR/N6Wwae2PpudTZmHFXqu7faXfHmA==", - "dev": true - }, "@typescript-eslint/typescript-estree": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.1.tgz", - "integrity": "sha512-bJ6Fn/6tW2g7WIkCWh3QRlaSU7CdUUK52shx36/J7T5oTQzANvi6raoTsbwGM11+7eBbeem8hCCKbyvAc0X3sQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz", + "integrity": "sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.8.1", - "@typescript-eslint/visitor-keys": "4.8.1", + "@typescript-eslint/types": "4.13.0", + "@typescript-eslint/visitor-keys": "4.13.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -2027,22 +2028,6 @@ "semver": "^7.3.2", "tsutils": "^3.17.1" } - }, - "@typescript-eslint/visitor-keys": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.1.tgz", - "integrity": "sha512-3nrwXFdEYALQh/zW8rFwP4QltqsanCDz4CwWMPiIZmwlk9GlvBeueEIbq05SEq4ganqM0g9nh02xXgv5XI3PeQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.8.1", - "eslint-visitor-keys": "^2.0.0" - } - }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", - "dev": true } } }, @@ -2081,9 +2066,9 @@ } }, "@typescript-eslint/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.7.0.tgz", - "integrity": "sha512-reCaK+hyKkKF+itoylAnLzFeNYAEktB0XVfSQvf0gcVgpz1l49Lt6Vo9x4MVCCxiDydA0iLAjTF/ODH0pbfnpg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.13.0.tgz", + "integrity": "sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ==", "dev": true }, "@typescript-eslint/typescript-estree": { @@ -2127,12 +2112,21 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.0.tgz", - "integrity": "sha512-k5PiZdB4vklUpUX4NBncn5RBKty8G3ihTY+hqJsCdMuD0v4jofI5xuqwnVcWxfv6iTm2P/dfEa2wMUnsUY8ODw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.13.0.tgz", + "integrity": "sha512-6RoxWK05PAibukE7jElqAtNMq+RWZyqJ6Q/GdIxaiUj2Ept8jh8+FUVlbq9WxMYxkmEOPvCE5cRSyupMpwW31g==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/types": "4.13.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } } }, "@vercel/ncc": { diff --git a/package.json b/package.json index 824e79616..b25b4f79a 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "build": "tsc", "format": "prettier --write **/*.ts", "format-check": "prettier --check **/*.ts", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", + "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", "pack": "ncc build", "test": "jest", "all": "npm run build && npm run format && npm run lint && npm run pack && npm test" @@ -37,7 +37,8 @@ "@types/lodash.deburr": "^4.1.6", "@types/node": "^14.10.0", "@types/semver": "^7.3.4", - "@typescript-eslint/parser": "^4.8.1", + "@typescript-eslint/eslint-plugin": "^4.13.0", + "@typescript-eslint/parser": "^4.13.0", "@vercel/ncc": "^0.27.0", "eslint": "^7.17.0", "eslint-plugin-github": "^4.0.1", diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts index 78b9b90ca..a37128a33 100644 --- a/src/IssueProcessor.ts +++ b/src/IssueProcessor.ts @@ -2,8 +2,12 @@ import * as core from '@actions/core'; import {context, getOctokit} from '@actions/github'; import {GitHub} from '@actions/github/lib/utils'; import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; +import {IssueType} from './enums/issue-type.enum'; +import {getIssueType} from './functions/get-issue-type'; 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'; export interface Issue { title: string; @@ -48,7 +52,11 @@ export interface IssueProcessorOptions { closeIssueMessage: string; closePrMessage: string; daysBeforeStale: number; + daysBeforeIssueStale: number; // Could be NaN + daysBeforePrStale: number; // Could be NaN daysBeforeClose: number; + daysBeforeIssueClose: number; // Could be NaN + daysBeforePrClose: number; // Could be NaN staleIssueLabel: string; closeIssueLabel: string; exemptIssueLabels: string; @@ -69,14 +77,21 @@ export interface IssueProcessorOptions { * Handle processing of issues for staleness/closure. */ export class IssueProcessor { + private static updatedSince(timestamp: string, num_days: number): boolean { + const daysInMillis = 1000 * 60 * 60 * 24 * num_days; + const millisSinceLastUpdated = + new Date().getTime() - new Date(timestamp).getTime(); + + return millisSinceLastUpdated <= daysInMillis; + } + readonly client: InstanceType; readonly options: IssueProcessorOptions; - private operationsLeft = 0; - readonly staleIssues: Issue[] = []; readonly closedIssues: Issue[] = []; readonly deletedBranchIssues: Issue[] = []; readonly removedLabelIssues: Issue[] = []; + private operationsLeft = 0; constructor( options: IssueProcessorOptions, @@ -131,7 +146,7 @@ export class IssueProcessor { } for (const issue of issues.values()) { - const isPr = !!issue.pull_request; + const isPr = isPullRequest(issue); core.info( `Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})` @@ -156,10 +171,20 @@ export class IssueProcessor { const skipMessage = isPr ? this.options.skipStalePrMessage : this.options.skipStaleIssueMessage; - const issueType: string = isPr ? 'pr' : 'issue'; - const shouldMarkWhenStale = this.options.daysBeforeStale > -1; + const issueType: IssueType = getIssueType(isPr); + const daysBeforeStale: number = isPr + ? this._getDaysBeforePrStale() + : this._getDaysBeforeIssueStale(); + + if (isPr) { + core.info(`Days before pull request stale: ${daysBeforeStale}`); + } else { + core.info(`Days before issue stale: ${daysBeforeStale}`); + } + + const shouldMarkAsStale: boolean = shouldMarkWhenStale(daysBeforeStale); - if (!staleMessage && shouldMarkWhenStale) { + if (!staleMessage && shouldMarkAsStale) { core.info(`Skipping ${issueType} due to empty stale message`); continue; } @@ -199,7 +224,7 @@ export class IssueProcessor { ); // determine if this issue needs to be marked stale first - if (!isStale && shouldBeStale && shouldMarkWhenStale) { + if (!isStale && shouldBeStale && shouldMarkAsStale) { core.info( `Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label` ); @@ -233,7 +258,7 @@ export class IssueProcessor { // handle all of the stale issue logic when we find a stale issue private async processStaleIssue( issue: Issue, - issueType: string, + issueType: IssueType, staleLabel: string, actor: string, closeMessage?: string, @@ -252,9 +277,20 @@ export class IssueProcessor { `Issue #${issue.number} has been commented on: ${issueHasComments}` ); + const isPr: boolean = isPullRequest(issue); + const daysBeforeClose: number = isPr + ? this._getDaysBeforePrClose() + : this._getDaysBeforeIssueClose(); + + if (isPr) { + core.info(`Days before pull request close: ${daysBeforeClose}`); + } else { + core.info(`Days before issue close: ${daysBeforeClose}`); + } + const issueHasUpdate: boolean = IssueProcessor.updatedSince( issue.updated_at, - this.options.daysBeforeClose + daysBeforeClose ); core.info(`Issue #${issue.number} has been updated: ${issueHasUpdate}`); @@ -267,7 +303,7 @@ export class IssueProcessor { } // now start closing logic - if (this.options.daysBeforeClose < 0) { + if (daysBeforeClose < 0) { return; // nothing to do because we aren't closing stale issues } @@ -590,11 +626,27 @@ export class IssueProcessor { return staleLabeledEvent.created_at; } - private static updatedSince(timestamp: string, num_days: number): boolean { - const daysInMillis = 1000 * 60 * 60 * 24 * num_days; - const millisSinceLastUpdated = - new Date().getTime() - new Date(timestamp).getTime(); + private _getDaysBeforeIssueStale(): number { + return isNaN(this.options.daysBeforeIssueStale) + ? this.options.daysBeforeStale + : this.options.daysBeforeIssueStale; + } - return millisSinceLastUpdated <= daysInMillis; + private _getDaysBeforePrStale(): number { + return isNaN(this.options.daysBeforePrStale) + ? this.options.daysBeforeStale + : this.options.daysBeforePrStale; + } + + private _getDaysBeforeIssueClose(): number { + return isNaN(this.options.daysBeforeIssueClose) + ? this.options.daysBeforeClose + : this.options.daysBeforeIssueClose; + } + + private _getDaysBeforePrClose(): number { + return isNaN(this.options.daysBeforePrClose) + ? this.options.daysBeforeClose + : this.options.daysBeforePrClose; } } diff --git a/src/enums/issue-type.enum.ts b/src/enums/issue-type.enum.ts new file mode 100644 index 000000000..682871e6a --- /dev/null +++ b/src/enums/issue-type.enum.ts @@ -0,0 +1,4 @@ +export enum IssueType { + Issue = 'issue', + PullRequest = 'pr' +} diff --git a/src/functions/get-issue-type.spec.ts b/src/functions/get-issue-type.spec.ts new file mode 100644 index 000000000..993cb725c --- /dev/null +++ b/src/functions/get-issue-type.spec.ts @@ -0,0 +1,33 @@ +import {getIssueType} from './get-issue-type'; + +describe('getIssueType()', (): void => { + let isPullRequest: boolean; + + describe('when the issue is a not pull request', (): void => { + beforeEach((): void => { + isPullRequest = false; + }); + + it('should return that the issue is really an issue', (): void => { + expect.assertions(1); + + const result = getIssueType(isPullRequest); + + expect(result).toStrictEqual('issue'); + }); + }); + + describe('when the issue is a pull request', (): void => { + beforeEach((): void => { + isPullRequest = true; + }); + + it('should return that the issue is a pull request', (): void => { + expect.assertions(1); + + const result = getIssueType(isPullRequest); + + expect(result).toStrictEqual('pr'); + }); + }); +}); diff --git a/src/functions/get-issue-type.ts b/src/functions/get-issue-type.ts new file mode 100644 index 000000000..86830a4b7 --- /dev/null +++ b/src/functions/get-issue-type.ts @@ -0,0 +1,5 @@ +import {IssueType} from '../enums/issue-type.enum'; + +export function getIssueType(isPullRequest: Readonly): IssueType { + return isPullRequest ? IssueType.PullRequest : IssueType.Issue; +} diff --git a/src/functions/is-pull-request.spec.ts b/src/functions/is-pull-request.spec.ts new file mode 100644 index 000000000..cfa20d604 --- /dev/null +++ b/src/functions/is-pull-request.spec.ts @@ -0,0 +1,57 @@ +import {Issue} from '../IssueProcessor'; +import {isPullRequest} from './is-pull-request'; + +describe('isPullRequest()', (): void => { + let issue: Issue; + + describe('when the given issue has an undefined pull request', (): void => { + beforeEach((): void => { + issue = { + pull_request: undefined + } as Issue; + }); + + it('should return false', (): void => { + expect.assertions(1); + + const result = isPullRequest(issue); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue has a null pull request', (): void => { + beforeEach((): void => { + issue = { + pull_request: null + } as Issue; + }); + + it('should return false', (): void => { + expect.assertions(1); + + const result = isPullRequest(issue); + + expect(result).toStrictEqual(false); + }); + }); + + describe.each([{}, true])( + 'when the given issue has pull request', + (value): void => { + beforeEach((): void => { + issue = { + pull_request: value + } as Issue; + }); + + it('should return true', (): void => { + expect.assertions(1); + + const result = isPullRequest(issue); + + expect(result).toStrictEqual(true); + }); + } + ); +}); diff --git a/src/functions/is-pull-request.ts b/src/functions/is-pull-request.ts new file mode 100644 index 000000000..4c3ae99de --- /dev/null +++ b/src/functions/is-pull-request.ts @@ -0,0 +1,5 @@ +import {Issue} from '../IssueProcessor'; + +export function isPullRequest(issue: Readonly): boolean { + return !!issue.pull_request; +} diff --git a/src/functions/should-mark-when-stale.spec.ts b/src/functions/should-mark-when-stale.spec.ts new file mode 100644 index 000000000..aad38bb07 --- /dev/null +++ b/src/functions/should-mark-when-stale.spec.ts @@ -0,0 +1,47 @@ +import {shouldMarkWhenStale} from './should-mark-when-stale'; + +describe('shouldMarkWhenStale()', (): void => { + let daysBeforeStale: number; + + describe('when the given number of days indicate that it should be stalled', (): void => { + beforeEach((): void => { + daysBeforeStale = -1; + }); + + it('should return false', (): void => { + expect.assertions(1); + + const result = shouldMarkWhenStale(daysBeforeStale); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given number of days indicate that it should be stalled today', (): void => { + beforeEach((): void => { + daysBeforeStale = 0; + }); + + it('should return true', (): void => { + expect.assertions(1); + + const result = shouldMarkWhenStale(daysBeforeStale); + + expect(result).toStrictEqual(true); + }); + }); + + describe('when the given number of days indicate that it should be stalled tomorrow', (): void => { + beforeEach((): void => { + daysBeforeStale = 1; + }); + + it('should return true', (): void => { + expect.assertions(1); + + const result = shouldMarkWhenStale(daysBeforeStale); + + expect(result).toStrictEqual(true); + }); + }); +}); diff --git a/src/functions/should-mark-when-stale.ts b/src/functions/should-mark-when-stale.ts new file mode 100644 index 000000000..c8f16936d --- /dev/null +++ b/src/functions/should-mark-when-stale.ts @@ -0,0 +1,5 @@ +export function shouldMarkWhenStale( + daysBeforeStale: Readonly +): boolean { + return daysBeforeStale >= 0; +} diff --git a/src/main.ts b/src/main.ts index 17e57c1a2..dfc99a6e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,9 +23,13 @@ function getAndValidateArgs(): IssueProcessorOptions { daysBeforeStale: parseInt( core.getInput('days-before-stale', {required: true}) ), + daysBeforeIssueStale: parseInt(core.getInput('days-before-issue-stale')), + daysBeforePrStale: parseInt(core.getInput('days-before-pr-stale')), daysBeforeClose: parseInt( core.getInput('days-before-close', {required: true}) ), + daysBeforeIssueClose: parseInt(core.getInput('days-before-issue-close')), + daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')), staleIssueLabel: core.getInput('stale-issue-label', {required: true}), closeIssueLabel: core.getInput('close-issue-label'), exemptIssueLabels: core.getInput('exempt-issue-labels'), @@ -48,7 +52,11 @@ function getAndValidateArgs(): IssueProcessorOptions { 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/tsconfig.json b/tsconfig.json index a3871fa7f..4ba71e8d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ //"sourceMap": true }, "exclude": ["node_modules", "**/*.test.ts"] -} \ No newline at end of file +}