mirror of
https://github.com/actions/stale.git
synced 2025-12-10 03:57:04 +00:00
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:
10
README.md
10
README.md
@@ -98,7 +98,8 @@ Every argument is optional.
|
|||||||
| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | |
|
| [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 | |
|
| [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
|
||||||
|
|
||||||
|
|||||||
@@ -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}} : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
125
__tests__/only-issue-types.spec.ts
Normal file
125
__tests__/only-issue-types.spec.ts
Normal 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'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
20
dist/index.js
vendored
@@ -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 = {}));
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -55,4 +55,5 @@ export interface IIssuesProcessorOptions {
|
|||||||
exemptDraftPr: boolean;
|
exemptDraftPr: boolean;
|
||||||
closeIssueReason: string;
|
closeIssueReason: string;
|
||||||
includeOnlyAssigned: boolean;
|
includeOnlyAssigned: boolean;
|
||||||
|
onlyIssueTypes?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user