From 7e06b131d5dc41c6fbe67df1e6aef39024ee0ff4 Mon Sep 17 00:00:00 2001 From: zhews <118117386+zhews@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:54:08 +0200 Subject: [PATCH] Allow the processing of locked issues and pull requests --- README.md | 11 ++ .../constants/default-processor-options.ts | 3 +- __tests__/main.spec.ts | 166 ++++++++++++++++++ action.yml | 4 + dist/index.js | 5 +- src/classes/issue.spec.ts | 3 +- src/classes/issues-processor.ts | 6 +- src/interfaces/issues-processor-options.ts | 1 + src/main.ts | 3 +- 9 files changed, 195 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eb65b46b..ab1f0622 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Every argument is optional. | [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | | | [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | | | [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` | +| [exempt-locked](#exempt-locked) | Issues and pull requests that are locked will not be marked as stale. | `true` | ### List of output options @@ -547,6 +548,16 @@ If set to `true`, only the issues or the pull requests with an assignee will be Default value: `false` +#### exempt-locked + +If set to `false` issues or pull requests that are locked will be marked as +stale automatically. If you process locked issues and pull requests and want to +add a closing message the default repo-token will not be sufficient. For that +you will have to use a PAT which has write access, is repository owner or +collaborator. + +Default value: `true` + ### Usage See also [action.yml](./action.yml) for a comprehensive list of all the options. diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index 0265b644..42805852 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -55,5 +55,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ ignorePrUpdates: undefined, exemptDraftPr: false, closeIssueReason: 'not_planned', - includeOnlyAssigned: false + includeOnlyAssigned: false, + exemptLocked: true }); diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts index 80d660e8..2aa284ca 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -930,6 +930,38 @@ test('locked issues will not be marked stale', async () => { expect(processor.closedIssues).toHaveLength(0); }); +test('locked issues are marked stale', async () => { + const opts: IIssuesProcessorOptions = {...DefaultProcessorOptions}; + opts.exemptLocked = false; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A locked issue that will be stale', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + true + ) + ]; + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + 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('stale locked issues will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( @@ -960,6 +992,38 @@ test('stale locked issues will not be closed', async () => { expect(processor.closedIssues).toHaveLength(0); }); +test('stale locked issues will be closed', async () => { + const opts: IIssuesProcessorOptions = {...DefaultProcessorOptions}; + opts.exemptLocked = false; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A stale locked issue that will not be closed', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + ['Stale'], + false, + true + ) + ]; + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + 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(1); +}); + test('locked prs will not be marked stale', async () => { const TestIssueList: Issue[] = [ generateIssue( @@ -988,6 +1052,38 @@ test('locked prs will not be marked stale', async () => { expect(processor.closedIssues).toHaveLength(0); }); +test('locked prs will be marked stale', async () => { + const opts: IIssuesProcessorOptions = {...DefaultProcessorOptions}; + opts.exemptLocked = false; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A locked PR that will not be marked stale', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + true, + [], + false, + true + ) + ]; + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + 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('stale locked prs will not be closed', async () => { const TestIssueList: Issue[] = [ generateIssue( @@ -1018,6 +1114,38 @@ test('stale locked prs will not be closed', async () => { expect(processor.closedIssues).toHaveLength(0); }); +test('stale locked prs will be closed', async () => { + const opts: IIssuesProcessorOptions = {...DefaultProcessorOptions}; + opts.exemptLocked = false; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A stale locked PR that will not be closed', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + true, + ['Stale'], + false, + true + ) + ]; + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + 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(1); +}); + test('exempt issue labels will not be marked stale', async () => { expect.assertions(3); const opts = {...DefaultProcessorOptions}; @@ -2516,6 +2644,44 @@ test('processing a locked issue with a close label will not remove the close lab expect(processor.removedLabelIssues).toHaveLength(0); }); +test('processing a locked issue with a close label will remove the close label', async () => { + expect.assertions(1); + const opts: IIssuesProcessorOptions = { + ...DefaultProcessorOptions, + closeIssueLabel: 'close', + staleIssueLabel: 'stale', + exemptLocked: false + }; + const now: Date = new Date(); + const oneWeekAgo: Date = new Date(now.setDate(now.getDate() - 7)); + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A closed issue with a close label', + oneWeekAgo.toDateString(), + now.toDateString(), + false, + false, + ['close'], + false, + true + ) + ]; + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + 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 an issue stale since less than the daysBeforeStale with a stale label created after daysBeforeClose should close the issue', async () => { expect.assertions(3); const opts: IIssuesProcessorOptions = { diff --git a/action.yml b/action.yml index d55f8547..e6e77849 100644 --- a/action.yml +++ b/action.yml @@ -204,6 +204,10 @@ inputs: description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.' default: 'false' required: false + exempt-locked: + description: 'Issues and pull requests that are locked will not be marked as stale.' + default: 'true' + required: false outputs: closed-issues-prs: description: 'List of all closed issues and pull requests.' diff --git a/dist/index.js b/dist/index.js index f2786a0f..1bb2c954 100644 --- a/dist/index.js +++ b/dist/index.js @@ -495,7 +495,7 @@ class IssuesProcessor { IssuesProcessor._endIssueProcessing(issue); return; // Don't process closed issues } - if (issue.locked) { + if (issue.locked && this.options.exemptLocked) { issueLogger.info(`Skipping this $$type because it is locked`); IssuesProcessor._endIssueProcessing(issue); return; // Don't process locked issues @@ -2567,7 +2567,8 @@ function _getAndValidateArgs() { ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'), exemptDraftPr: core.getInput('exempt-draft-pr') === 'true', closeIssueReason: core.getInput('close-issue-reason'), - includeOnlyAssigned: core.getInput('include-only-assigned') === 'true' + includeOnlyAssigned: core.getInput('include-only-assigned') === 'true', + exemptLocked: core.getInput('exempt-locked') === 'true' }; for (const numberInput of ['days-before-stale']) { if (isNaN(parseFloat(core.getInput(numberInput)))) { diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index a2c82e26..4fc3f1cd 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -64,7 +64,8 @@ describe('Issue', (): void => { ignorePrUpdates: undefined, exemptDraftPr: false, closeIssueReason: '', - includeOnlyAssigned: false + includeOnlyAssigned: false, + exemptLocked: true }; issueInterface = { title: 'dummy-title', diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 486c6a78..db92d657 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -237,7 +237,7 @@ export class IssuesProcessor { return; // Don't process closed issues } - if (issue.locked) { + if (issue.locked && this.options.exemptLocked) { issueLogger.info(`Skipping this $$type because it is locked`); IssuesProcessor._endIssueProcessing(issue); return; // Don't process locked issues @@ -1196,7 +1196,9 @@ export class IssuesProcessor { const issueLogger: IssueLogger = new IssueLogger(issue); issueLogger.info( - `The $$type is not closed nor locked. Trying to remove the close label...` + `The $$type is not closed${ + this.options.exemptLocked ? ' nor locked' : '' + }. Trying to remove the close label...` ); if (!closeLabel) { diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 93099228..a31e6da2 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -54,4 +54,5 @@ export interface IIssuesProcessorOptions { exemptDraftPr: boolean; closeIssueReason: string; includeOnlyAssigned: boolean; + exemptLocked: boolean; } diff --git a/src/main.ts b/src/main.ts index a7836c16..d614138e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -123,7 +123,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'), exemptDraftPr: core.getInput('exempt-draft-pr') === 'true', closeIssueReason: core.getInput('close-issue-reason'), - includeOnlyAssigned: core.getInput('include-only-assigned') === 'true' + includeOnlyAssigned: core.getInput('include-only-assigned') === 'true', + exemptLocked: core.getInput('exempt-locked') === 'true' }; for (const numberInput of ['days-before-stale']) {