diff --git a/README.md b/README.md index 839afb107..6fd4b20ac 100644 --- a/README.md +++ b/README.md @@ -8,52 +8,54 @@ Warns and then closes issues and PRs that have had no activity for a specified a Every argument is optional. -| Input | Description | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `repo-token` | PAT(Personal Access Token) for authorizing repository.
_Defaults to **${{ github.token }}**_. | -| `days-before-stale` | Idle number of days before marking an issue/PR as stale.
_Defaults to **60**_. | -| `days-before-issue-stale` | Idle number of days before marking an issue as stale.
_Override `days-before-stale`_. | -| `days-before-pr-stale` | Idle number of days before marking an PR as stale.
_Override `days-before-stale`_. | -| `days-before-close` | Idle number of days before closing an stale issue/PR.
_Defaults to **7**_. | -| `days-before-issue-close` | Idle number of days before closing an stale issue.
_Override `days-before-close`_. | -| `days-before-pr-close` | Idle number of days before closing an stale PR.
_Override `days-before-close`_. | -| `stale-issue-message` | Message to post on the stale issue. | -| `stale-pr-message` | Message to post on the stale PR. | -| `close-issue-message` | Message to post on the stale issue while closing it. | -| `close-pr-message` | Message to post on the stale PR while closing it. | -| `stale-issue-label` | Label to apply on the stale issue.
_Defaults to **Stale**_. | -| `close-issue-label` | Label to apply on closing issue.
Automatically removed if no longer closed nor locked). | -| `stale-pr-label` | Label to apply on the stale PR.
_Defaults to **Stale**_. | -| `close-pr-label` | Label to apply on the closing PR.
Automatically removed if no longer closed nor locked). | -| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | -| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | -| `only-labels` | Only issues and PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered"). | -| `only-issue-labels` | Only issues with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | -| `only-pr-labels` | Only PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | -| `any-of-labels` | Only issues and PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | -| `any-of-issue-labels` | Only issues with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | -| `any-of-pr-labels` | Only PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | -| `operations-per-run` | Maximum number of operations per run.
GitHub API CRUD related.
_Defaults to **30**_. | -| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments.
_Defaults to **true**_. | -| `debug-only` | Dry-run on action.
_Defaults to **false**_. | -| `ascending` | Order to get issues/PR.
`true` is ascending, `false` is descending.
_Defaults to **false**_. | -| `skip-stale-issue-message` | Skip adding stale message on stale issue.
_Defaults to **false**_. | -| `skip-stale-pr-message` | Skip adding stale message on stale PR.
_Defaults to **false**_. | -| `start-date` | The date used to skip the stale action on issue/PR created before it.
ISO 8601 or RFC 2822. | -| `delete-branch` | Delete the git branch after closing a stale pull request.
_Defaults to **false**_. | -| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | -| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale.
_Override `exempt-milestones`_. | -| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale.
_Override `exempt-milestones`_. | -| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale.
_Priority over `exempt-milestones` rules_. | -| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | -| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | -| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | -| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale.
_Override `exempt-assignees`_. | -| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale.
_Override `exempt-assignees`_. | -| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale.
_Priority over `exempt-assignees` rules_. | -| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | -| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | -| `enable-statistics` | Display some statistics at the end of the logs regarding the stale workflow.
Only when the logs are enabled.
_Defaults to **true**_. | +| Input | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `repo-token` | PAT(Personal Access Token) for authorizing repository.
_Defaults to **${{ github.token }}**_. | +| `days-before-stale` | Idle number of days before marking an issue/PR as stale.
_Defaults to **60**_. | +| `days-before-issue-stale` | Idle number of days before marking an issue as stale.
_Override `days-before-stale`_. | +| `days-before-pr-stale` | Idle number of days before marking an PR as stale.
_Override `days-before-stale`_. | +| `days-before-close` | Idle number of days before closing an stale issue/PR.
_Defaults to **7**_. | +| `days-before-issue-close` | Idle number of days before closing an stale issue.
_Override `days-before-close`_. | +| `days-before-pr-close` | Idle number of days before closing an stale PR.
_Override `days-before-close`_. | +| `stale-issue-message` | Message to post on the stale issue. | +| `stale-pr-message` | Message to post on the stale PR. | +| `close-issue-message` | Message to post on the stale issue while closing it. | +| `close-pr-message` | Message to post on the stale PR while closing it. | +| `stale-issue-label` | Label to apply on the stale issue.
_Defaults to **Stale**_. | +| `close-issue-label` | Label to apply on closing issue.
Automatically removed if no longer closed nor locked). | +| `stale-pr-label` | Label to apply on the stale PR.
_Defaults to **Stale**_. | +| `close-pr-label` | Label to apply on the closing PR.
Automatically removed if no longer closed nor locked). | +| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | +| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | +| `only-labels` | Only issues and PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered"). | +| `only-issue-labels` | Only issues with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | +| `only-pr-labels` | Only PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | +| `any-of-labels` | Only issues and PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | +| `any-of-issue-labels` | Only issues with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | +| `any-of-pr-labels` | Only PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | +| `operations-per-run` | Maximum number of operations per run.
GitHub API CRUD related.
_Defaults to **30**_. | +| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments.
_Defaults to **true**_. | +| `remove-issue-stale-when-updated` | Remove stale label from issue on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `remove-pr-stale-when-updated` | Remove stale label from PR on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `debug-only` | Dry-run on action.
_Defaults to **false**_. | +| `ascending` | Order to get issues/PR.
`true` is ascending, `false` is descending.
_Defaults to **false**_. | +| `skip-stale-issue-message` | Skip adding stale message on stale issue.
_Defaults to **false**_. | +| `skip-stale-pr-message` | Skip adding stale message on stale PR.
_Defaults to **false**_. | +| `start-date` | The date used to skip the stale action on issue/PR created before it.
ISO 8601 or RFC 2822. | +| `delete-branch` | Delete the git branch after closing a stale pull request.
_Defaults to **false**_. | +| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | +| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale.
_Override `exempt-milestones`_. | +| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale.
_Override `exempt-milestones`_. | +| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale.
_Priority over `exempt-milestones` rules_. | +| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | +| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | +| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | +| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale.
_Override `exempt-assignees`_. | +| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale.
_Override `exempt-assignees`_. | +| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale.
_Priority over `exempt-assignees` rules_. | +| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | +| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | +| `enable-statistics` | Display some statistics at the end of the logs regarding the stale workflow.
Only when the logs are enabled.
_Defaults to **true**_. | ### Detailed options diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index 035e4ac19..3b87f1e96 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -27,6 +27,8 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ operationsPerRun: 100, debugOnly: true, removeStaleWhenUpdated: false, + removeIssueStaleWhenUpdated: undefined, + removePrStaleWhenUpdated: undefined, ascending: false, skipStaleIssueMessage: false, skipStalePrMessage: false, diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts index 6d6a0a155..3a86eb227 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -1220,8 +1220,7 @@ 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 opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true}; const TestIssueList: Issue[] = [ generateIssue( opts, @@ -1257,8 +1256,7 @@ 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; + const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true}; github.context.actor = 'abot'; const TestIssueList: Issue[] = [ generateIssue( diff --git a/__tests__/remove-stale-when-updated.spec.ts b/__tests__/remove-stale-when-updated.spec.ts new file mode 100644 index 000000000..24eeef592 --- /dev/null +++ b/__tests__/remove-stale-when-updated.spec.ts @@ -0,0 +1,567 @@ +import {Issue} from '../src/classes/issue'; +import {IIssue} from '../src/interfaces/issue'; +import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; +import {ILabel} from '../src/interfaces/label'; +import {IssuesProcessorMock} from './classes/issues-processor-mock'; +import {DefaultProcessorOptions} from './constants/default-processor-options'; +import {generateIssue} from './functions/generate-issue'; + +let issuesProcessorBuilder: IssuesProcessorBuilder; +let issuesProcessor: IssuesProcessorMock; + +/** + * @description + * Assuming there is a comment on the issue + */ +describe('remove-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); +}); + +describe('remove-issue-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + describe('when the option "remove-issue-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + describe('when the option "remove-issue-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); +}); + +describe('remove-pr-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + describe('when the option "remove-pr-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + describe('when the option "remove-pr-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); +}); + +class IssuesProcessorBuilder { + private _options: IIssuesProcessorOptions = { + ...DefaultProcessorOptions + }; + private _issues: Issue[] = []; + + keepStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeStaleWhenUpdated = false; + + return this; + } + + removeStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeStaleWhenUpdated = true; + + return this; + } + + unsetIssueStaleWhenUpdated(): IssuesProcessorBuilder { + delete this._options.removeIssueStaleWhenUpdated; + + return this; + } + + keepIssueStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenUpdated = false; + + return this; + } + + removeIssueStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenUpdated = true; + + return this; + } + + unsetPrStaleWhenUpdated(): IssuesProcessorBuilder { + delete this._options.removePrStaleWhenUpdated; + + return this; + } + + keepPrStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removePrStaleWhenUpdated = false; + + return this; + } + + removePrStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removePrStaleWhenUpdated = true; + + return this; + } + + issuesOrPrs(issues: Partial[]): IssuesProcessorBuilder { + this._issues = issues.map( + (issue: Readonly>, index: Readonly): Issue => + generateIssue( + this._options, + index, + issue.title ?? 'dummy-title', + issue.updated_at ?? new Date().toDateString(), + issue.created_at ?? new Date().toDateString(), + !!issue.pull_request, + issue.labels ? issue.labels.map(label => label.name) : [] + ) + ); + + return this; + } + + issues(issues: Partial[]): IssuesProcessorBuilder { + this.issuesOrPrs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + pull_request: null + }; + } + ) + ); + + return this; + } + + staleIssues(issues: Partial[]): IssuesProcessorBuilder { + this.issues( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + updated_at: '2020-01-01T17:00:00Z', + created_at: '2020-01-01T17:00:00Z', + labels: issue.labels?.map( + (label: Readonly): ILabel => { + return { + ...label, + name: 'Stale' + }; + } + ) ?? [ + { + name: 'Stale' + } + ] + }; + } + ) + ); + + return this; + } + + prs(issues: Partial[]): IssuesProcessorBuilder { + this.issuesOrPrs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + pull_request: {key: 'value'} + }; + } + ) + ); + + return this; + } + + stalePrs(issues: Partial[]): IssuesProcessorBuilder { + this.prs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + updated_at: '2020-01-01T17:00:00Z', + created_at: '2020-01-01T17:00:00Z', + labels: issue.labels?.map( + (label: Readonly): ILabel => { + return { + ...label, + name: 'Stale' + }; + } + ) ?? [ + { + name: 'Stale' + } + ] + }; + } + ) + ); + + return this; + } + + build(): IssuesProcessorMock { + return new IssuesProcessorMock( + this._options, + async () => 'abot', + async p => (p === 1 ? this._issues : []), + async () => [ + { + user: { + login: 'notme', + type: 'User' + } + } + ], + async () => new Date().toDateString() + ); + } +} diff --git a/action.yml b/action.yml index 43f92678f..0fc0ac6ba 100644 --- a/action.yml +++ b/action.yml @@ -113,7 +113,15 @@ inputs: default: '30' required: false remove-stale-when-updated: - description: 'Remove stale labels from issues when they are updated or commented on.' + description: 'Remove stale labels from issues and pull requests when they are updated or commented on.' + default: 'true' + required: false + remove-issue-stale-when-updated: + description: 'Remove stale labels from issues when they are updated or commented on. Override "remove-stale-when-updated" option regarding only the issues.' + default: 'true' + required: false + remove-pr-stale-when-updated: + description: 'Remove stale labels from pull requests when they are updated or commented on. Override "remove-stale-when-updated" option regarding only the pull requests.' default: 'true' required: false debug-only: diff --git a/dist/index.js b/dist/index.js index 11c43e498..6367819d1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -233,6 +233,7 @@ const option_1 = __nccwpck_require__(5931); 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 is_boolean_1 = __nccwpck_require__(8236); const is_labeled_1 = __nccwpck_require__(6792); const should_mark_when_stale_1 = __nccwpck_require__(2461); const words_to_list_1 = __nccwpck_require__(1883); @@ -519,18 +520,20 @@ class IssuesProcessor { return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); const markedStaleOn = (yield this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at; - issueLogger.info(`$$type marked stale on: ${markedStaleOn}`); + issueLogger.info(`$$type marked stale on: ${chalk_1.default.cyan(markedStaleOn)}`); const issueHasComments = yield this._hasCommentsSince(issue, markedStaleOn, actor); - issueLogger.info(`$$type has been commented on: ${issueHasComments}`); + issueLogger.info(`$$type has been commented on: ${chalk_1.default.cyan(issueHasComments)}`); const daysBeforeClose = issue.isPullRequest ? this._getDaysBeforePrClose() : this._getDaysBeforeIssueClose(); issueLogger.info(`Days before $$type close: ${daysBeforeClose}`); const issueHasUpdate = IssuesProcessor._updatedSince(issue.updated_at, daysBeforeClose); - issueLogger.info(`$$type has been updated: ${issueHasUpdate}`); + issueLogger.info(`$$type has been updated: ${chalk_1.default.cyan(issueHasUpdate)}`); // should we un-stale this issue? - if (this.options.removeStaleWhenUpdated && issueHasComments) { + if (this._shouldRemoveStaleWhenUpdated(issue) && issueHasComments) { yield this._removeStaleLabel(issue, staleLabel); + issueLogger.info(`Skipping the process since the $$type is now un-stale`); + return; // nothing to do because it is no longer stale } // now start closing logic if (daysBeforeClose < 0) { @@ -592,7 +595,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } try { @@ -607,7 +610,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } }); } @@ -633,7 +636,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } if (closeLabel) { @@ -648,7 +651,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } try { @@ -662,7 +665,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error updating this $$type: ${error.message}`); + issueLogger.error(`Error when updating this $$type: ${error.message}`); } }); } @@ -684,7 +687,7 @@ class IssuesProcessor { return pullRequest.data; } catch (error) { - issueLogger.error(`Error getting this $$type: ${error.message}`); + issueLogger.error(`Error when getting this $$type: ${error.message}`); } }); } @@ -703,7 +706,7 @@ class IssuesProcessor { return; } const branch = pullRequest.head.ref; - issueLogger.info(`Deleting branch ${branch} from closed $$type`); + issueLogger.info(`Deleting the branch "${chalk_1.default.cyan(branch)}" from closed $$type`); try { this._operations.consumeOperation(); (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedBranchesCount(); @@ -714,7 +717,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error deleting branch ${branch} from $$type: ${error.message}`); + issueLogger.error(`Error when deleting the branch "${chalk_1.default.cyan(branch)}" from $$type: ${error.message}`); } }); } @@ -723,7 +726,7 @@ class IssuesProcessor { var _a; return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - issueLogger.info(`Removing label "${label}" from $$type`); + issueLogger.info(`Removing the label "${chalk_1.default.cyan(label)}" from the $$type...`); this.removedLabelIssues.push(issue); if (this.options.debugOnly) { return; @@ -737,9 +740,10 @@ class IssuesProcessor { issue_number: issue.number, name: label }); + issueLogger.info(`The label "${chalk_1.default.cyan(label)}" was removed`); } catch (error) { - issueLogger.error(`Error removing a label: ${error.message}`); + issueLogger.error(`Error when removing the label: "${chalk_1.default.cyan(error.message)}"`); } }); } @@ -789,6 +793,18 @@ class IssuesProcessor { } return this.options.anyOfLabels; } + _shouldRemoveStaleWhenUpdated(issue) { + if (issue.isPullRequest) { + if (is_boolean_1.isBoolean(this.options.removePrStaleWhenUpdated)) { + return this.options.removePrStaleWhenUpdated; + } + return this.options.removeStaleWhenUpdated; + } + if (is_boolean_1.isBoolean(this.options.removeIssueStaleWhenUpdated)) { + return this.options.removeIssueStaleWhenUpdated; + } + return this.options.removeStaleWhenUpdated; + } _removeStaleLabel(issue, staleLabel) { var _a; return __awaiter(this, void 0, void 0, function* () { @@ -808,7 +824,7 @@ class IssuesProcessor { return Promise.resolve(); } if (is_labeled_1.isLabeled(issue, closeLabel)) { - issueLogger.info(`The $$type has a close label "${closeLabel}". Removing the close label...`); + issueLogger.info(`The $$type has a close label "${chalk_1.default.cyan(closeLabel)}". Removing the close label...`); yield this._removeLabel(issue, closeLabel); (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedCloseItemsLabelsCount(issue); } @@ -1619,6 +1635,21 @@ function isValidDate(date) { exports.isValidDate = isValidDate; +/***/ }), + +/***/ 8236: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.isBoolean = void 0; +function isBoolean(value) { + return value === true || value === false; +} +exports.isBoolean = isBoolean; + + /***/ }), /***/ 6792: @@ -1794,6 +1825,8 @@ function _getAndValidateArgs() { anyOfPrLabels: core.getInput('any-of-pr-labels'), operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'), + removeIssueStaleWhenUpdated: _toOptionalBoolean(core.getInput('remove-issue-stale-when-updated')), + removePrStaleWhenUpdated: _toOptionalBoolean(core.getInput('remove-pr-stale-when-updated')), debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index 6d10ae9eb..139edf66a 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -35,6 +35,8 @@ describe('Issue', (): void => { anyOfPrLabels: '', operationsPerRun: 0, removeStaleWhenUpdated: false, + removeIssueStaleWhenUpdated: undefined, + removePrStaleWhenUpdated: undefined, repoToken: '', skipStaleIssueMessage: false, skipStalePrMessage: false, diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 2662b0f87..9d3d4356e 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -7,6 +7,7 @@ import {Option} from '../enums/option'; 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 {isBoolean} from '../functions/is-boolean'; import {isLabeled} from '../functions/is-labeled'; import {shouldMarkWhenStale} from '../functions/should-mark-when-stale'; import {wordsToList} from '../functions/words-to-list'; @@ -453,14 +454,16 @@ export class IssuesProcessor { const issueLogger: IssueLogger = new IssueLogger(issue); const markedStaleOn: string = (await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at; - issueLogger.info(`$$type marked stale on: ${markedStaleOn}`); + issueLogger.info(`$$type marked stale on: ${chalk.cyan(markedStaleOn)}`); const issueHasComments: boolean = await this._hasCommentsSince( issue, markedStaleOn, actor ); - issueLogger.info(`$$type has been commented on: ${issueHasComments}`); + issueLogger.info( + `$$type has been commented on: ${chalk.cyan(issueHasComments)}` + ); const daysBeforeClose: number = issue.isPullRequest ? this._getDaysBeforePrClose() @@ -472,11 +475,15 @@ export class IssuesProcessor { issue.updated_at, daysBeforeClose ); - issueLogger.info(`$$type has been updated: ${issueHasUpdate}`); + issueLogger.info(`$$type has been updated: ${chalk.cyan(issueHasUpdate)}`); // should we un-stale this issue? - if (this.options.removeStaleWhenUpdated && issueHasComments) { + if (this._shouldRemoveStaleWhenUpdated(issue) && issueHasComments) { await this._removeStaleLabel(issue, staleLabel); + + issueLogger.info(`Skipping the process since the $$type is now un-stale`); + + return; // nothing to do because it is no longer stale } // now start closing logic @@ -565,7 +572,7 @@ export class IssuesProcessor { body: staleMessage }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } @@ -580,7 +587,7 @@ export class IssuesProcessor { labels: [staleLabel] }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } @@ -610,7 +617,7 @@ export class IssuesProcessor { body: closeMessage }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } @@ -625,7 +632,7 @@ export class IssuesProcessor { labels: [closeLabel] }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } @@ -639,7 +646,7 @@ export class IssuesProcessor { state: 'closed' }); } catch (error) { - issueLogger.error(`Error updating this $$type: ${error.message}`); + issueLogger.error(`Error when updating this $$type: ${error.message}`); } } @@ -663,7 +670,7 @@ export class IssuesProcessor { return pullRequest.data; } catch (error) { - issueLogger.error(`Error getting this $$type: ${error.message}`); + issueLogger.error(`Error when getting this $$type: ${error.message}`); } } @@ -687,7 +694,9 @@ export class IssuesProcessor { } const branch = pullRequest.head.ref; - issueLogger.info(`Deleting branch ${branch} from closed $$type`); + issueLogger.info( + `Deleting the branch "${chalk.cyan(branch)}" from closed $$type` + ); try { this._operations.consumeOperation(); @@ -699,7 +708,9 @@ export class IssuesProcessor { }); } catch (error) { issueLogger.error( - `Error deleting branch ${branch} from $$type: ${error.message}` + `Error when deleting the branch "${chalk.cyan(branch)}" from $$type: ${ + error.message + }` ); } } @@ -708,7 +719,9 @@ export class IssuesProcessor { private async _removeLabel(issue: Issue, label: string): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - issueLogger.info(`Removing label "${label}" from $$type`); + issueLogger.info( + `Removing the label "${chalk.cyan(label)}" from the $$type...` + ); this.removedLabelIssues.push(issue); if (this.options.debugOnly) { @@ -724,8 +737,11 @@ export class IssuesProcessor { issue_number: issue.number, name: label }); + issueLogger.info(`The label "${chalk.cyan(label)}" was removed`); } catch (error) { - issueLogger.error(`Error removing a label: ${error.message}`); + issueLogger.error( + `Error when removing the label: "${chalk.cyan(error.message)}"` + ); } } @@ -781,6 +797,22 @@ export class IssuesProcessor { return this.options.anyOfLabels; } + private _shouldRemoveStaleWhenUpdated(issue: Issue): boolean { + if (issue.isPullRequest) { + if (isBoolean(this.options.removePrStaleWhenUpdated)) { + return this.options.removePrStaleWhenUpdated; + } + + return this.options.removeStaleWhenUpdated; + } + + if (isBoolean(this.options.removeIssueStaleWhenUpdated)) { + return this.options.removeIssueStaleWhenUpdated; + } + + return this.options.removeStaleWhenUpdated; + } + private async _removeStaleLabel( issue: Issue, staleLabel: Readonly @@ -813,7 +845,9 @@ export class IssuesProcessor { if (isLabeled(issue, closeLabel)) { issueLogger.info( - `The $$type has a close label "${closeLabel}". Removing the close label...` + `The $$type has a close label "${chalk.cyan( + closeLabel + )}". Removing the close label...` ); await this._removeLabel(issue, closeLabel); diff --git a/src/functions/is-boolean.spec.ts b/src/functions/is-boolean.spec.ts new file mode 100644 index 000000000..62d9ea130 --- /dev/null +++ b/src/functions/is-boolean.spec.ts @@ -0,0 +1,29 @@ +import {isBoolean} from './is-boolean'; + +describe('isBoolean()', (): void => { + describe.each([0, 1, undefined, null, ''])( + 'when the given value is not a boolean', + (value): void => { + it('should return false', (): void => { + expect.assertions(1); + + const result = isBoolean(value); + + expect(result).toStrictEqual(false); + }); + } + ); + + describe.each([false, true])( + 'when the given value is a boolean', + (value): void => { + it('should return true', (): void => { + expect.assertions(1); + + const result = isBoolean(value); + + expect(result).toStrictEqual(true); + }); + } + ); +}); diff --git a/src/functions/is-boolean.ts b/src/functions/is-boolean.ts new file mode 100644 index 000000000..4721bc505 --- /dev/null +++ b/src/functions/is-boolean.ts @@ -0,0 +1,3 @@ +export function isBoolean(value: unknown): value is boolean { + return value === true || value === false; +} diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 0c981f865..dc00421d1 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -26,6 +26,8 @@ export interface IIssuesProcessorOptions { anyOfPrLabels: string; operationsPerRun: number; removeStaleWhenUpdated: boolean; + removeIssueStaleWhenUpdated: boolean | undefined; + removePrStaleWhenUpdated: boolean | undefined; debugOnly: boolean; ascending: boolean; skipStaleIssueMessage: boolean; diff --git a/src/main.ts b/src/main.ts index 0cdf69e30..7b8d75f55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,6 +49,12 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { removeStaleWhenUpdated: !( core.getInput('remove-stale-when-updated') === 'false' ), + removeIssueStaleWhenUpdated: _toOptionalBoolean( + core.getInput('remove-issue-stale-when-updated') + ), + removePrStaleWhenUpdated: _toOptionalBoolean( + core.getInput('remove-pr-stale-when-updated') + ), debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true',