diff --git a/README.md b/README.md index 53ac224a..839afb10 100644 --- a/README.md +++ b/README.md @@ -8,50 +8,52 @@ 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"). | -| `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**_. | +| `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 @@ -244,7 +246,7 @@ jobs: - uses: actions/stale@v3 with: any-of-labels: 'needs-more-info,needs-demo' - # You can opt for 'only-labels' instead if your usecase requires all labels + # You can opt for 'only-labels' instead if your use-case requires all labels # to be present in the issue/PR ``` diff --git a/__tests__/any-of-labels.spec.ts b/__tests__/any-of-labels.spec.ts index f69d1eed..cfed44bd 100644 --- a/__tests__/any-of-labels.spec.ts +++ b/__tests__/any-of-labels.spec.ts @@ -1,77 +1,1083 @@ import {Issue} from '../src/classes/issue'; +import {IIssue} from '../src/interfaces/issue'; import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; import {IssuesProcessorMock} from './classes/issues-processor-mock'; import {DefaultProcessorOptions} from './constants/default-processor-options'; import {generateIssue} from './functions/generate-issue'; -describe('any-of-labels option', () => { - test('should do nothing when not set', async () => { - const sut = new IssuesProcessorBuilder() +let issuesProcessorBuilder: IssuesProcessorBuilder; +let issuesProcessor: IssuesProcessorMock; + +describe('any-of-labels option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + test('should stale when not set even if the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder .emptyAnyOfLabels() - .issues([{labels: [{name: 'some-label'}]}]) + .issuesOrPrs([{labels: []}]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + expect(issuesProcessor.staleIssues).toHaveLength(1); }); - test('should skip it when none of the issue labels match', async () => { - const sut = new IssuesProcessorBuilder() - .anyOfLabels('skip-this-issue,and-this-one') - .issues([{labels: [{name: 'some-label'}, {name: 'some-other-label'}]}]) + test('should stale when not set even if the issue has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfLabels() + .issuesOrPrs([{labels: [{name: 'label'}]}]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(0); + expect(issuesProcessor.staleIssues).toHaveLength(1); }); - test('should skip it when the issue has no labels', async () => { - const sut = new IssuesProcessorBuilder() - .anyOfLabels('skip-this-issue,and-this-one') - .issues([{labels: []}]) + test('should not stale when set and the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label') + .issuesOrPrs([{labels: []}]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(0); + expect(issuesProcessor.staleIssues).toHaveLength(0); }); - test('should process it when one of the issue labels match', async () => { - const sut = new IssuesProcessorBuilder() - .anyOfLabels('skip-this-issue,and-this-one') - .issues([{labels: [{name: 'some-label'}, {name: 'skip-this-issue'}]}]) + test('should not stale when set and the issue has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label') + .issuesOrPrs([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + expect(issuesProcessor.staleIssues).toHaveLength(0); }); - test('should process it when all the issue labels match', async () => { - const sut = new IssuesProcessorBuilder() - .anyOfLabels('skip-this-issue,and-this-one') - .issues([{labels: [{name: 'and-this-one'}, {name: 'skip-this-issue'}]}]) + test('should not stale when set and the issue has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label') + .issuesOrPrs([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the issue has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label') + .issuesOrPrs([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label-1,dummy-label-2') + .issuesOrPrs([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfLabels('dummy-label-1,dummy-label-2') + .issuesOrPrs([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); +}); + +describe('any-of-issue-labels option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the any-of-labels options is not set', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.emptyAnyOfLabels(); + }); + + test('should stale when not set even if the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when not set even if the issue has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: [{name: 'dummy-label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should not stale when set and the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the issue has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + }); + + describe('when the any-of-labels options is set (same as any-of-issue-labels)', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.anyOfLabels('dummy-label'); + }); + + test('should not stale when not set even if the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when not set even if the issue has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: [{name: 'label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the issue has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + }); + + describe('when the any-of-labels options is set (different than any-of-issue-labels)', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.anyOfLabels('dummy-any-of-label'); + }); + + test('should not stale when not set even if the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when not set even if the issue has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfIssueLabels() + .issues([{labels: [{name: 'label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the issue has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the issue has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label') + .issues([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the issue has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfIssueLabels('dummy-label-1,dummy-label-2') + .issues([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + }); +}); + +describe('any-of-pr-labels option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the any-of-labels options is not set', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.emptyAnyOfLabels(); + }); + + test('should stale when not set even if the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when not set even if the pr has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: [{name: 'dummy-label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should not stale when set and the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the pr has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + }); + + describe('when the any-of-labels options is set (same as any-of-pr-labels)', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.anyOfLabels('dummy-label'); + }); + + test('should not stale when not set even if the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when not set even if the pr has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: [{name: 'label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the pr has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + }); + + describe('when the any-of-labels options is set (different than any-of-pr-labels)', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.anyOfLabels('dummy-any-of-label'); + }); + + test('should not stale when not set even if the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when not set even if the pr has a label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .emptyAnyOfPrLabels() + .prs([{labels: [{name: 'label'}]}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has no label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([{labels: []}]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has a different label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should not stale when set and the pr has different labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'label-1' + }, + { + name: 'label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(0); + }); + + test('should stale when set and the pr has the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label') + .prs([ + { + labels: [ + { + name: 'dummy-label' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has only one of the same label', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); + + test('should stale when set and the pr has all the same labels', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder + .anyOfPrLabels('dummy-label-1,dummy-label-2') + .prs([ + { + labels: [ + { + name: 'dummy-label-1' + }, + { + name: 'dummy-label-2' + } + ] + } + ]) + .build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.staleIssues).toHaveLength(1); + }); }); }); class IssuesProcessorBuilder { - private _options: IIssuesProcessorOptions; - private _issues: Issue[]; - - constructor() { - this._options = {...DefaultProcessorOptions}; - this._issues = []; - } + private _options: IIssuesProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeStale: 0 + }; + private _issues: Issue[] = []; anyOfLabels(labels: string): IssuesProcessorBuilder { this._options.anyOfLabels = labels; + + return this; + } + + anyOfIssueLabels(labels: string): IssuesProcessorBuilder { + this._options.anyOfIssueLabels = labels; + + return this; + } + + anyOfPrLabels(labels: string): IssuesProcessorBuilder { + this._options.anyOfPrLabels = labels; + return this; } @@ -79,19 +1085,58 @@ class IssuesProcessorBuilder { return this.anyOfLabels(''); } - issues(issues: Partial[]): IssuesProcessorBuilder { + emptyAnyOfIssueLabels(): IssuesProcessorBuilder { + return this.anyOfIssueLabels(''); + } + + emptyAnyOfPrLabels(): IssuesProcessorBuilder { + return this.anyOfPrLabels(''); + } + + issuesOrPrs(issues: Partial[]): IssuesProcessorBuilder { this._issues = issues.map( - (issue, index): Issue => + (issue: Readonly>, index: Readonly): Issue => generateIssue( this._options, index, - issue.title || 'Issue title', - issue.updated_at || '2000-01-01T00:00:00Z', // we only care about stale/expired issues here - issue.created_at || '2000-01-01T00:00:00Z', - issue.isPullRequest || false, + 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; + } + + prs(issues: Partial[]): IssuesProcessorBuilder { + this.issuesOrPrs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + pull_request: {key: 'value'} + }; + } + ) + ); + return this; } diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index e89a80f6..035e4ac1 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -22,6 +22,8 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ onlyIssueLabels: '', onlyPrLabels: '', anyOfLabels: '', + anyOfIssueLabels: '', + anyOfPrLabels: '', operationsPerRun: 100, debugOnly: true, removeStaleWhenUpdated: false, diff --git a/action.yml b/action.yml index 0b3f202e..43f92678 100644 --- a/action.yml +++ b/action.yml @@ -92,6 +92,14 @@ inputs: description: 'Only issues or pull requests with at least one of these labels are checked if stale. Defaults to `` (disabled) and can be a comma-separated list of labels.' default: '' required: false + any-of-issue-labels: + description: 'Only issues with at least one of these labels are checked if stale. Defaults to `` (disabled) and can be a comma-separated list of labels. Override "any-of-labels" option regarding only the issues.' + default: '' + required: false + any-of-pr-labels: + description: 'Only pull requests with at least one of these labels are checked if stale. Defaults to `` (disabled) and can be a comma-separated list of labels. Override "any-of-labels" option regarding only the pull requests.' + default: '' + required: false only-issue-labels: description: 'Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled) and can be a comma-separated list of labels. Override "only-labels" option regarding only the issues.' default: '' diff --git a/dist/index.js b/dist/index.js index baeb586e..965191e9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -312,15 +312,17 @@ class IssuesProcessor { return is_labeled_1.isLabeled(issue, label); }); if (!hasAllWhitelistedLabels) { - issueLogger.info(`Skipping this $$type because it doesn't have all the required labels`); + issueLogger.info(chalk_1.default.white('└──'), `Skipping this $$type because it doesn't have all the required labels`); continue; // Don't process issues without all of the required labels } else { - issueLogger.info(`All the required labels are present on this $$type. Continuing the process`); + issueLogger.info(chalk_1.default.white('├──'), `All the required labels are present on this $$type`); + issueLogger.info(chalk_1.default.white('└──'), `Continuing the process for this $$type`); } } else { - issueLogger.info(`The option "onlyLabels" was not specified. Continuing the process for this $$type`); + issueLogger.info(`The option "onlyLabels" was not specified`); + issueLogger.info(chalk_1.default.white('└──'), `Continuing the process for this $$type`); } issueLogger.info(`Days before $$type stale: ${daysBeforeStale}`); const shouldMarkAsStale = should_mark_when_stale_1.shouldMarkWhenStale(daysBeforeStale); @@ -370,11 +372,24 @@ class IssuesProcessor { issueLogger.info(`Skipping $$type because it has an exempt label`); continue; // don't process exempt issues } - const anyOfLabels = words_to_list_1.wordsToList(this.options.anyOfLabels); - if (anyOfLabels.length && - !anyOfLabels.some((label) => is_labeled_1.isLabeled(issue, label))) { - issueLogger.info(`Skipping $$type because it does not have any of the required labels`); - continue; // don't process issues without any of the required labels + const anyOfLabels = words_to_list_1.wordsToList(this._getAnyOfLabels(issue)); + if (anyOfLabels.length > 0) { + issueLogger.info(`The option "anyOfLabels" was specified to only processed the issues and pull requests with one of those labels (${anyOfLabels.length})`); + const hasOneOfWhitelistedLabels = anyOfLabels.some((label) => { + return is_labeled_1.isLabeled(issue, label); + }); + if (!hasOneOfWhitelistedLabels) { + issueLogger.info(chalk_1.default.white('└──'), `Skipping this $$type because it doesn't have one of the required labels`); + continue; // Don't process issues without any of the required labels + } + else { + issueLogger.info(chalk_1.default.white('├──'), `One of the required labels is present on this $$type`); + issueLogger.info(chalk_1.default.white('└──'), `Continuing the process for this $$type`); + } + } + else { + issueLogger.info(`The option "anyOfLabels" was not specified`); + issueLogger.info(chalk_1.default.white('└──'), `Continuing the process for this $$type`); } const milestones = new milestones_1.Milestones(this.options, issue); if (milestones.shouldExemptMilestones()) { @@ -759,6 +774,19 @@ class IssuesProcessor { } return this.options.onlyLabels; } + _getAnyOfLabels(issue) { + if (issue.isPullRequest) { + if (this.options.anyOfPrLabels !== '') { + return this.options.anyOfPrLabels; + } + } + else { + if (this.options.anyOfIssueLabels !== '') { + return this.options.anyOfIssueLabels; + } + } + return this.options.anyOfLabels; + } _removeStaleLabel(issue, staleLabel) { var _a; return __awaiter(this, void 0, void 0, function* () { @@ -1758,6 +1786,8 @@ function _getAndValidateArgs() { onlyIssueLabels: core.getInput('only-issue-labels'), onlyPrLabels: core.getInput('only-pr-labels'), anyOfLabels: core.getInput('any-of-labels'), + anyOfIssueLabels: core.getInput('any-of-issue-labels'), + anyOfPrLabels: core.getInput('any-of-pr-labels'), operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'), debugOnly: core.getInput('debug-only') === 'true', diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index 8e7afff6..6d10ae9e 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -31,6 +31,8 @@ describe('Issue', (): void => { onlyIssueLabels: '', onlyPrLabels: '', anyOfLabels: '', + anyOfIssueLabels: '', + anyOfPrLabels: '', operationsPerRun: 0, removeStaleWhenUpdated: false, repoToken: '', diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 43ee5219..2662b0f8 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -132,17 +132,25 @@ export class IssuesProcessor { if (!hasAllWhitelistedLabels) { issueLogger.info( + chalk.white('└──'), `Skipping this $$type because it doesn't have all the required labels` ); continue; // Don't process issues without all of the required labels } else { issueLogger.info( - `All the required labels are present on this $$type. Continuing the process` + chalk.white('├──'), + `All the required labels are present on this $$type` + ); + issueLogger.info( + chalk.white('└──'), + `Continuing the process for this $$type` ); } } else { + issueLogger.info(`The option "onlyLabels" was not specified`); issueLogger.info( - `The option "onlyLabels" was not specified. Continuing the process for this $$type` + chalk.white('└──'), + `Continuing the process for this $$type` ); } @@ -229,18 +237,41 @@ export class IssuesProcessor { continue; // don't process exempt issues } - const anyOfLabels: string[] = wordsToList(this.options.anyOfLabels); + const anyOfLabels: string[] = wordsToList(this._getAnyOfLabels(issue)); - if ( - anyOfLabels.length && - !anyOfLabels.some((label: Readonly): boolean => - isLabeled(issue, label) - ) - ) { + if (anyOfLabels.length > 0) { issueLogger.info( - `Skipping $$type because it does not have any of the required labels` + `The option "anyOfLabels" was specified to only processed the issues and pull requests with one of those labels (${anyOfLabels.length})` + ); + + const hasOneOfWhitelistedLabels: boolean = anyOfLabels.some( + (label: Readonly): boolean => { + return isLabeled(issue, label); + } + ); + + if (!hasOneOfWhitelistedLabels) { + issueLogger.info( + chalk.white('└──'), + `Skipping this $$type because it doesn't have one of the required labels` + ); + continue; // Don't process issues without any of the required labels + } else { + issueLogger.info( + chalk.white('├──'), + `One of the required labels is present on this $$type` + ); + issueLogger.info( + chalk.white('└──'), + `Continuing the process for this $$type` + ); + } + } else { + issueLogger.info(`The option "anyOfLabels" was not specified`); + issueLogger.info( + chalk.white('└──'), + `Continuing the process for this $$type` ); - continue; // don't process issues without any of the required labels } const milestones: Milestones = new Milestones(this.options, issue); @@ -736,6 +767,20 @@ export class IssuesProcessor { return this.options.onlyLabels; } + private _getAnyOfLabels(issue: Issue): string { + if (issue.isPullRequest) { + if (this.options.anyOfPrLabels !== '') { + return this.options.anyOfPrLabels; + } + } else { + if (this.options.anyOfIssueLabels !== '') { + return this.options.anyOfIssueLabels; + } + } + + return this.options.anyOfLabels; + } + private async _removeStaleLabel( issue: Issue, staleLabel: Readonly diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 3c62d95b..0c981f86 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -22,6 +22,8 @@ export interface IIssuesProcessorOptions { onlyIssueLabels: string; onlyPrLabels: string; anyOfLabels: string; + anyOfIssueLabels: string; + anyOfPrLabels: string; operationsPerRun: number; removeStaleWhenUpdated: boolean; debugOnly: boolean; diff --git a/src/main.ts b/src/main.ts index 5aa0bc48..0cdf69e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,6 +41,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { onlyIssueLabels: core.getInput('only-issue-labels'), onlyPrLabels: core.getInput('only-pr-labels'), anyOfLabels: core.getInput('any-of-labels'), + anyOfIssueLabels: core.getInput('any-of-issue-labels'), + anyOfPrLabels: core.getInput('any-of-pr-labels'), operationsPerRun: parseInt( core.getInput('operations-per-run', {required: true}) ),