Add only-issue-types option to filter issues by type (#1255)

* Add `only-issue-types` Option to Filter Issues by Type

* white-space fix in readme table

Co-authored-by: andig <cpuidle@gmail.com>

---------

Co-authored-by: andig <cpuidle@gmail.com>
This commit is contained in:
Bibo-Joshi
2025-10-03 20:57:41 +02:00
committed by GitHub
parent 3a9db7e6a4
commit 5f858e3efb
10 changed files with 193 additions and 4 deletions

View File

@@ -99,6 +99,7 @@ Every argument is optional.
| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs 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` | | [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` |
| [sort-by](#sort-by) | What to sort issues and PRs by | `created` | | [sort-by](#sort-by) | What to sort issues and PRs by | `created` |
| [only-issue-types](#only-issue-types) | Only issues with a matching type are processed as stale/closed. | |
### List of output options ### List of output options
@@ -555,6 +556,13 @@ Useful to sort the issues and PRs by the specified field. It accepts `created`,
Default value: `created` Default value: `created`
#### only-issue-types
A comma separated list of allowed issue types. Only issues with a matching type will be processed (e.g.: `bug,question`).
If unset (or an empty string), this option will not alter the stale workflow.
Default value: unset
### Usage ### Usage

View File

@@ -15,7 +15,8 @@ export function generateIssue(
isClosed = false, isClosed = false,
isLocked = false, isLocked = false,
milestone: string | undefined = undefined, milestone: string | undefined = undefined,
assignees: string[] = [] assignees: string[] = [],
issue_type?: string
): Issue { ): Issue {
return new Issue(options, { return new Issue(options, {
number: id, number: id,
@@ -39,6 +40,7 @@ export function generateIssue(
login: assignee, login: assignee,
type: 'User' type: 'User'
}; };
}) }),
...(issue_type ? {type: {name: issue_type}} : {})
}); });
} }

View File

@@ -0,0 +1,125 @@
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('only-issue-types option', () => {
test('should only process issues with allowed type', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
onlyIssueTypes: 'bug,question'
};
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'
),
generateIssue(
opts,
3,
'A question',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
[],
false,
false,
undefined,
[],
'question'
)
];
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 question'
]);
});
test('should process all issues if onlyIssueTypes is unset', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
onlyIssueTypes: ''
};
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'
]);
});
});

View File

@@ -208,6 +208,10 @@ inputs:
description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.' description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.'
default: 'false' default: 'false'
required: false 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: ''
required: false
outputs: outputs:
closed-issues-prs: closed-issues-prs:
description: 'List of all closed issues and pull requests.' description: 'List of all closed issues and pull requests.'

20
dist/index.js vendored
View File

@@ -289,6 +289,13 @@ class Issue {
this.assignees = issue.assignees || []; this.assignees = issue.assignees || [];
this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel); this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel);
this.markedStaleThisRun = false; this.markedStaleThisRun = false;
if (typeof issue.type === 'object' &&
issue.type !== null) {
this.issue_type = issue.type.name;
}
else {
this.issue_type = undefined;
}
} }
get isPullRequest() { get isPullRequest() {
return (0, is_pull_request_1.isPullRequest)(this); return (0, is_pull_request_1.isPullRequest)(this);
@@ -506,6 +513,18 @@ class IssuesProcessor {
IssuesProcessor._endIssueProcessing(issue); IssuesProcessor._endIssueProcessing(issue);
return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list
} }
if (this.options.onlyIssueTypes) {
const allowedTypes = this.options.onlyIssueTypes
.split(',')
.map(t => t.trim().toLowerCase())
.filter(Boolean);
const issueType = (issue.issue_type || '').toLowerCase();
if (!allowedTypes.includes(issueType)) {
issueLogger.info(`Skipping this $$type because its type ('${issue.issue_type}') is not in onlyIssueTypes (${allowedTypes.join(', ')})`);
IssuesProcessor._endIssueProcessing(issue);
return;
}
}
const onlyLabels = (0, words_to_list_1.wordsToList)(this._getOnlyLabels(issue)); const onlyLabels = (0, words_to_list_1.wordsToList)(this._getOnlyLabels(issue));
if (onlyLabels.length > 0) { 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)})`); 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)})`);
@@ -2225,6 +2244,7 @@ var Option;
Option["IgnorePrUpdates"] = "ignore-pr-updates"; Option["IgnorePrUpdates"] = "ignore-pr-updates";
Option["ExemptDraftPr"] = "exempt-draft-pr"; Option["ExemptDraftPr"] = "exempt-draft-pr";
Option["CloseIssueReason"] = "close-issue-reason"; Option["CloseIssueReason"] = "close-issue-reason";
Option["OnlyIssueTypes"] = "only-issue-types";
})(Option || (exports.Option = Option = {})); })(Option || (exports.Option = Option = {}));

