From 63ae8ac024c030e4b1daff4524790218ad3d1f6d Mon Sep 17 00:00:00 2001 From: Jose Veiga Date: Mon, 1 Mar 2021 11:05:53 -0500 Subject: [PATCH] Feat: add any-of-labels option (#319) * feat: add any-of-labels option * chore: run pack script * fix: error in milestones spec * chore: update readme * chore: fix default value in action.yml * chore: add some unit tests * docs: update README.md Co-authored-by: Geoffrey Testelin * refactor: add return type to lambda Co-authored-by: Geoffrey Testelin --- README.md | 111 +++++++++++------- __tests__/any-of-labels.spec.ts | 107 +++++++++++++++++ .../constants/default-processor-options.ts | 1 + action.yml | 6 +- dist/index.js | 7 ++ src/classes/issue.spec.ts | 1 + src/classes/issues-processor.ts | 13 ++ src/interfaces/issues-processor-options.ts | 1 + src/main.ts | 1 + 9 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 __tests__/any-of-labels.spec.ts diff --git a/README.md b/README.md index 97b4557c8..eda9bfe8a 100644 --- a/README.md +++ b/README.md @@ -4,48 +4,56 @@ Warns and then closes issues and PRs that have had no activity for a specified a ### Arguments -| Input | Description | Usage | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | -| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional | -| `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional | -| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional | -| `days-before-pr-stale` | Idle number of days before marking an PR as stale (override `days-before-stale`). | Optional | -| `days-before-close` | Idle number of days before closing an stale issue/PR. _Defaults to **7**_ | Optional | -| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional | -| `days-before-pr-close` | Idle number of days before closing an stale PR (override `days-before-close`). | Optional | -| `stale-issue-message` | Message to post on the stale issue. | Optional | -| `stale-pr-message` | Message to post on the stale PR. | Optional | -| `close-issue-message` | Message to post on the stale issue while closing it. | Optional | -| `close-pr-message` | Message to post on the stale PR while closing it. | Optional | -| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **Stale**_ | Optional | -| `close-issue-label` | Label to apply on closing issue (automatically removed if no longer closed nor locked). | Optional | -| `stale-pr-label` | Label to apply on the stale PR. _Defaults to **Stale**_ | Optional | -| `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional | -| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional | -| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional | -| `only-labels` | Only labels checked for stale issue/PR. | Optional | -| `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional | -| `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional | -| `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional | -| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional | -| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional | -| `ascending` | Order to get issues/PR. _Defaults to **false**_ | Optional | -| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional | -| `skip-stale-pr-message` | Skip adding stale message on stale PR. _Defaults to **false**_ | Optional | -| `start-date` | The date used to skip the stale action on issue/PR created before it (ISO 8601 or RFC 2822). | Optional | -| `delete-branch` | Delete the git branch after closing a stale pull request. _Defaults to **false**_ | Optional | -| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional | -| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional | -| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional | -| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional | -| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | -| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | -| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | Optional | -| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale (override `exempt-assignees`). | Optional | -| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale (override `exempt-assignees`). | Optional | -| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale. (priority over `exempt-assignees` rules) | Optional | -| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional | -| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional | +| Input | Description | Usage | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional | +| `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional | +| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional | +| `days-before-pr-stale` | Idle number of days before marking an PR as stale (override `days-before-stale`). | Optional | +| `days-before-close` | Idle number of days before closing an stale issue/PR. _Defaults to **7**_ | Optional | +| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional | +| `days-before-pr-close` | Idle number of days before closing an stale PR (override `days-before-close`). | Optional | +| `stale-issue-message` | Message to post on the stale issue. | Optional | +| `stale-pr-message` | Message to post on the stale PR. | Optional | +| `close-issue-message` | Message to post on the stale issue while closing it. | Optional | +| `close-pr-message` | Message to post on the stale PR while closing it. | Optional | +| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **Stale**_ | Optional | +| `close-issue-label` | Label to apply on closing issue (automatically removed if no longer closed nor locked). | Optional | +| `stale-pr-label` | Label to apply on the stale PR. _Defaults to **Stale**_ | Optional | +| `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional | +| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional | +| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional | +| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional | +| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional | +| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional | +| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional | +| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | +| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | +| `only-labels` | Only issues and PRs with ALL these labels are checked. Separate multiple labels with commas (eg. "question,answered"). | Optional | +| `only-labels` | Only labels checked for stale issue/PR. | Optional | +| `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional | +| `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional | +| `any-of-labels` | Only issues and PRs with ANY of these labels are checked. Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | Optional | +| `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional | +| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional | +| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional | +| `ascending` | Order to get issues/PR. _Defaults to **false**_ | Optional | +| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional | +| `skip-stale-pr-message` | Skip adding stale message on stale PR. _Defaults to **false**_ | Optional | +| `start-date` | The date used to skip the stale action on issue/PR created before it (ISO 8601 or RFC 2822). | Optional | +| `delete-branch` | Delete the git branch after closing a stale pull request. _Defaults to **false**_ | Optional | +| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional | +| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional | +| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional | +| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional | +| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | +| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional | +| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | Optional | +| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale (override `exempt-assignees`). | Optional | +| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale (override `exempt-assignees`). | Optional | +| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale. (priority over `exempt-assignees` rules) | Optional | +| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional | +| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional | ### Usage @@ -211,6 +219,25 @@ jobs: exempt-all-pr-milestones: true ``` +Avoid stale for specific labels: + +```yaml +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - 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 + # to be present in the issue/PR +``` + Avoid stale for specific assignees: ```yaml diff --git a/__tests__/any-of-labels.spec.ts b/__tests__/any-of-labels.spec.ts new file mode 100644 index 000000000..e3e3cfcb9 --- /dev/null +++ b/__tests__/any-of-labels.spec.ts @@ -0,0 +1,107 @@ +import {Issue} from '../src/classes/issue'; +import {IssuesProcessor} from '../src/classes/issues-processor'; +import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; +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() + .emptyAnyOfLabels() + .issues([{labels: [{name: 'some-label'}]}]) + .build(); + + await sut.processIssues(); + + expect(sut.staleIssues).toHaveLength(1); + }); + + test('should skip it when none of the issue labels match', async () => { + const sut = new IssuesProcessorBuilder() + .anyOfLabels('skip-this-issue,and-this-one') + .issues([{labels: [{name: 'some-label'}, {name: 'some-other-label'}]}]) + .build(); + + await sut.processIssues(); + + expect(sut.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: []}]) + .build(); + + await sut.processIssues(); + + expect(sut.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'}]}]) + .build(); + + await sut.processIssues(); + + expect(sut.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'}]}]) + .build(); + + await sut.processIssues(); + + expect(sut.staleIssues).toHaveLength(1); + }); +}); + +class IssuesProcessorBuilder { + private _options: IIssuesProcessorOptions; + private _issues: Issue[]; + + constructor() { + this._options = {...DefaultProcessorOptions}; + this._issues = []; + } + + anyOfLabels(labels: string): IssuesProcessorBuilder { + this._options.anyOfLabels = labels; + return this; + } + + emptyAnyOfLabels(): IssuesProcessorBuilder { + return this.anyOfLabels(''); + } + + issues(issues: Partial[]): IssuesProcessorBuilder { + this._issues = issues.map( + (issue, index): 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.labels ? issue.labels.map(label => label.name) : [] + ) + ); + return this; + } + + build(): IssuesProcessor { + return new IssuesProcessor( + this._options, + async () => 'abot', + async p => (p === 1 ? this._issues : []), + async () => [], + async () => new Date().toDateString() + ); + } +} diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index da7b7ca63..f1385b15f 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -21,6 +21,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ onlyLabels: '', onlyIssueLabels: '', onlyPrLabels: '', + anyOfLabels: '', operationsPerRun: 100, debugOnly: true, removeStaleWhenUpdated: false, diff --git a/action.yml b/action.yml index 211470c48..44a4cb5f4 100644 --- a/action.yml +++ b/action.yml @@ -85,7 +85,11 @@ inputs: default: '' required: false only-labels: - description: 'Only issues or pull requests with all of these labels are checked if stale. Defaults to `[]` (disabled) and can be a comma-separated list of labels.' + description: 'Only issues or pull requests with all of these labels are checked if stale. Defaults to `` (disabled) and can be a comma-separated list of labels.' + default: '' + required: false + any-of-labels: + 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 only-issue-labels: diff --git a/dist/index.js b/dist/index.js index f1c1bdba1..4e18b46e1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -350,6 +350,12 @@ class IssuesProcessor { issueLogger.info(`Skipping $$type because it has an exempt label`); continue; // don't process exempt issues } + const anyOfLabels = words_to_list_1.wordsToList(this.options.anyOfLabels); + if (anyOfLabels.length && + !anyOfLabels.some((label) => is_labeled_1.isLabeled(issue, label))) { + issueLogger.info(`Skipping ${issueType} because it does not have any of the required labels`); + continue; // don't process issues without any of the required labels + } const milestones = new milestones_1.Milestones(this.options, issue); if (milestones.shouldExemptMilestones()) { issueLogger.info(`Skipping $$type because it has an exempted milestone`); @@ -1201,6 +1207,7 @@ function _getAndValidateArgs() { onlyLabels: core.getInput('only-labels'), onlyIssueLabels: core.getInput('only-issue-labels'), onlyPrLabels: core.getInput('only-pr-labels'), + anyOfLabels: core.getInput('any-of-labels'), operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'), debugOnly: core.getInput('debug-only') === 'true', diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index 8d852b230..a79ab5dec 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -30,6 +30,7 @@ describe('Issue', (): void => { onlyLabels: '', onlyIssueLabels: '', onlyPrLabels: '', + anyOfLabels: '', operationsPerRun: 0, removeStaleWhenUpdated: false, repoToken: '', diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index cbed3f5c1..b82cb5275 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -230,6 +230,19 @@ export class IssuesProcessor { continue; // don't process exempt issues } + const anyOfLabels: string[] = wordsToList(this.options.anyOfLabels); + if ( + anyOfLabels.length && + !anyOfLabels.some((label: Readonly): boolean => + isLabeled(issue, label) + ) + ) { + issueLogger.info( + `Skipping ${issueType} because it does not have any of the required labels` + ); + continue; // don't process issues without any of the required labels + } + const milestones: Milestones = new Milestones(this.options, issue); if (milestones.shouldExemptMilestones()) { diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 28027e97b..155e94f2c 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -21,6 +21,7 @@ export interface IIssuesProcessorOptions { onlyLabels: string; onlyIssueLabels: string; onlyPrLabels: string; + anyOfLabels: string; operationsPerRun: number; removeStaleWhenUpdated: boolean; debugOnly: boolean; diff --git a/src/main.ts b/src/main.ts index f5dd90524..255a57d72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,6 +41,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { onlyLabels: core.getInput('only-labels'), onlyIssueLabels: core.getInput('only-issue-labels'), onlyPrLabels: core.getInput('only-pr-labels'), + anyOfLabels: core.getInput('any-of-labels'), operationsPerRun: parseInt( core.getInput('operations-per-run', {required: true}) ),