Repository: actions/stale
Pull request #319 (2021-02-16 16:36:30)

Feat: add any-of-labels option
#302 

C0ZEN: Closes #302
C0ZEN: @hross if you have the time to check this one and this one #327 🙏🏻 🤟🏻 
Thanks!
C0ZEN: @joeveiga I gave you some conflicts 😛 
hross: I can merge this when the conflicts are resolved
joeveiga: @C0ZEN @hross Thanks fellas! Conflicts resolved.

Patch:
diff --git a/README.md b/README.md
index 97b4557..eda9bfe 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/action.yml b/action.yml
index 211470c..44a4cb5 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 f1c1bdb..4e18b46 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/package-lock.json b/package-lock.json
index a9a99c2..115b980 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9611,9 +9611,9 @@
       }
     },
     "typescript": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
-      "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz",
+      "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==",
       "dev": true
     },
     "union-value": {
diff --git a/package.json b/package.json
index 5f44a18..7ee29ea 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,6 @@
     "js-yaml": "^4.0.0",
     "prettier": "^2.2.1",
     "ts-jest": "^26.5.2",
-    "typescript": "^4.1.5"
+    "typescript": "^4.2.2"
   }
 }
diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts
index 8d852b2..a79ab5d 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 cbed3f5..b82cb52 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<string>): 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 28027e9..155e94f 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 f5dd905..255a57d 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})
     ),


Test patch:
diff --git a/__tests__/any-of-labels.spec.ts b/__tests__/any-of-labels.spec.ts
new file mode 100644
index 0000000..e3e3cfc
--- /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<Issue>[]): 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 da7b7ca..f1385b1 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,