diff --git a/README.md b/README.md index fa8d2c91..5909d38e 100644 --- a/README.md +++ b/README.md @@ -14,54 +14,57 @@ The default configuration will: Every argument is optional. -| Input | Description | Default | -| ------------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------- | -| [repo-token](#repo-token) | PAT for GitHub API authentication | `${{ github.token }}` | -| [days-before-stale](#days-before-stale) | Idle number of days before marking issues/PRs stale | `60` | -| [days-before-issue-stale](#days-before-issue-stale) | Override [days-before-stale](#days-before-stale) for issues only | | -| [days-before-pr-stale](#days-before-pr-stale) | Override [days-before-stale](#days-before-stale) for PRs only | | -| [days-before-close](#days-before-close) | Idle number of days before closing stale issues/PRs | `7` | -| [days-before-issue-close](#days-before-issue-close) | Override [days-before-close](#days-before-close) for issues only | | -| [days-before-pr-close](#days-before-pr-close) | Override [days-before-close](#days-before-close) for PRs only | | -| [stale-issue-message](#stale-issue-message) | Comment on the staled issues | | -| [stale-pr-message](#stale-pr-message) | Comment on the staled PRs | | -| [close-issue-message](#close-issue-message) | Comment on the staled issues while closed | | -| [close-pr-message](#close-pr-message) | Comment on the staled PRs while closed | | -| [stale-issue-label](#stale-issue-label) | Label to apply on staled issues | `Stale` | -| [close-issue-label](#close-issue-label) | Label to apply on closed issues | | -| [stale-pr-label](#stale-pr-label) | Label to apply on staled PRs | `Stale` | -| [close-pr-label](#close-pr-label) | Label to apply on closed PRs | | -| [exempt-issue-labels](#exempt-issue-labels) | Labels on issues exempted from stale | | -| [exempt-pr-labels](#exempt-pr-labels) | Labels on PRs exempted from stale | | -| [only-labels](#only-labels) | Only issues/PRs with ALL these labels are checked | | -| [only-issue-labels](#only-issue-labels) | Only issues with ALL these labels are checked | | -| [only-pr-labels](#only-pr-labels) | Only PRs with ALL these labels are checked | | -| [any-of-labels](#any-of-labels) | Only issues/PRs with ANY of these labels are checked | | -| [any-of-issue-labels](#any-of-issue-labels) | Only issues with ANY of these labels are checked | | -| [any-of-pr-labels](#any-of-pr-labels) | Only PRs with ANY of these labels are checked | | -| [operations-per-run](#operations-per-run) | Max number of operations per run | `30` | -| [remove-stale-when-updated](#remove-stale-when-updated) | Remove stale label from issues/PRs on updates/comments | `true` | -| [remove-issue-stale-when-updated](#remove-issue-stale-when-updated) | Remove stale label from issues on updates/comments | | -| [remove-pr-stale-when-updated](#remove-pr-stale-when-updated) | Remove stale label from PRs on updates/comments | | -| [labels-to-add-when-unstale](#labels-to-add-when-unstale) | Add specified labels from issues/PRs when they become unstale | | -| [labels-to-remove-when-unstale](#labels-to-remove-when-unstale) | Remove specified labels from issues/PRs when they become unstale | | -| [debug-only](#debug-only) | Dry-run | `false` | -| [ascending](#ascending) | Order to get issues/PRs | `false` | -| [start-date](#start-date) | Skip stale action for issues/PRs created before it | | -| [delete-branch](#delete-branch) | Delete branch after closing a stale PR | `false` | -| [exempt-milestones](#exempt-milestones) | Milestones on issues/PRs exempted from stale | | -| [exempt-issue-milestones](#exempt-issue-milestones) | Override [exempt-milestones](#exempt-milestones) for issues only | | -| [exempt-pr-milestones](#exempt-pr-milestones) | Override [exempt-milestones](#exempt-milestones) for PRs only | | -| [exempt-all-milestones](#exempt-all-milestones) | Exempt all issues/PRs with milestones from stale | | -| [exempt-all-issue-milestones](#exempt-all-issue-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for issues only | | -| [exempt-all-pr-milestones](#exempt-all-pr-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for PRs only | | -| [exempt-assignees](#exempt-assignees) | Assignees on issues/PRs exempted from stale | | -| [exempt-issue-assignees](#exempt-issue-assignees) | Override [exempt-assignees](#exempt-assignees) for issues only | | -| [exempt-pr-assignees](#exempt-pr-assignees) | Override [exempt-assignees](#exempt-assignees) for PRs only | | -| [exempt-all-assignees](#exempt-all-assignees) | Exempt all issues/PRs with assignees from stale | | -| [exempt-all-issue-assignees](#exempt-all-issue-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for issues only | | -| [exempt-all-pr-assignees](#exempt-all-pr-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for PRs only | | -| [enable-statistics](#enable-statistics) | Display statistics in the logs | `true` | +| Input | Description | Default | +| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------- | +| [repo-token](#repo-token) | PAT for GitHub API authentication | `${{ github.token }}` | +| [days-before-stale](#days-before-stale) | Idle number of days before marking issues/PRs stale | `60` | +| [days-before-issue-stale](#days-before-issue-stale) | Override [days-before-stale](#days-before-stale) for issues only | | +| [days-before-pr-stale](#days-before-pr-stale) | Override [days-before-stale](#days-before-stale) for PRs only | | +| [days-before-close](#days-before-close) | Idle number of days before closing stale issues/PRs | `7` | +| [days-before-issue-close](#days-before-issue-close) | Override [days-before-close](#days-before-close) for issues only | | +| [days-before-pr-close](#days-before-pr-close) | Override [days-before-close](#days-before-close) for PRs only | | +| [stale-issue-message](#stale-issue-message) | Comment on the staled issues | | +| [stale-pr-message](#stale-pr-message) | Comment on the staled PRs | | +| [close-issue-message](#close-issue-message) | Comment on the staled issues while closed | | +| [close-pr-message](#close-pr-message) | Comment on the staled PRs while closed | | +| [stale-issue-label](#stale-issue-label) | Label to apply on staled issues | `Stale` | +| [close-issue-label](#close-issue-label) | Label to apply on closed issues | | +| [stale-pr-label](#stale-pr-label) | Label to apply on staled PRs | `Stale` | +| [close-pr-label](#close-pr-label) | Label to apply on closed PRs | | +| [exempt-issue-labels](#exempt-issue-labels) | Labels on issues exempted from stale | | +| [exempt-pr-labels](#exempt-pr-labels) | Labels on PRs exempted from stale | | +| [only-labels](#only-labels) | Only issues/PRs with ALL these labels are checked | | +| [only-issue-labels](#only-issue-labels) | Only issues with ALL these labels are checked | | +| [only-pr-labels](#only-pr-labels) | Only PRs with ALL these labels are checked | | +| [any-of-labels](#any-of-labels) | Only issues/PRs with ANY of these labels are checked | | +| [any-of-issue-labels](#any-of-issue-labels) | Only issues with ANY of these labels are checked | | +| [any-of-pr-labels](#any-of-pr-labels) | Only PRs with ANY of these labels are checked | | +| [operations-per-run](#operations-per-run) | Max number of operations per run | `30` | +| [remove-stale-when-updated](#remove-stale-when-updated) | Remove stale label from issues/PRs on updates | `true` | +| [remove-issue-stale-when-updated](#remove-issue-stale-when-updated) | Override [remove-stale-when-updated](#remove-stale-when-updated) for issues only | | +| [remove-pr-stale-when-updated](#remove-pr-stale-when-updated) | Override [remove-stale-when-updated](#remove-stale-when-updated) for PRs only | | +| [remove-stale-when-commented](#remove-stale-when-commented) | Remove stale label from issues/PRs on comments | `true` | +| [remove-issue-stale-when-commented](#remove-issue-stale-when-commented) | Override [remove-stale-when-commented](#remove-stale-when-commented) for issues only | | +| [remove-pr-stale-when-commented](#remove-pr-stale-when-commented) | Override [remove-stale-when-commented](#remove-stale-when-commented) for PRs only | | +| [labels-to-add-when-unstale](#labels-to-add-when-unstale) | Add specified labels from issues/PRs when they become unstale | | +| [labels-to-remove-when-unstale](#labels-to-remove-when-unstale) | Remove specified labels from issues/PRs when they become unstale | | +| [debug-only](#debug-only) | Dry-run | `false` | +| [ascending](#ascending) | Order to get issues/PRs | `false` | +| [start-date](#start-date) | Skip stale action for issues/PRs created before it | | +| [delete-branch](#delete-branch) | Delete branch after closing a stale PR | `false` | +| [exempt-milestones](#exempt-milestones) | Milestones on issues/PRs exempted from stale | | +| [exempt-issue-milestones](#exempt-issue-milestones) | Override [exempt-milestones](#exempt-milestones) for issues only | | +| [exempt-pr-milestones](#exempt-pr-milestones) | Override [exempt-milestones](#exempt-milestones) for PRs only | | +| [exempt-all-milestones](#exempt-all-milestones) | Exempt all issues/PRs with milestones from stale | | +| [exempt-all-issue-milestones](#exempt-all-issue-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for issues only | | +| [exempt-all-pr-milestones](#exempt-all-pr-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for PRs only | | +| [exempt-assignees](#exempt-assignees) | Assignees on issues/PRs exempted from stale | | +| [exempt-issue-assignees](#exempt-issue-assignees) | Override [exempt-assignees](#exempt-assignees) for issues only | | +| [exempt-pr-assignees](#exempt-pr-assignees) | Override [exempt-assignees](#exempt-assignees) for PRs only | | +| [exempt-all-assignees](#exempt-all-assignees) | Exempt all issues/PRs with assignees from stale | | +| [exempt-all-issue-assignees](#exempt-all-issue-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for issues only | | +| [exempt-all-pr-assignees](#exempt-all-pr-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for PRs only | | +| [enable-statistics](#enable-statistics) | Display statistics in the logs | `true` | ### List of output options @@ -293,22 +296,43 @@ Default value: `30` #### remove-stale-when-updated -Automatically remove the stale label when the issues or the pull requests are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`) or commented. +Automatically remove the stale label when the issues or the pull requests are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`). Default value: `true` #### remove-issue-stale-when-updated -Override [remove-stale-when-updated](#remove-stale-when-updated) but only to automatically remove the stale label when the issues are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`) or commented. +Override [remove-stale-when-updated](#remove-stale-when-updated) but only to automatically remove the stale label when the issues are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`). Default value: unset #### remove-pr-stale-when-updated -Override [remove-stale-when-updated](#remove-stale-when-updated) but only to automatically remove the stale label when the pull requests are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`) or commented. +Override [remove-stale-when-updated](#remove-stale-when-updated) but only to automatically remove the stale label when the pull requests are updated (based on [GitHub issue](https://docs.github.com/en/rest/reference/issues) field `updated_at`). Default value: unset +#### remove-stale-when-commented + +Automatically remove the stale label when the issues or the pull requests are commented. + +Default value: `true` +Required Permission: `issues: write` and `pull-requests: write` + +#### remove-issue-stale-when-commented + +Override [remove-stale-when-commented](#remove-stale-when-commented) but only to automatically remove the stale label when the issues are commented. + +Default value: unset +Required Permission: `issues: write` and `pull-requests: write` + +#### remove-pr-stale-when-commented + +Override [remove-stale-when-commented](#remove-stale-when-commented) but only to automatically remove the stale label when the pull requests are commented. + +Default value: unset +Required Permission: `issues: write` and `pull-requests: write` + #### labels-to-add-when-unstale A comma delimited list of labels to add when a stale issue or pull request receives activity and has the [stale-issue-label](#stale-issue-label) or [stale-pr-label](#stale-pr-label) removed from it. diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index 6f3cc5d8..ac864844 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -29,6 +29,9 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ removeStaleWhenUpdated: false, removeIssueStaleWhenUpdated: undefined, removePrStaleWhenUpdated: undefined, + removeStaleWhenCommented: false, + removeIssueStaleWhenCommented: undefined, + removePrStaleWhenCommented: undefined, ascending: false, deleteBranch: false, startDate: '', diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts index 303ca955..da335719 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -1220,7 +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, removeStaleWhenUpdated: true}; + const opts = {...DefaultProcessorOptions, removeStaleWhenCommented: true}; const TestIssueList: Issue[] = [ generateIssue( opts, @@ -1259,7 +1259,7 @@ test('when the option "labelsToAddWhenUnstale" is set, the labels should be adde expect.assertions(4); const opts = { ...DefaultProcessorOptions, - removeStaleWhenUpdated: true, + removeStaleWhenCommented: true, labelsToAddWhenUnstale: 'test' }; const TestIssueList: Issue[] = [ @@ -1299,8 +1299,37 @@ test('when the option "labelsToAddWhenUnstale" is set, the labels should be adde expect(processor.addedLabelIssues).toHaveLength(1); }); -test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => { +test('stale label should be removed if a stale issue was updated', async () => { const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true}; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'An issue that should un-stale', + new Date().toDateString(), + '2020-01-01T17:00:00Z', + false, + ['Stale'] + ) + ]; + const processor = new IssuesProcessorMock( + opts, + async () => 'abot', + async p => (p === 1 ? TestIssueList : []), + async () => [], + async () => '2020-01-02T17:00:00Z' + ); + + // process our fake issue list + await processor.processIssues(1); + + expect(processor.closedIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(1); +}); + +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, removeStaleWhenCommented: true}; github.context.actor = 'abot'; const TestIssueList: Issue[] = [ generateIssue( @@ -1339,7 +1368,7 @@ test('stale label should not be removed if a comment was added by the bot (and t test('stale label containing a space should be removed if a comment was added to a stale issue', async () => { const opts: IIssuesProcessorOptions = { ...DefaultProcessorOptions, - removeStaleWhenUpdated: true, + removeStaleWhenCommented: true, staleIssueLabel: 'stat: stale' }; const TestIssueList: Issue[] = [ @@ -2278,7 +2307,7 @@ test('processing an issue stale since less than the daysBeforeStale with a stale daysBeforeStale: 30, daysBeforeClose: 7, closeIssueMessage: 'close message', - removeStaleWhenUpdated: false + removeStaleWhenCommented: false }; const now: Date = new Date(); const updatedAt: Date = new Date(now.setDate(now.getDate() - 9)); @@ -2320,7 +2349,7 @@ test('processing an issue stale since less than the daysBeforeStale without a st daysBeforeStale: 30, daysBeforeClose: 7, closeIssueMessage: 'close message', - removeStaleWhenUpdated: false + removeStaleWhenCommented: false }; const now: Date = new Date(); const updatedAt: Date = new Date(now.setDate(now.getDate() - 9)); diff --git a/__tests__/remove-stale-when-commented.spec.ts b/__tests__/remove-stale-when-commented.spec.ts new file mode 100644 index 00000000..531223d2 --- /dev/null +++ b/__tests__/remove-stale-when-commented.spec.ts @@ -0,0 +1,556 @@ +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-commented option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenCommented(); + }); + + 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-commented option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenCommented(); + }); + + describe('when the option "remove-issue-stale-when-commented" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenCommented(); + }); + + 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-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenCommented(); + }); + + describe('when the option "remove-issue-stale-when-commented" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenCommented(); + }); + + 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-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenCommented(); + }); + + 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-commented option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenCommented(); + }); + + describe('when the option "remove-pr-stale-when-commented" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenCommented(); + }); + + 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-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenCommented(); + }); + + describe('when the option "remove-pr-stale-when-commented" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenCommented(); + }); + + 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-commented" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenCommented(); + }); + + 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-commented" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenCommented(); + }); + + 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[] = []; + + keepStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removeStaleWhenCommented = false; + + return this; + } + + removeStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removeStaleWhenCommented = true; + + return this; + } + + unsetIssueStaleWhenCommented(): IssuesProcessorBuilder { + delete this._options.removeIssueStaleWhenCommented; + + return this; + } + + keepIssueStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenCommented = false; + + return this; + } + + removeIssueStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenCommented = true; + + return this; + } + + unsetPrStaleWhenCommented(): IssuesProcessorBuilder { + delete this._options.removePrStaleWhenCommented; + + return this; + } + + keepPrStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removePrStaleWhenCommented = false; + + return this; + } + + removePrStaleWhenCommented(): IssuesProcessorBuilder { + this._options.removePrStaleWhenCommented = 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 () => [ + { + // Note this comment + user: { + login: 'notme', + type: 'User' + } + } + ], + async () => new Date().toDateString() + ); + } +} diff --git a/__tests__/remove-stale-when-updated.spec.ts b/__tests__/remove-stale-when-updated.spec.ts index 91b5bef6..26eb0aff 100644 --- a/__tests__/remove-stale-when-updated.spec.ts +++ b/__tests__/remove-stale-when-updated.spec.ts @@ -11,7 +11,7 @@ let issuesProcessor: IssuesProcessorMock; /** * @description - * Assuming there is a comment on the issue + * Assuming there is an update on the issue */ describe('remove-stale-when-updated option', (): void => { beforeEach((): void => { @@ -480,8 +480,9 @@ class IssuesProcessorBuilder { issues.map((issue: Readonly>): Partial => { return { ...issue, - updated_at: '2020-01-01T17:00:00Z', - created_at: '2020-01-01T17:00:00Z', + // Note this update + updated_at: new Date().toDateString(), + created_at: new Date().toDateString(), labels: issue.labels?.map((label: Readonly): ILabel => { return { ...label, @@ -517,8 +518,9 @@ class IssuesProcessorBuilder { issues.map((issue: Readonly>): Partial => { return { ...issue, - updated_at: '2020-01-01T17:00:00Z', - created_at: '2020-01-01T17:00:00Z', + // Note this update + updated_at: new Date().toDateString(), + created_at: new Date().toDateString(), labels: issue.labels?.map((label: Readonly): ILabel => { return { ...label, @@ -541,14 +543,7 @@ class IssuesProcessorBuilder { this._options, async () => 'abot', async p => (p === 1 ? this._issues : []), - async () => [ - { - user: { - login: 'notme', - type: 'User' - } - } - ], + async () => [], async () => new Date().toDateString() ); } diff --git a/action.yml b/action.yml index 4adb85a7..d346279c 100644 --- a/action.yml +++ b/action.yml @@ -113,15 +113,27 @@ inputs: default: '30' required: false remove-stale-when-updated: - description: 'Remove stale labels from issues and pull requests when they are updated or commented on.' + description: 'Remove stale labels from issues and pull requests when they are updated.' 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.' + description: 'Remove stale labels from issues when they are updated. Override "remove-stale-when-updated" option regarding only the issues.' default: '' 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.' + description: 'Remove stale labels from pull requests when they are updated. Override "remove-stale-when-updated" option regarding only the pull requests.' + default: '' + required: false + remove-stale-when-commented: + description: 'Remove stale labels from issues and pull requests when they are commented on.' + default: 'true' + required: false + remove-issue-stale-when-commented: + description: 'Remove stale labels from issues when they are commented on. Override "remove-stale-when-commented" option regarding only the issues.' + default: '' + required: false + remove-pr-stale-when-commented: + description: 'Remove stale labels from pull requests when they are commented on. Override "remove-stale-when-commented" option regarding only the pull requests.' default: '' required: false debug-only: diff --git a/dist/index.js b/dist/index.js index 66468492..0968b222 100644 --- a/dist/index.js +++ b/dist/index.js @@ -580,14 +580,28 @@ class IssuesProcessor { const shouldRemoveStaleWhenUpdated = this._shouldRemoveStaleWhenUpdated(issue); issueLogger.info(`The option ${issueLogger.createOptionLink(this._getRemoveStaleWhenUpdatedUsedOptionName(issue))} is: ${logger_service_1.LoggerService.cyan(shouldRemoveStaleWhenUpdated)}`); if (shouldRemoveStaleWhenUpdated) { - issueLogger.info(`The stale label should not be removed`); + issueLogger.info(`The stale label should not be removed due to an update`); + } + else { + issueLogger.info(`The stale label should be removed if all conditions met`); + } + const shouldRemoveStaleWhenCommented = this._shouldRemoveStaleWhenCommented(issue); + issueLogger.info(`The option ${issueLogger.createOptionLink(this._getRemoveStaleWhenCommentedUsedOptionName(issue))} is: ${logger_service_1.LoggerService.cyan(shouldRemoveStaleWhenCommented)}`); + if (shouldRemoveStaleWhenCommented) { + issueLogger.info(`The stale label should not be removed due to a comment`); } else { issueLogger.info(`The stale label should be removed if all conditions met`); } // Should we un-stale this issue? - if (shouldRemoveStaleWhenUpdated && issueHasComments) { - issueLogger.info(`Remove the stale label since the $$type has a comment and the workflow should remove the stale label when updated`); + if (shouldRemoveStaleWhenUpdated && issueHasUpdate) { + issueLogger.info(`Remove the stale label since the $$type has an update and the workflow should remove the stale label when updated`); + 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 + } + else if (shouldRemoveStaleWhenCommented && issueHasComments) { + issueLogger.info(`Remove the stale label since the $$type has a comment and the workflow should remove the stale label when commented`); yield this._removeStaleLabel(issue, staleLabel); // Are there labels to remove or add when an issue is no longer stale? yield this._removeLabelsWhenUnstale(issue, labelsToRemoveWhenUnstale); @@ -902,6 +916,18 @@ class IssuesProcessor { } }); } + _shouldRemoveStaleWhenCommented(issue) { + if (issue.isPullRequest) { + if (is_boolean_1.isBoolean(this.options.removePrStaleWhenCommented)) { + return this.options.removePrStaleWhenCommented; + } + return this.options.removeStaleWhenCommented; + } + if (is_boolean_1.isBoolean(this.options.removeIssueStaleWhenCommented)) { + return this.options.removeIssueStaleWhenCommented; + } + return this.options.removeStaleWhenCommented; + } _removeStaleLabel(issue, staleLabel) { var _a; return __awaiter(this, void 0, void 0, function* () { @@ -963,6 +989,18 @@ class IssuesProcessor { } return option_1.Option.RemoveStaleWhenUpdated; } + _getRemoveStaleWhenCommentedUsedOptionName(issue) { + if (issue.isPullRequest) { + if (is_boolean_1.isBoolean(this.options.removePrStaleWhenCommented)) { + return option_1.Option.RemovePrStaleWhenCommented; + } + return option_1.Option.RemoveStaleWhenCommented; + } + if (is_boolean_1.isBoolean(this.options.removeIssueStaleWhenCommented)) { + return option_1.Option.RemoveIssueStaleWhenCommented; + } + return option_1.Option.RemoveStaleWhenCommented; + } } exports.IssuesProcessor = IssuesProcessor; @@ -1723,6 +1761,9 @@ var Option; Option["RemoveStaleWhenUpdated"] = "remove-stale-when-updated"; Option["RemoveIssueStaleWhenUpdated"] = "remove-issue-stale-when-updated"; Option["RemovePrStaleWhenUpdated"] = "remove-pr-stale-when-updated"; + Option["RemoveStaleWhenCommented"] = "remove-stale-when-commented"; + Option["RemoveIssueStaleWhenCommented"] = "remove-issue-stale-when-commented"; + Option["RemovePrStaleWhenCommented"] = "remove-pr-stale-when-commented"; Option["DebugOnly"] = "debug-only"; Option["Ascending"] = "ascending"; Option["DeleteBranch"] = "delete-branch"; @@ -2007,6 +2048,9 @@ function _getAndValidateArgs() { 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')), + removeStaleWhenCommented: !(core.getInput('remove-stale-when-commented') === 'false'), + removeIssueStaleWhenCommented: _toOptionalBoolean(core.getInput('remove-issue-stale-when-commented')), + removePrStaleWhenCommented: _toOptionalBoolean(core.getInput('remove-pr-stale-when-commented')), debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', deleteBranch: core.getInput('delete-branch') === 'true', diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index 4d44ddc5..243709cf 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -37,6 +37,9 @@ describe('Issue', (): void => { removeStaleWhenUpdated: false, removeIssueStaleWhenUpdated: undefined, removePrStaleWhenUpdated: undefined, + removeStaleWhenCommented: false, + removeIssueStaleWhenCommented: undefined, + removePrStaleWhenCommented: undefined, repoToken: '', staleIssueMessage: '', stalePrMessage: '', diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 6acc00db..2509ff07 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -616,7 +616,28 @@ export class IssuesProcessor { ); if (shouldRemoveStaleWhenUpdated) { - issueLogger.info(`The stale label should not be removed`); + issueLogger.info( + `The stale label should not be removed due to an update` + ); + } else { + issueLogger.info( + `The stale label should be removed if all conditions met` + ); + } + + const shouldRemoveStaleWhenCommented: boolean = + this._shouldRemoveStaleWhenCommented(issue); + + issueLogger.info( + `The option ${issueLogger.createOptionLink( + this._getRemoveStaleWhenCommentedUsedOptionName(issue) + )} is: ${LoggerService.cyan(shouldRemoveStaleWhenCommented)}` + ); + + if (shouldRemoveStaleWhenCommented) { + issueLogger.info( + `The stale label should not be removed due to a comment` + ); } else { issueLogger.info( `The stale label should be removed if all conditions met` @@ -624,9 +645,18 @@ export class IssuesProcessor { } // Should we un-stale this issue? - if (shouldRemoveStaleWhenUpdated && issueHasComments) { + if (shouldRemoveStaleWhenUpdated && issueHasUpdate) { issueLogger.info( - `Remove the stale label since the $$type has a comment and the workflow should remove the stale label when updated` + `Remove the stale label since the $$type has an update and the workflow should remove the stale label when updated` + ); + 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 + } else if (shouldRemoveStaleWhenCommented && issueHasComments) { + issueLogger.info( + `Remove the stale label since the $$type has a comment and the workflow should remove the stale label when commented` ); await this._removeStaleLabel(issue, staleLabel); @@ -1046,6 +1076,22 @@ export class IssuesProcessor { } } + private _shouldRemoveStaleWhenCommented(issue: Issue): boolean { + if (issue.isPullRequest) { + if (isBoolean(this.options.removePrStaleWhenCommented)) { + return this.options.removePrStaleWhenCommented; + } + + return this.options.removeStaleWhenCommented; + } + + if (isBoolean(this.options.removeIssueStaleWhenCommented)) { + return this.options.removeIssueStaleWhenCommented; + } + + return this.options.removeStaleWhenCommented; + } + private async _removeStaleLabel( issue: Issue, staleLabel: Readonly @@ -1157,4 +1203,25 @@ export class IssuesProcessor { return Option.RemoveStaleWhenUpdated; } + + private _getRemoveStaleWhenCommentedUsedOptionName( + issue: Readonly + ): + | Option.RemovePrStaleWhenCommented + | Option.RemoveStaleWhenCommented + | Option.RemoveIssueStaleWhenCommented { + if (issue.isPullRequest) { + if (isBoolean(this.options.removePrStaleWhenCommented)) { + return Option.RemovePrStaleWhenCommented; + } + + return Option.RemoveStaleWhenCommented; + } + + if (isBoolean(this.options.removeIssueStaleWhenCommented)) { + return Option.RemoveIssueStaleWhenCommented; + } + + return Option.RemoveStaleWhenCommented; + } } diff --git a/src/enums/option.ts b/src/enums/option.ts index b127fc8d..f50e720c 100644 --- a/src/enums/option.ts +++ b/src/enums/option.ts @@ -24,6 +24,9 @@ export enum Option { RemoveStaleWhenUpdated = 'remove-stale-when-updated', RemoveIssueStaleWhenUpdated = 'remove-issue-stale-when-updated', RemovePrStaleWhenUpdated = 'remove-pr-stale-when-updated', + RemoveStaleWhenCommented = 'remove-stale-when-commented', + RemoveIssueStaleWhenCommented = 'remove-issue-stale-when-commented', + RemovePrStaleWhenCommented = 'remove-pr-stale-when-commented', DebugOnly = 'debug-only', Ascending = 'ascending', DeleteBranch = 'delete-branch', diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index d77ee072..1f29ab81 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -28,6 +28,9 @@ export interface IIssuesProcessorOptions { removeStaleWhenUpdated: boolean; removeIssueStaleWhenUpdated: boolean | undefined; removePrStaleWhenUpdated: boolean | undefined; + removeStaleWhenCommented: boolean; + removeIssueStaleWhenCommented: boolean | undefined; + removePrStaleWhenCommented: boolean | undefined; debugOnly: boolean; ascending: boolean; deleteBranch: boolean; diff --git a/src/main.ts b/src/main.ts index c759fe34..b38a5bbe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,6 +62,15 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { removePrStaleWhenUpdated: _toOptionalBoolean( core.getInput('remove-pr-stale-when-updated') ), + removeStaleWhenCommented: !( + core.getInput('remove-stale-when-commented') === 'false' + ), + removeIssueStaleWhenCommented: _toOptionalBoolean( + core.getInput('remove-issue-stale-when-commented') + ), + removePrStaleWhenCommented: _toOptionalBoolean( + core.getInput('remove-pr-stale-when-commented') + ), debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', deleteBranch: core.getInput('delete-branch') === 'true',