From 1c041b9c57862f82abbf23d73a12e9c31c28220e Mon Sep 17 00:00:00 2001 From: Josh Schultz Date: Fri, 3 Oct 2025 21:49:53 +0000 Subject: [PATCH 1/2] Add exempt-issue-types option for blocklist filter by issue type --- README.md | 10 ++ __tests__/exempt-issue-types.spec.ts | 130 +++++++++++++++++++++ action.yml | 4 + dist/index.js | 14 +++ src/classes/issues-processor.ts | 18 +++ src/enums/option.ts | 1 + src/interfaces/issues-processor-options.ts | 1 + 7 files changed, 178 insertions(+) create mode 100644 __tests__/exempt-issue-types.spec.ts diff --git a/README.md b/README.md index 4710e9478..852c89174 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Every argument is optional. | [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` | | [sort-by](#sort-by) | What to sort issues and PRs by | `created` | +| [exempt-issue-types](#exempt-issue-types) | Issue types on issues exempted from stale/closed. | | | [only-issue-types](#only-issue-types) | Only issues with a matching type are processed as stale/closed. | | ### List of output options @@ -563,6 +564,15 @@ Useful to sort the issues and PRs by the specified field. It accepts `created`, Default value: `created` +#### exempt-issue-types + +A comma separated list of issue types that can be assigned to issues to exclude them from being marked as stale +(e.g: `Bug,Feature`) + +If unset (or an empty string), this option will not alter the stale workflow. + +Default value: unset + #### only-issue-types A comma separated list of allowed issue types. Only issues with a matching type will be processed (e.g.: `bug,question`). diff --git a/__tests__/exempt-issue-types.spec.ts b/__tests__/exempt-issue-types.spec.ts new file mode 100644 index 000000000..037a78123 --- /dev/null +++ b/__tests__/exempt-issue-types.spec.ts @@ -0,0 +1,130 @@ +import {Issue} from '../src/classes/issue'; +import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; +import {IssuesProcessorMock} from './classes/issues-processor-mock'; +import {DefaultProcessorOptions} from './constants/default-processor-options'; +import {generateIssue} from './functions/generate-issue'; +import {alwaysFalseStateMock} from './classes/state-mock'; + +describe('exempt-issue-types option', () => { + test('should skip issues with an exempt type', async () => { + const opts: IIssuesProcessorOptions = { + ...DefaultProcessorOptions, + exemptIssueTypes: 'question,discussion' + }; + + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A bug', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + false, + undefined, + [], + 'bug' + ), + generateIssue( + opts, + 2, + 'A question', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + false, + undefined, + [], + 'question' + ), + generateIssue( + opts, + 3, + 'A discussion', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + false, + undefined, + [], + 'discussion' + ) + ]; + + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + async p => (p === 1 ? TestIssueList : []), + async () => [], + async () => new Date().toDateString() + ); + + await processor.processIssues(1); + + // The exempt types should not be processed/marked stale + expect(processor.staleIssues.map(i => i.title)).toEqual(['A bug']); + }); + + test('should process all issues if exemptIssueTypes is unset', async () => { + const opts: IIssuesProcessorOptions = { + ...DefaultProcessorOptions, + exemptIssueTypes: '' + }; + + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'A bug', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + false, + undefined, + [], + 'bug' + ), + generateIssue( + opts, + 2, + 'A feature', + '2020-01-01T17:00:00Z', + '2020-01-01T17:00:00Z', + false, + false, + [], + false, + false, + undefined, + [], + 'feature' + ) + ]; + + const processor = new IssuesProcessorMock( + opts, + alwaysFalseStateMock, + async p => (p === 1 ? TestIssueList : []), + async () => [], + async () => new Date().toDateString() + ); + + await processor.processIssues(1); + expect(processor.staleIssues.map(i => i.title)).toEqual([ + 'A bug', + 'A feature' + ]); + }); +}); diff --git a/action.yml b/action.yml index b3354e9d5..8ed1d3690 100644 --- a/action.yml +++ b/action.yml @@ -208,6 +208,10 @@ inputs: description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.' default: 'false' required: false + exempt-issue-types: + description: 'Issues with a matching type are exempt from being processed as stale/closed. Defaults to `[]` (disabled) and can be a comma-separated list of issue types.' + default: '' + required: false only-issue-types: description: 'Only issues with a matching type are processed as stale/closed. Defaults to `[]` (disabled) and can be a comma-separated list of issue types.' default: '' diff --git a/dist/index.js b/dist/index.js index d6d5ab606..8c2e834f5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -525,6 +525,19 @@ class IssuesProcessor { return; } } + // exemptIssueTypes wins if both it and onlyIssueTypes are specified + if (this.options.exemptIssueTypes) { + const exemptTypes = this.options.exemptIssueTypes + .split(',') + .map(t => t.trim().toLowerCase()) + .filter(Boolean); + const issueType = (issue.issue_type || '').toLowerCase(); + if (exemptTypes.includes(issueType)) { + issueLogger.info(`Skipping this $$type because its type ('${issue.issue_type}') is in exemptIssueTypes (${exemptTypes.join(', ')})`); + IssuesProcessor._endIssueProcessing(issue); + return; + } + } const onlyLabels = (0, 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)})`); @@ -2244,6 +2257,7 @@ var Option; Option["IgnorePrUpdates"] = "ignore-pr-updates"; Option["ExemptDraftPr"] = "exempt-draft-pr"; Option["CloseIssueReason"] = "close-issue-reason"; + Option["ExemptIssueTypes"] = "exempt-issue-types"; Option["OnlyIssueTypes"] = "only-issue-types"; })(Option || (exports.Option = Option = {})); diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 3f7b55630..521fd4353 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -269,6 +269,24 @@ export class IssuesProcessor { } } + // exemptIssueTypes wins if both it and onlyIssueTypes are specified + if (this.options.exemptIssueTypes) { + const exemptTypes = this.options.exemptIssueTypes + .split(',') + .map(t => t.trim().toLowerCase()) + .filter(Boolean); + const issueType = (issue.issue_type || '').toLowerCase(); + if (exemptTypes.includes(issueType)) { + issueLogger.info( + `Skipping this $$type because its type ('${ + issue.issue_type + }') is in exemptIssueTypes (${exemptTypes.join(', ')})` + ); + IssuesProcessor._endIssueProcessing(issue); + return; + } + } + const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue)); if (onlyLabels.length > 0) { diff --git a/src/enums/option.ts b/src/enums/option.ts index 3c1bb5158..86104a3c2 100644 --- a/src/enums/option.ts +++ b/src/enums/option.ts @@ -50,5 +50,6 @@ export enum Option { IgnorePrUpdates = 'ignore-pr-updates', ExemptDraftPr = 'exempt-draft-pr', CloseIssueReason = 'close-issue-reason', + ExemptIssueTypes = 'exempt-issue-types', OnlyIssueTypes = 'only-issue-types' } diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 273ae461c..be9f27c1d 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -55,5 +55,6 @@ export interface IIssuesProcessorOptions { exemptDraftPr: boolean; closeIssueReason: string; includeOnlyAssigned: boolean; + exemptIssueTypes?: string; onlyIssueTypes?: string; } From 7d5ee1740a273c10807cf620dca9851d44a7bedb Mon Sep 17 00:00:00 2001 From: Josh Schultz Date: Wed, 15 Oct 2025 22:12:14 +0000 Subject: [PATCH 2/2] Add missing input reading for exempt-issue-types --- dist/index.js | 3 ++- src/main.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 8c2e834f5..58b409d24 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2625,7 +2625,8 @@ function _getAndValidateArgs() { exemptDraftPr: core.getInput('exempt-draft-pr') === 'true', closeIssueReason: core.getInput('close-issue-reason'), includeOnlyAssigned: core.getInput('include-only-assigned') === 'true', - onlyIssueTypes: core.getInput('only-issue-types') + onlyIssueTypes: core.getInput('only-issue-types'), + exemptIssueTypes: core.getInput('exempt-issue-types') }; for (const numberInput of ['days-before-stale']) { if (isNaN(parseFloat(core.getInput(numberInput)))) { diff --git a/src/main.ts b/src/main.ts index 92a33ab58..74b31b25f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -125,7 +125,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { exemptDraftPr: core.getInput('exempt-draft-pr') === 'true', closeIssueReason: core.getInput('close-issue-reason'), includeOnlyAssigned: core.getInput('include-only-assigned') === 'true', - onlyIssueTypes: core.getInput('only-issue-types') + onlyIssueTypes: core.getInput('only-issue-types'), + exemptIssueTypes: core.getInput('exempt-issue-types') }; for (const numberInput of ['days-before-stale']) {