diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/other_issue_report.md b/.github/ISSUE_TEMPLATE/other_issue_report.md new file mode 100644 index 000000000..01c372389 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other_issue_report.md @@ -0,0 +1,13 @@ +--- +name: Other issue report +about: Report other issue +title: '' +labels: bug +assignees: '' +--- + +## Describe your issue + +## Further context + + diff --git a/.github/ISSUE_TEMPLATE/stale_issue_report.md b/.github/ISSUE_TEMPLATE/stale_issue_report.md new file mode 100644 index 000000000..4c732d611 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/stale_issue_report.md @@ -0,0 +1,29 @@ +--- +name: Stale issue report +about: Report issues with using the stale action +title: '' +labels: bug +assignees: '' +--- + + + +## Describe your issue + +## Your stale action configuration + + + +```yml +jobs: + stale: + runs-on: ... + steps: + - uses: actions/stale@... + with: + ... +``` + +## Further context + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..213ffea82 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ + +## Changes +- [x] ... + +## Context + + + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a93753428..4ac813f6e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,8 @@ name: 'Code scanning' on: push: + branches: + - main pull_request: schedule: - cron: '0 19 * * 0' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 699ef3c7d..fcd39da73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - run: | - npm install + npm ci npm run all test: # make sure the action works on a clean machine without building runs-on: ubuntu-latest diff --git a/README.md b/README.md index 3709c6abb..c1f6ecfab 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,85 @@ Warns and then closes issues and PRs that have had no activity for a specified amount of time. -### Arguments +## All options + +### List of options 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 the 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 a PR as stale.
_Override `days-before-stale`_. | +| `days-before-close` | Idle number of days before closing a stale issue/PR.
_Defaults to **7**_. | +| `days-before-issue-close` | Idle number of days before closing a stale issue.
_Override `days-before-close`_. | +| `days-before-pr-close` | Idle number of days before closing a stale PR.
_Override `days-before-close`_. | +| `stale-issue-message` | Message to post on the stale issue. | +| `stale-pr-message` | Message to post on the stale PR. | +| `close-issue-message` | Message to post on the stale issue while closing it. | +| `close-pr-message` | Message to post on the stale PR while closing it. | +| `stale-issue-label` | Label to apply on the stale issue.
_Defaults to **Stale**_. | +| `close-issue-label` | Label to apply on closing issue.
Automatically removed if no longer closed nor locked). | +| `stale-pr-label` | Label to apply on the stale PR.
_Defaults to **Stale**_. | +| `close-pr-label` | Label to apply on the closing PR.
Automatically removed if no longer closed nor locked). | +| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | +| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | +| `only-labels` | Only issues and PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered"). | +| `only-issue-labels` | Only issues with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | +| `only-pr-labels` | Only PRs with ALL these labels are checked.
Separate multiple labels with commas (eg. "question,answered").
_Override `only-labels`_. | +| `any-of-labels` | Only issues and PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | +| `any-of-issue-labels` | Only issues with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | +| `any-of-pr-labels` | Only PRs with ANY of these labels are checked.
Separate multiple labels with commas (eg. "incomplete,waiting-feedback").
_Override `any-of-labels`_. | +| `operations-per-run` | Maximum number of operations per run.
GitHub API CRUD related.
_Defaults to **30**_. | +| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments.
_Defaults to **true**_. | +| `remove-issue-stale-when-updated` | Remove stale label from issue on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `remove-pr-stale-when-updated` | Remove stale label from PR on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `remove-issue-stale-when-updated` | Remove stale label from issue on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `remove-pr-stale-when-updated` | Remove stale label from PR on updates or comments.
_Defaults to **true**_.
_Override `remove-stale-when-updated`_. | +| `debug-only` | Dry-run on action.
_Defaults to **false**_. | +| `ascending` | Order to get issues/PR.
`true` is ascending, `false` is descending.
_Defaults to **false**_. | +| `skip-stale-issue-message` | Skip adding stale message on stale issue.
_Defaults to **false**_. | +| `skip-stale-pr-message` | Skip adding stale message on stale PR.
_Defaults to **false**_. | +| `start-date` | The date used to skip the stale action on issue/PR created before it.
ISO 8601 or RFC 2822. | +| `delete-branch` | Delete the git branch after closing a stale pull request.
_Defaults to **false**_. | +| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | +| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale.
_Override `exempt-milestones`_. | +| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale.
_Override `exempt-milestones`_. | +| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale.
_Priority over `exempt-milestones` rules_. | +| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | +| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale.
_Override `exempt-all-milestones`_. | +| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | +| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale.
_Override `exempt-assignees`_. | +| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale.
_Override `exempt-assignees`_. | +| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale.
_Priority over `exempt-assignees` rules_. | +| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | +| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale.
_Override `exempt-all-assignees`_. | +| `enable-statistics` | Display some statistics at the end of the logs regarding the stale workflow.
Only when the logs are enabled.
_Defaults to **true**_. | + +### Detailed options + +#### operations-per-run + +_Context:_ +This action performs some API calls to GitHub to fetch or close issues and pull requests, set or update labels, add comments, delete branches, etc. +These operations are made in a very short period of time - because the action is very fast to run - and can be numerous based on your project action configuration and the quantity of issues and pull requests within it. +GitHub has a [rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) and if reached will block all of these API calls for one hour (or API calls from other actions using the same user (a.k.a: the github-token from the [repo-token](#repo-token) option)). +This option helps you to stay within the GitHub rate limits, as you can use this option to limit the number of operations for a single run. + +_Purpose:_ +This option aims to limit the number of operations made with the GitHub API to avoid reaching the [rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). + +Based on your project, your GitHub business plan and the date of the cron job you set for this action, you can increase this limit to a higher number. +If you are not sure which is the right value for you or if the default value is good enough, you could enable the logs and look at the end of the stale action. +If you reached the limit, you will see a warning message in the logs, telling you that you should increase the number of operations. +If you choose not to increase the limit, you might end up with un-processed issues or pull requests after a stale action run. + +When [debugging](#Debugging), you can set it to a much higher number like `1000` since there will be fewer operations made with the GitHub API. +Only the [actor](#repo-token) and the batch of issues (100 per batch) will consume the operations. + +Default value: `30` ### Usage @@ -215,7 +246,7 @@ jobs: exempt-all-pr-milestones: true ``` -Avoid stale for specific labels: +Check stale for specific labels: ```yaml name: 'Close stale issues and PRs' @@ -230,7 +261,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 f69d1eed2..cfed44bde 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 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 + .emptyAnyOfLabels() + .issuesOrPrs([{labels: [{name: '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 + .anyOfLabels('dummy-label') + .issuesOrPrs([{labels: []}]) .build(); - await sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + expect(issuesProcessor.staleIssues).toHaveLength(0); }); - 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 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(0); + expect(issuesProcessor.staleIssues).toHaveLength(0); }); - 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 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(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 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 sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + expect(issuesProcessor.staleIssues).toHaveLength(1); }); - 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 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 sut.processIssues(); + await issuesProcessor.processIssues(); - expect(sut.staleIssues).toHaveLength(1); + 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); }); }); -class IssuesProcessorBuilder { - private _options: IIssuesProcessorOptions; - private _issues: Issue[]; +describe('any-of-issue-labels option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); - constructor() { - this._options = {...DefaultProcessorOptions}; - this._issues = []; - } + 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 = { + ...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 4c8979388..3b87f1e96 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -22,9 +22,13 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ onlyIssueLabels: '', onlyPrLabels: '', anyOfLabels: '', + anyOfIssueLabels: '', + anyOfPrLabels: '', operationsPerRun: 100, debugOnly: true, removeStaleWhenUpdated: false, + removeIssueStaleWhenUpdated: undefined, + removePrStaleWhenUpdated: undefined, ascending: false, skipStaleIssueMessage: false, skipStalePrMessage: false, @@ -42,5 +46,5 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ exemptAllAssignees: false, exemptAllIssueAssignees: undefined, exemptAllPrAssignees: undefined, - enableStatistics: false + enableStatistics: true }); diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts index 1fe15800b..3a86eb227 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -25,8 +25,8 @@ test('processing an issue with no label will make it stale and close it, if it i // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(1); }); test('processing an issue with no label and a start date as ECMAScript epoch in seconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => { @@ -305,9 +305,9 @@ test('processing an issue with no label will make it stale and close it, if it i // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(1); - expect(processor.deletedBranchIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(1); + expect(processor.deletedBranchIssues).toHaveLength(0); }); test('processing an issue with no label will make it stale and not close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to > 0', async () => { @@ -330,8 +330,8 @@ test('processing an issue with no label will make it stale and not close it, if // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue with no label will make it stale and not close it if days-before-close is set to > 0', async () => { @@ -353,8 +353,8 @@ test('processing an issue with no label will make it stale and not close it if d // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue with no label will make it stale and not close it if days-before-close is set to -1 and days-before-issue-close is set to > 0', async () => { @@ -377,8 +377,8 @@ test('processing an issue with no label will make it stale and not close it if d // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue with no label will not make it stale if days-before-stale is set to -1', async () => { @@ -401,8 +401,8 @@ test('processing an issue with no label will not make it stale if days-before-st // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue with no label will not make it stale if days-before-stale and days-before-issue-stale are set to -1', async () => { @@ -426,8 +426,8 @@ test('processing an issue with no label will not make it stale if days-before-st // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue with no label will make it stale but not close it', async () => { @@ -454,8 +454,8 @@ test('processing an issue with no label will make it stale but not close it', as // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing a stale issue will close it', async () => { @@ -485,8 +485,8 @@ test('processing a stale issue will close it', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale issue containing a space in the label will close it', async () => { @@ -516,8 +516,8 @@ test('processing a stale issue containing a space in the label will close it', a // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale issue containing a slash in the label will close it', async () => { @@ -547,8 +547,8 @@ test('processing a stale issue containing a slash in the label will close it', a // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => { @@ -579,8 +579,8 @@ test('processing a stale issue will close it when days-before-issue-stale overri // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale PR will close it', async () => { @@ -610,8 +610,8 @@ test('processing a stale PR will close it', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => { @@ -642,8 +642,8 @@ test('processing a stale PR will close it when days-before-pr-stale override day // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale issue will close it even if configured not to mark as stale', async () => { @@ -674,8 +674,8 @@ test('processing a stale issue will close it even if configured not to mark as s // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => { @@ -707,8 +707,8 @@ test('processing a stale issue will close it even if configured not to mark as s // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale PR will close it even if configured not to mark as stale', async () => { @@ -739,8 +739,8 @@ test('processing a stale PR will close it even if configured not to mark as stal // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => { @@ -772,8 +772,8 @@ test('processing a stale PR will close it even if configured not to mark as stal // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(1); }); test('closed issues will not be marked stale', async () => { @@ -799,8 +799,8 @@ test('closed issues will not be marked stale', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('stale closed issues will not be closed', async () => { @@ -827,8 +827,8 @@ test('stale closed issues will not be closed', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('closed prs will not be marked stale', async () => { @@ -855,8 +855,8 @@ test('closed prs will not be marked stale', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('stale closed prs will not be closed', async () => { @@ -883,8 +883,8 @@ test('stale closed prs will not be closed', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('locked issues will not be marked stale', async () => { @@ -910,8 +910,8 @@ test('locked issues will not be marked stale', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('stale locked issues will not be closed', async () => { @@ -939,8 +939,8 @@ test('stale locked issues will not be closed', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('locked prs will not be marked stale', async () => { @@ -966,8 +966,8 @@ test('locked prs will not be marked stale', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('stale locked prs will not be closed', async () => { @@ -995,8 +995,8 @@ test('stale locked prs will not be closed', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('exempt issue labels will not be marked stale', async () => { @@ -1055,8 +1055,8 @@ test('exempt issue labels will not be marked stale (multi issue label with space // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('exempt issue labels will not be marked stale (multi issue label)', async () => { @@ -1084,9 +1084,9 @@ test('exempt issue labels will not be marked stale (multi issue label)', async ( // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); }); test('exempt pr labels will not be marked stale', async () => { @@ -1131,7 +1131,7 @@ test('exempt pr labels will not be marked stale', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(2); // PR should get processed even though it has an exempt **issue** label + expect(processor.staleIssues).toHaveLength(2); // PR should get processed even though it has an exempt **issue** label }); test('exempt issue labels will not be marked stale and will remove the existing stale label', async () => { @@ -1215,13 +1215,12 @@ test('stale issues should not be closed if days is set to -1', async () => { // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); }); test('stale label should be removed if a comment was added to a stale issue', async () => { - const opts = {...DefaultProcessorOptions}; - opts.removeStaleWhenUpdated = true; + const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true}; const TestIssueList: Issue[] = [ generateIssue( opts, @@ -1251,14 +1250,13 @@ test('stale label should be removed if a comment was added to a stale issue', as // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(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}; - opts.removeStaleWhenUpdated = true; + const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true}; github.context.actor = 'abot'; const TestIssueList: Issue[] = [ generateIssue( @@ -1289,9 +1287,9 @@ test('stale label should not be removed if a comment was added by the bot (and t // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(1); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); }); test('stale label containing a space should be removed if a comment was added to a stale issue', async () => { @@ -1322,9 +1320,9 @@ test('stale label containing a space should be removed if a comment was added to // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(1); }); test('stale issues should not be closed until after the closed number of days', async () => { @@ -1354,9 +1352,9 @@ test('stale issues should not be closed until after the closed number of days', // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); }); test('stale issues should be closed if the closed nubmer of days (additive) is also passed', async () => { @@ -1387,9 +1385,9 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(1); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(1); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); }); test('stale issues should not be closed until after the closed number of days (long)', async () => { @@ -1419,9 +1417,9 @@ test('stale issues should not be closed until after the closed number of days (l // process our fake issue list await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); }); test('skips stale message on issues when skip-stale-issue-message is set', async () => { @@ -1455,9 +1453,9 @@ test('skips stale message on issues when skip-stale-issue-message is set', async await processor.processIssues(1); // issue should be staled - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); // comment should not be created expect(markSpy).toHaveBeenCalledWith( @@ -1500,9 +1498,9 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () => await processor.processIssues(1); // issue should be staled - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); // comment should not be created expect(markSpy).toHaveBeenCalledWith( @@ -1543,9 +1541,9 @@ test('not providing state takes precedence over skipStaleIssueMessage', async () await processor.processIssues(1); // issue should be staled - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); }); test('not providing stalePrMessage takes precedence over skipStalePrMessage', async () => { @@ -1577,9 +1575,9 @@ test('not providing stalePrMessage takes precedence over skipStalePrMessage', as await processor.processIssues(1); // issue should be staled - expect(processor.closedIssues.length).toEqual(0); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); }); test('git branch is deleted when option is enabled', async () => { @@ -1606,10 +1604,10 @@ test('git branch is deleted when option is enabled', async () => { await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(1); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.deletedBranchIssues.length).toEqual(1); + expect(processor.closedIssues).toHaveLength(1); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.deletedBranchIssues).toHaveLength(1); }); test('git branch is not deleted when issue is not pull request', async () => { @@ -1636,10 +1634,10 @@ test('git branch is not deleted when issue is not pull request', async () => { await processor.processIssues(1); - expect(processor.closedIssues.length).toEqual(1); - expect(processor.removedLabelIssues.length).toEqual(0); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.deletedBranchIssues.length).toEqual(0); + expect(processor.closedIssues).toHaveLength(1); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.deletedBranchIssues).toHaveLength(0); }); test('an issue without a milestone will be marked as stale', async () => { @@ -1938,8 +1936,8 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue opened since 2 days and with the option "daysBeforeIssueStale" at 2 will make it stale', async () => { @@ -1965,8 +1963,8 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing an issue opened since 2 days and with the option "daysBeforeIssueStale" at 1 will make it stale', async () => { @@ -1992,8 +1990,8 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing a pull request opened since 2 days and with the option "daysBeforePrStale" at 3 will not make it stale', async () => { @@ -2026,8 +2024,8 @@ test('processing a pull request opened since 2 days and with the option "daysBef // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(0); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(0); + expect(processor.closedIssues).toHaveLength(0); }); test('processing a pull request opened since 2 days and with the option "daysBeforePrStale" at 2 will make it stale', async () => { @@ -2060,8 +2058,8 @@ test('processing a pull request opened since 2 days and with the option "daysBef // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing a pull request opened since 2 days and with the option "daysBeforePrStale" at 1 will make it stale', async () => { @@ -2094,8 +2092,8 @@ test('processing a pull request opened since 2 days and with the option "daysBef // process our fake issue list await processor.processIssues(1); - expect(processor.staleIssues.length).toEqual(1); - expect(processor.closedIssues.length).toEqual(0); + expect(processor.staleIssues).toHaveLength(1); + expect(processor.closedIssues).toHaveLength(0); }); test('processing a previously closed issue with a close label will remove the close label', async () => { diff --git a/__tests__/remove-stale-when-updated.spec.ts b/__tests__/remove-stale-when-updated.spec.ts new file mode 100644 index 000000000..24eeef592 --- /dev/null +++ b/__tests__/remove-stale-when-updated.spec.ts @@ -0,0 +1,567 @@ +import {Issue} from '../src/classes/issue'; +import {IIssue} from '../src/interfaces/issue'; +import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; +import {ILabel} from '../src/interfaces/label'; +import {IssuesProcessorMock} from './classes/issues-processor-mock'; +import {DefaultProcessorOptions} from './constants/default-processor-options'; +import {generateIssue} from './functions/generate-issue'; + +let issuesProcessorBuilder: IssuesProcessorBuilder; +let issuesProcessor: IssuesProcessorMock; + +/** + * @description + * Assuming there is a comment on the issue + */ +describe('remove-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); +}); + +describe('remove-issue-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + describe('when the option "remove-issue-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + describe('when the option "remove-issue-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepIssueStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-issue-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeIssueStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); +}); + +describe('remove-pr-stale-when-updated option', (): void => { + beforeEach((): void => { + issuesProcessorBuilder = new IssuesProcessorBuilder(); + }); + + describe('when the option "remove-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepStaleWhenUpdated(); + }); + + describe('when the option "remove-pr-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenUpdated(); + }); + + test('should not remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); + + describe('when the option "remove-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removeStaleWhenUpdated(); + }); + + describe('when the option "remove-pr-stale-when-updated" is unset', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.unsetPrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is disabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.keepPrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should not remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(0); + }); + }); + + describe('when the option "remove-pr-stale-when-updated" is enabled', (): void => { + beforeEach((): void => { + issuesProcessorBuilder.removePrStaleWhenUpdated(); + }); + + test('should remove the stale label on the issue', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.staleIssues([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + + test('should remove the stale label on the pull request', async (): Promise => { + expect.assertions(1); + issuesProcessor = issuesProcessorBuilder.stalePrs([{}]).build(); + + await issuesProcessor.processIssues(); + + expect(issuesProcessor.removedLabelIssues).toHaveLength(1); + }); + }); + }); +}); + +class IssuesProcessorBuilder { + private _options: IIssuesProcessorOptions = { + ...DefaultProcessorOptions + }; + private _issues: Issue[] = []; + + keepStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeStaleWhenUpdated = false; + + return this; + } + + removeStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeStaleWhenUpdated = true; + + return this; + } + + unsetIssueStaleWhenUpdated(): IssuesProcessorBuilder { + delete this._options.removeIssueStaleWhenUpdated; + + return this; + } + + keepIssueStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenUpdated = false; + + return this; + } + + removeIssueStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removeIssueStaleWhenUpdated = true; + + return this; + } + + unsetPrStaleWhenUpdated(): IssuesProcessorBuilder { + delete this._options.removePrStaleWhenUpdated; + + return this; + } + + keepPrStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removePrStaleWhenUpdated = false; + + return this; + } + + removePrStaleWhenUpdated(): IssuesProcessorBuilder { + this._options.removePrStaleWhenUpdated = true; + + return this; + } + + issuesOrPrs(issues: Partial[]): IssuesProcessorBuilder { + this._issues = issues.map( + (issue: Readonly>, index: Readonly): Issue => + generateIssue( + this._options, + index, + issue.title ?? 'dummy-title', + issue.updated_at ?? new Date().toDateString(), + issue.created_at ?? new Date().toDateString(), + !!issue.pull_request, + issue.labels ? issue.labels.map(label => label.name) : [] + ) + ); + + return this; + } + + issues(issues: Partial[]): IssuesProcessorBuilder { + this.issuesOrPrs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + pull_request: null + }; + } + ) + ); + + return this; + } + + staleIssues(issues: Partial[]): IssuesProcessorBuilder { + this.issues( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + updated_at: '2020-01-01T17:00:00Z', + created_at: '2020-01-01T17:00:00Z', + labels: issue.labels?.map( + (label: Readonly): ILabel => { + return { + ...label, + name: 'Stale' + }; + } + ) ?? [ + { + name: 'Stale' + } + ] + }; + } + ) + ); + + return this; + } + + prs(issues: Partial[]): IssuesProcessorBuilder { + this.issuesOrPrs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + pull_request: {key: 'value'} + }; + } + ) + ); + + return this; + } + + stalePrs(issues: Partial[]): IssuesProcessorBuilder { + this.prs( + issues.map( + (issue: Readonly>): Partial => { + return { + ...issue, + updated_at: '2020-01-01T17:00:00Z', + created_at: '2020-01-01T17:00:00Z', + labels: issue.labels?.map( + (label: Readonly): ILabel => { + return { + ...label, + name: 'Stale' + }; + } + ) ?? [ + { + name: 'Stale' + } + ] + }; + } + ) + ); + + return this; + } + + build(): IssuesProcessorMock { + return new IssuesProcessorMock( + this._options, + async () => 'abot', + async p => (p === 1 ? this._issues : []), + async () => [ + { + user: { + login: 'notme', + type: 'User' + } + } + ], + async () => new Date().toDateString() + ); + } +} diff --git a/action.yml b/action.yml index 0b3f202ef..0fc0ac6ba 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: '' @@ -105,7 +113,15 @@ inputs: default: '30' required: false remove-stale-when-updated: - description: 'Remove stale labels from issues when they are updated or commented on.' + description: 'Remove stale labels from issues and pull requests when they are updated or commented on.' + default: 'true' + required: false + remove-issue-stale-when-updated: + description: 'Remove stale labels from issues when they are updated or commented on. Override "remove-stale-when-updated" option regarding only the issues.' + default: 'true' + required: false + remove-pr-stale-when-updated: + description: 'Remove stale labels from pull requests when they are updated or commented on. Override "remove-stale-when-updated" option regarding only the pull requests.' default: 'true' required: false debug-only: diff --git a/dist/index.js b/dist/index.js index cbe4c6af4..bd0990e82 100644 --- a/dist/index.js +++ b/dist/index.js @@ -12,7 +12,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Assignees = void 0; +const chalk_1 = __importDefault(__nccwpck_require__(8818)); const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601)); +const option_1 = __nccwpck_require__(5931); const words_to_list_1 = __nccwpck_require__(1883); const issue_logger_1 = __nccwpck_require__(2984); class Assignees { @@ -31,23 +33,23 @@ class Assignees { return false; } if (this._shouldExemptAllAssignees()) { - this._issueLogger.info('Skipping $$type because it has an exempt assignee'); + this._issueLogger.info(chalk_1.default.white('└──'), 'Skipping this $$type because it has an exempt assignee'); return true; } const exemptAssignees = this._getExemptAssignees(); if (exemptAssignees.length === 0) { - this._issueLogger.info(`No option was specified to skip the stale process for this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `No assignee option was specified to skip the stale process for this $$type`); this._logSkip(); return false; } - this._issueLogger.info(`Found ${exemptAssignees.length} assignee${exemptAssignees.length > 1 ? 's' : ''} on this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `Found ${chalk_1.default.cyan(exemptAssignees.length)} assignee${exemptAssignees.length > 1 ? 's' : ''} that can exempt stale on this $$type`); const hasExemptAssignee = exemptAssignees.some((exemptAssignee) => this._hasAssignee(exemptAssignee)); if (!hasExemptAssignee) { - this._issueLogger.info('No assignee on this $$type can exempt the stale process'); + this._issueLogger.info(chalk_1.default.white('├──'), 'No assignee on this $$type can exempt the stale process'); this._logSkip(); } else { - this._issueLogger.info('Skipping this $$type because it has an exempt assignee'); + this._issueLogger.info(chalk_1.default.white('└──'), 'Skipping this $$type because it has an exempt assignee'); } return hasExemptAssignee; } @@ -58,32 +60,32 @@ class Assignees { } _getExemptIssueAssignees() { if (this._options.exemptIssueAssignees === '') { - this._issueLogger.info('The option "exemptIssueAssignees" is disabled. No specific assignee can skip the stale process for this $$type'); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptIssueAssignees)} is disabled. No specific assignee can skip the stale process for this $$type`); if (this._options.exemptAssignees === '') { - this._issueLogger.info('The option "exemptAssignees" is disabled. No specific assignee can skip the stale process for this $$type'); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAssignees)} is disabled. No specific assignee can skip the stale process for this $$type`); return []; } const exemptAssignees = words_to_list_1.wordsToList(this._options.exemptAssignees); - this._issueLogger.info(`The option "exemptAssignees" is set. ${exemptAssignees.length} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAssignees)} is set. ${chalk_1.default.cyan(exemptAssignees.length)} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); return exemptAssignees; } const exemptAssignees = words_to_list_1.wordsToList(this._options.exemptIssueAssignees); - this._issueLogger.info(`The option "exemptIssueAssignees" is set. ${exemptAssignees.length} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptIssueAssignees)} is set. ${chalk_1.default.cyan(exemptAssignees.length)} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); return exemptAssignees; } _getExemptPullRequestAssignees() { if (this._options.exemptPrAssignees === '') { - this._issueLogger.info('The option "exemptPrAssignees" is disabled. No specific assignee can skip the stale process for this $$type'); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptPrAssignees)} is disabled. No specific assignee can skip the stale process for this $$type`); if (this._options.exemptAssignees === '') { - this._issueLogger.info('The option "exemptAssignees" is disabled. No specific assignee can skip the stale process for this $$type'); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAssignees)} is disabled. No specific assignee can skip the stale process for this $$type`); return []; } const exemptAssignees = words_to_list_1.wordsToList(this._options.exemptAssignees); - this._issueLogger.info(`The option "exemptAssignees" is set. ${exemptAssignees.length} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAssignees)} is set. ${chalk_1.default.cyan(exemptAssignees.length)} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); return exemptAssignees; } const exemptAssignees = words_to_list_1.wordsToList(this._options.exemptPrAssignees); - this._issueLogger.info(`The option "exemptPrAssignees" is set. ${exemptAssignees.length} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptPrAssignees)} is set. ${chalk_1.default.cyan(exemptAssignees.length)} assignee${exemptAssignees.length === 1 ? '' : 's'} can skip the stale process for this $$type`); return exemptAssignees; } _hasAssignee(assignee) { @@ -91,7 +93,7 @@ class Assignees { return this._issue.assignees.some((issueAssignee) => { const isSameAssignee = cleanAssignee === Assignees._cleanAssignee(issueAssignee.login); if (isSameAssignee) { - this._issueLogger.info(`@${issueAssignee.login} is assigned on this $$type and is an exempt assignee`); + this._issueLogger.info(chalk_1.default.white('├──'), `@${issueAssignee.login} is assigned on this $$type and is an exempt assignee`); } return isSameAssignee; }); @@ -103,11 +105,11 @@ class Assignees { } _shouldExemptAllIssueAssignees() { if (this._options.exemptAllIssueAssignees === true) { - this._issueLogger.info('The option "exemptAllIssueAssignees" is enabled. Any assignee on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllIssueAssignees)} is enabled. Any assignee on this $$type will skip the stale process`); return true; } else if (this._options.exemptAllIssueAssignees === false) { - this._issueLogger.info('The option "exemptAllIssueAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllIssueAssignees)} is disabled. Only some specific assignees on this $$type will skip the stale process`); return false; } this._logExemptAllAssigneesOption(); @@ -115,11 +117,11 @@ class Assignees { } _shouldExemptAllPullRequestAssignees() { if (this._options.exemptAllPrAssignees === true) { - this._issueLogger.info('The option "exemptAllPrAssignees" is enabled. Any assignee on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllPrAssignees)} is enabled. Any assignee on this $$type will skip the stale process`); return true; } else if (this._options.exemptAllPrAssignees === false) { - this._issueLogger.info('The option "exemptAllPrAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllPrAssignees)} is disabled. Only some specific assignees on this $$type will skip the stale process`); return false; } this._logExemptAllAssigneesOption(); @@ -127,14 +129,14 @@ class Assignees { } _logExemptAllAssigneesOption() { if (this._options.exemptAllAssignees) { - this._issueLogger.info('The option "exemptAllAssignees" is enabled. Any assignee on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllAssignees)} is enabled. Any assignee on this $$type will skip the stale process`); } else { - this._issueLogger.info('The option "exemptAllAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process'); + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllAssignees)} is disabled. Only some specific assignees on this $$type will skip the stale process`); } } _logSkip() { - this._issueLogger.info('Skip the assignees checks'); + this._issueLogger.info(chalk_1.default.white('└──'), 'Skip the assignees checks'); } } exports.Assignees = Assignees; @@ -151,8 +153,10 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Issue = void 0; const is_labeled_1 = __nccwpck_require__(6792); const is_pull_request_1 = __nccwpck_require__(5400); +const operations_1 = __nccwpck_require__(7957); class Issue { constructor(options, issue) { + this.operations = new operations_1.Operations(); this._options = options; this.title = issue.title; this.number = issue.number; @@ -191,6 +195,25 @@ exports.Issue = Issue; "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -200,14 +223,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.IssuesProcessor = void 0; +const core = __importStar(__nccwpck_require__(2186)); const github_1 = __nccwpck_require__(5438); +const chalk_1 = __importDefault(__nccwpck_require__(8818)); +const option_1 = __nccwpck_require__(5931); const get_humanized_date_1 = __nccwpck_require__(965); const is_date_more_recent_than_1 = __nccwpck_require__(1473); const is_valid_date_1 = __nccwpck_require__(891); +const is_boolean_1 = __nccwpck_require__(8236); const is_labeled_1 = __nccwpck_require__(6792); -const is_pull_request_1 = __nccwpck_require__(5400); const should_mark_when_stale_1 = __nccwpck_require__(2461); const words_to_list_1 = __nccwpck_require__(1883); const assignees_1 = __nccwpck_require__(7236); @@ -215,6 +244,7 @@ const issue_1 = __nccwpck_require__(4783); const issue_logger_1 = __nccwpck_require__(2984); const logger_1 = __nccwpck_require__(6212); const milestones_1 = __nccwpck_require__(4601); +const stale_operations_1 = __nccwpck_require__(5080); const statistics_1 = __nccwpck_require__(3334); /*** * Handle processing of issues for staleness/closure. @@ -222,19 +252,20 @@ const statistics_1 = __nccwpck_require__(3334); class IssuesProcessor { constructor(options) { this._logger = new logger_1.Logger(); - this._operationsLeft = 0; this.staleIssues = []; this.closedIssues = []; this.deletedBranchIssues = []; this.removedLabelIssues = []; this.options = options; - this._operationsLeft = this.options.operationsPerRun; this.client = github_1.getOctokit(this.options.repoToken); + this._operations = new stale_operations_1.StaleOperations(this.options); + this._logger.info(chalk_1.default.yellow('Starting the stale action process...')); if (this.options.debugOnly) { - this._logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.'); + this._logger.warning(chalk_1.default.yellowBright('Executing in debug mode!')); + this._logger.warning(chalk_1.default.yellowBright('The debug output will be written but no issues/PRs will be processed.')); } if (this.options.enableStatistics) { - this._statistics = new statistics_1.Statistics(this.options); + this._statistics = new statistics_1.Statistics(); } } static _updatedSince(timestamp, num_days) { @@ -242,6 +273,18 @@ class IssuesProcessor { const millisSinceLastUpdated = new Date().getTime() - new Date(timestamp).getTime(); return millisSinceLastUpdated <= daysInMillis; } + static _endIssueProcessing(issue) { + const consumedOperationsCount = issue.operations.getConsumedOperationsCount(); + if (consumedOperationsCount > 0) { + const issueLogger = new issue_logger_1.IssueLogger(issue); + issueLogger.info(chalk_1.default.cyan(consumedOperationsCount), `operation${consumedOperationsCount > 1 ? 's' : ''} consumed for this $$type`); + } + } + static _getStaleMessageUsedOptionName(issue) { + return issue.isPullRequest + ? option_1.Option.StalePrMessage + : option_1.Option.StaleIssueMessage; + } processIssues(page = 1) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { @@ -249,15 +292,17 @@ class IssuesProcessor { const issues = yield this.getIssues(page); const actor = yield this.getActor(); if (issues.length <= 0) { - this._logger.info('---'); - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.setOperationsLeft(this._operationsLeft).logStats(); - this._logger.info('No more issues found to process. Exiting.'); - return this._operationsLeft; + this._logger.info(chalk_1.default.green('No more issues found to process. Exiting...')); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.setRemainingOperations(this._operations.getRemainingOperationsCount()).logStats(); + return this._operations.getRemainingOperationsCount(); + } + else { + this._logger.info(chalk_1.default.yellow(`Processing the batch of issues ${chalk_1.default.cyan(`#${page}`)} containing ${chalk_1.default.cyan(issues.length)} issue${issues.length > 1 ? 's' : ''}...`)); } for (const issue of issues.values()) { const issueLogger = new issue_logger_1.IssueLogger(issue); - (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementProcessedIssuesCount(); - issueLogger.info(`Found this $$type last updated ${issue.updated_at}`); + (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementProcessedItemsCount(issue); + issueLogger.info(`Found this $$type last updated at: ${chalk_1.default.cyan(issue.updated_at)}`); // calculate string based messages for this issue const staleMessage = issue.isPullRequest ? this.options.stalePrMessage @@ -279,50 +324,58 @@ class IssuesProcessor { : this._getDaysBeforeIssueStale(); const onlyLabels = words_to_list_1.wordsToList(this._getOnlyLabels(issue)); if (onlyLabels.length > 0) { - issueLogger.info(`The option "onlyLabels" was specified to only processed the issues and pull requests with all those labels (${onlyLabels.length})`); + issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was specified to only process issues and pull requests with all those labels (${chalk_1.default.cyan(onlyLabels.length)})`); const hasAllWhitelistedLabels = onlyLabels.every((label) => { 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`); + IssuesProcessor._endIssueProcessing(issue); 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 ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was not specified`); + issueLogger.info(chalk_1.default.white('└──'), `Continuing the process for this $$type`); } - issueLogger.info(`Days before $$type stale: ${daysBeforeStale}`); + issueLogger.info(`Days before $$type stale: ${chalk_1.default.cyan(daysBeforeStale)}`); const shouldMarkAsStale = should_mark_when_stale_1.shouldMarkWhenStale(daysBeforeStale); if (!staleMessage && shouldMarkAsStale) { - issueLogger.info(`Skipping $$type due to empty stale message`); + issueLogger.info(`Skipping this $$type because it should be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${chalk_1.default.cyan(daysBeforeStale)}) but the option ${issueLogger.createOptionLink(IssuesProcessor._getStaleMessageUsedOptionName(issue))} is not set`); + IssuesProcessor._endIssueProcessing(issue); continue; } if (issue.state === 'closed') { - issueLogger.info(`Skipping $$type because it is closed`); - continue; // don't process closed issues + issueLogger.info(`Skipping this $$type because it is closed`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process closed issues } if (issue.locked) { - issueLogger.info(`Skipping $$type because it is locked`); - continue; // don't process locked issues + issueLogger.info(`Skipping this $$type because it is locked`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process locked issues } // Try to remove the close label when not close/locked issue or PR yield this._removeCloseLabel(issue, closeLabel); if (this.options.startDate) { const startDate = new Date(this.options.startDate); const createdAt = new Date(issue.created_at); - issueLogger.info(`A start date was specified for the ${get_humanized_date_1.getHumanizedDate(startDate)} (${this.options.startDate})`); + issueLogger.info(`A start date was specified for the ${get_humanized_date_1.getHumanizedDate(startDate)} (${chalk_1.default.cyan(this.options.startDate)})`); // Expecting that GitHub will always set a creation date on the issues and PRs // But you never know! if (!is_valid_date_1.isValidDate(createdAt)) { - throw new Error(`Invalid issue field: "created_at". Expected a valid date`); + IssuesProcessor._endIssueProcessing(issue); + core.setFailed(new Error(`Invalid issue field: "created_at". Expected a valid date`)); } - issueLogger.info(`$$type created the ${get_humanized_date_1.getHumanizedDate(createdAt)} (${issue.created_at})`); + issueLogger.info(`$$type created the ${get_humanized_date_1.getHumanizedDate(createdAt)} (${chalk_1.default.cyan(issue.created_at)})`); if (!is_date_more_recent_than_1.isDateMoreRecentThan(createdAt, startDate)) { - issueLogger.info(`Skipping $$type because it was created before the specified start date`); - continue; // don't process issues which were created before the start date + issueLogger.info(`Skipping this $$type because it was created before the specified start date`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process issues which were created before the start date } } if (issue.isStale) { @@ -339,57 +392,87 @@ class IssuesProcessor { issueLogger.info(`An exempt label was added after the stale label.`); yield this._removeStaleLabel(issue, staleLabel); } - issueLogger.info(`Skipping $$type because it has an exempt label`); - continue; // don't process exempt issues + issueLogger.info(`Skipping this $$type because it has an exempt label`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt issues + } + const anyOfLabels = words_to_list_1.wordsToList(this._getAnyOfLabels(issue)); + if (anyOfLabels.length > 0) { + issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.AnyOfLabels)} was specified to only process the issues and pull requests with one of those labels (${chalk_1.default.cyan(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`); + IssuesProcessor._endIssueProcessing(issue); + 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`); + } } - 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 + else { + issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.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()) { - issueLogger.info(`Skipping $$type because it has an exempted milestone`); - continue; // don't process exempt milestones + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt milestones } const assignees = new assignees_1.Assignees(this.options, issue); if (assignees.shouldExemptAssignees()) { - continue; // don't process exempt assignees + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt assignees } - // should this issue be marked stale? + // Should this issue be marked stale? const shouldBeStale = !IssuesProcessor._updatedSince(issue.updated_at, daysBeforeStale); - // determine if this issue needs to be marked stale first - if (!issue.isStale && shouldBeStale && shouldMarkAsStale) { - issueLogger.info(`Marking $$type stale because it was last updated on ${issue.updated_at} and it does not have a stale label`); - yield this._markStale(issue, staleMessage, staleLabel, skipMessage); - issue.isStale = true; // this issue is now considered stale - } - else if (!issue.isStale) { - issueLogger.info(`Not marking as stale: shouldBeStale=${shouldBeStale}, shouldMarkAsStale=${shouldMarkAsStale}`); + // Determine if this issue needs to be marked stale first + if (!issue.isStale) { + issueLogger.info(`This $$type is not stale`); + const updatedAtDate = new Date(issue.updated_at); + if (shouldBeStale) { + issueLogger.info(`This $$type should be stale based on the last update date the ${get_humanized_date_1.getHumanizedDate(updatedAtDate)} (${chalk_1.default.cyan(issue.updated_at)})`); + if (shouldMarkAsStale) { + issueLogger.info(`This $$type should be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${chalk_1.default.cyan(daysBeforeStale)})`); + yield this._markStale(issue, staleMessage, staleLabel, skipMessage); + issue.isStale = true; // This issue is now considered stale + issueLogger.info(`This $$type is now stale`); + } + else { + issueLogger.info(`This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${chalk_1.default.cyan(daysBeforeStale)})`); + } + } + else { + issueLogger.info(`This $$type should not be stale based on the last update date the ${get_humanized_date_1.getHumanizedDate(updatedAtDate)} (${chalk_1.default.cyan(issue.updated_at)})`); + } } - // process the issue if it was marked stale + // Process the issue if it was marked stale if (issue.isStale) { - issueLogger.info(`Found a stale $$type`); + issueLogger.info(`This $$type is already stale`); yield this._processStaleIssue(issue, staleLabel, actor, closeMessage, closeLabel); } + IssuesProcessor._endIssueProcessing(issue); } - if (this._operationsLeft <= 0) { - this._logger.warning('Reached max number of operations to process. Exiting.'); + if (!this._operations.hasRemainingOperations()) { + this._logger.warning(chalk_1.default.yellowBright('No more operations left! Exiting...')); + this._logger.warning(chalk_1.default.yellowBright(`If you think that not enough issues were processed you could try to increase the quantity related to the ${this._logger.createOptionLink(option_1.Option.OperationsPerRun)} option which is currently set to ${chalk_1.default.cyan(this.options.operationsPerRun)}`)); return 0; } - // do the next batch + this._logger.info(chalk_1.default.green(`Batch ${chalk_1.default.cyan(`#${page}`)} processed.`)); + // Do the next batch return this.processIssues(page + 1); }); } - // grab comments for an issue since a given date + // Grab comments for an issue since a given date listIssueComments(issueNumber, sinceDate) { var _a; return __awaiter(this, void 0, void 0, function* () { - // find any comments since date on the given issue + // Find any comments since date on the given issue try { - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesCommentsCount(); + this._operations.consumeOperation(); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCommentsCount(); const comments = yield this.client.issues.listComments({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -409,7 +492,7 @@ class IssuesProcessor { return __awaiter(this, void 0, void 0, function* () { let actor; try { - this._operationsLeft -= 1; + this._operations.consumeOperation(); actor = yield this.client.users.getAuthenticated(); } catch (error) { @@ -425,8 +508,7 @@ class IssuesProcessor { // generate type for response const endpoint = this.client.issues.listForRepo; try { - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesCount(); + this._operations.consumeOperation(); const issueResult = yield this.client.issues.listForRepo({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -435,6 +517,7 @@ class IssuesProcessor { direction: this.options.ascending ? 'asc' : 'desc', page }); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCount(issueResult.data.length); return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue)); } catch (error) { @@ -449,9 +532,9 @@ class IssuesProcessor { var _a; return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - issueLogger.info(`Checking for label on $$type`); - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesEventsCount(); + issueLogger.info(`Checking for label on this $$type`); + this._consumeIssueOperation(issue); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsEventsCount(); const options = this.client.issues.listEvents.endpoint.merge({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -473,29 +556,30 @@ class IssuesProcessor { return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); const markedStaleOn = (yield this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at; - issueLogger.info(`$$type marked stale on: ${markedStaleOn}`); + issueLogger.info(`$$type marked stale on: ${chalk_1.default.cyan(markedStaleOn)}`); const issueHasComments = yield this._hasCommentsSince(issue, markedStaleOn, actor); - issueLogger.info(`$$type has been commented on: ${issueHasComments}`); - const isPr = is_pull_request_1.isPullRequest(issue); - const daysBeforeClose = isPr + issueLogger.info(`$$type has been commented on: ${chalk_1.default.cyan(issueHasComments)}`); + const daysBeforeClose = issue.isPullRequest ? this._getDaysBeforePrClose() : this._getDaysBeforeIssueClose(); - issueLogger.info(`Days before $$type close: ${daysBeforeClose}`); + issueLogger.info(`Days before $$type close: ${chalk_1.default.cyan(daysBeforeClose)}`); const issueHasUpdate = IssuesProcessor._updatedSince(issue.updated_at, daysBeforeClose); - issueLogger.info(`$$type has been updated: ${issueHasUpdate}`); + issueLogger.info(`$$type has been updated: ${chalk_1.default.cyan(issueHasUpdate)}`); // should we un-stale this issue? - if (this.options.removeStaleWhenUpdated && issueHasComments) { + if (this._shouldRemoveStaleWhenUpdated(issue) && issueHasComments) { yield this._removeStaleLabel(issue, staleLabel); + issueLogger.info(`Skipping the process since the $$type is now un-stale`); + return; // nothing to do because it is no longer stale } // now start closing logic if (daysBeforeClose < 0) { return; // nothing to do because we aren't closing stale issues } if (!issueHasComments && !issueHasUpdate) { - issueLogger.info(`Closing $$type because it was last updated on ${issue.updated_at}`); + issueLogger.info(`Closing $$type because it was last updated on! ${chalk_1.default.cyan(issue.updated_at)}`); yield this._closeIssue(issue, closeMessage, closeLabel); if (this.options.deleteBranch && issue.pull_request) { - issueLogger.info(`Deleting branch for as delete-branch option was specified`); + issueLogger.info(`Deleting the branch the option ${issueLogger.createOptionLink(option_1.Option.DeleteBranch)} was specified`); yield this._deleteBranch(issue); this.deletedBranchIssues.push(issue); } @@ -509,14 +593,14 @@ class IssuesProcessor { _hasCommentsSince(issue, sinceDate, actor) { return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - issueLogger.info(`Checking for comments on $$type since ${sinceDate}`); + issueLogger.info(`Checking for comments on $$type since: ${chalk_1.default.cyan(sinceDate)}`); if (!sinceDate) { return true; } // find any comments since the date const comments = yield this.listIssueComments(issue.number, sinceDate); const filteredComments = comments.filter(comment => comment.user.type === 'User' && comment.user.login !== actor); - issueLogger.info(`Comments not made by actor or another bot: ${filteredComments.length}`); + issueLogger.info(`Comments not made by actor or another bot: ${chalk_1.default.cyan(filteredComments.length)}`); // if there are any user comments returned return filteredComments.length > 0; }); @@ -526,7 +610,7 @@ class IssuesProcessor { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - issueLogger.info(`Marking $$type as stale`); + issueLogger.info(`Marking this $$type as stale`); this.staleIssues.push(issue); // if the issue is being marked stale, the updated date should be changed to right now // so that close calculations work correctly @@ -537,8 +621,8 @@ class IssuesProcessor { } if (!skipMessage) { try { - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedComment(); + this._consumeIssueOperation(issue); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsComment(issue); yield this.client.issues.createComment({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -547,13 +631,13 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } try { - this._operationsLeft -= 1; - (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedLabel(); - (_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleIssuesCount(); + this._consumeIssueOperation(issue); + (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedItemsLabel(issue); + (_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleItemsCount(issue); yield this.client.issues.addLabels({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -562,7 +646,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } }); } @@ -578,8 +662,8 @@ class IssuesProcessor { } if (closeMessage) { try { - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedComment(); + this._consumeIssueOperation(issue); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsComment(issue); yield this.client.issues.createComment({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -588,13 +672,13 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } if (closeLabel) { try { - this._operationsLeft -= 1; - (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedLabel(); + this._consumeIssueOperation(issue); + (_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedItemsLabel(issue); yield this.client.issues.addLabels({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -603,12 +687,12 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } try { - this._operationsLeft -= 1; - (_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementClosedIssuesCount(); + this._consumeIssueOperation(issue); + (_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementClosedItemsCount(issue); yield this.client.issues.update({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, @@ -617,7 +701,7 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error updating this $$type: ${error.message}`); + issueLogger.error(`Error when updating this $$type: ${error.message}`); } }); } @@ -629,7 +713,7 @@ class IssuesProcessor { return; } try { - this._operationsLeft -= 1; + this._consumeIssueOperation(issue); (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedPullRequestsCount(); const pullRequest = yield this.client.pulls.get({ owner: github_1.context.repo.owner, @@ -639,7 +723,7 @@ class IssuesProcessor { return pullRequest.data; } catch (error) { - issueLogger.error(`Error getting this $$type: ${error.message}`); + issueLogger.error(`Error when getting this $$type: ${error.message}`); } }); } @@ -651,16 +735,16 @@ class IssuesProcessor { issueLogger.info(`Delete branch from closed $$type - ${issue.title}`); const pullRequest = yield this._getPullRequest(issue); if (!pullRequest) { - issueLogger.info(`Not deleting branch as pull request not found for this $$type`); + issueLogger.info(`Not deleting this branch as no pull request was found for this $$type`); return; } if (this.options.debugOnly) { return; } const branch = pullRequest.head.ref; - issueLogger.info(`Deleting branch ${branch} from closed $$type`); + issueLogger.info(`Deleting the branch "${chalk_1.default.cyan(branch)}" from closed $$type`); try { - this._operationsLeft -= 1; + this._consumeIssueOperation(issue); (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedBranchesCount(); yield this.client.git.deleteRef({ owner: github_1.context.repo.owner, @@ -669,33 +753,33 @@ class IssuesProcessor { }); } catch (error) { - issueLogger.error(`Error deleting branch ${branch} from $$type: ${error.message}`); + issueLogger.error(`Error when deleting the branch "${chalk_1.default.cyan(branch)}" from $$type: ${error.message}`); } }); } - // Remove a label from an issue + // Remove a label from an issue or a pull request _removeLabel(issue, label) { var _a; return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); - issueLogger.info(`Removing label "${label}" from $$type`); + issueLogger.info(`Removing the label "${chalk_1.default.cyan(label)}" from this $$type...`); this.removedLabelIssues.push(issue); - // @todo remove the debug only to be able to test the code below if (this.options.debugOnly) { return; } try { - this._operationsLeft -= 1; - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedLabelsCount(); + this._consumeIssueOperation(issue); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedItemsLabelsCount(issue); yield this.client.issues.removeLabel({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, issue_number: issue.number, name: label }); + issueLogger.info(`The label "${chalk_1.default.cyan(label)}" was removed`); } catch (error) { - issueLogger.error(`Error removing a label: ${error.message}`); + issueLogger.error(`Error when removing the label: "${chalk_1.default.cyan(error.message)}"`); } }); } @@ -732,13 +816,38 @@ 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; + } + _shouldRemoveStaleWhenUpdated(issue) { + if (issue.isPullRequest) { + if (is_boolean_1.isBoolean(this.options.removePrStaleWhenUpdated)) { + return this.options.removePrStaleWhenUpdated; + } + return this.options.removeStaleWhenUpdated; + } + if (is_boolean_1.isBoolean(this.options.removeIssueStaleWhenUpdated)) { + return this.options.removeIssueStaleWhenUpdated; + } + return this.options.removeStaleWhenUpdated; + } _removeStaleLabel(issue, staleLabel) { var _a; return __awaiter(this, void 0, void 0, function* () { const issueLogger = new issue_logger_1.IssueLogger(issue); issueLogger.info(`The $$type is no longer stale. Removing the stale label...`); yield this._removeLabel(issue, staleLabel); - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementUndoStaleIssuesCount(); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementUndoStaleItemsCount(issue); }); } _removeCloseLabel(issue, closeLabel) { @@ -751,12 +860,31 @@ class IssuesProcessor { return Promise.resolve(); } if (is_labeled_1.isLabeled(issue, closeLabel)) { - issueLogger.info(`The $$type has a close label "${closeLabel}". Removing the close label...`); + issueLogger.info(`The $$type has a close label "${chalk_1.default.cyan(closeLabel)}". Removing the close label...`); yield this._removeLabel(issue, closeLabel); - (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedCloseLabelsCount(); + (_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedCloseItemsLabelsCount(issue); } }); } + _consumeIssueOperation(issue) { + this._operations.consumeOperation(); + issue.operations.consumeOperation(); + } + _getDaysBeforeStaleUsedOptionName(issue) { + return issue.isPullRequest + ? this._getDaysBeforePrStaleUsedOptionName() + : this._getDaysBeforeIssueStaleUsedOptionName(); + } + _getDaysBeforeIssueStaleUsedOptionName() { + return isNaN(this.options.daysBeforeIssueStale) + ? option_1.Option.DaysBeforeStale + : option_1.Option.DaysBeforeIssueStale; + } + _getDaysBeforePrStaleUsedOptionName() { + return isNaN(this.options.daysBeforePrStale) + ? option_1.Option.DaysBeforeStale + : option_1.Option.DaysBeforePrStale; + } } exports.IssuesProcessor = IssuesProcessor; @@ -768,28 +896,13 @@ exports.IssuesProcessor = IssuesProcessor; "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.IssueLogger = void 0; -const core = __importStar(__nccwpck_require__(2186)); +const chalk_1 = __importDefault(__nccwpck_require__(8818)); +const logger_1 = __nccwpck_require__(6212); /** * @description * Each log will prefix the message with the issue number @@ -803,18 +916,19 @@ const core = __importStar(__nccwpck_require__(2186)); * @example * warning('The $$type will stale') => "The pull request will stale" */ -class IssueLogger { +class IssueLogger extends logger_1.Logger { constructor(issue) { + super(); this._issue = issue; } - warning(message) { - core.warning(this._format(message)); + warning(...message) { + super.warning(this._format(...message)); } - info(message) { - core.info(this._format(message)); + info(...message) { + super.info(this._format(...message)); } - error(message) { - core.error(this._format(message)); + error(...message) { + super.error(this._format(...message)); } _replaceTokens(message) { return this._replaceTypeToken(message); @@ -825,13 +939,24 @@ class IssueLogger { .replace(/\$\$type/g, this._issue.isPullRequest ? 'pull request' : 'issue'); } _prefixWithIssueNumber(message) { - return `[#${this._getIssueNumber()}] ${message}`; + return `${this._getPrefix()} ${message}`; } _getIssueNumber() { return this._issue.number; } - _format(message) { - return this._prefixWithIssueNumber(this._replaceTokens(message)); + _format(...message) { + return this._prefixWithIssueNumber(this._replaceTokens(message.join(' '))); + } + _getPrefix() { + return this._issue.isPullRequest + ? this._getPullRequestPrefix() + : this._getIssuePrefix(); + } + _getIssuePrefix() { + return chalk_1.default.red(`[#${this._getIssueNumber()}]`); + } + _getPullRequestPrefix() { + return chalk_1.default.blue(`[#${this._getIssueNumber()}]`); } } exports.IssueLogger = IssueLogger; @@ -863,18 +988,29 @@ var __importStar = (this && this.__importStar) || function (mod) { __setModuleDefault(result, mod); return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Logger = void 0; const core = __importStar(__nccwpck_require__(2186)); +const chalk_1 = __importDefault(__nccwpck_require__(8818)); +const terminal_link_1 = __importDefault(__nccwpck_require__(1898)); class Logger { - warning(message) { - core.warning(message); + warning(...message) { + core.warning(chalk_1.default.whiteBright(...message)); + } + info(...message) { + core.info(chalk_1.default.whiteBright(...message)); } - info(message) { - core.info(message); + error(...message) { + core.error(chalk_1.default.whiteBright(...message)); } - error(message) { - core.error(message); + createLink(name, link) { + return terminal_link_1.default(name, link); + } + createOptionLink(option) { + return chalk_1.default.magenta(this.createLink(option, `https://github.com/actions/stale#${option}`)); } } exports.Logger = Logger; @@ -892,44 +1028,93 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Milestones = void 0; +const chalk_1 = __importDefault(__nccwpck_require__(8818)); const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601)); +const option_1 = __nccwpck_require__(5931); const words_to_list_1 = __nccwpck_require__(1883); +const issue_logger_1 = __nccwpck_require__(2984); class Milestones { constructor(options, issue) { this._options = options; this._issue = issue; + this._issueLogger = new issue_logger_1.IssueLogger(issue); } static _cleanMilestone(milestone) { return lodash_deburr_1.default(milestone.toLowerCase()); } shouldExemptMilestones() { + if (!this._issue.milestone) { + this._issueLogger.info('This $$type has no milestone'); + this._logSkip(); + return false; + } if (this._shouldExemptAllMilestones()) { + this._issueLogger.info(chalk_1.default.white('└──'), 'Skipping this $$type because it has a milestone'); return true; } const exemptMilestones = this._getExemptMilestones(); - return exemptMilestones.some((exemptMilestone) => this._hasMilestone(exemptMilestone)); + if (exemptMilestones.length === 0) { + this._issueLogger.info(chalk_1.default.white('├──'), `No milestone option was specified to skip the stale process for this $$type`); + this._logSkip(); + return false; + } + this._issueLogger.info(chalk_1.default.white('├──'), `Found ${chalk_1.default.cyan(exemptMilestones.length)} milestone${exemptMilestones.length > 1 ? 's' : ''} that can exempt stale on this $$type`); + const hasExemptMilestone = exemptMilestones.some((exemptMilestone) => this._hasMilestone(exemptMilestone)); + if (!hasExemptMilestone) { + this._issueLogger.info(chalk_1.default.white('├──'), 'No milestone on this $$type can exempt the stale process'); + this._logSkip(); + } + else { + this._issueLogger.info(chalk_1.default.white('└──'), 'Skipping this $$type because it has an exempt milestone'); + } + return hasExemptMilestone; } _getExemptMilestones() { - return words_to_list_1.wordsToList(this._issue.isPullRequest + return this._issue.isPullRequest ? this._getExemptPullRequestMilestones() - : this._getExemptIssueMilestones()); + : this._getExemptIssueMilestones(); } _getExemptIssueMilestones() { - return this._options.exemptIssueMilestones !== '' - ? this._options.exemptIssueMilestones - : this._options.exemptMilestones; + if (this._options.exemptIssueMilestones === '') { + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptIssueMilestones)} is disabled. No specific milestone can skip the stale process for this $$type`); + if (this._options.exemptMilestones === '') { + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptMilestones)} is disabled. No specific milestone can skip the stale process for this $$type`); + return []; + } + const exemptMilestones = words_to_list_1.wordsToList(this._options.exemptMilestones); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptMilestones)} is set. ${chalk_1.default.cyan(exemptMilestones.length)} milestone${exemptMilestones.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + return exemptMilestones; + } + const exemptMilestones = words_to_list_1.wordsToList(this._options.exemptIssueMilestones); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptIssueMilestones)} is set. ${chalk_1.default.cyan(exemptMilestones.length)} milestone${exemptMilestones.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + return exemptMilestones; } _getExemptPullRequestMilestones() { - return this._options.exemptPrMilestones !== '' - ? this._options.exemptPrMilestones - : this._options.exemptMilestones; + if (this._options.exemptPrMilestones === '') { + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptPrMilestones)} is disabled. No specific milestone can skip the stale process for this $$type`); + if (this._options.exemptMilestones === '') { + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptMilestones)} is disabled. No specific milestone can skip the stale process for this $$type`); + return []; + } + const exemptMilestones = words_to_list_1.wordsToList(this._options.exemptMilestones); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptMilestones)} is set. ${chalk_1.default.cyan(exemptMilestones.length)} milestone${exemptMilestones.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + return exemptMilestones; + } + const exemptMilestones = words_to_list_1.wordsToList(this._options.exemptPrMilestones); + this._issueLogger.info(chalk_1.default.white('├──'), `The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptPrMilestones)} is set. ${chalk_1.default.cyan(exemptMilestones.length)} milestone${exemptMilestones.length === 1 ? '' : 's'} can skip the stale process for this $$type`); + return exemptMilestones; } _hasMilestone(milestone) { if (!this._issue.milestone) { return false; } - return (Milestones._cleanMilestone(milestone) === - Milestones._cleanMilestone(this._issue.milestone.title)); + const cleanMilestone = Milestones._cleanMilestone(milestone); + const isSameMilestone = cleanMilestone === + Milestones._cleanMilestone(this._issue.milestone.title); + if (isSameMilestone) { + this._issueLogger.info(chalk_1.default.white('├──'), `The milestone "${milestone}" is set on this $$type and is an exempt milestone`); + } + return isSameMilestone; } _shouldExemptAllMilestones() { if (this._issue.milestone) { @@ -941,181 +1126,520 @@ class Milestones { } _shouldExemptAllIssueMilestones() { if (this._options.exemptAllIssueMilestones === true) { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllIssueMilestones)} is enabled. Any milestone on this $$type will skip the stale process`); return true; } else if (this._options.exemptAllIssueMilestones === false) { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllIssueMilestones)} is disabled. Only some specific milestones on this $$type will skip the stale process`); return false; } + this._logExemptAllMilestonesOption(); return this._options.exemptAllMilestones; } _shouldExemptAllPullRequestMilestones() { if (this._options.exemptAllPrMilestones === true) { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllPrMilestones)} is enabled. Any milestone on this $$type will skip the stale process`); return true; } else if (this._options.exemptAllPrMilestones === false) { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllPrMilestones)} is disabled. Only some specific milestones on this $$type will skip the stale process`); return false; } + this._logExemptAllMilestonesOption(); return this._options.exemptAllMilestones; } + _logExemptAllMilestonesOption() { + if (this._options.exemptAllMilestones) { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllMilestones)} is enabled. Any milestone on this $$type will skip the stale process`); + } + else { + this._issueLogger.info(`The option ${this._issueLogger.createOptionLink(option_1.Option.ExemptAllMilestones)} is disabled. Only some specific milestones on this $$type will skip the stale process`); + } + } + _logSkip() { + this._issueLogger.info(chalk_1.default.white('└──'), 'Skip the milestones checks'); + } } exports.Milestones = Milestones; /***/ }), -/***/ 3334: +/***/ 7957: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Operations = void 0; +class Operations { + constructor() { + this._operationsConsumed = 0; + } + consumeOperation() { + return this.consumeOperations(1); + } + consumeOperations(quantity) { + this._operationsConsumed += quantity; + return this; + } + getConsumedOperationsCount() { + return this._operationsConsumed; + } +} +exports.Operations = Operations; + + +/***/ }), + +/***/ 5080: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.StaleOperations = void 0; +const operations_1 = __nccwpck_require__(7957); +class StaleOperations extends operations_1.Operations { + constructor(options) { + super(); + this._options = options; + } + hasRemainingOperations() { + return this._operationsConsumed < this._options.operationsPerRun; + } + getRemainingOperationsCount() { + return this._options.operationsPerRun - this._operationsConsumed; + } +} +exports.StaleOperations = StaleOperations; + + +/***/ }), + +/***/ 3334: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Statistics = void 0; +const chalk_1 = __importDefault(__nccwpck_require__(8818)); const logger_1 = __nccwpck_require__(6212); class Statistics { - constructor(options) { + constructor() { this._logger = new logger_1.Logger(); this._processedIssuesCount = 0; + this._processedPullRequestsCount = 0; this._staleIssuesCount = 0; + this._stalePullRequestsCount = 0; this._undoStaleIssuesCount = 0; + this._undoStalePullRequestsCount = 0; this._operationsCount = 0; this._closedIssuesCount = 0; - this._deletedLabelsCount = 0; - this._deletedCloseLabelsCount = 0; + this._closedPullRequestsCount = 0; + this._deletedIssuesLabelsCount = 0; + this._deletedPullRequestsLabelsCount = 0; + this._deletedCloseIssuesLabelsCount = 0; + this._deletedClosePullRequestsLabelsCount = 0; this._deletedBranchesCount = 0; - this._addedLabelsCount = 0; - this._addedCommentsCount = 0; - this._fetchedIssuesCount = 0; - this._fetchedIssuesEventsCount = 0; - this._fetchedIssuesCommentsCount = 0; + this._addedIssuesLabelsCount = 0; + this._addedPullRequestsLabelsCount = 0; + this._addedIssuesCommentsCount = 0; + this._addedPullRequestsCommentsCount = 0; + this._fetchedItemsCount = 0; + this._fetchedItemsEventsCount = 0; + this._fetchedItemsCommentsCount = 0; this._fetchedPullRequestsCount = 0; - this._options = options; } - incrementProcessedIssuesCount(increment = 1) { - this._processedIssuesCount += increment; + incrementProcessedItemsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementProcessedPullRequestsCount(increment); + } + return this._incrementProcessedIssuesCount(increment); + } + incrementStaleItemsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementStalePullRequestsCount(increment); + } + return this._incrementStaleIssuesCount(increment); + } + incrementUndoStaleItemsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementUndoStalePullRequestsCount(increment); + } + return this._incrementUndoStaleIssuesCount(increment); + } + setRemainingOperations(remainingOperations) { + this._operationsCount = remainingOperations; return this; } - incrementStaleIssuesCount(increment = 1) { - this._staleIssuesCount += increment; + incrementClosedItemsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementClosedPullRequestsCount(increment); + } + return this._incrementClosedIssuesCount(increment); + } + incrementDeletedItemsLabelsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementDeletedPullRequestsLabelsCount(increment); + } + return this._incrementDeletedIssuesLabelsCount(increment); + } + incrementDeletedCloseItemsLabelsCount(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementDeletedClosePullRequestsLabelsCount(increment); + } + return this._incrementDeletedCloseIssuesLabelsCount(increment); + } + incrementDeletedBranchesCount(increment = 1) { + this._deletedBranchesCount += increment; return this; } - incrementUndoStaleIssuesCount(increment = 1) { - this._undoStaleIssuesCount += increment; + incrementAddedItemsLabel(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementAddedPullRequestsLabel(increment); + } + return this._incrementAddedIssuesLabel(increment); + } + incrementAddedItemsComment(issue, increment = 1) { + if (issue.isPullRequest) { + return this._incrementAddedPullRequestsComment(increment); + } + return this._incrementAddedIssuesComment(increment); + } + incrementFetchedItemsCount(increment = 1) { + this._fetchedItemsCount += increment; return this; } - setOperationsLeft(operationsLeft) { - this._operationsCount = this._options.operationsPerRun - operationsLeft; + incrementFetchedItemsEventsCount(increment = 1) { + this._fetchedItemsEventsCount += increment; return this; } - incrementClosedIssuesCount(increment = 1) { - this._closedIssuesCount += increment; + incrementFetchedItemsCommentsCount(increment = 1) { + this._fetchedItemsCommentsCount += increment; + return this; + } + incrementFetchedPullRequestsCount(increment = 1) { + this._fetchedPullRequestsCount += increment; return this; } - incrementDeletedLabelsCount(increment = 1) { - this._deletedLabelsCount += increment; + logStats() { + this._logger.info(chalk_1.default.yellow.bold('Statistics:')); + this._logProcessedIssuesAndPullRequestsCount(); + this._logStaleIssuesAndPullRequestsCount(); + this._logUndoStaleIssuesAndPullRequestsCount(); + this._logClosedIssuesAndPullRequestsCount(); + this._logDeletedIssuesAndPullRequestsLabelsCount(); + this._logDeletedCloseIssuesAndPullRequestsLabelsCount(); + this._logDeletedBranchesCount(); + this._logAddedIssuesAndPullRequestsLabelsCount(); + this._logAddedIssuesAndPullRequestsCommentsCount(); + this._logFetchedItemsCount(); + this._logFetchedItemsEventsCount(); + this._logFetchedItemsCommentsCount(); + this._logFetchedPullRequestsCount(); + this._logOperationsCount(); return this; } - incrementDeletedCloseLabelsCount(increment = 1) { - this._deletedCloseLabelsCount += increment; + _incrementProcessedIssuesCount(increment = 1) { + this._processedIssuesCount += increment; return this; } - incrementDeletedBranchesCount(increment = 1) { - this._deletedBranchesCount += increment; + _incrementProcessedPullRequestsCount(increment = 1) { + this._processedPullRequestsCount += increment; return this; } - incrementAddedLabel(increment = 1) { - this._addedLabelsCount += increment; + _incrementStaleIssuesCount(increment = 1) { + this._staleIssuesCount += increment; return this; } - incrementAddedComment(increment = 1) { - this._addedCommentsCount += increment; + _incrementStalePullRequestsCount(increment = 1) { + this._stalePullRequestsCount += increment; return this; } - incrementFetchedIssuesCount(increment = 1) { - this._fetchedIssuesCount += increment; + _incrementUndoStaleIssuesCount(increment = 1) { + this._undoStaleIssuesCount += increment; return this; } - incrementFetchedIssuesEventsCount(increment = 1) { - this._fetchedIssuesEventsCount += increment; + _incrementUndoStalePullRequestsCount(increment = 1) { + this._undoStalePullRequestsCount += increment; return this; } - incrementFetchedIssuesCommentsCount(increment = 1) { - this._fetchedIssuesCommentsCount += increment; + _incrementClosedIssuesCount(increment = 1) { + this._closedIssuesCount += increment; return this; } - incrementFetchedPullRequestsCount(increment = 1) { - this._fetchedPullRequestsCount += increment; + _incrementClosedPullRequestsCount(increment = 1) { + this._closedPullRequestsCount += increment; return this; } - logStats() { - this._logger.info('Statistics'); - this._logProcessedIssuesCount(); - this._logStaleIssuesCount(); - this._logUndoStaleIssuesCount(); - this._logOperationsCount(); - this._logClosedIssuesCount(); - this._logDeletedLabelsCount(); - this._logDeletedCloseLabelsCount(); - this._logDeletedBranchesCount(); - this._logAddedLabelsCount(); - this._logAddedCommentsCount(); - this._logFetchedIssuesCount(); - this._logFetchedIssuesEventsCount(); - this._logFetchedIssuesCommentsCount(); - this._logFetchedPullRequestsCount(); - this._logger.info('---'); + _incrementDeletedIssuesLabelsCount(increment = 1) { + this._deletedIssuesLabelsCount += increment; + return this; + } + _incrementDeletedPullRequestsLabelsCount(increment = 1) { + this._deletedPullRequestsLabelsCount += increment; return this; } - _logProcessedIssuesCount() { - this._logCount('Processed issues/PRs', this._processedIssuesCount); + _incrementDeletedCloseIssuesLabelsCount(increment = 1) { + this._deletedCloseIssuesLabelsCount += increment; + return this; } - _logStaleIssuesCount() { - this._logCount('New stale issues/PRs', this._staleIssuesCount); + _incrementDeletedClosePullRequestsLabelsCount(increment = 1) { + this._deletedClosePullRequestsLabelsCount += increment; + return this; } - _logUndoStaleIssuesCount() { - this._logCount('No longer stale issues/PRs', this._undoStaleIssuesCount); + _incrementAddedIssuesLabel(increment = 1) { + this._addedIssuesLabelsCount += increment; + return this; } - _logOperationsCount() { - this._logCount('Operations performed', this._operationsCount); + _incrementAddedPullRequestsLabel(increment = 1) { + this._addedPullRequestsLabelsCount += increment; + return this; } - _logClosedIssuesCount() { - this._logCount('Closed issues', this._closedIssuesCount); + _incrementAddedIssuesComment(increment = 1) { + this._addedIssuesCommentsCount += increment; + return this; } - _logDeletedLabelsCount() { - this._logCount('Deleted labels', this._deletedLabelsCount); + _incrementAddedPullRequestsComment(increment = 1) { + this._addedPullRequestsCommentsCount += increment; + return this; } - _logDeletedCloseLabelsCount() { - this._logCount('Deleted close labels', this._deletedCloseLabelsCount); + _logProcessedIssuesAndPullRequestsCount() { + this._logGroup('Processed items', [ + { + name: 'Processed issues', + count: this._processedIssuesCount + }, + { + name: 'Processed PRs', + count: this._processedPullRequestsCount + } + ]); + } + _logStaleIssuesAndPullRequestsCount() { + this._logGroup('New stale items', [ + { + name: 'New stale issues', + count: this._staleIssuesCount + }, + { + name: 'New stale PRs', + count: this._stalePullRequestsCount + } + ]); + } + _logUndoStaleIssuesAndPullRequestsCount() { + this._logGroup('No longer stale items', [ + { + name: 'No longer stale issues', + count: this._undoStaleIssuesCount + }, + { + name: 'No longer stale PRs', + count: this._undoStalePullRequestsCount + } + ]); + } + _logClosedIssuesAndPullRequestsCount() { + this._logGroup('Closed items', [ + { + name: 'Closed issues', + count: this._closedIssuesCount + }, + { + name: 'Closed PRs', + count: this._closedPullRequestsCount + } + ]); + } + _logDeletedIssuesAndPullRequestsLabelsCount() { + this._logGroup('Deleted items labels', [ + { + name: 'Deleted issues labels', + count: this._deletedIssuesLabelsCount + }, + { + name: 'Deleted PRs labels', + count: this._deletedPullRequestsLabelsCount + } + ]); + } + _logDeletedCloseIssuesAndPullRequestsLabelsCount() { + this._logGroup('Deleted close items labels', [ + { + name: 'Deleted close issues labels', + count: this._deletedCloseIssuesLabelsCount + }, + { + name: 'Deleted close PRs labels', + count: this._deletedClosePullRequestsLabelsCount + } + ]); } _logDeletedBranchesCount() { this._logCount('Deleted branches', this._deletedBranchesCount); } - _logAddedLabelsCount() { - this._logCount('Added labels', this._addedLabelsCount); - } - _logAddedCommentsCount() { - this._logCount('Added comments', this._addedCommentsCount); + _logAddedIssuesAndPullRequestsLabelsCount() { + this._logGroup('Added items labels', [ + { + name: 'Added issues labels', + count: this._addedIssuesLabelsCount + }, + { + name: 'Added PRs labels', + count: this._addedPullRequestsLabelsCount + } + ]); + } + _logAddedIssuesAndPullRequestsCommentsCount() { + this._logGroup('Added items comments', [ + { + name: 'Added issues comments', + count: this._addedIssuesCommentsCount + }, + { + name: 'Added PRs comments', + count: this._addedPullRequestsCommentsCount + } + ]); } - _logFetchedIssuesCount() { - this._logCount('Fetched issues', this._fetchedIssuesCount); + _logFetchedItemsCount() { + this._logCount('Fetched items', this._fetchedItemsCount); } - _logFetchedIssuesEventsCount() { - this._logCount('Fetched issues events', this._fetchedIssuesEventsCount); + _logFetchedItemsEventsCount() { + this._logCount('Fetched items events', this._fetchedItemsEventsCount); } - _logFetchedIssuesCommentsCount() { - this._logCount('Fetched issues comments', this._fetchedIssuesCommentsCount); + _logFetchedItemsCommentsCount() { + this._logCount('Fetched items comments', this._fetchedItemsCommentsCount); } _logFetchedPullRequestsCount() { this._logCount('Fetched pull requests', this._fetchedPullRequestsCount); } + _logOperationsCount() { + this._logCount('Operations performed', this._operationsCount); + } _logCount(name, count) { if (count > 0) { - this._logger.info(`${name}: ${count}`); + this._logger.info(`${name}:`, chalk_1.default.cyan(count)); + } + } + _logGroup(groupName, values) { + if (this._isGroupValuesPartiallySet(values)) { + this._logCount(groupName, this._getGroupValuesTotalCount(values)); + this._logGroupValues(values); + } + else { + // Only one value will be display + for (const value of values) { + this._logCount(value.name, value.count); + } + } + } + /** + * @private + * @description + * If there is a least two elements with a valid count then it's partially set + * Useful to defined if we should display the values as a group or not + * + * @param {IGroupValue[]} values The list of group values to check + */ + _isGroupValuesPartiallySet(values) { + return (values + .map((value) => { + return value.count > 0; + }) + .filter((isSet) => isSet).length >= 2); + } + _getGroupValuesTotalCount(values) { + return values.reduce((count, value) => { + return count + value.count; + }, 0); + } + _getAllGroupValuesSet(values) { + return values.filter((value) => { + return value.count > 0; + }); + } + _logGroupValues(values) { + const onlyValuesSet = this._getAllGroupValuesSet(values); + const longestValue = this._getLongestGroupValue(onlyValuesSet); + for (const [index, value] of onlyValuesSet.entries()) { + const prefix = index === onlyValuesSet.length - 1 ? '└──' : '├──'; + this._logCount(`${chalk_1.default.white(prefix)} ${value.name.padEnd(longestValue, ' ')}`, value.count); } } + _getLongestGroupValue(values) { + return values.reduce((longestValue, value) => { + return value.name.length > longestValue + ? value.name.length + : longestValue; + }, 0); + } } exports.Statistics = Statistics; +/***/ }), + +/***/ 5931: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.Option = void 0; +var Option; +(function (Option) { + Option["RepoToken"] = "repo-token"; + Option["StaleIssueMessage"] = "stale-issue-message"; + Option["StalePrMessage"] = "stale-pr-message"; + Option["CloseIssueMessage"] = "close-issue-message"; + Option["ClosePrMessage"] = "close-pr-message"; + Option["DaysBeforeStale"] = "days-before-stale"; + Option["DaysBeforeIssueStale"] = "days-before-issue-stale"; + Option["DaysBeforePrStale"] = "days-before-pr-stale"; + Option["DaysBeforeClose"] = "days-before-close"; + Option["DaysBeforeIssueClose"] = "days-before-issue-close"; + Option["DaysBeforePrClose"] = "days-before-pr-close"; + Option["StaleIssueLabel"] = "stale-issue-label"; + Option["CloseIssueLabel"] = "close-issue-label"; + Option["ExemptIssueLabels"] = "exempt-issue-labels"; + Option["StalePrLabel"] = "stale-pr-label"; + Option["ClosePrLabel"] = "close-pr-label"; + Option["ExemptPrLabels"] = "exempt-pr-labels"; + Option["OnlyLabels"] = "only-labels"; + Option["OnlyIssueLabels"] = "only-issue-labels"; + Option["OnlyPrLabels"] = "only-pr-labels"; + Option["AnyOfLabels"] = "any-of-labels"; + Option["OperationsPerRun"] = "operations-per-run"; + Option["RemoveStaleWhenUpdated"] = "remove-stale-when-updated"; + Option["DebugOnly"] = "debug-only"; + Option["Ascending"] = "ascending"; + Option["SkipStaleIssueMessage"] = "skip-stale-issue-message"; + Option["SkipStalePrMessage"] = "skip-stale-pr-message"; + Option["DeleteBranch"] = "delete-branch"; + Option["StartDate"] = "start-date"; + Option["ExemptMilestones"] = "exempt-milestones"; + Option["ExemptIssueMilestones"] = "exempt-issue-milestones"; + Option["ExemptPrMilestones"] = "exempt-pr-milestones"; + Option["ExemptAllMilestones"] = "exempt-all-milestones"; + Option["ExemptAllIssueMilestones"] = "exempt-all-issue-milestones"; + Option["ExemptAllPrMilestones"] = "exempt-all-pr-milestones"; + Option["ExemptAssignees"] = "exempt-assignees"; + Option["ExemptIssueAssignees"] = "exempt-issue-assignees"; + Option["ExemptPrAssignees"] = "exempt-pr-assignees"; + Option["ExemptAllAssignees"] = "exempt-all-assignees"; + Option["ExemptAllIssueAssignees"] = "exempt-all-issue-assignees"; + Option["ExemptAllPrAssignees"] = "exempt-all-pr-assignees"; + Option["EnableStatistics"] = "enable-statistics"; +})(Option = exports.Option || (exports.Option = {})); + + /***/ }), /***/ 965: @@ -1186,7 +1710,22 @@ exports.isValidDate = isValidDate; /***/ }), -/***/ 6792: +/***/ 8236: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.isBoolean = void 0; +function isBoolean(value) { + return value === true || value === false; +} +exports.isBoolean = isBoolean; + + +/***/ }), + +/***/ 6792: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -1318,14 +1857,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", ({ value: true })); const core = __importStar(__nccwpck_require__(2186)); -const is_valid_date_1 = __nccwpck_require__(891); const issues_processor_1 = __nccwpck_require__(3292); +const is_valid_date_1 = __nccwpck_require__(891); function _run() { return __awaiter(this, void 0, void 0, function* () { try { const args = _getAndValidateArgs(); - const processor = new issues_processor_1.IssuesProcessor(args); - yield processor.processIssues(); + yield new issues_processor_1.IssuesProcessor(args).processIssues(); } catch (error) { core.error(error); @@ -1356,8 +1894,12 @@ 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'), + removeIssueStaleWhenUpdated: _toOptionalBoolean(core.getInput('remove-issue-stale-when-updated')), + removePrStaleWhenUpdated: _toOptionalBoolean(core.getInput('remove-pr-stale-when-updated')), debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', @@ -1386,14 +1928,18 @@ function _getAndValidateArgs() { 'operations-per-run' ]) { if (isNaN(parseInt(core.getInput(numberInput)))) { - throw Error(`input ${numberInput} did not parse to a valid integer`); + const errorMessage = `Option "${numberInput}" did not parse to a valid integer`; + core.setFailed(errorMessage); + throw new Error(errorMessage); } } for (const optionalDateInput of ['start-date']) { // Ignore empty dates because it is considered as the right type for a default value (so a valid one) if (core.getInput(optionalDateInput) !== '') { if (!is_valid_date_1.isValidDate(new Date(core.getInput(optionalDateInput)))) { - throw new Error(`input ${optionalDateInput} did not parse to a valid date`); + const errorMessage = `Option "${optionalDateInput}" did not parse to a valid date`; + core.setFailed(errorMessage); + throw new Error(errorMessage); } } } @@ -4914,6 +5460,171 @@ exports.request = request; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 8512: +/***/ ((module) => { + +"use strict"; + +const ansiEscapes = module.exports; +// TODO: remove this in the next major version +module.exports.default = ansiEscapes; + +const ESC = '\u001B['; +const OSC = '\u001B]'; +const BEL = '\u0007'; +const SEP = ';'; +const isTerminalApp = process.env.TERM_PROGRAM === 'Apple_Terminal'; + +ansiEscapes.cursorTo = (x, y) => { + if (typeof x !== 'number') { + throw new TypeError('The `x` argument is required'); + } + + if (typeof y !== 'number') { + return ESC + (x + 1) + 'G'; + } + + return ESC + (y + 1) + ';' + (x + 1) + 'H'; +}; + +ansiEscapes.cursorMove = (x, y) => { + if (typeof x !== 'number') { + throw new TypeError('The `x` argument is required'); + } + + let ret = ''; + + if (x < 0) { + ret += ESC + (-x) + 'D'; + } else if (x > 0) { + ret += ESC + x + 'C'; + } + + if (y < 0) { + ret += ESC + (-y) + 'A'; + } else if (y > 0) { + ret += ESC + y + 'B'; + } + + return ret; +}; + +ansiEscapes.cursorUp = (count = 1) => ESC + count + 'A'; +ansiEscapes.cursorDown = (count = 1) => ESC + count + 'B'; +ansiEscapes.cursorForward = (count = 1) => ESC + count + 'C'; +ansiEscapes.cursorBackward = (count = 1) => ESC + count + 'D'; + +ansiEscapes.cursorLeft = ESC + 'G'; +ansiEscapes.cursorSavePosition = isTerminalApp ? '\u001B7' : ESC + 's'; +ansiEscapes.cursorRestorePosition = isTerminalApp ? '\u001B8' : ESC + 'u'; +ansiEscapes.cursorGetPosition = ESC + '6n'; +ansiEscapes.cursorNextLine = ESC + 'E'; +ansiEscapes.cursorPrevLine = ESC + 'F'; +ansiEscapes.cursorHide = ESC + '?25l'; +ansiEscapes.cursorShow = ESC + '?25h'; + +ansiEscapes.eraseLines = count => { + let clear = ''; + + for (let i = 0; i < count; i++) { + clear += ansiEscapes.eraseLine + (i < count - 1 ? ansiEscapes.cursorUp() : ''); + } + + if (count) { + clear += ansiEscapes.cursorLeft; + } + + return clear; +}; + +ansiEscapes.eraseEndLine = ESC + 'K'; +ansiEscapes.eraseStartLine = ESC + '1K'; +ansiEscapes.eraseLine = ESC + '2K'; +ansiEscapes.eraseDown = ESC + 'J'; +ansiEscapes.eraseUp = ESC + '1J'; +ansiEscapes.eraseScreen = ESC + '2J'; +ansiEscapes.scrollUp = ESC + 'S'; +ansiEscapes.scrollDown = ESC + 'T'; + +ansiEscapes.clearScreen = '\u001Bc'; + +ansiEscapes.clearTerminal = process.platform === 'win32' ? + `${ansiEscapes.eraseScreen}${ESC}0f` : + // 1. Erases the screen (Only done in case `2` is not supported) + // 2. Erases the whole screen including scrollback buffer + // 3. Moves cursor to the top-left position + // More info: https://www.real-world-systems.com/docs/ANSIcode.html + `${ansiEscapes.eraseScreen}${ESC}3J${ESC}H`; + +ansiEscapes.beep = BEL; + +ansiEscapes.link = (text, url) => { + return [ + OSC, + '8', + SEP, + SEP, + url, + BEL, + text, + OSC, + '8', + SEP, + SEP, + BEL + ].join(''); +}; + +ansiEscapes.image = (buffer, options = {}) => { + let ret = `${OSC}1337;File=inline=1`; + + if (options.width) { + ret += `;width=${options.width}`; + } + + if (options.height) { + ret += `;height=${options.height}`; + } + + if (options.preserveAspectRatio === false) { + ret += ';preserveAspectRatio=0'; + } + + return ret + ':' + buffer.toString('base64') + BEL; +}; + +ansiEscapes.iTerm = { + setCwd: (cwd = process.cwd()) => `${OSC}50;CurrentDir=${cwd}${BEL}`, + + annotation: (message, options = {}) => { + let ret = `${OSC}1337;`; + + const hasX = typeof options.x !== 'undefined'; + const hasY = typeof options.y !== 'undefined'; + if ((hasX || hasY) && !(hasX && hasY && typeof options.length !== 'undefined')) { + throw new Error('`x`, `y` and `length` must be defined when `x` or `y` is defined'); + } + + message = message.replace(/\|/g, ''); + + ret += options.isHidden ? 'AddHiddenAnnotation=' : 'AddAnnotation='; + + if (options.length > 0) { + ret += + (hasX ? + [message, options.length, options.x, options.y] : + [options.length, message]).join('|'); + } else { + ret += message; + } + + return ret + BEL; + } +}; + + /***/ }), /***/ 3682: @@ -4978,116 +5689,2071 @@ module.exports.Singular = Hook.Singular module.exports.Collection = Hook.Collection -/***/ }), +/***/ }), + +/***/ 5549: +/***/ ((module) => { + +module.exports = addHook + +function addHook (state, kind, name, hook) { + var orig = hook + if (!state.registry[name]) { + state.registry[name] = [] + } + + if (kind === 'before') { + hook = function (method, options) { + return Promise.resolve() + .then(orig.bind(null, options)) + .then(method.bind(null, options)) + } + } + + if (kind === 'after') { + hook = function (method, options) { + var result + return Promise.resolve() + .then(method.bind(null, options)) + .then(function (result_) { + result = result_ + return orig(result, options) + }) + .then(function () { + return result + }) + } + } + + if (kind === 'error') { + hook = function (method, options) { + return Promise.resolve() + .then(method.bind(null, options)) + .catch(function (error) { + return orig(error, options) + }) + } + } + + state.registry[name].push({ + hook: hook, + orig: orig + }) +} + + +/***/ }), + +/***/ 4670: +/***/ ((module) => { + +module.exports = register + +function register (state, name, method, options) { + if (typeof method !== 'function') { + throw new Error('method for before hook must be a function') + } + + if (!options) { + options = {} + } + + if (Array.isArray(name)) { + return name.reverse().reduce(function (callback, name) { + return register.bind(null, state, name, callback, options) + }, method)() + } + + return Promise.resolve() + .then(function () { + if (!state.registry[name]) { + return method(options) + } + + return (state.registry[name]).reduce(function (method, registered) { + return registered.hook.bind(null, method, options) + }, method)() + }) +} + + +/***/ }), + +/***/ 6819: +/***/ ((module) => { + +module.exports = removeHook + +function removeHook (state, name, method) { + if (!state.registry[name]) { + return + } + + var index = state.registry[name] + .map(function (registered) { return registered.orig }) + .indexOf(method) + + if (index === -1) { + return + } + + state.registry[name].splice(index, 1) +} + + +/***/ }), + +/***/ 6734: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* module decorator */ module = __nccwpck_require__.nmd(module); + + +const wrapAnsi16 = (fn, offset) => (...args) => { + const code = fn(...args); + return `\u001B[${code + offset}m`; +}; + +const wrapAnsi256 = (fn, offset) => (...args) => { + const code = fn(...args); + return `\u001B[${38 + offset};5;${code}m`; +}; + +const wrapAnsi16m = (fn, offset) => (...args) => { + const rgb = fn(...args); + return `\u001B[${38 + offset};2;${rgb[0]};${rgb[1]};${rgb[2]}m`; +}; + +const ansi2ansi = n => n; +const rgb2rgb = (r, g, b) => [r, g, b]; + +const setLazyProperty = (object, property, get) => { + Object.defineProperty(object, property, { + get: () => { + const value = get(); + + Object.defineProperty(object, property, { + value, + enumerable: true, + configurable: true + }); + + return value; + }, + enumerable: true, + configurable: true + }); +}; + +/** @type {typeof import('color-convert')} */ +let colorConvert; +const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { + if (colorConvert === undefined) { + colorConvert = __nccwpck_require__(5121); + } + + const offset = isBackground ? 10 : 0; + const styles = {}; + + for (const [sourceSpace, suite] of Object.entries(colorConvert)) { + const name = sourceSpace === 'ansi16' ? 'ansi' : sourceSpace; + if (sourceSpace === targetSpace) { + styles[name] = wrap(identity, offset); + } else if (typeof suite === 'object') { + styles[name] = wrap(suite[targetSpace], offset); + } + } + + return styles; +}; + +function assembleStyles() { + const codes = new Map(); + const styles = { + modifier: { + reset: [0, 0], + // 21 isn't widely supported and 22 does the same thing + bold: [1, 22], + dim: [2, 22], + italic: [3, 23], + underline: [4, 24], + inverse: [7, 27], + hidden: [8, 28], + strikethrough: [9, 29] + }, + color: { + black: [30, 39], + red: [31, 39], + green: [32, 39], + yellow: [33, 39], + blue: [34, 39], + magenta: [35, 39], + cyan: [36, 39], + white: [37, 39], + + // Bright color + blackBright: [90, 39], + redBright: [91, 39], + greenBright: [92, 39], + yellowBright: [93, 39], + blueBright: [94, 39], + magentaBright: [95, 39], + cyanBright: [96, 39], + whiteBright: [97, 39] + }, + bgColor: { + bgBlack: [40, 49], + bgRed: [41, 49], + bgGreen: [42, 49], + bgYellow: [43, 49], + bgBlue: [44, 49], + bgMagenta: [45, 49], + bgCyan: [46, 49], + bgWhite: [47, 49], + + // Bright color + bgBlackBright: [100, 49], + bgRedBright: [101, 49], + bgGreenBright: [102, 49], + bgYellowBright: [103, 49], + bgBlueBright: [104, 49], + bgMagentaBright: [105, 49], + bgCyanBright: [106, 49], + bgWhiteBright: [107, 49] + } + }; + + // Alias bright black as gray (and grey) + styles.color.gray = styles.color.blackBright; + styles.bgColor.bgGray = styles.bgColor.bgBlackBright; + styles.color.grey = styles.color.blackBright; + styles.bgColor.bgGrey = styles.bgColor.bgBlackBright; + + for (const [groupName, group] of Object.entries(styles)) { + for (const [styleName, style] of Object.entries(group)) { + styles[styleName] = { + open: `\u001B[${style[0]}m`, + close: `\u001B[${style[1]}m` + }; + + group[styleName] = styles[styleName]; + + codes.set(style[0], style[1]); + } + + Object.defineProperty(styles, groupName, { + value: group, + enumerable: false + }); + } + + Object.defineProperty(styles, 'codes', { + value: codes, + enumerable: false + }); + + styles.color.close = '\u001B[39m'; + styles.bgColor.close = '\u001B[49m'; + + setLazyProperty(styles.color, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, false)); + setLazyProperty(styles.color, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, false)); + setLazyProperty(styles.color, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, false)); + setLazyProperty(styles.bgColor, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, true)); + setLazyProperty(styles.bgColor, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, true)); + setLazyProperty(styles.bgColor, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, true)); + + return styles; +} + +// Make the export immutable +Object.defineProperty(module, 'exports', { + enumerable: true, + get: assembleStyles +}); + + +/***/ }), + +/***/ 8159: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +/* MIT license */ +/* eslint-disable no-mixed-operators */ +const cssKeywords = __nccwpck_require__(4057); + +// NOTE: conversions should only return primitive values (i.e. arrays, or +// values that give correct `typeof` results). +// do not use box values types (i.e. Number(), String(), etc.) + +const reverseKeywords = {}; +for (const key of Object.keys(cssKeywords)) { + reverseKeywords[cssKeywords[key]] = key; +} + +const convert = { + rgb: {channels: 3, labels: 'rgb'}, + hsl: {channels: 3, labels: 'hsl'}, + hsv: {channels: 3, labels: 'hsv'}, + hwb: {channels: 3, labels: 'hwb'}, + cmyk: {channels: 4, labels: 'cmyk'}, + xyz: {channels: 3, labels: 'xyz'}, + lab: {channels: 3, labels: 'lab'}, + lch: {channels: 3, labels: 'lch'}, + hex: {channels: 1, labels: ['hex']}, + keyword: {channels: 1, labels: ['keyword']}, + ansi16: {channels: 1, labels: ['ansi16']}, + ansi256: {channels: 1, labels: ['ansi256']}, + hcg: {channels: 3, labels: ['h', 'c', 'g']}, + apple: {channels: 3, labels: ['r16', 'g16', 'b16']}, + gray: {channels: 1, labels: ['gray']} +}; + +module.exports = convert; + +// Hide .channels and .labels properties +for (const model of Object.keys(convert)) { + if (!('channels' in convert[model])) { + throw new Error('missing channels property: ' + model); + } + + if (!('labels' in convert[model])) { + throw new Error('missing channel labels property: ' + model); + } + + if (convert[model].labels.length !== convert[model].channels) { + throw new Error('channel and label counts mismatch: ' + model); + } + + const {channels, labels} = convert[model]; + delete convert[model].channels; + delete convert[model].labels; + Object.defineProperty(convert[model], 'channels', {value: channels}); + Object.defineProperty(convert[model], 'labels', {value: labels}); +} + +convert.rgb.hsl = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const min = Math.min(r, g, b); + const max = Math.max(r, g, b); + const delta = max - min; + let h; + let s; + + if (max === min) { + h = 0; + } else if (r === max) { + h = (g - b) / delta; + } else if (g === max) { + h = 2 + (b - r) / delta; + } else if (b === max) { + h = 4 + (r - g) / delta; + } + + h = Math.min(h * 60, 360); + + if (h < 0) { + h += 360; + } + + const l = (min + max) / 2; + + if (max === min) { + s = 0; + } else if (l <= 0.5) { + s = delta / (max + min); + } else { + s = delta / (2 - max - min); + } + + return [h, s * 100, l * 100]; +}; + +convert.rgb.hsv = function (rgb) { + let rdif; + let gdif; + let bdif; + let h; + let s; + + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const v = Math.max(r, g, b); + const diff = v - Math.min(r, g, b); + const diffc = function (c) { + return (v - c) / 6 / diff + 1 / 2; + }; + + if (diff === 0) { + h = 0; + s = 0; + } else { + s = diff / v; + rdif = diffc(r); + gdif = diffc(g); + bdif = diffc(b); + + if (r === v) { + h = bdif - gdif; + } else if (g === v) { + h = (1 / 3) + rdif - bdif; + } else if (b === v) { + h = (2 / 3) + gdif - rdif; + } + + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + } + + return [ + h * 360, + s * 100, + v * 100 + ]; +}; + +convert.rgb.hwb = function (rgb) { + const r = rgb[0]; + const g = rgb[1]; + let b = rgb[2]; + const h = convert.rgb.hsl(rgb)[0]; + const w = 1 / 255 * Math.min(r, Math.min(g, b)); + + b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); + + return [h, w * 100, b * 100]; +}; + +convert.rgb.cmyk = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + + const k = Math.min(1 - r, 1 - g, 1 - b); + const c = (1 - r - k) / (1 - k) || 0; + const m = (1 - g - k) / (1 - k) || 0; + const y = (1 - b - k) / (1 - k) || 0; + + return [c * 100, m * 100, y * 100, k * 100]; +}; + +function comparativeDistance(x, y) { + /* + See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance + */ + return ( + ((x[0] - y[0]) ** 2) + + ((x[1] - y[1]) ** 2) + + ((x[2] - y[2]) ** 2) + ); +} + +convert.rgb.keyword = function (rgb) { + const reversed = reverseKeywords[rgb]; + if (reversed) { + return reversed; + } + + let currentClosestDistance = Infinity; + let currentClosestKeyword; + + for (const keyword of Object.keys(cssKeywords)) { + const value = cssKeywords[keyword]; + + // Compute comparative distance + const distance = comparativeDistance(rgb, value); + + // Check if its less, if so set as closest + if (distance < currentClosestDistance) { + currentClosestDistance = distance; + currentClosestKeyword = keyword; + } + } + + return currentClosestKeyword; +}; + +convert.keyword.rgb = function (keyword) { + return cssKeywords[keyword]; +}; + +convert.rgb.xyz = function (rgb) { + let r = rgb[0] / 255; + let g = rgb[1] / 255; + let b = rgb[2] / 255; + + // Assume sRGB + r = r > 0.04045 ? (((r + 0.055) / 1.055) ** 2.4) : (r / 12.92); + g = g > 0.04045 ? (((g + 0.055) / 1.055) ** 2.4) : (g / 12.92); + b = b > 0.04045 ? (((b + 0.055) / 1.055) ** 2.4) : (b / 12.92); + + const x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + const y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + const z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y * 100, z * 100]; +}; + +convert.rgb.lab = function (rgb) { + const xyz = convert.rgb.xyz(rgb); + let x = xyz[0]; + let y = xyz[1]; + let z = xyz[2]; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116); + + const l = (116 * y) - 16; + const a = 500 * (x - y); + const b = 200 * (y - z); + + return [l, a, b]; +}; + +convert.hsl.rgb = function (hsl) { + const h = hsl[0] / 360; + const s = hsl[1] / 100; + const l = hsl[2] / 100; + let t2; + let t3; + let val; + + if (s === 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) { + t2 = l * (1 + s); + } else { + t2 = l + s - l * s; + } + + const t1 = 2 * l - t2; + + const rgb = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + t3 = h + 1 / 3 * -(i - 1); + if (t3 < 0) { + t3++; + } + + if (t3 > 1) { + t3--; + } + + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } else if (2 * t3 < 1) { + val = t2; + } else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } else { + val = t1; + } + + rgb[i] = val * 255; + } + + return rgb; +}; + +convert.hsl.hsv = function (hsl) { + const h = hsl[0]; + let s = hsl[1] / 100; + let l = hsl[2] / 100; + let smin = s; + const lmin = Math.max(l, 0.01); + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + smin *= lmin <= 1 ? lmin : 2 - lmin; + const v = (l + s) / 2; + const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s); + + return [h, sv * 100, v * 100]; +}; + +convert.hsv.rgb = function (hsv) { + const h = hsv[0] / 60; + const s = hsv[1] / 100; + let v = hsv[2] / 100; + const hi = Math.floor(h) % 6; + + const f = h - Math.floor(h); + const p = 255 * v * (1 - s); + const q = 255 * v * (1 - (s * f)); + const t = 255 * v * (1 - (s * (1 - f))); + v *= 255; + + switch (hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +}; + +convert.hsv.hsl = function (hsv) { + const h = hsv[0]; + const s = hsv[1] / 100; + const v = hsv[2] / 100; + const vmin = Math.max(v, 0.01); + let sl; + let l; + + l = (2 - s) * v; + const lmin = (2 - s) * vmin; + sl = s * vmin; + sl /= (lmin <= 1) ? lmin : 2 - lmin; + sl = sl || 0; + l /= 2; + + return [h, sl * 100, l * 100]; +}; + +// http://dev.w3.org/csswg/css-color/#hwb-to-rgb +convert.hwb.rgb = function (hwb) { + const h = hwb[0] / 360; + let wh = hwb[1] / 100; + let bl = hwb[2] / 100; + const ratio = wh + bl; + let f; + + // Wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + const i = Math.floor(6 * h); + const v = 1 - bl; + f = 6 * h - i; + + if ((i & 0x01) !== 0) { + f = 1 - f; + } + + const n = wh + f * (v - wh); // Linear interpolation + + let r; + let g; + let b; + /* eslint-disable max-statements-per-line,no-multi-spaces */ + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + /* eslint-enable max-statements-per-line,no-multi-spaces */ + + return [r * 255, g * 255, b * 255]; +}; + +convert.cmyk.rgb = function (cmyk) { + const c = cmyk[0] / 100; + const m = cmyk[1] / 100; + const y = cmyk[2] / 100; + const k = cmyk[3] / 100; + + const r = 1 - Math.min(1, c * (1 - k) + k); + const g = 1 - Math.min(1, m * (1 - k) + k); + const b = 1 - Math.min(1, y * (1 - k) + k); + + return [r * 255, g * 255, b * 255]; +}; + +convert.xyz.rgb = function (xyz) { + const x = xyz[0] / 100; + const y = xyz[1] / 100; + const z = xyz[2] / 100; + let r; + let g; + let b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // Assume sRGB + r = r > 0.0031308 + ? ((1.055 * (r ** (1.0 / 2.4))) - 0.055) + : r * 12.92; + + g = g > 0.0031308 + ? ((1.055 * (g ** (1.0 / 2.4))) - 0.055) + : g * 12.92; + + b = b > 0.0031308 + ? ((1.055 * (b ** (1.0 / 2.4))) - 0.055) + : b * 12.92; + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +}; + +convert.xyz.lab = function (xyz) { + let x = xyz[0]; + let y = xyz[1]; + let z = xyz[2]; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116); + + const l = (116 * y) - 16; + const a = 500 * (x - y); + const b = 200 * (y - z); + + return [l, a, b]; +}; + +convert.lab.xyz = function (lab) { + const l = lab[0]; + const a = lab[1]; + const b = lab[2]; + let x; + let y; + let z; + + y = (l + 16) / 116; + x = a / 500 + y; + z = y - b / 200; + + const y2 = y ** 3; + const x2 = x ** 3; + const z2 = z ** 3; + y = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787; + x = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787; + z = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787; + + x *= 95.047; + y *= 100; + z *= 108.883; + + return [x, y, z]; +}; + +convert.lab.lch = function (lab) { + const l = lab[0]; + const a = lab[1]; + const b = lab[2]; + let h; + + const hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + + if (h < 0) { + h += 360; + } + + const c = Math.sqrt(a * a + b * b); + + return [l, c, h]; +}; + +convert.lch.lab = function (lch) { + const l = lch[0]; + const c = lch[1]; + const h = lch[2]; + + const hr = h / 360 * 2 * Math.PI; + const a = c * Math.cos(hr); + const b = c * Math.sin(hr); + + return [l, a, b]; +}; + +convert.rgb.ansi16 = function (args, saturation = null) { + const [r, g, b] = args; + let value = saturation === null ? convert.rgb.hsv(args)[2] : saturation; // Hsv -> ansi16 optimization + + value = Math.round(value / 50); + + if (value === 0) { + return 30; + } + + let ansi = 30 + + ((Math.round(b / 255) << 2) + | (Math.round(g / 255) << 1) + | Math.round(r / 255)); + + if (value === 2) { + ansi += 60; + } + + return ansi; +}; + +convert.hsv.ansi16 = function (args) { + // Optimization here; we already know the value and don't need to get + // it converted for us. + return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]); +}; + +convert.rgb.ansi256 = function (args) { + const r = args[0]; + const g = args[1]; + const b = args[2]; + + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (r === g && g === b) { + if (r < 8) { + return 16; + } + + if (r > 248) { + return 231; + } + + return Math.round(((r - 8) / 247) * 24) + 232; + } + + const ansi = 16 + + (36 * Math.round(r / 255 * 5)) + + (6 * Math.round(g / 255 * 5)) + + Math.round(b / 255 * 5); + + return ansi; +}; + +convert.ansi16.rgb = function (args) { + let color = args % 10; + + // Handle greyscale + if (color === 0 || color === 7) { + if (args > 50) { + color += 3.5; + } + + color = color / 10.5 * 255; + + return [color, color, color]; + } + + const mult = (~~(args > 50) + 1) * 0.5; + const r = ((color & 1) * mult) * 255; + const g = (((color >> 1) & 1) * mult) * 255; + const b = (((color >> 2) & 1) * mult) * 255; + + return [r, g, b]; +}; + +convert.ansi256.rgb = function (args) { + // Handle greyscale + if (args >= 232) { + const c = (args - 232) * 10 + 8; + return [c, c, c]; + } + + args -= 16; + + let rem; + const r = Math.floor(args / 36) / 5 * 255; + const g = Math.floor((rem = args % 36) / 6) / 5 * 255; + const b = (rem % 6) / 5 * 255; + + return [r, g, b]; +}; + +convert.rgb.hex = function (args) { + const integer = ((Math.round(args[0]) & 0xFF) << 16) + + ((Math.round(args[1]) & 0xFF) << 8) + + (Math.round(args[2]) & 0xFF); + + const string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; + +convert.hex.rgb = function (args) { + const match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i); + if (!match) { + return [0, 0, 0]; + } + + let colorString = match[0]; + + if (match[0].length === 3) { + colorString = colorString.split('').map(char => { + return char + char; + }).join(''); + } + + const integer = parseInt(colorString, 16); + const r = (integer >> 16) & 0xFF; + const g = (integer >> 8) & 0xFF; + const b = integer & 0xFF; + + return [r, g, b]; +}; + +convert.rgb.hcg = function (rgb) { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const max = Math.max(Math.max(r, g), b); + const min = Math.min(Math.min(r, g), b); + const chroma = (max - min); + let grayscale; + let hue; + + if (chroma < 1) { + grayscale = min / (1 - chroma); + } else { + grayscale = 0; + } + + if (chroma <= 0) { + hue = 0; + } else + if (max === r) { + hue = ((g - b) / chroma) % 6; + } else + if (max === g) { + hue = 2 + (b - r) / chroma; + } else { + hue = 4 + (r - g) / chroma; + } + + hue /= 6; + hue %= 1; + + return [hue * 360, chroma * 100, grayscale * 100]; +}; + +convert.hsl.hcg = function (hsl) { + const s = hsl[1] / 100; + const l = hsl[2] / 100; + + const c = l < 0.5 ? (2.0 * s * l) : (2.0 * s * (1.0 - l)); + + let f = 0; + if (c < 1.0) { + f = (l - 0.5 * c) / (1.0 - c); + } + + return [hsl[0], c * 100, f * 100]; +}; + +convert.hsv.hcg = function (hsv) { + const s = hsv[1] / 100; + const v = hsv[2] / 100; + + const c = s * v; + let f = 0; + + if (c < 1.0) { + f = (v - c) / (1 - c); + } + + return [hsv[0], c * 100, f * 100]; +}; + +convert.hcg.rgb = function (hcg) { + const h = hcg[0] / 360; + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + if (c === 0.0) { + return [g * 255, g * 255, g * 255]; + } + + const pure = [0, 0, 0]; + const hi = (h % 1) * 6; + const v = hi % 1; + const w = 1 - v; + let mg = 0; + + /* eslint-disable max-statements-per-line */ + switch (Math.floor(hi)) { + case 0: + pure[0] = 1; pure[1] = v; pure[2] = 0; break; + case 1: + pure[0] = w; pure[1] = 1; pure[2] = 0; break; + case 2: + pure[0] = 0; pure[1] = 1; pure[2] = v; break; + case 3: + pure[0] = 0; pure[1] = w; pure[2] = 1; break; + case 4: + pure[0] = v; pure[1] = 0; pure[2] = 1; break; + default: + pure[0] = 1; pure[1] = 0; pure[2] = w; + } + /* eslint-enable max-statements-per-line */ + + mg = (1.0 - c) * g; + + return [ + (c * pure[0] + mg) * 255, + (c * pure[1] + mg) * 255, + (c * pure[2] + mg) * 255 + ]; +}; + +convert.hcg.hsv = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + const v = c + g * (1.0 - c); + let f = 0; + + if (v > 0.0) { + f = c / v; + } + + return [hcg[0], f * 100, v * 100]; +}; + +convert.hcg.hsl = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + + const l = g * (1.0 - c) + 0.5 * c; + let s = 0; + + if (l > 0.0 && l < 0.5) { + s = c / (2 * l); + } else + if (l >= 0.5 && l < 1.0) { + s = c / (2 * (1 - l)); + } + + return [hcg[0], s * 100, l * 100]; +}; + +convert.hcg.hwb = function (hcg) { + const c = hcg[1] / 100; + const g = hcg[2] / 100; + const v = c + g * (1.0 - c); + return [hcg[0], (v - c) * 100, (1 - v) * 100]; +}; + +convert.hwb.hcg = function (hwb) { + const w = hwb[1] / 100; + const b = hwb[2] / 100; + const v = 1 - b; + const c = v - w; + let g = 0; + + if (c < 1) { + g = (v - c) / (1 - c); + } + + return [hwb[0], c * 100, g * 100]; +}; + +convert.apple.rgb = function (apple) { + return [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255]; +}; + +convert.rgb.apple = function (rgb) { + return [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535]; +}; + +convert.gray.rgb = function (args) { + return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255]; +}; + +convert.gray.hsl = function (args) { + return [0, 0, args[0]]; +}; + +convert.gray.hsv = convert.gray.hsl; + +convert.gray.hwb = function (gray) { + return [0, 100, gray[0]]; +}; + +convert.gray.cmyk = function (gray) { + return [0, 0, 0, gray[0]]; +}; + +convert.gray.lab = function (gray) { + return [gray[0], 0, 0]; +}; + +convert.gray.hex = function (gray) { + const val = Math.round(gray[0] / 100 * 255) & 0xFF; + const integer = (val << 16) + (val << 8) + val; + + const string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; +}; + +convert.rgb.gray = function (rgb) { + const val = (rgb[0] + rgb[1] + rgb[2]) / 3; + return [val / 255 * 100]; +}; + + +/***/ }), + +/***/ 5121: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const conversions = __nccwpck_require__(8159); +const route = __nccwpck_require__(4663); + +const convert = {}; + +const models = Object.keys(conversions); + +function wrapRaw(fn) { + const wrappedFn = function (...args) { + const arg0 = args[0]; + if (arg0 === undefined || arg0 === null) { + return arg0; + } + + if (arg0.length > 1) { + args = arg0; + } + + return fn(args); + }; + + // Preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +function wrapRounded(fn) { + const wrappedFn = function (...args) { + const arg0 = args[0]; + + if (arg0 === undefined || arg0 === null) { + return arg0; + } + + if (arg0.length > 1) { + args = arg0; + } + + const result = fn(args); + + // We're assuming the result is an array here. + // see notice in conversions.js; don't use box types + // in conversion functions. + if (typeof result === 'object') { + for (let len = result.length, i = 0; i < len; i++) { + result[i] = Math.round(result[i]); + } + } + + return result; + }; + + // Preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; +} + +models.forEach(fromModel => { + convert[fromModel] = {}; + + Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels}); + Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels}); + + const routes = route(fromModel); + const routeModels = Object.keys(routes); + + routeModels.forEach(toModel => { + const fn = routes[toModel]; + + convert[fromModel][toModel] = wrapRounded(fn); + convert[fromModel][toModel].raw = wrapRaw(fn); + }); +}); + +module.exports = convert; + + +/***/ }), + +/***/ 4663: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const conversions = __nccwpck_require__(8159); + +/* + This function routes a model to all other models. + + all functions that are routed have a property `.conversion` attached + to the returned synthetic function. This property is an array + of strings, each with the steps in between the 'from' and 'to' + color models (inclusive). + + conversions that are not possible simply are not included. +*/ + +function buildGraph() { + const graph = {}; + // https://jsperf.com/object-keys-vs-for-in-with-closure/3 + const models = Object.keys(conversions); + + for (let len = models.length, i = 0; i < len; i++) { + graph[models[i]] = { + // http://jsperf.com/1-vs-infinity + // micro-opt, but this is simple. + distance: -1, + parent: null + }; + } + + return graph; +} + +// https://en.wikipedia.org/wiki/Breadth-first_search +function deriveBFS(fromModel) { + const graph = buildGraph(); + const queue = [fromModel]; // Unshift -> queue -> pop + + graph[fromModel].distance = 0; + + while (queue.length) { + const current = queue.pop(); + const adjacents = Object.keys(conversions[current]); + + for (let len = adjacents.length, i = 0; i < len; i++) { + const adjacent = adjacents[i]; + const node = graph[adjacent]; + + if (node.distance === -1) { + node.distance = graph[current].distance + 1; + node.parent = current; + queue.unshift(adjacent); + } + } + } + + return graph; +} + +function link(from, to) { + return function (args) { + return to(from(args)); + }; +} + +function wrapConversion(toModel, graph) { + const path = [graph[toModel].parent, toModel]; + let fn = conversions[graph[toModel].parent][toModel]; + + let cur = graph[toModel].parent; + while (graph[cur].parent) { + path.unshift(graph[cur].parent); + fn = link(conversions[graph[cur].parent][cur], fn); + cur = graph[cur].parent; + } + + fn.conversion = path; + return fn; +} + +module.exports = function (fromModel) { + const graph = deriveBFS(fromModel); + const conversion = {}; + + const models = Object.keys(graph); + for (let len = models.length, i = 0; i < len; i++) { + const toModel = models[i]; + const node = graph[toModel]; + + if (node.parent === null) { + // No possible conversion, or this node is the source model. + continue; + } + + conversion[toModel] = wrapConversion(toModel, graph); + } + + return conversion; +}; + + + +/***/ }), + +/***/ 4057: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] +}; + + +/***/ }), + +/***/ 1538: +/***/ ((module) => { + +"use strict"; + + +module.exports = (flag, argv = process.argv) => { + const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--'); + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf('--'); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +}; + + +/***/ }), + +/***/ 4955: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const os = __nccwpck_require__(2087); +const tty = __nccwpck_require__(3867); +const hasFlag = __nccwpck_require__(1538); + +const {env} = process; + +let forceColor; +if (hasFlag('no-color') || + hasFlag('no-colors') || + hasFlag('color=false') || + hasFlag('color=never')) { + forceColor = 0; +} else if (hasFlag('color') || + hasFlag('colors') || + hasFlag('color=true') || + hasFlag('color=always')) { + forceColor = 1; +} + +if ('FORCE_COLOR' in env) { + if (env.FORCE_COLOR === 'true') { + forceColor = 1; + } else if (env.FORCE_COLOR === 'false') { + forceColor = 0; + } else { + forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3); + } +} + +function translateLevel(level) { + if (level === 0) { + return false; + } + + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3 + }; +} + +function supportsColor(haveStream, streamIsTTY) { + if (forceColor === 0) { + return 0; + } + + if (hasFlag('color=16m') || + hasFlag('color=full') || + hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + + if (haveStream && !streamIsTTY && forceColor === undefined) { + return 0; + } + + const min = forceColor || 0; + + if (env.TERM === 'dumb') { + return min; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 colors. + // Windows 10 build 14931 is the first release that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(osRelease[0]) >= 10 && + Number(osRelease[2]) >= 10586 + ) { + return Number(osRelease[2]) >= 14931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI', 'GITHUB_ACTIONS', 'BUILDKITE'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + return version >= 3 ? 3 : 2; + case 'Apple_Terminal': + return 2; + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + return min; +} + +function getSupportLevel(stream) { + const level = supportsColor(stream, stream && stream.isTTY); + return translateLevel(level); +} + +module.exports = { + supportsColor: getSupportLevel, + stdout: translateLevel(supportsColor(true, tty.isatty(1))), + stderr: translateLevel(supportsColor(true, tty.isatty(2))) +}; + + +/***/ }), + +/***/ 8818: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const ansiStyles = __nccwpck_require__(6734); +const {stdout: stdoutColor, stderr: stderrColor} = __nccwpck_require__(4955); +const { + stringReplaceAll, + stringEncaseCRLFWithFirstIndex +} = __nccwpck_require__(2415); + +const {isArray} = Array; + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = [ + 'ansi', + 'ansi', + 'ansi256', + 'ansi16m' +]; + +const styles = Object.create(null); + +const applyOptions = (object, options = {}) => { + if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) { + throw new Error('The `level` option should be an integer from 0 to 3'); + } + + // Detect level if not set manually + const colorLevel = stdoutColor ? stdoutColor.level : 0; + object.level = options.level === undefined ? colorLevel : options.level; +}; + +class ChalkClass { + constructor(options) { + // eslint-disable-next-line no-constructor-return + return chalkFactory(options); + } +} + +const chalkFactory = options => { + const chalk = {}; + applyOptions(chalk, options); + + chalk.template = (...arguments_) => chalkTag(chalk.template, ...arguments_); + + Object.setPrototypeOf(chalk, Chalk.prototype); + Object.setPrototypeOf(chalk.template, chalk); + + chalk.template.constructor = () => { + throw new Error('`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.'); + }; + + chalk.template.Instance = ChalkClass; + + return chalk.template; +}; + +function Chalk(options) { + return chalkFactory(options); +} + +for (const [styleName, style] of Object.entries(ansiStyles)) { + styles[styleName] = { + get() { + const builder = createBuilder(this, createStyler(style.open, style.close, this._styler), this._isEmpty); + Object.defineProperty(this, styleName, {value: builder}); + return builder; + } + }; +} + +styles.visible = { + get() { + const builder = createBuilder(this, this._styler, true); + Object.defineProperty(this, 'visible', {value: builder}); + return builder; + } +}; + +const usedModels = ['rgb', 'hex', 'keyword', 'hsl', 'hsv', 'hwb', 'ansi', 'ansi256']; + +for (const model of usedModels) { + styles[model] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(ansiStyles.color[levelMapping[level]][model](...arguments_), ansiStyles.color.close, this._styler); + return createBuilder(this, styler, this._isEmpty); + }; + } + }; +} + +for (const model of usedModels) { + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const {level} = this; + return function (...arguments_) { + const styler = createStyler(ansiStyles.bgColor[levelMapping[level]][model](...arguments_), ansiStyles.bgColor.close, this._styler); + return createBuilder(this, styler, this._isEmpty); + }; + } + }; +} + +const proto = Object.defineProperties(() => {}, { + ...styles, + level: { + enumerable: true, + get() { + return this._generator.level; + }, + set(level) { + this._generator.level = level; + } + } +}); + +const createStyler = (open, close, parent) => { + let openAll; + let closeAll; + if (parent === undefined) { + openAll = open; + closeAll = close; + } else { + openAll = parent.openAll + open; + closeAll = close + parent.closeAll; + } + + return { + open, + close, + openAll, + closeAll, + parent + }; +}; + +const createBuilder = (self, _styler, _isEmpty) => { + const builder = (...arguments_) => { + if (isArray(arguments_[0]) && isArray(arguments_[0].raw)) { + // Called as a template literal, for example: chalk.red`2 + 3 = {bold ${2+3}}` + return applyStyle(builder, chalkTag(builder, ...arguments_)); + } + + // Single argument is hot path, implicit coercion is faster than anything + // eslint-disable-next-line no-implicit-coercion + return applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' ')); + }; + + // We alter the prototype because we must return a function, but there is + // no way to create a function with a different prototype + Object.setPrototypeOf(builder, proto); + + builder._generator = self; + builder._styler = _styler; + builder._isEmpty = _isEmpty; + + return builder; +}; + +const applyStyle = (self, string) => { + if (self.level <= 0 || !string) { + return self._isEmpty ? '' : string; + } + + let styler = self._styler; + + if (styler === undefined) { + return string; + } + + const {openAll, closeAll} = styler; + if (string.indexOf('\u001B') !== -1) { + while (styler !== undefined) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + string = stringReplaceAll(string, styler.close, styler.open); + + styler = styler.parent; + } + } + + // We can move both next actions out of loop, because remaining actions in loop won't have + // any/visible effect on parts we add here. Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92 + const lfIndex = string.indexOf('\n'); + if (lfIndex !== -1) { + string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); + } + + return openAll + string + closeAll; +}; + +let template; +const chalkTag = (chalk, ...strings) => { + const [firstString] = strings; + + if (!isArray(firstString) || !isArray(firstString.raw)) { + // If chalk() was called by itself or with a string, + // return the string itself as a string. + return strings.join(' '); + } + + const arguments_ = strings.slice(1); + const parts = [firstString.raw[0]]; + + for (let i = 1; i < firstString.length; i++) { + parts.push( + String(arguments_[i - 1]).replace(/[{}\\]/g, '\\$&'), + String(firstString.raw[i]) + ); + } + + if (template === undefined) { + template = __nccwpck_require__(500); + } + + return template(chalk, parts.join('')); +}; + +Object.defineProperties(Chalk.prototype, styles); + +const chalk = Chalk(); // eslint-disable-line new-cap +chalk.supportsColor = stdoutColor; +chalk.stderr = Chalk({level: stderrColor ? stderrColor.level : 0}); // eslint-disable-line new-cap +chalk.stderr.supportsColor = stderrColor; + +module.exports = chalk; + + +/***/ }), + +/***/ 500: +/***/ ((module) => { + +"use strict"; + +const TEMPLATE_REGEX = /(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi; +const STYLE_REGEX = /(?:^|\.)(\w+)(?:\(([^)]*)\))?/g; +const STRING_REGEX = /^(['"])((?:\\.|(?!\1)[^\\])*)\1$/; +const ESCAPE_REGEX = /\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi; + +const ESCAPES = new Map([ + ['n', '\n'], + ['r', '\r'], + ['t', '\t'], + ['b', '\b'], + ['f', '\f'], + ['v', '\v'], + ['0', '\0'], + ['\\', '\\'], + ['e', '\u001B'], + ['a', '\u0007'] +]); + +function unescape(c) { + const u = c[0] === 'u'; + const bracket = c[1] === '{'; + + if ((u && !bracket && c.length === 5) || (c[0] === 'x' && c.length === 3)) { + return String.fromCharCode(parseInt(c.slice(1), 16)); + } + + if (u && bracket) { + return String.fromCodePoint(parseInt(c.slice(2, -1), 16)); + } + + return ESCAPES.get(c) || c; +} + +function parseArguments(name, arguments_) { + const results = []; + const chunks = arguments_.trim().split(/\s*,\s*/g); + let matches; -/***/ 5549: -/***/ ((module) => { + for (const chunk of chunks) { + const number = Number(chunk); + if (!Number.isNaN(number)) { + results.push(number); + } else if ((matches = chunk.match(STRING_REGEX))) { + results.push(matches[2].replace(ESCAPE_REGEX, (m, escape, character) => escape ? unescape(escape) : character)); + } else { + throw new Error(`Invalid Chalk template style argument: ${chunk} (in style '${name}')`); + } + } -module.exports = addHook + return results; +} -function addHook (state, kind, name, hook) { - var orig = hook - if (!state.registry[name]) { - state.registry[name] = [] - } +function parseStyle(style) { + STYLE_REGEX.lastIndex = 0; - if (kind === 'before') { - hook = function (method, options) { - return Promise.resolve() - .then(orig.bind(null, options)) - .then(method.bind(null, options)) - } - } + const results = []; + let matches; - if (kind === 'after') { - hook = function (method, options) { - var result - return Promise.resolve() - .then(method.bind(null, options)) - .then(function (result_) { - result = result_ - return orig(result, options) - }) - .then(function () { - return result - }) - } - } + while ((matches = STYLE_REGEX.exec(style)) !== null) { + const name = matches[1]; - if (kind === 'error') { - hook = function (method, options) { - return Promise.resolve() - .then(method.bind(null, options)) - .catch(function (error) { - return orig(error, options) - }) - } - } + if (matches[2]) { + const args = parseArguments(name, matches[2]); + results.push([name].concat(args)); + } else { + results.push([name]); + } + } - state.registry[name].push({ - hook: hook, - orig: orig - }) + return results; } +function buildStyle(chalk, styles) { + const enabled = {}; -/***/ }), + for (const layer of styles) { + for (const style of layer.styles) { + enabled[style[0]] = layer.inverse ? null : style.slice(1); + } + } -/***/ 4670: -/***/ ((module) => { + let current = chalk; + for (const [styleName, styles] of Object.entries(enabled)) { + if (!Array.isArray(styles)) { + continue; + } -module.exports = register + if (!(styleName in current)) { + throw new Error(`Unknown Chalk style: ${styleName}`); + } -function register (state, name, method, options) { - if (typeof method !== 'function') { - throw new Error('method for before hook must be a function') - } + current = styles.length > 0 ? current[styleName](...styles) : current[styleName]; + } - if (!options) { - options = {} - } + return current; +} + +module.exports = (chalk, temporary) => { + const styles = []; + const chunks = []; + let chunk = []; + + // eslint-disable-next-line max-params + temporary.replace(TEMPLATE_REGEX, (m, escapeCharacter, inverse, style, close, character) => { + if (escapeCharacter) { + chunk.push(unescape(escapeCharacter)); + } else if (style) { + const string = chunk.join(''); + chunk = []; + chunks.push(styles.length === 0 ? string : buildStyle(chalk, styles)(string)); + styles.push({inverse, styles: parseStyle(style)}); + } else if (close) { + if (styles.length === 0) { + throw new Error('Found extraneous } in Chalk template literal'); + } - if (Array.isArray(name)) { - return name.reverse().reduce(function (callback, name) { - return register.bind(null, state, name, callback, options) - }, method)() - } + chunks.push(buildStyle(chalk, styles)(chunk.join(''))); + chunk = []; + styles.pop(); + } else { + chunk.push(character); + } + }); - return Promise.resolve() - .then(function () { - if (!state.registry[name]) { - return method(options) - } + chunks.push(chunk.join('')); - return (state.registry[name]).reduce(function (method, registered) { - return registered.hook.bind(null, method, options) - }, method)() - }) -} + if (styles.length > 0) { + const errMessage = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`; + throw new Error(errMessage); + } + + return chunks.join(''); +}; /***/ }), -/***/ 6819: +/***/ 2415: /***/ ((module) => { -module.exports = removeHook +"use strict"; -function removeHook (state, name, method) { - if (!state.registry[name]) { - return - } - var index = state.registry[name] - .map(function (registered) { return registered.orig }) - .indexOf(method) +const stringReplaceAll = (string, substring, replacer) => { + let index = string.indexOf(substring); + if (index === -1) { + return string; + } - if (index === -1) { - return - } + const substringLength = substring.length; + let endIndex = 0; + let returnValue = ''; + do { + returnValue += string.substr(endIndex, index - endIndex) + substring + replacer; + endIndex = index + substringLength; + index = string.indexOf(substring, endIndex); + } while (index !== -1); + + returnValue += string.substr(endIndex); + return returnValue; +}; - state.registry[name].splice(index, 1) -} +const stringEncaseCRLFWithFirstIndex = (string, prefix, postfix, index) => { + let endIndex = 0; + let returnValue = ''; + do { + const gotCR = string[index - 1] === '\r'; + returnValue += string.substr(endIndex, (gotCR ? index - 1 : index) - endIndex) + prefix + (gotCR ? '\r\n' : '\n') + postfix; + endIndex = index + 1; + index = string.indexOf('\n', endIndex); + } while (index !== -1); + + returnValue += string.substr(endIndex); + return returnValue; +}; + +module.exports = { + stringReplaceAll, + stringEncaseCRLFWithFirstIndex +}; /***/ }), @@ -7132,6 +9798,295 @@ function onceStrict (fn) { } +/***/ }), + +/***/ 8824: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const supportsColor = __nccwpck_require__(1507); +const hasFlag = __nccwpck_require__(7362); + +function parseVersion(versionString) { + if (/^\d{3,4}$/.test(versionString)) { + // Env var doesn't always use dots. example: 4601 => 46.1.0 + const m = /(\d{1,2})(\d{2})/.exec(versionString); + return { + major: 0, + minor: parseInt(m[1], 10), + patch: parseInt(m[2], 10) + }; + } + + const versions = (versionString || '').split('.').map(n => parseInt(n, 10)); + return { + major: versions[0], + minor: versions[1], + patch: versions[2] + }; +} + +function supportsHyperlink(stream) { + const {env} = process; + + if ('FORCE_HYPERLINK' in env) { + return !(env.FORCE_HYPERLINK.length > 0 && parseInt(env.FORCE_HYPERLINK, 10) === 0); + } + + if (hasFlag('no-hyperlink') || hasFlag('no-hyperlinks') || hasFlag('hyperlink=false') || hasFlag('hyperlink=never')) { + return false; + } + + if (hasFlag('hyperlink=true') || hasFlag('hyperlink=always')) { + return true; + } + + // If they specify no colors, they probably don't want hyperlinks. + if (!supportsColor.supportsColor(stream)) { + return false; + } + + if (stream && !stream.isTTY) { + return false; + } + + if (process.platform === 'win32') { + return false; + } + + if ('CI' in env) { + return false; + } + + if ('TEAMCITY_VERSION' in env) { + return false; + } + + if ('TERM_PROGRAM' in env) { + const version = parseVersion(env.TERM_PROGRAM_VERSION); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + if (version.major === 3) { + return version.minor >= 1; + } + + return version.major > 3; + // No default + } + } + + if ('VTE_VERSION' in env) { + // 0.50.0 was supposed to support hyperlinks, but throws a segfault + if (env.VTE_VERSION === '0.50.0') { + return false; + } + + const version = parseVersion(env.VTE_VERSION); + return version.major > 0 || version.minor >= 50; + } + + return false; +} + +module.exports = { + supportsHyperlink, + stdout: supportsHyperlink(process.stdout), + stderr: supportsHyperlink(process.stderr) +}; + + +/***/ }), + +/***/ 7362: +/***/ ((module) => { + +"use strict"; + + +module.exports = (flag, argv = process.argv) => { + const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--'); + const position = argv.indexOf(prefix + flag); + const terminatorPosition = argv.indexOf('--'); + return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); +}; + + +/***/ }), + +/***/ 1507: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const os = __nccwpck_require__(2087); +const tty = __nccwpck_require__(3867); +const hasFlag = __nccwpck_require__(7362); + +const {env} = process; + +let forceColor; +if (hasFlag('no-color') || + hasFlag('no-colors') || + hasFlag('color=false') || + hasFlag('color=never')) { + forceColor = 0; +} else if (hasFlag('color') || + hasFlag('colors') || + hasFlag('color=true') || + hasFlag('color=always')) { + forceColor = 1; +} + +if ('FORCE_COLOR' in env) { + if (env.FORCE_COLOR === 'true') { + forceColor = 1; + } else if (env.FORCE_COLOR === 'false') { + forceColor = 0; + } else { + forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3); + } +} + +function translateLevel(level) { + if (level === 0) { + return false; + } + + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3 + }; +} + +function supportsColor(haveStream, streamIsTTY) { + if (forceColor === 0) { + return 0; + } + + if (hasFlag('color=16m') || + hasFlag('color=full') || + hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + + if (haveStream && !streamIsTTY && forceColor === undefined) { + return 0; + } + + const min = forceColor || 0; + + if (env.TERM === 'dumb') { + return min; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 colors. + // Windows 10 build 14931 is the first release that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(osRelease[0]) >= 10 && + Number(osRelease[2]) >= 10586 + ) { + return Number(osRelease[2]) >= 14931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI', 'GITHUB_ACTIONS', 'BUILDKITE'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + return version >= 3 ? 3 : 2; + case 'Apple_Terminal': + return 2; + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + return min; +} + +function getSupportLevel(stream) { + const level = supportsColor(stream, stream && stream.isTTY); + return translateLevel(level); +} + +module.exports = { + supportsColor: getSupportLevel, + stdout: translateLevel(supportsColor(true, tty.isatty(1))), + stderr: translateLevel(supportsColor(true, tty.isatty(2))) +}; + + +/***/ }), + +/***/ 1898: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +const ansiEscapes = __nccwpck_require__(8512); +const supportsHyperlinks = __nccwpck_require__(8824); + +const terminalLink = (text, url, {target = 'stdout', ...options} = {}) => { + if (!supportsHyperlinks[target]) { + // If the fallback has been explicitly disabled, don't modify the text itself. + if (options.fallback === false) { + return text; + } + + return typeof options.fallback === 'function' ? options.fallback(text, url) : `${text} (\u200B${url}\u200B)`; + } + + return ansiEscapes.link(text, url); +}; + +module.exports = (text, url, options = {}) => terminalLink(text, url, options); + +module.exports.stderr = (text, url, options = {}) => terminalLink(text, url, {target: 'stderr', ...options}); + +module.exports.isSupported = supportsHyperlinks.stdout; +module.exports.stderr.isSupported = supportsHyperlinks.stderr; + + /***/ }), /***/ 4294: @@ -7568,6 +10523,14 @@ module.exports = require("tls");; /***/ }), +/***/ 3867: +/***/ ((module) => { + +"use strict"; +module.exports = require("tty");; + +/***/ }), + /***/ 8835: /***/ ((module) => { @@ -7605,8 +10568,8 @@ module.exports = require("zlib");; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed +/******/ id: moduleId, +/******/ loaded: false, /******/ exports: {} /******/ }; /******/ @@ -7619,11 +10582,23 @@ module.exports = require("zlib");; /******/ if(threw) delete __webpack_module_cache__[moduleId]; /******/ } /******/ +/******/ // Flag the module as loaded +/******/ module.loaded = true; +/******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ +/******/ /* webpack/runtime/node module decorator */ +/******/ (() => { +/******/ __nccwpck_require__.nmd = (module) => { +/******/ module.paths = []; +/******/ if (!module.children) module.children = []; +/******/ return module; +/******/ }; +/******/ })(); +/******/ /******/ /* webpack/runtime/compat */ /******/ /******/ __nccwpck_require__.ab = __dirname + "/";/************************************************************************/ diff --git a/jest.config.js b/jest.config.js index a27374193..6eaa59da9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,5 +7,6 @@ module.exports = { transform: { '^.+\\.ts$': 'ts-jest' }, - verbose: true + verbose: true, + setupFilesAfterEnv: [`./jest/test.ts`] }; diff --git a/jest/test.ts b/jest/test.ts new file mode 100644 index 000000000..f8eaa1bdf --- /dev/null +++ b/jest/test.ts @@ -0,0 +1,11 @@ +import chalk from 'chalk'; + +// Disabled the colors to: +// - improve the performances +// - avoid to mock chalk +// - avoid to have failing tests when testing the logs due to the extra text the log message will contains +// +// Note: +// If you need to debug the log colours you can remove this line temporarily +// But some tests will fail +chalk.level = 0; diff --git a/package-lock.json b/package-lock.json index f55af56f7..5cc0047ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,6 +206,19 @@ "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } } }, "@babel/parser": { @@ -1560,11 +1573,6 @@ "universal-user-agent": "^6.0.0" } }, - "@octokit/openapi-types": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-5.3.0.tgz", - "integrity": "sha512-5q2qBz4iZ0xS/DEJ0ROusFbN4cVlbJE9GvOByen+mv7artuGXfVhONqcuRd7jYN2glTmCnzcZw+X6LrjRVqs0A==" - }, "@octokit/plugin-paginate-rest": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.4.tgz", @@ -1573,11 +1581,6 @@ "@octokit/types": "^5.0.0" } }, - "@octokit/plugin-request-log": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz", - "integrity": "sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ==" - }, "@octokit/plugin-rest-endpoint-methods": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.1.2.tgz", @@ -1612,95 +1615,6 @@ "once": "^1.4.0" } }, - "@octokit/rest": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.3.3.tgz", - "integrity": "sha512-OxElwBanZn1AShCaIrRTLM9PwhGE5/busMke/go30OWAQ+eJMD7Us/67mtapE77EYY4FM2tvb4Eg25rZaA/NPA==", - "requires": { - "@octokit/core": "^3.2.3", - "@octokit/plugin-paginate-rest": "^2.6.2", - "@octokit/plugin-request-log": "^1.0.2", - "@octokit/plugin-rest-endpoint-methods": "4.13.3" - }, - "dependencies": { - "@octokit/auth-token": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", - "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", - "requires": { - "@octokit/types": "^6.0.3" - } - }, - "@octokit/core": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.5.tgz", - "integrity": "sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg==", - "requires": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.4.12", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.1.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/graphql": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.0.tgz", - "integrity": "sha512-CJ6n7izLFXLvPZaWzCQDjU/RP+vHiZmWdOunaCS87v+2jxMsW9FB5ktfIxybRBxZjxuJGRnxk7xJecWTVxFUYQ==", - "requires": { - "@octokit/request": "^5.3.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/plugin-paginate-rest": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.11.0.tgz", - "integrity": "sha512-7L9xQank2G3r1dGqrVPo1z62V5utbykOUzlmNHPz87Pww/JpZQ9KyG5CHtUzgmB4n5iDRKYNK/86A8D98HP0yA==", - "requires": { - "@octokit/types": "^6.11.0" - } - }, - "@octokit/plugin-rest-endpoint-methods": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.13.3.tgz", - "integrity": "sha512-nMGS2osFcWXRfHkDR0d+lB1zpMPTZJ0NjysPUfs7BT5/juNG/Q0+5UB6nC1f62jPzun154qekzwOb7Q5oahCXQ==", - "requires": { - "@octokit/types": "^6.12.0", - "deprecation": "^2.3.1" - } - }, - "@octokit/request": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.14.tgz", - "integrity": "sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA==", - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.0.0", - "@octokit/types": "^6.7.1", - "deprecation": "^2.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.1", - "once": "^1.4.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.12.0.tgz", - "integrity": "sha512-KwOf16soD7aDEEi/PgNeJlHzjZPfrmmNy+7WezSdrpnqZ7YImBJcNnX9+5RUHt1MnA4h8oISRHTqaZDGsX9DRQ==", - "requires": { - "@octokit/openapi-types": "^5.3.0" - } - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - } - } - }, "@octokit/types": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.1.2.tgz", @@ -1839,9 +1753,9 @@ } }, "@types/node": { - "version": "14.14.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz", - "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==" + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -1856,9 +1770,9 @@ "dev": true }, "@types/semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-iotVxtCCsPLRAvxMFFgxL8HD2l4mAZ2Oin7/VJ2ooWO0VOK4EGOGmZWZn1uCq7RofR3I/1IOSjCHlFT71eVK0Q==", "dev": true }, "@types/stack-utils": { @@ -1962,55 +1876,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz", - "integrity": "sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz", + "integrity": "sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.15.1", - "@typescript-eslint/types": "4.15.1", - "@typescript-eslint/typescript-estree": "4.15.1", + "@typescript-eslint/scope-manager": "4.22.0", + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/typescript-estree": "4.22.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.16.1.tgz", - "integrity": "sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.1.tgz", + "integrity": "sha512-l+sUJFInWhuMxA6rtirzjooh8cM/AATAe3amvIkqKFeMzkn85V+eLzb1RyuXkHak4dLfYzOmF6DXPyflJvjQnw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.16.1", - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/typescript-estree": "4.16.1", + "@typescript-eslint/scope-manager": "4.22.1", + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/typescript-estree": "4.22.1", "debug": "^4.1.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz", - "integrity": "sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz", + "integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1" + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/visitor-keys": "4.22.1" } }, "@typescript-eslint/types": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.16.1.tgz", - "integrity": "sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz", + "integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz", - "integrity": "sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz", + "integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1", + "@typescript-eslint/types": "4.22.1", + "@typescript-eslint/visitor-keys": "4.22.1", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -2019,47 +1933,47 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz", - "integrity": "sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz", + "integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", + "@typescript-eslint/types": "4.22.1", "eslint-visitor-keys": "^2.0.0" } }, "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true } } }, "@typescript-eslint/scope-manager": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz", - "integrity": "sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz", + "integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==", "dev": true, "requires": { - "@typescript-eslint/types": "4.15.1", - "@typescript-eslint/visitor-keys": "4.15.1" + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/visitor-keys": "4.22.0" } }, "@typescript-eslint/types": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.1.tgz", - "integrity": "sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz", + "integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz", - "integrity": "sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz", + "integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.15.1", - "@typescript-eslint/visitor-keys": "4.15.1", + "@typescript-eslint/types": "4.22.0", + "@typescript-eslint/visitor-keys": "4.22.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -2068,12 +1982,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz", - "integrity": "sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz", + "integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.15.1", + "@typescript-eslint/types": "4.22.0", "eslint-visitor-keys": "^2.0.0" }, "dependencies": { @@ -2643,14 +2557,54 @@ "dev": true }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "char-regex": { @@ -3367,12 +3321,6 @@ "type-check": "~0.4.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3607,9 +3555,9 @@ } }, "eslint-plugin-jest": { - "version": "24.1.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.5.tgz", - "integrity": "sha512-FIP3lwC8EzEG+rOs1y96cOJmMVpdFNreoDJv29B5vIupVssRi8zrSY3QadogT0K3h1Y8TMxJ6ZSAzYUmFCp2hg==", + "version": "24.3.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.6.tgz", + "integrity": "sha512-WOVH4TIaBLIeCX576rLcOgjNXqP+jNlCiEmRgFTfQtJ52DpwnIQKAVGlGPAN7CZ33bW6eNfHD6s8ZbEUTQubJg==", "dev": true, "requires": { "@typescript-eslint/experimental-utils": "^4.0.1" @@ -4370,9 +4318,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -7843,9 +7791,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.deburr": { @@ -8835,9 +8783,9 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "requires": { "lru-cache": "^6.0.0" } @@ -9422,12 +9370,6 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true } } }, @@ -9652,9 +9594,9 @@ } }, "typescript": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true }, "unbox-primitive": { @@ -9971,9 +9913,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yallist": { diff --git a/package.json b/package.json index 445b7615d..a14e0a4bc 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "pack": "ncc build", "test": "jest", "test:only-errors": "jest --reporters jest-silent-reporter --silent", - "test:watch": "jest --watch --notify --expandf", + "test:watch": "jest --watch --notify --expand", "all": "npm run build && npm run format && npm run lint && npm run pack && npm test" }, "repository": { @@ -32,27 +32,28 @@ "dependencies": { "@actions/core": "^1.2.6", "@actions/github": "^4.0.0", - "@octokit/rest": "^18.3.3", "lodash.deburr": "^4.1.0", - "semver": "^7.3.4" + "semver": "^7.3.5" }, "devDependencies": { "@types/jest": "^26.0.20", "@types/lodash.deburr": "^4.1.6", - "@types/node": "^14.14.31", - "@types/semver": "^7.3.4", + "@types/node": "^15.0.2", + "@types/semver": "^7.3.5", "@typescript-eslint/eslint-plugin": "^4.16.1", - "@typescript-eslint/parser": "^4.16.1", + "@typescript-eslint/parser": "^4.22.1", "@vercel/ncc": "^0.27.0", + "chalk": "^4.1.0", "eslint": "^7.21.0", "eslint-plugin-github": "^4.1.2", - "eslint-plugin-jest": "^24.1.5", + "eslint-plugin-jest": "^24.3.6", "jest": "^26.6.3", "jest-circus": "^26.6.3", "jest-silent-reporter": "^0.4.0", "js-yaml": "^4.0.0", "prettier": "^2.2.1", + "terminal-link": "^2.1.1", "ts-jest": "^26.5.3", - "typescript": "^4.2.3" + "typescript": "^4.2.4" } } diff --git a/src/classes/assignees.spec.ts b/src/classes/assignees.spec.ts index 26b689d15..d40bacc15 100644 --- a/src/classes/assignees.spec.ts +++ b/src/classes/assignees.spec.ts @@ -2,8 +2,8 @@ import {DefaultProcessorOptions} from '../../__tests__/constants/default-process import {generateIIssue} from '../../__tests__/functions/generate-iissue'; import {IIssue} from '../interfaces/issue'; import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options'; -import {Issue} from './issue'; import {Assignees} from './assignees'; +import {Issue} from './issue'; describe('Assignees', (): void => { let assignees: Assignees; @@ -12,7 +12,10 @@ describe('Assignees', (): void => { let issueInterface: IIssue; beforeEach((): void => { - optionsInterface = {...DefaultProcessorOptions}; + optionsInterface = { + ...DefaultProcessorOptions, + exemptAllAssignees: false + }; issueInterface = generateIIssue(); }); @@ -303,6 +306,132 @@ describe('Assignees', (): void => { }); }); }); + + describe('when the given options are configured to exempt all assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllAssignees = true; + }); + + describe('when the given issue does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-assignee' + } + ]; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(true); + }); + }); + + describe('when the given options are not configured to exempt all issue assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllIssueAssignees = false; + }); + + describe('when the given issue does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-assignee' + } + ]; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + }); + + describe('when the given options are configured to exempt all issue assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllIssueAssignees = true; + }); + + describe('when the given issue does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given issue does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-issue-assignee' + } + ]; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(true); + }); + }); + }); + }); }); describe('when the given issue is a pull request', (): void => { @@ -320,7 +449,7 @@ describe('Assignees', (): void => { optionsInterface.exemptPrAssignees = ''; }); - describe('when the given issue does not have an assignee', (): void => { + describe('when the given pull request does not have an assignee', (): void => { beforeEach((): void => { issueInterface.assignees = []; }); @@ -336,7 +465,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee', (): void => { + describe('when the given pull request does have an assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -362,7 +491,7 @@ describe('Assignees', (): void => { optionsInterface.exemptPrAssignees = 'dummy-exempt-pr-assignee'; }); - describe('when the given issue does not have an assignee', (): void => { + describe('when the given pull request does not have an assignee', (): void => { beforeEach((): void => { issueInterface.assignees = []; }); @@ -378,7 +507,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee different than the exempt pull request assignee', (): void => { + describe('when the given pull request does have an assignee different than the exempt pull request assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -398,7 +527,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee equaling the exempt pull request assignee', (): void => { + describe('when the given pull request does have an assignee equaling the exempt pull request assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -430,7 +559,7 @@ describe('Assignees', (): void => { optionsInterface.exemptPrAssignees = ''; }); - describe('when the given issue does not have an assignee', (): void => { + describe('when the given pull request does not have an assignee', (): void => { beforeEach((): void => { issueInterface.assignees = []; }); @@ -446,7 +575,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee different than the exempt assignee', (): void => { + describe('when the given pull request does have an assignee different than the exempt assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -466,7 +595,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee equaling the exempt assignee', (): void => { + describe('when the given pull request does have an assignee equaling the exempt assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -492,7 +621,7 @@ describe('Assignees', (): void => { optionsInterface.exemptPrAssignees = 'dummy-exempt-pr-assignee'; }); - describe('when the given issue does not have an assignee', (): void => { + describe('when the given pull request does not have an assignee', (): void => { beforeEach((): void => { issueInterface.assignees = []; }); @@ -508,7 +637,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee different than the exempt pull request assignee', (): void => { + describe('when the given pull request does have an assignee different than the exempt pull request assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -528,7 +657,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee equaling the exempt pull request assignee', (): void => { + describe('when the given pull request does have an assignee equaling the exempt pull request assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -548,7 +677,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee different than the exempt assignee', (): void => { + describe('when the given pull request does have an assignee different than the exempt assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -568,7 +697,7 @@ describe('Assignees', (): void => { }); }); - describe('when the given issue does have an assignee equaling the exempt assignee', (): void => { + describe('when the given pull request does have an assignee equaling the exempt assignee', (): void => { beforeEach((): void => { issueInterface.assignees = [ { @@ -589,6 +718,132 @@ describe('Assignees', (): void => { }); }); }); + + describe('when the given options are configured to exempt all assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllAssignees = true; + }); + + describe('when the given pull request does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given pull request does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-assignee' + } + ]; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(true); + }); + }); + + describe('when the given options are not configured to exempt all pull request assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllPrAssignees = false; + }); + + describe('when the given pull request does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given pull request does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-assignee' + } + ]; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + }); + + describe('when the given options are configured to exempt all pull request assignees', (): void => { + beforeEach((): void => { + optionsInterface.exemptAllPrAssignees = true; + }); + + describe('when the given pull request does not have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = []; + }); + + it('should return false', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(false); + }); + }); + + describe('when the given pull request does have an assignee', (): void => { + beforeEach((): void => { + issueInterface.assignees = [ + { + login: 'dummy-exempt-issue-assignee' + } + ]; + }); + + it('should return true', (): void => { + expect.assertions(1); + issue = new Issue(optionsInterface, issueInterface); + assignees = new Assignees(optionsInterface, issue); + + const result = assignees.shouldExemptAssignees(); + + expect(result).toStrictEqual(true); + }); + }); + }); + }); }); }); }); diff --git a/src/classes/assignees.ts b/src/classes/assignees.ts index af844dcdd..9a0a2a259 100644 --- a/src/classes/assignees.ts +++ b/src/classes/assignees.ts @@ -1,4 +1,6 @@ +import chalk from 'chalk'; import deburr from 'lodash.deburr'; +import {Option} from '../enums/option'; import {wordsToList} from '../functions/words-to-list'; import {IAssignee} from '../interfaces/assignee'; import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options'; @@ -32,7 +34,8 @@ export class Assignees { if (this._shouldExemptAllAssignees()) { this._issueLogger.info( - 'Skipping $$type because it has an exempt assignee' + chalk.white('└──'), + 'Skipping this $$type because it has an exempt assignee' ); return true; @@ -42,7 +45,8 @@ export class Assignees { if (exemptAssignees.length === 0) { this._issueLogger.info( - `No option was specified to skip the stale process for this $$type` + chalk.white('├──'), + `No assignee option was specified to skip the stale process for this $$type` ); this._logSkip(); @@ -50,9 +54,10 @@ export class Assignees { } this._issueLogger.info( - `Found ${exemptAssignees.length} assignee${ + chalk.white('├──'), + `Found ${chalk.cyan(exemptAssignees.length)} assignee${ exemptAssignees.length > 1 ? 's' : '' - } on this $$type` + } that can exempt stale on this $$type` ); const hasExemptAssignee: boolean = exemptAssignees.some( @@ -62,11 +67,13 @@ export class Assignees { if (!hasExemptAssignee) { this._issueLogger.info( + chalk.white('├──'), 'No assignee on this $$type can exempt the stale process' ); this._logSkip(); } else { this._issueLogger.info( + chalk.white('└──'), 'Skipping this $$type because it has an exempt assignee' ); } @@ -83,12 +90,18 @@ export class Assignees { private _getExemptIssueAssignees(): string[] { if (this._options.exemptIssueAssignees === '') { this._issueLogger.info( - 'The option "exemptIssueAssignees" is disabled. No specific assignee can skip the stale process for this $$type' + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptIssueAssignees + )} is disabled. No specific assignee can skip the stale process for this $$type` ); if (this._options.exemptAssignees === '') { this._issueLogger.info( - 'The option "exemptAssignees" is disabled. No specific assignee can skip the stale process for this $$type' + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAssignees + )} is disabled. No specific assignee can skip the stale process for this $$type` ); return []; @@ -99,9 +112,10 @@ export class Assignees { ); this._issueLogger.info( - `The option "exemptAssignees" is set. ${ - exemptAssignees.length - } assignee${ + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAssignees + )} is set. ${chalk.cyan(exemptAssignees.length)} assignee${ exemptAssignees.length === 1 ? '' : 's' } can skip the stale process for this $$type` ); @@ -114,9 +128,10 @@ export class Assignees { ); this._issueLogger.info( - `The option "exemptIssueAssignees" is set. ${ - exemptAssignees.length - } assignee${ + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptIssueAssignees + )} is set. ${chalk.cyan(exemptAssignees.length)} assignee${ exemptAssignees.length === 1 ? '' : 's' } can skip the stale process for this $$type` ); @@ -127,12 +142,18 @@ export class Assignees { private _getExemptPullRequestAssignees(): string[] { if (this._options.exemptPrAssignees === '') { this._issueLogger.info( - 'The option "exemptPrAssignees" is disabled. No specific assignee can skip the stale process for this $$type' + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptPrAssignees + )} is disabled. No specific assignee can skip the stale process for this $$type` ); if (this._options.exemptAssignees === '') { this._issueLogger.info( - 'The option "exemptAssignees" is disabled. No specific assignee can skip the stale process for this $$type' + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAssignees + )} is disabled. No specific assignee can skip the stale process for this $$type` ); return []; @@ -143,9 +164,10 @@ export class Assignees { ); this._issueLogger.info( - `The option "exemptAssignees" is set. ${ - exemptAssignees.length - } assignee${ + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAssignees + )} is set. ${chalk.cyan(exemptAssignees.length)} assignee${ exemptAssignees.length === 1 ? '' : 's' } can skip the stale process for this $$type` ); @@ -158,9 +180,10 @@ export class Assignees { ); this._issueLogger.info( - `The option "exemptPrAssignees" is set. ${ - exemptAssignees.length - } assignee${ + chalk.white('├──'), + `The option ${this._issueLogger.createOptionLink( + Option.ExemptPrAssignees + )} is set. ${chalk.cyan(exemptAssignees.length)} assignee${ exemptAssignees.length === 1 ? '' : 's' } can skip the stale process for this $$type` ); @@ -178,6 +201,7 @@ export class Assignees { if (isSameAssignee) { this._issueLogger.info( + chalk.white('├──'), `@${issueAssignee.login} is assigned on this $$type and is an exempt assignee` ); } @@ -196,13 +220,17 @@ export class Assignees { private _shouldExemptAllIssueAssignees(): boolean { if (this._options.exemptAllIssueAssignees === true) { this._issueLogger.info( - 'The option "exemptAllIssueAssignees" is enabled. Any assignee on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllIssueAssignees + )} is enabled. Any assignee on this $$type will skip the stale process` ); return true; } else if (this._options.exemptAllIssueAssignees === false) { this._issueLogger.info( - 'The option "exemptAllIssueAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllIssueAssignees + )} is disabled. Only some specific assignees on this $$type will skip the stale process` ); return false; @@ -216,13 +244,17 @@ export class Assignees { private _shouldExemptAllPullRequestAssignees(): boolean { if (this._options.exemptAllPrAssignees === true) { this._issueLogger.info( - 'The option "exemptAllPrAssignees" is enabled. Any assignee on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllPrAssignees + )} is enabled. Any assignee on this $$type will skip the stale process` ); return true; } else if (this._options.exemptAllPrAssignees === false) { this._issueLogger.info( - 'The option "exemptAllPrAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllPrAssignees + )} is disabled. Only some specific assignees on this $$type will skip the stale process` ); return false; @@ -236,16 +268,20 @@ export class Assignees { private _logExemptAllAssigneesOption(): void { if (this._options.exemptAllAssignees) { this._issueLogger.info( - 'The option "exemptAllAssignees" is enabled. Any assignee on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllAssignees + )} is enabled. Any assignee on this $$type will skip the stale process` ); } else { this._issueLogger.info( - 'The option "exemptAllAssignees" is disabled. Only some specific assignees on this $$type will skip the stale process' + `The option ${this._issueLogger.createOptionLink( + Option.ExemptAllAssignees + )} is disabled. Only some specific assignees on this $$type will skip the stale process` ); } } private _logSkip(): void { - this._issueLogger.info('Skip the assignees checks'); + this._issueLogger.info(chalk.white('└──'), 'Skip the assignees checks'); } } diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index 8e7afff67..139edf66a 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -31,8 +31,12 @@ describe('Issue', (): void => { onlyIssueLabels: '', onlyPrLabels: '', anyOfLabels: '', + anyOfIssueLabels: '', + anyOfPrLabels: '', operationsPerRun: 0, removeStaleWhenUpdated: false, + removeIssueStaleWhenUpdated: undefined, + removePrStaleWhenUpdated: undefined, repoToken: '', skipStaleIssueMessage: false, skipStalePrMessage: false, diff --git a/src/classes/issue.ts b/src/classes/issue.ts index d48add6b8..932bdbc17 100644 --- a/src/classes/issue.ts +++ b/src/classes/issue.ts @@ -6,6 +6,7 @@ import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options'; import {ILabel} from '../interfaces/label'; import {IMilestone} from '../interfaces/milestone'; import {IsoDateString} from '../types/iso-date-string'; +import {Operations} from './operations'; export class Issue implements IIssue { private readonly _options: IIssuesProcessorOptions; @@ -20,6 +21,19 @@ export class Issue implements IIssue { readonly milestone: IMilestone | undefined; readonly assignees: IAssignee[]; isStale: boolean; + operations = new Operations(); + + get isPullRequest(): boolean { + return isPullRequest(this); + } + + get staleLabel(): string { + return this._getStaleLabel(); + } + + get hasAssignees(): boolean { + return this.assignees.length > 0; + } constructor( options: Readonly, @@ -40,18 +54,6 @@ export class Issue implements IIssue { this.isStale = isLabeled(this, this.staleLabel); } - get isPullRequest(): boolean { - return isPullRequest(this); - } - - get staleLabel(): string { - return this._getStaleLabel(); - } - - get hasAssignees(): boolean { - return this.assignees.length > 0; - } - private _getStaleLabel(): string { return this.isPullRequest ? this._options.stalePrLabel diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 70a704a1c..fac5778fd 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -1,11 +1,14 @@ +import * as core from '@actions/core'; import {context, getOctokit} from '@actions/github'; import {GitHub} from '@actions/github/lib/utils'; import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; +import chalk from 'chalk'; +import {Option} from '../enums/option'; import {getHumanizedDate} from '../functions/dates/get-humanized-date'; import {isDateMoreRecentThan} from '../functions/dates/is-date-more-recent-than'; import {isValidDate} from '../functions/dates/is-valid-date'; +import {isBoolean} from '../functions/is-boolean'; import {isLabeled} from '../functions/is-labeled'; -import {isPullRequest} from '../functions/is-pull-request'; import {shouldMarkWhenStale} from '../functions/should-mark-when-stale'; import {wordsToList} from '../functions/words-to-list'; import {IComment} from '../interfaces/comment'; @@ -18,6 +21,7 @@ import {Issue} from './issue'; import {IssueLogger} from './loggers/issue-logger'; import {Logger} from './loggers/logger'; import {Milestones} from './milestones'; +import {StaleOperations} from './stale-operations'; import {Statistics} from './statistics'; /*** @@ -32,9 +36,32 @@ export class IssuesProcessor { return millisSinceLastUpdated <= daysInMillis; } + private static _endIssueProcessing(issue: Issue): void { + const consumedOperationsCount: number = issue.operations.getConsumedOperationsCount(); + + if (consumedOperationsCount > 0) { + const issueLogger: IssueLogger = new IssueLogger(issue); + + issueLogger.info( + chalk.cyan(consumedOperationsCount), + `operation${ + consumedOperationsCount > 1 ? 's' : '' + } consumed for this $$type` + ); + } + } + + private static _getStaleMessageUsedOptionName( + issue: Readonly + ): Option.StalePrMessage | Option.StaleIssueMessage { + return issue.isPullRequest + ? Option.StalePrMessage + : Option.StaleIssueMessage; + } + private readonly _logger: Logger = new Logger(); + private readonly _operations: StaleOperations; private readonly _statistics: Statistics | undefined; - private _operationsLeft = 0; readonly client: InstanceType; readonly options: IIssuesProcessorOptions; readonly staleIssues: Issue[] = []; @@ -44,38 +71,58 @@ export class IssuesProcessor { constructor(options: IIssuesProcessorOptions) { this.options = options; - this._operationsLeft = this.options.operationsPerRun; this.client = getOctokit(this.options.repoToken); + this._operations = new StaleOperations(this.options); + + this._logger.info(chalk.yellow('Starting the stale action process...')); if (this.options.debugOnly) { + this._logger.warning(chalk.yellowBright('Executing in debug mode!')); this._logger.warning( - 'Executing in debug mode. Debug output will be written but no issues will be processed.' + chalk.yellowBright( + 'The debug output will be written but no issues/PRs will be processed.' + ) ); } if (this.options.enableStatistics) { - this._statistics = new Statistics(this.options); + this._statistics = new Statistics(); } } - async processIssues(page = 1): Promise { + async processIssues(page: Readonly = 1): Promise { // get the next batch of issues const issues: Issue[] = await this.getIssues(page); const actor: string = await this.getActor(); if (issues.length <= 0) { - this._logger.info('---'); - this._statistics?.setOperationsLeft(this._operationsLeft).logStats(); - this._logger.info('No more issues found to process. Exiting.'); + this._logger.info( + chalk.green('No more issues found to process. Exiting...') + ); + this._statistics + ?.setRemainingOperations(this._operations.getRemainingOperationsCount()) + .logStats(); - return this._operationsLeft; + return this._operations.getRemainingOperationsCount(); + } else { + this._logger.info( + chalk.yellow( + `Processing the batch of issues ${chalk.cyan( + `#${page}` + )} containing ${chalk.cyan(issues.length)} issue${ + issues.length > 1 ? 's' : '' + }...` + ) + ); } for (const issue of issues.values()) { const issueLogger: IssueLogger = new IssueLogger(issue); - this._statistics?.incrementProcessedIssuesCount(); + this._statistics?.incrementProcessedItemsCount(issue); - issueLogger.info(`Found this $$type last updated ${issue.updated_at}`); + issueLogger.info( + `Found this $$type last updated at: ${chalk.cyan(issue.updated_at)}` + ); // calculate string based messages for this issue const staleMessage: string = issue.isPullRequest @@ -100,7 +147,11 @@ export class IssuesProcessor { if (onlyLabels.length > 0) { issueLogger.info( - `The option "onlyLabels" was specified to only processed the issues and pull requests with all those labels (${onlyLabels.length})` + `The option ${issueLogger.createOptionLink( + Option.OnlyLabels + )} was specified to only process issues and pull requests with all those labels (${chalk.cyan( + onlyLabels.length + )})` ); const hasAllWhitelistedLabels: boolean = onlyLabels.every( @@ -111,37 +162,64 @@ export class IssuesProcessor { if (!hasAllWhitelistedLabels) { issueLogger.info( + chalk.white('└──'), `Skipping this $$type because it doesn't have all the required labels` ); + + IssuesProcessor._endIssueProcessing(issue); 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. Continuing the process for this $$type` + `The option ${issueLogger.createOptionLink( + Option.OnlyLabels + )} was not specified` + ); + issueLogger.info( + chalk.white('└──'), + `Continuing the process for this $$type` ); } - issueLogger.info(`Days before $$type stale: ${daysBeforeStale}`); + issueLogger.info( + `Days before $$type stale: ${chalk.cyan(daysBeforeStale)}` + ); const shouldMarkAsStale: boolean = shouldMarkWhenStale(daysBeforeStale); if (!staleMessage && shouldMarkAsStale) { - issueLogger.info(`Skipping $$type due to empty stale message`); + issueLogger.info( + `Skipping this $$type because it should be marked as stale based on the option ${issueLogger.createOptionLink( + this._getDaysBeforeStaleUsedOptionName(issue) + )} (${chalk.cyan( + daysBeforeStale + )}) but the option ${issueLogger.createOptionLink( + IssuesProcessor._getStaleMessageUsedOptionName(issue) + )} is not set` + ); + IssuesProcessor._endIssueProcessing(issue); continue; } if (issue.state === 'closed') { - issueLogger.info(`Skipping $$type because it is closed`); - continue; // don't process closed issues + issueLogger.info(`Skipping this $$type because it is closed`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process closed issues } if (issue.locked) { - issueLogger.info(`Skipping $$type because it is locked`); - continue; // don't process locked issues + issueLogger.info(`Skipping this $$type because it is locked`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process locked issues } // Try to remove the close label when not close/locked issue or PR @@ -152,31 +230,35 @@ export class IssuesProcessor { const createdAt: Date = new Date(issue.created_at); issueLogger.info( - `A start date was specified for the ${getHumanizedDate(startDate)} (${ - this.options.startDate - })` + `A start date was specified for the ${getHumanizedDate( + startDate + )} (${chalk.cyan(this.options.startDate)})` ); // Expecting that GitHub will always set a creation date on the issues and PRs // But you never know! if (!isValidDate(createdAt)) { - throw new Error( - `Invalid issue field: "created_at". Expected a valid date` + IssuesProcessor._endIssueProcessing(issue); + core.setFailed( + new Error( + `Invalid issue field: "created_at". Expected a valid date` + ) ); } issueLogger.info( - `$$type created the ${getHumanizedDate(createdAt)} (${ + `$$type created the ${getHumanizedDate(createdAt)} (${chalk.cyan( issue.created_at - })` + )})` ); if (!isDateMoreRecentThan(createdAt, startDate)) { issueLogger.info( - `Skipping $$type because it was created before the specified start date` + `Skipping this $$type because it was created before the specified start date` ); - continue; // don't process issues which were created before the start date + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process issues which were created before the start date } } @@ -202,60 +284,117 @@ export class IssuesProcessor { await this._removeStaleLabel(issue, staleLabel); } - issueLogger.info(`Skipping $$type because it has an exempt label`); - continue; // don't process exempt issues + issueLogger.info(`Skipping this $$type because it has an exempt label`); + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt issues } - const anyOfLabels: string[] = wordsToList(this.options.anyOfLabels); - if ( - anyOfLabels.length && - !anyOfLabels.some((label: Readonly): boolean => - isLabeled(issue, label) - ) - ) { + const anyOfLabels: string[] = wordsToList(this._getAnyOfLabels(issue)); + + if (anyOfLabels.length > 0) { + issueLogger.info( + `The option ${issueLogger.createOptionLink( + Option.AnyOfLabels + )} was specified to only process the issues and pull requests with one of those labels (${chalk.cyan( + 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` + ); + IssuesProcessor._endIssueProcessing(issue); + 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 ${issueLogger.createOptionLink( + Option.AnyOfLabels + )} was not specified` + ); issueLogger.info( - `Skipping $$type because it does not have any of the required labels` + 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); if (milestones.shouldExemptMilestones()) { - issueLogger.info( - `Skipping $$type because it has an exempted milestone` - ); - continue; // don't process exempt milestones + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt milestones } const assignees: Assignees = new Assignees(this.options, issue); if (assignees.shouldExemptAssignees()) { - continue; // don't process exempt assignees + IssuesProcessor._endIssueProcessing(issue); + continue; // Don't process exempt assignees } - // should this issue be marked stale? + // Should this issue be marked stale? const shouldBeStale = !IssuesProcessor._updatedSince( issue.updated_at, daysBeforeStale ); - // determine if this issue needs to be marked stale first - if (!issue.isStale && shouldBeStale && shouldMarkAsStale) { - issueLogger.info( - `Marking $$type stale because it was last updated on ${issue.updated_at} and it does not have a stale label` - ); - await this._markStale(issue, staleMessage, staleLabel, skipMessage); - issue.isStale = true; // this issue is now considered stale - } else if (!issue.isStale) { - issueLogger.info( - `Not marking as stale: shouldBeStale=${shouldBeStale}, shouldMarkAsStale=${shouldMarkAsStale}` - ); + // Determine if this issue needs to be marked stale first + if (!issue.isStale) { + issueLogger.info(`This $$type is not stale`); + const updatedAtDate: Date = new Date(issue.updated_at); + + if (shouldBeStale) { + issueLogger.info( + `This $$type should be stale based on the last update date the ${getHumanizedDate( + updatedAtDate + )} (${chalk.cyan(issue.updated_at)})` + ); + + if (shouldMarkAsStale) { + issueLogger.info( + `This $$type should be marked as stale based on the option ${issueLogger.createOptionLink( + this._getDaysBeforeStaleUsedOptionName(issue) + )} (${chalk.cyan(daysBeforeStale)})` + ); + await this._markStale(issue, staleMessage, staleLabel, skipMessage); + issue.isStale = true; // This issue is now considered stale + issueLogger.info(`This $$type is now stale`); + } else { + issueLogger.info( + `This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink( + this._getDaysBeforeStaleUsedOptionName(issue) + )} (${chalk.cyan(daysBeforeStale)})` + ); + } + } else { + issueLogger.info( + `This $$type should not be stale based on the last update date the ${getHumanizedDate( + updatedAtDate + )} (${chalk.cyan(issue.updated_at)})` + ); + } } - // process the issue if it was marked stale + // Process the issue if it was marked stale if (issue.isStale) { - issueLogger.info(`Found a stale $$type`); + issueLogger.info(`This $$type is already stale`); await this._processStaleIssue( issue, staleLabel, @@ -264,28 +403,44 @@ export class IssuesProcessor { closeLabel ); } + + IssuesProcessor._endIssueProcessing(issue); } - if (this._operationsLeft <= 0) { + if (!this._operations.hasRemainingOperations()) { + this._logger.warning( + chalk.yellowBright('No more operations left! Exiting...') + ); this._logger.warning( - 'Reached max number of operations to process. Exiting.' + chalk.yellowBright( + `If you think that not enough issues were processed you could try to increase the quantity related to the ${this._logger.createOptionLink( + Option.OperationsPerRun + )} option which is currently set to ${chalk.cyan( + this.options.operationsPerRun + )}` + ) ); + return 0; } - // do the next batch + this._logger.info( + chalk.green(`Batch ${chalk.cyan(`#${page}`)} processed.`) + ); + + // Do the next batch return this.processIssues(page + 1); } - // grab comments for an issue since a given date + // Grab comments for an issue since a given date async listIssueComments( - issueNumber: number, - sinceDate: string + issueNumber: Readonly, + sinceDate: Readonly ): Promise { - // find any comments since date on the given issue + // Find any comments since date on the given issue try { - this._operationsLeft -= 1; - this._statistics?.incrementFetchedIssuesCommentsCount(); + this._operations.consumeOperation(); + this._statistics?.incrementFetchedItemsCommentsCount(); const comments = await this.client.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, @@ -304,7 +459,7 @@ export class IssuesProcessor { let actor; try { - this._operationsLeft -= 1; + this._operations.consumeOperation(); actor = await this.client.users.getAuthenticated(); } catch (error) { return context.actor; @@ -320,8 +475,7 @@ export class IssuesProcessor { type OctoKitIssueList = GetResponseTypeFromEndpointMethod; try { - this._operationsLeft -= 1; - this._statistics?.incrementFetchedIssuesCount(); + this._operations.consumeOperation(); const issueResult: OctoKitIssueList = await this.client.issues.listForRepo( { owner: context.repo.owner, @@ -332,6 +486,7 @@ export class IssuesProcessor { page } ); + this._statistics?.incrementFetchedItemsCount(issueResult.data.length); return issueResult.data.map( (issue: Readonly): Issue => new Issue(this.options, issue) @@ -350,10 +505,10 @@ export class IssuesProcessor { ): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - issueLogger.info(`Checking for label on $$type`); + issueLogger.info(`Checking for label on this $$type`); - this._operationsLeft -= 1; - this._statistics?.incrementFetchedIssuesEventsCount(); + this._consumeIssueOperation(issue); + this._statistics?.incrementFetchedItemsEventsCount(); const options = this.client.issues.listEvents.endpoint.merge({ owner: context.repo.owner, repo: context.repo.repo, @@ -387,31 +542,38 @@ export class IssuesProcessor { const issueLogger: IssueLogger = new IssueLogger(issue); const markedStaleOn: string = (await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at; - issueLogger.info(`$$type marked stale on: ${markedStaleOn}`); + issueLogger.info(`$$type marked stale on: ${chalk.cyan(markedStaleOn)}`); const issueHasComments: boolean = await this._hasCommentsSince( issue, markedStaleOn, actor ); - issueLogger.info(`$$type has been commented on: ${issueHasComments}`); + issueLogger.info( + `$$type has been commented on: ${chalk.cyan(issueHasComments)}` + ); - const isPr: boolean = isPullRequest(issue); - const daysBeforeClose: number = isPr + const daysBeforeClose: number = issue.isPullRequest ? this._getDaysBeforePrClose() : this._getDaysBeforeIssueClose(); - issueLogger.info(`Days before $$type close: ${daysBeforeClose}`); + issueLogger.info( + `Days before $$type close: ${chalk.cyan(daysBeforeClose)}` + ); const issueHasUpdate: boolean = IssuesProcessor._updatedSince( issue.updated_at, daysBeforeClose ); - issueLogger.info(`$$type has been updated: ${issueHasUpdate}`); + issueLogger.info(`$$type has been updated: ${chalk.cyan(issueHasUpdate)}`); // should we un-stale this issue? - if (this.options.removeStaleWhenUpdated && issueHasComments) { + if (this._shouldRemoveStaleWhenUpdated(issue) && issueHasComments) { await this._removeStaleLabel(issue, staleLabel); + + issueLogger.info(`Skipping the process since the $$type is now un-stale`); + + return; // nothing to do because it is no longer stale } // now start closing logic @@ -421,13 +583,17 @@ export class IssuesProcessor { if (!issueHasComments && !issueHasUpdate) { issueLogger.info( - `Closing $$type because it was last updated on ${issue.updated_at}` + `Closing $$type because it was last updated on! ${chalk.cyan( + issue.updated_at + )}` ); await this._closeIssue(issue, closeMessage, closeLabel); if (this.options.deleteBranch && issue.pull_request) { issueLogger.info( - `Deleting branch for as delete-branch option was specified` + `Deleting the branch the option ${issueLogger.createOptionLink( + Option.DeleteBranch + )} was specified` ); await this._deleteBranch(issue); this.deletedBranchIssues.push(issue); @@ -447,7 +613,9 @@ export class IssuesProcessor { ): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - issueLogger.info(`Checking for comments on $$type since ${sinceDate}`); + issueLogger.info( + `Checking for comments on $$type since: ${chalk.cyan(sinceDate)}` + ); if (!sinceDate) { return true; @@ -461,7 +629,9 @@ export class IssuesProcessor { ); issueLogger.info( - `Comments not made by actor or another bot: ${filteredComments.length}` + `Comments not made by actor or another bot: ${chalk.cyan( + filteredComments.length + )}` ); // if there are any user comments returned @@ -477,7 +647,7 @@ export class IssuesProcessor { ): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - issueLogger.info(`Marking $$type as stale`); + issueLogger.info(`Marking this $$type as stale`); this.staleIssues.push(issue); // if the issue is being marked stale, the updated date should be changed to right now @@ -491,8 +661,8 @@ export class IssuesProcessor { if (!skipMessage) { try { - this._operationsLeft -= 1; - this._statistics?.incrementAddedComment(); + this._consumeIssueOperation(issue); + this._statistics?.incrementAddedItemsComment(issue); await this.client.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -500,14 +670,14 @@ export class IssuesProcessor { body: staleMessage }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } try { - this._operationsLeft -= 1; - this._statistics?.incrementAddedLabel(); - this._statistics?.incrementStaleIssuesCount(); + this._consumeIssueOperation(issue); + this._statistics?.incrementAddedItemsLabel(issue); + this._statistics?.incrementStaleItemsCount(issue); await this.client.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, @@ -515,7 +685,7 @@ export class IssuesProcessor { labels: [staleLabel] }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } @@ -536,8 +706,8 @@ export class IssuesProcessor { if (closeMessage) { try { - this._operationsLeft -= 1; - this._statistics?.incrementAddedComment(); + this._consumeIssueOperation(issue); + this._statistics?.incrementAddedItemsComment(issue); await this.client.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -545,14 +715,14 @@ export class IssuesProcessor { body: closeMessage }); } catch (error) { - issueLogger.error(`Error creating a comment: ${error.message}`); + issueLogger.error(`Error when creating a comment: ${error.message}`); } } if (closeLabel) { try { - this._operationsLeft -= 1; - this._statistics?.incrementAddedLabel(); + this._consumeIssueOperation(issue); + this._statistics?.incrementAddedItemsLabel(issue); await this.client.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, @@ -560,13 +730,13 @@ export class IssuesProcessor { labels: [closeLabel] }); } catch (error) { - issueLogger.error(`Error adding a label: ${error.message}`); + issueLogger.error(`Error when adding a label: ${error.message}`); } } try { - this._operationsLeft -= 1; - this._statistics?.incrementClosedIssuesCount(); + this._consumeIssueOperation(issue); + this._statistics?.incrementClosedItemsCount(issue); await this.client.issues.update({ owner: context.repo.owner, repo: context.repo.repo, @@ -574,7 +744,7 @@ export class IssuesProcessor { state: 'closed' }); } catch (error) { - issueLogger.error(`Error updating this $$type: ${error.message}`); + issueLogger.error(`Error when updating this $$type: ${error.message}`); } } @@ -588,7 +758,7 @@ export class IssuesProcessor { } try { - this._operationsLeft -= 1; + this._consumeIssueOperation(issue); this._statistics?.incrementFetchedPullRequestsCount(); const pullRequest = await this.client.pulls.get({ owner: context.repo.owner, @@ -598,7 +768,7 @@ export class IssuesProcessor { return pullRequest.data; } catch (error) { - issueLogger.error(`Error getting this $$type: ${error.message}`); + issueLogger.error(`Error when getting this $$type: ${error.message}`); } } @@ -612,7 +782,7 @@ export class IssuesProcessor { if (!pullRequest) { issueLogger.info( - `Not deleting branch as pull request not found for this $$type` + `Not deleting this branch as no pull request was found for this $$type` ); return; } @@ -622,10 +792,12 @@ export class IssuesProcessor { } const branch = pullRequest.head.ref; - issueLogger.info(`Deleting branch ${branch} from closed $$type`); + issueLogger.info( + `Deleting the branch "${chalk.cyan(branch)}" from closed $$type` + ); try { - this._operationsLeft -= 1; + this._consumeIssueOperation(issue); this._statistics?.incrementDeletedBranchesCount(); await this.client.git.deleteRef({ owner: context.repo.owner, @@ -634,34 +806,40 @@ export class IssuesProcessor { }); } catch (error) { issueLogger.error( - `Error deleting branch ${branch} from $$type: ${error.message}` + `Error when deleting the branch "${chalk.cyan(branch)}" from $$type: ${ + error.message + }` ); } } - // Remove a label from an issue + // Remove a label from an issue or a pull request private async _removeLabel(issue: Issue, label: string): Promise { const issueLogger: IssueLogger = new IssueLogger(issue); - issueLogger.info(`Removing label "${label}" from $$type`); + issueLogger.info( + `Removing the label "${chalk.cyan(label)}" from this $$type...` + ); this.removedLabelIssues.push(issue); - // @todo remove the debug only to be able to test the code below if (this.options.debugOnly) { return; } try { - this._operationsLeft -= 1; - this._statistics?.incrementDeletedLabelsCount(); + this._consumeIssueOperation(issue); + this._statistics?.incrementDeletedItemsLabelsCount(issue); await this.client.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, name: label }); + issueLogger.info(`The label "${chalk.cyan(label)}" was removed`); } catch (error) { - issueLogger.error(`Error removing a label: ${error.message}`); + issueLogger.error( + `Error when removing the label: "${chalk.cyan(error.message)}"` + ); } } @@ -703,6 +881,36 @@ 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 _shouldRemoveStaleWhenUpdated(issue: Issue): boolean { + if (issue.isPullRequest) { + if (isBoolean(this.options.removePrStaleWhenUpdated)) { + return this.options.removePrStaleWhenUpdated; + } + + return this.options.removeStaleWhenUpdated; + } + + if (isBoolean(this.options.removeIssueStaleWhenUpdated)) { + return this.options.removeIssueStaleWhenUpdated; + } + + return this.options.removeStaleWhenUpdated; + } + private async _removeStaleLabel( issue: Issue, staleLabel: Readonly @@ -714,7 +922,7 @@ export class IssuesProcessor { ); await this._removeLabel(issue, staleLabel); - this._statistics?.incrementUndoStaleIssuesCount(); + this._statistics?.incrementUndoStaleItemsCount(issue); } private async _removeCloseLabel( @@ -735,11 +943,45 @@ export class IssuesProcessor { if (isLabeled(issue, closeLabel)) { issueLogger.info( - `The $$type has a close label "${closeLabel}". Removing the close label...` + `The $$type has a close label "${chalk.cyan( + closeLabel + )}". Removing the close label...` ); await this._removeLabel(issue, closeLabel); - this._statistics?.incrementDeletedCloseLabelsCount(); + this._statistics?.incrementDeletedCloseItemsLabelsCount(issue); } } + + private _consumeIssueOperation(issue: Readonly): void { + this._operations.consumeOperation(); + issue.operations.consumeOperation(); + } + + private _getDaysBeforeStaleUsedOptionName( + issue: Readonly + ): + | Option.DaysBeforeStale + | Option.DaysBeforeIssueStale + | Option.DaysBeforePrStale { + return issue.isPullRequest + ? this._getDaysBeforePrStaleUsedOptionName() + : this._getDaysBeforeIssueStaleUsedOptionName(); + } + + private _getDaysBeforeIssueStaleUsedOptionName(): + | Option.DaysBeforeStale + | Option.DaysBeforeIssueStale { + return isNaN(this.options.daysBeforeIssueStale) + ? Option.DaysBeforeStale + : Option.DaysBeforeIssueStale; + } + + private _getDaysBeforePrStaleUsedOptionName(): + | Option.DaysBeforeStale + | Option.DaysBeforePrStale { + return isNaN(this.options.daysBeforePrStale) + ? Option.DaysBeforeStale + : Option.DaysBeforePrStale; + } } diff --git a/src/classes/loggers/issue-logger.ts b/src/classes/loggers/issue-logger.ts index ef4cd1a98..5db113018 100644 --- a/src/classes/loggers/issue-logger.ts +++ b/src/classes/loggers/issue-logger.ts @@ -1,4 +1,4 @@ -import * as core from '@actions/core'; +import chalk from 'chalk'; import {Issue} from '../issue'; import {Logger} from './logger'; @@ -15,23 +15,24 @@ import {Logger} from './logger'; * @example * warning('The $$type will stale') => "The pull request will stale" */ -export class IssueLogger implements Logger { +export class IssueLogger extends Logger { private readonly _issue: Issue; constructor(issue: Issue) { + super(); this._issue = issue; } - warning(message: Readonly): void { - core.warning(this._format(message)); + warning(...message: string[]): void { + super.warning(this._format(...message)); } - info(message: Readonly): void { - core.info(this._format(message)); + info(...message: string[]): void { + super.info(this._format(...message)); } - error(message: Readonly): void { - core.error(this._format(message)); + error(...message: string[]): void { + super.error(this._format(...message)); } private _replaceTokens(message: Readonly): string { @@ -51,14 +52,28 @@ export class IssueLogger implements Logger { } private _prefixWithIssueNumber(message: Readonly): string { - return `[#${this._getIssueNumber()}] ${message}`; + return `${this._getPrefix()} ${message}`; } private _getIssueNumber(): number { return this._issue.number; } - private _format(message: Readonly): string { - return this._prefixWithIssueNumber(this._replaceTokens(message)); + private _format(...message: string[]): string { + return this._prefixWithIssueNumber(this._replaceTokens(message.join(' '))); + } + + private _getPrefix(): string { + return this._issue.isPullRequest + ? this._getPullRequestPrefix() + : this._getIssuePrefix(); + } + + private _getIssuePrefix(): string { + return chalk.red(`[#${this._getIssueNumber()}]`); + } + + private _getPullRequestPrefix(): string { + return chalk.blue(`[#${this._getIssueNumber()}]`); } } diff --git a/src/classes/loggers/logger.ts b/src/classes/loggers/logger.ts index 8dce898e7..65bb83afd 100644 --- a/src/classes/loggers/logger.ts +++ b/src/classes/loggers/logger.ts @@ -1,15 +1,28 @@ import * as core from '@actions/core'; +import chalk from 'chalk'; +import terminalLink from 'terminal-link'; +import {Option} from '../../enums/option'; export class Logger { - warning(message: Readonly): void { - core.warning(message); + warning(...message: string[]): void { + core.warning(chalk.whiteBright(...message)); } - info(message: Readonly): void { - core.info(message); + info(...message: string[]): void { + core.info(chalk.whiteBright(...message)); } - error(message: Readonly): void { - core.error(message); + error(...message: string[]): void { + core.error(chalk.whiteBright(...message)); + } + + createLink(name: Readonly, link: Readonly): string { + return terminalLink(name, link); + } + + createOptionLink(option: Readonly