View File

@@ -24,6 +24,7 @@ export class Issue implements IIssue {
markedStaleThisRun: boolean; markedStaleThisRun: boolean;
operations = new Operations(); operations = new Operations();
private readonly _options: IIssuesProcessorOptions; private readonly _options: IIssuesProcessorOptions;
readonly issue_type?: string;
constructor( constructor(
options: Readonly<IIssuesProcessorOptions>, options: Readonly<IIssuesProcessorOptions>,
@@ -43,6 +44,15 @@ export class Issue implements IIssue {
this.assignees = issue.assignees || []; this.assignees = issue.assignees || [];
this.isStale = isLabeled(this, this.staleLabel); this.isStale = isLabeled(this, this.staleLabel);
this.markedStaleThisRun = false; this.markedStaleThisRun = false;
if (
typeof (issue as any).type === 'object' &&
(issue as any).type !== null
) {
this.issue_type = (issue as any).type.name;
} else {
this.issue_type = undefined;
}
} }
get isPullRequest(): boolean { get isPullRequest(): boolean {

View File

@@ -252,6 +252,23 @@ export class IssuesProcessor {
return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list
} }
if (this.options.onlyIssueTypes) {
const allowedTypes = this.options.onlyIssueTypes
.split(',')
.map(t => t.trim().toLowerCase())
.filter(Boolean);
const issueType = (issue.issue_type || '').toLowerCase();
if (!allowedTypes.includes(issueType)) {
issueLogger.info(
`Skipping this $$type because its type ('${
issue.issue_type
}') is not in onlyIssueTypes (${allowedTypes.join(', ')})`
);
IssuesProcessor._endIssueProcessing(issue);
return;
}
}
const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue)); const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue));
if (onlyLabels.length > 0) { if (onlyLabels.length > 0) {

View File

@@ -49,5 +49,6 @@ export enum Option {
IgnoreIssueUpdates = 'ignore-issue-updates', IgnoreIssueUpdates = 'ignore-issue-updates',
IgnorePrUpdates = 'ignore-pr-updates', IgnorePrUpdates = 'ignore-pr-updates',
ExemptDraftPr = 'exempt-draft-pr', ExemptDraftPr = 'exempt-draft-pr',
CloseIssueReason = 'close-issue-reason' CloseIssueReason = 'close-issue-reason',
OnlyIssueTypes = 'only-issue-types'
} }

View File

@@ -15,6 +15,7 @@ export interface IIssue {
locked: boolean; locked: boolean;
milestone?: IMilestone | null; milestone?: IMilestone | null;
assignees?: Assignee[] | null; assignees?: Assignee[] | null;
issue_type?: string;
} }
export type OctokitIssue = components['schemas']['issue']; export type OctokitIssue = components['schemas']['issue'];

View File

@@ -55,4 +55,5 @@ export interface IIssuesProcessorOptions {
exemptDraftPr: boolean; exemptDraftPr: boolean;
closeIssueReason: string; closeIssueReason: string;
includeOnlyAssigned: boolean; includeOnlyAssigned: boolean;
onlyIssueTypes?: string;
} }