Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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` |
| [only-issue-types](#only-issue-types) | Only issues with a matching type are processed as stale/closed. | |

### List of output options

Expand Down Expand Up @@ -548,6 +549,14 @@ If set to `true`, only the issues or the pull requests with an assignee will be

Default value: `false`

#### 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

See also [action.yml](./action.yml) for a comprehensive list of all the options.
Expand Down
6 changes: 4 additions & 2 deletions __tests__/functions/generate-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export function generateIssue(
isClosed = false,
isLocked = false,
milestone: string | undefined = undefined,
assignees: string[] = []
assignees: string[] = [],
issue_type?: string
): Issue {
return new Issue(options, {
number: id,
Expand All @@ -39,6 +40,7 @@ export function generateIssue(
login: assignee,
type: 'User'
};
})
}),
...(issue_type ? {type: {name: issue_type}} : {})
});
}
125 changes: 125 additions & 0 deletions __tests__/only-issue-types.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
]);
});
});
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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:
closed-issues-prs:
description: 'List of all closed issues and pull requests.'
Expand Down
20 changes: 20 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ class Issue {
this.assignees = issue.assignees || [];
this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel);
this.markedStaleThisRun = false;
if (typeof issue.type === 'object' &&
issue.type !== null) {
this.issue_type = issue.type.name;
}
else {
this.issue_type = undefined;
}
}
get isPullRequest() {
return (0, is_pull_request_1.isPullRequest)(this);
Expand Down Expand Up @@ -505,6 +512,18 @@ class IssuesProcessor {
IssuesProcessor._endIssueProcessing(issue);
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));
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)})`);
Expand Down Expand Up @@ -2222,6 +2241,7 @@ var Option;
Option["IgnorePrUpdates"] = "ignore-pr-updates";
Option["ExemptDraftPr"] = "exempt-draft-pr";
Option["CloseIssueReason"] = "close-issue-reason";
Option["OnlyIssueTypes"] = "only-issue-types";
})(Option || (exports.Option = Option = {}));


Expand Down
10 changes: 10 additions & 0 deletions src/classes/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class Issue implements IIssue {
markedStaleThisRun: boolean;
operations = new Operations();
private readonly _options: IIssuesProcessorOptions;
readonly issue_type?: string;

constructor(
options: Readonly<IIssuesProcessorOptions>,
Expand All @@ -43,6 +44,15 @@ export class Issue implements IIssue {
this.assignees = issue.assignees || [];
this.isStale = isLabeled(this, this.staleLabel);
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 {
Expand Down
17 changes: 17 additions & 0 deletions src/classes/issues-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,23 @@ export class IssuesProcessor {
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));

if (onlyLabels.length > 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/enums/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ export enum Option {
IgnoreIssueUpdates = 'ignore-issue-updates',
IgnorePrUpdates = 'ignore-pr-updates',
ExemptDraftPr = 'exempt-draft-pr',
CloseIssueReason = 'close-issue-reason'
CloseIssueReason = 'close-issue-reason',
OnlyIssueTypes = 'only-issue-types'
}
1 change: 1 addition & 0 deletions src/interfaces/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface IIssue {
locked: boolean;
milestone?: IMilestone | null;
assignees?: Assignee[] | null;
issue_type?: string;
}

export type OctokitIssue = components['schemas']['issue'];
1 change: 1 addition & 0 deletions src/interfaces/issues-processor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ export interface IIssuesProcessorOptions {
exemptDraftPr: boolean;
closeIssueReason: string;
includeOnlyAssigned: boolean;
onlyIssueTypes?: string;
}