Repository: actions/stale
Pull request #817 (2022-09-12 09:55:09)

Issue 596/include only assigned
<!-- List the change(s) you're making with this PR. -->

## Changes

- Add new boolean option `include-only-assigned`. 

## Context

<!-- Explain why you're making the change(s). -->
<!-- If you're closing an issue with this PR, [link them with a keyword](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). -->
New option `include-only-assigned` enables users to process only issues/PRs that are already assigned. If there is no assignees and this option is set, issue will not be processed.

Resolves https://github.com/actions/stale/issues/596


Patch:
diff --git a/dist/index.js b/dist/index.js
index 18754b2..c4b2c13 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -476,6 +476,11 @@ class IssuesProcessor {
                 IssuesProcessor._endIssueProcessing(issue);
                 return; // Don't process locked issues
             }
+            if (this._isIncludeOnlyAssigned(issue)) {
+                issueLogger.info(`Skipping this $$type because its assignees list is empty`);
+                IssuesProcessor._endIssueProcessing(issue);
+                return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list
+            }
             const onlyLabels = words_to_list_1.wordsToList(this._getOnlyLabels(issue));
             if (onlyLabels.length > 0) {
                 issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was specified to only process issues and pull requests with all those labels (${logger_service_1.LoggerService.cyan(onlyLabels.length)})`);
@@ -988,6 +993,9 @@ class IssuesProcessor {
         }
         return this.options.onlyLabels;
     }
+    _isIncludeOnlyAssigned(issue) {
+        return this.options.includeOnlyAssigned && !issue.hasAssignees;
+    }
     _getAnyOfLabels(issue) {
         if (issue.isPullRequest) {
             if (this.options.anyOfPrLabels !== '') {
@@ -2207,7 +2215,8 @@ function _getAndValidateArgs() {
         ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
         ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
         exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
-        closeIssueReason: core.getInput('close-issue-reason')
+        closeIssueReason: core.getInput('close-issue-reason'),
+        includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
     };
     for (const numberInput of [
         'days-before-stale',
diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts
index 0a60d1a..977b767 100644
--- a/src/classes/issue.spec.ts
+++ b/src/classes/issue.spec.ts
@@ -62,7 +62,8 @@ describe('Issue', (): void => {
       ignoreIssueUpdates: undefined,
       ignorePrUpdates: undefined,
       exemptDraftPr: false,
-      closeIssueReason: ''
+      closeIssueReason: '',
+      includeOnlyAssigned: false
     };
     issueInterface = {
       title: 'dummy-title',
diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts
index d0397eb..be2334d 100644
--- a/src/classes/issues-processor.ts
+++ b/src/classes/issues-processor.ts
@@ -221,6 +221,14 @@ export class IssuesProcessor {
       return; // Don't process locked issues
     }
 
+    if (this._isIncludeOnlyAssigned(issue)) {
+      issueLogger.info(
+        `Skipping this $$type because its assignees list is empty`
+      );
+      IssuesProcessor._endIssueProcessing(issue);
+      return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list
+    }
+
     const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue));
 
     if (onlyLabels.length > 0) {
@@ -1012,6 +1020,10 @@ export class IssuesProcessor {
     return this.options.onlyLabels;
   }
 
+  private _isIncludeOnlyAssigned(issue: Issue): boolean {
+    return this.options.includeOnlyAssigned && !issue.hasAssignees;
+  }
+
   private _getAnyOfLabels(issue: Issue): string {
     if (issue.isPullRequest) {
       if (this.options.anyOfPrLabels !== '') {
diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts
index 496584e..b056a28 100644
--- a/src/interfaces/issues-processor-options.ts
+++ b/src/interfaces/issues-processor-options.ts
@@ -52,4 +52,5 @@ export interface IIssuesProcessorOptions {
   ignorePrUpdates: boolean | undefined;
   exemptDraftPr: boolean;
   closeIssueReason: string;
+  includeOnlyAssigned: boolean;
 }
diff --git a/src/main.ts b/src/main.ts
index 770aa11..4b7adea 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -88,7 +88,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
     ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
     ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
     exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
-    closeIssueReason: core.getInput('close-issue-reason')
+    closeIssueReason: core.getInput('close-issue-reason'),
+    includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
   };
 
   for (const numberInput of [


Test patch:
diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts
index ee3e730..cda29d4 100644
--- a/__tests__/constants/default-processor-options.ts
+++ b/__tests__/constants/default-processor-options.ts
@@ -51,5 +51,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
   ignoreIssueUpdates: undefined,
   ignorePrUpdates: undefined,
   exemptDraftPr: false,
-  closeIssueReason: ''
+  closeIssueReason: '',
+  includeOnlyAssigned: false
 });
diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts
index 2abb5ee..eba2e9d 100644
--- a/__tests__/main.spec.ts
+++ b/__tests__/main.spec.ts
@@ -2352,3 +2352,69 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
   expect(processor.closedIssues).toHaveLength(0);
   expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(0);
 });
+
+test('processing an issue with the "includeOnlyAssigned" option and nonempty assignee list will stale the issue', async () => {
+  const issueDate = new Date();
+  issueDate.setDate(issueDate.getDate() - 2);
+
+  const opts: IIssuesProcessorOptions = {
+    ...DefaultProcessorOptions,
+    staleIssueLabel: 'This issue is stale',
+    includeOnlyAssigned: true
+  };
+
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'An issue with no label',
+      issueDate.toDateString(),
+      issueDate.toDateString(),
+      false,
+      [],
+      false,
+      false,
+      undefined,
+      ['assignee1']
+    )
+  ];
+  const processor = new IssuesProcessorMock(
+    opts,
+    async p => (p === 1 ? TestIssueList : []),
+    async () => [],
+    async () => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues).toHaveLength(1);
+  expect(processor.closedIssues).toHaveLength(0);
+});
+
+test('processing an issue with the "includeOnlyAssigned" option set and no assignees will not stale the issue', async () => {
+  const issueDate = new Date();
+  issueDate.setDate(issueDate.getDate() - 2);
+
+  const opts: IIssuesProcessorOptions = {
+    ...DefaultProcessorOptions,
+    staleIssueLabel: 'This issue is stale',
+    includeOnlyAssigned: true
+  };
+
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', issueDate.toDateString())
+  ];
+  const processor = new IssuesProcessorMock(
+    opts,
+    async p => (p === 1 ? TestIssueList : []),
+    async () => [],
+    async () => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues).toHaveLength(0);
+  expect(processor.closedIssues).toHaveLength(0);
+});