Repository: actions/stale (ts)

Issue:
Feature Request: Remove "CLOSED" tag if an auto-closed issue gets reopened
Version: v3.0.14

We had an issue get closed through the action script after being marked stale.  After it was marked stale, our developer re-opened the issue.  They did not touch anything else or add any comments, they just re-opened the issue.  The actions/stale bot script did remove the "STALE" flag but it did not remove the "CLOSED_DUE_TO_INACTIVITY" flag that we have set up.  

This left a confusing issue that was open but also had the 'CLOSED' tag assigned to it.

Can we get a new feature added to this to have the script/bot also remove the CLOSED label that gets added to an issue when closed if it's ever re-opened (but not if it has activity but is kept closed since a closed issue can still have activity)?

Pull request description:
feat(close-label): automatically remove close-label when no longer closed nor locked
**Changes:**
- [x] Update the doc to include some missing default values
- [x] Add a new processing right after the checks to prevent the process on closed or locked issues to remove the close label only if set

Closes #278

Patch:
diff --git a/README.md b/README.md
index 512ed47..71d5eaa 100644
--- a/README.md
+++ b/README.md
@@ -17,10 +17,10 @@ Warns and then closes issues and PRs that have had no activity for a specified a
 | `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.                                                                                | Optional |
-| `stale-pr-label`              | Label to apply on the stale PR.                                                                                 | Optional |
-| `close-pr-label`              | Label to apply on the closing PR.                                                                               | 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 |
diff --git a/dist/index.js b/dist/index.js
index 9465aef..ec5014e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -299,6 +299,8 @@ class IssuesProcessor {
                     issueLogger.info(`Skipping $$type because it is locked`);
                     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);
@@ -677,10 +679,24 @@ class IssuesProcessor {
     _removeStaleLabel(issue, staleLabel) {
         return __awaiter(this, void 0, void 0, function* () {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
-            issueLogger.info(`$$type is no longer stale. Removing stale label.`);
+            issueLogger.info(`The $$type is no longer stale. Removing the stale label...`);
             return this._removeLabel(issue, staleLabel);
         });
     }
+    _removeCloseLabel(issue, closeLabel) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const issueLogger = new issue_logger_1.IssueLogger(issue);
+            issueLogger.info(`The $$type is not closed nor locked. Trying to remove the close label...`);
+            if (!closeLabel) {
+                issueLogger.info(`There is no close label on this $$type. Skip`);
+                return Promise.resolve();
+            }
+            if (is_labeled_1.isLabeled(issue, closeLabel)) {
+                issueLogger.info(`The $$type has a close label "${closeLabel}". Removing the close label...`);
+                return this._removeLabel(issue, closeLabel);
+            }
+        });
+    }
 }
 exports.IssuesProcessor = IssuesProcessor;
 
diff --git a/src/classes/issue.ts b/src/classes/issue.ts
index 816de1f..d48add6 100644
--- a/src/classes/issue.ts
+++ b/src/classes/issue.ts
@@ -15,7 +15,7 @@ export class Issue implements IIssue {
   updated_at: IsoDateString;
   readonly labels: ILabel[];
   readonly pull_request: Object | null | undefined;
-  readonly state: string;
+  readonly state: string | 'closed' | 'open';
   readonly locked: boolean;
   readonly milestone: IMilestone | undefined;
   readonly assignees: IAssignee[];
diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts
index 758950b..cf7650f 100644
--- a/src/classes/issues-processor.ts
+++ b/src/classes/issues-processor.ts
@@ -140,6 +140,9 @@ export class IssuesProcessor {
         continue; // don't process locked issues
       }
 
+      // Try to remove the close label when not close/locked issue or PR
+      await this._removeCloseLabel(issue, closeLabel);
+
       if (this.options.startDate) {
         const startDate: Date = new Date(this.options.startDate);
         const createdAt: Date = new Date(issue.created_at);
@@ -663,8 +666,35 @@ export class IssuesProcessor {
   ): Promise<void> {
     const issueLogger: IssueLogger = new IssueLogger(issue);
 
-    issueLogger.info(`$$type is no longer stale. Removing stale label.`);
+    issueLogger.info(
+      `The $$type is no longer stale. Removing the stale label...`
+    );
 
     return this._removeLabel(issue, staleLabel);
   }
+
+  private async _removeCloseLabel(
+    issue: Issue,
+    closeLabel: Readonly<string | undefined>
+  ): Promise<void> {
+    const issueLogger: IssueLogger = new IssueLogger(issue);
+
+    issueLogger.info(
+      `The $$type is not closed nor locked. Trying to remove the close label...`
+    );
+
+    if (!closeLabel) {
+      issueLogger.info(`There is no close label on this $$type. Skip`);
+
+      return Promise.resolve();
+    }
+
+    if (isLabeled(issue, closeLabel)) {
+      issueLogger.info(
+        `The $$type has a close label "${closeLabel}". Removing the close label...`
+      );
+
+      return this._removeLabel(issue, closeLabel);
+    }
+  }
 }


Test patch:
diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts
index 46ea62f..fcb5544 100644
--- a/__tests__/main.spec.ts
+++ b/__tests__/main.spec.ts
@@ -2112,3 +2112,111 @@ test('processing a pull request opened since 2 days and with the option "daysBef
   expect(processor.staleIssues.length).toEqual(1);
   expect(processor.closedIssues.length).toEqual(0);
 });
+
+test('processing a previously closed issue with a close label will remove the close label', async () => {
+  expect.assertions(1);
+  const opts: IIssuesProcessorOptions = {
+    ...DefaultProcessorOptions,
+    closeIssueLabel: 'close',
+    staleIssueLabel: 'stale'
+  };
+  const now: Date = new Date();
+  const oneWeekAgo: Date = new Date(now.getDate() - 7);
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'An opened issue with a close label',
+      oneWeekAgo.toDateString(),
+      now.toDateString(),
+      false,
+      ['close'],
+      false,
+      false
+    )
+  ];
+  const processor = new IssuesProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p === 1 ? TestIssueList : []),
+    async () => [],
+    async () => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.removedLabelIssues).toHaveLength(1);
+});
+
+test('processing a closed issue with a close label will not remove the close label', async () => {
+  expect.assertions(1);
+  const opts: IIssuesProcessorOptions = {
+    ...DefaultProcessorOptions,
+    closeIssueLabel: 'close',
+    staleIssueLabel: 'stale'
+  };
+  const now: Date = new Date();
+  const oneWeekAgo: Date = new Date(now.getDate() - 7);
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'A closed issue with a close label',
+      oneWeekAgo.toDateString(),
+      now.toDateString(),
+      false,
+      ['close'],
+      true,
+      false
+    )
+  ];
+  const processor = new IssuesProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p === 1 ? TestIssueList : []),
+    async () => [],
+    async () => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.removedLabelIssues).toHaveLength(0);
+});
+
+test('processing a locked issue with a close label will not remove the close label', async () => {
+  expect.assertions(1);
+  const opts: IIssuesProcessorOptions = {
+    ...DefaultProcessorOptions,
+    closeIssueLabel: 'close',
+    staleIssueLabel: 'stale'
+  };
+  const now: Date = new Date();
+  const oneWeekAgo: Date = new Date(now.getDate() - 7);
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'A closed issue with a close label',
+      oneWeekAgo.toDateString(),
+      now.toDateString(),
+      false,
+      ['close'],
+      false,
+      true
+    )
+  ];
+  const processor = new IssuesProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p === 1 ? TestIssueList : []),
+    async () => [],
+    async () => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.removedLabelIssues).toHaveLength(0);
+});