Skip to content

Commit c845031

Browse files
committed
Exempt issue/pr stale for specific author names
This allows excluding specific authors' issues/PRs. Useful for those created by project owners, for example. Fixes #933
1 parent 3f3b017 commit c845031

15 files changed

+154
-7
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Every argument is optional.
9797
| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | |
9898
| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | |
9999
| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` |
100+
| [exempt-authors](#exempt-authors) | Skip issues or pull requests by these authors | |
100101

101102
### List of output options
102103

@@ -547,6 +548,12 @@ If set to `true`, only the issues or the pull requests with an assignee will be
547548

548549
Default value: `false`
549550

551+
#### exempt-authors
552+
553+
Comma separated list of authors to exclude from being marked as stale (e.g: issue, pull request).
554+
555+
Default value: unset
556+
550557
### Usage
551558

552559
See also [action.yml](./action.yml) for a comprehensive list of all the options.

__tests__/constants/default-processor-options.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
5555
ignorePrUpdates: undefined,
5656
exemptDraftPr: false,
5757
closeIssueReason: 'not_planned',
58-
includeOnlyAssigned: false
58+
includeOnlyAssigned: false,
59+
exemptAuthors: ''
5960
});

__tests__/functions/generate-iissue.ts

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export function generateIIssue(
1515
title: 'dummy-title',
1616
locked: false,
1717
state: 'dummy-state',
18+
user: {
19+
login: 'dummy-login',
20+
type: 'User'
21+
},
1822
...partialIssue
1923
};
2024
}

__tests__/functions/generate-issue.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ export function generateIssue(
1515
isClosed = false,
1616
isLocked = false,
1717
milestone: string | undefined = undefined,
18-
assignees: string[] = []
18+
assignees: string[] = [],
19+
author: string = 'author'
1920
): Issue {
2021
return new Issue(options, {
2122
number: id,
23+
user: {
24+
login: author,
25+
type: 'User'
26+
},
2227
labels: labels.map(l => {
2328
return {name: l};
2429
}),

__tests__/main.spec.ts

+61
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,67 @@ test('stale locked prs will not be closed', async () => {
10181018
expect(processor.closedIssues).toHaveLength(0);
10191019
});
10201020

1021+
test('exempt issue authors will not be marked stale', async () => {
1022+
const opts = {...DefaultProcessorOptions};
1023+
opts.exemptAuthors = 'author';
1024+
const TestIssueList: Issue[] = [
1025+
generateIssue(
1026+
opts,
1027+
1,
1028+
'My first issue',
1029+
'2020-01-01T17:00:00Z',
1030+
'2020-01-01T17:00:00Z',
1031+
false,
1032+
false,
1033+
['Exempt'],
1034+
)
1035+
];
1036+
const processor = new IssuesProcessorMock(
1037+
opts,
1038+
alwaysFalseStateMock,
1039+
async p => (p === 1 ? TestIssueList : []),
1040+
async () => [],
1041+
async () => new Date().toDateString()
1042+
);
1043+
1044+
// process our fake issue list
1045+
await processor.processIssues(1);
1046+
1047+
expect(processor.staleIssues.length).toStrictEqual(0);
1048+
expect(processor.closedIssues.length).toStrictEqual(0);
1049+
expect(processor.removedLabelIssues.length).toStrictEqual(0);
1050+
});
1051+
1052+
test('non exempt issue authors will be marked stale', async () => {
1053+
const opts = {...DefaultProcessorOptions};
1054+
opts.exemptAuthors = 'dummy1,dummy2';
1055+
const TestIssueList: Issue[] = [
1056+
generateIssue(
1057+
opts,
1058+
1,
1059+
'My first issue',
1060+
'2020-01-01T17:00:00Z',
1061+
'2020-01-01T17:00:00Z',
1062+
false,
1063+
false,
1064+
['Exempt'],
1065+
)
1066+
];
1067+
const processor = new IssuesProcessorMock(
1068+
opts,
1069+
alwaysFalseStateMock,
1070+
async p => (p === 1 ? TestIssueList : []),
1071+
async () => [],
1072+
async () => new Date().toDateString()
1073+
);
1074+
1075+
// process our fake issue list
1076+
await processor.processIssues(1);
1077+
1078+
expect(processor.staleIssues).toHaveLength(1);
1079+
expect(processor.closedIssues).toHaveLength(0);
1080+
});
1081+
10211082
test('exempt issue labels will not be marked stale', async () => {
10221083
expect.assertions(3);
10231084
const opts = {...DefaultProcessorOptions};

action.yml

+4
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ inputs:
204204
description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.'
205205
default: 'false'
206206
required: false
207+
exempt-authors:
208+
description: 'Skip issues or pull requests by these authors.'
209+
default: ''
210+
required: false
207211
outputs:
208212
closed-issues-prs:
209213
description: 'List of all closed issues and pull requests.'

dist/index.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,16 @@ const is_pull_request_1 = __nccwpck_require__(5400);
274274
const operations_1 = __nccwpck_require__(7957);
275275
class Issue {
276276
constructor(options, issue) {
277+
var _a, _b, _c, _d;
277278
this.operations = new operations_1.Operations();
278279
this._options = options;
279280
this.title = issue.title;
280281
this.number = issue.number;
282+
//this.user = issue.user;
283+
this.user = {
284+
login: (_b = (_a = issue.user) === null || _a === void 0 ? void 0 : _a.login) !== null && _b !== void 0 ? _b : "",
285+
type: (_d = (_c = issue.user) === null || _c === void 0 ? void 0 : _c.type) !== null && _d !== void 0 ? _d : "User"
286+
};
281287
this.created_at = issue.created_at;
282288
this.updated_at = issue.updated_at;
283289
this.draft = Boolean(issue.draft);
@@ -561,6 +567,13 @@ class IssuesProcessor {
561567
IssuesProcessor._endIssueProcessing(issue);
562568
return; // Don't process exempt issues
563569
}
570+
const exemptAuthors = (0, words_to_list_1.wordsToList)(this.options.exemptAuthors);
571+
const hasExemptAuthors = exemptAuthors.some((exemptAuthor) => issue.user.login == exemptAuthor);
572+
if (hasExemptAuthors) {
573+
issueLogger.info(`Skipping this $$type because it contains an exempt author, see ${issueLogger.createOptionLink(option_1.Option.ExemptAuthors)} for more details`);
574+
IssuesProcessor._endIssueProcessing(issue);
575+
return; // Don't process exempt issues
576+
}
564577
const anyOfLabels = (0, words_to_list_1.wordsToList)(this._getAnyOfLabels(issue));
565578
if (anyOfLabels.length > 0) {
566579
issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.AnyOfLabels)} was specified to only process the issues and pull requests with one of those labels (${logger_service_1.LoggerService.cyan(anyOfLabels.length)})`);
@@ -2222,6 +2235,7 @@ var Option;
22222235
Option["IgnorePrUpdates"] = "ignore-pr-updates";
22232236
Option["ExemptDraftPr"] = "exempt-draft-pr";
22242237
Option["CloseIssueReason"] = "close-issue-reason";
2238+
Option["ExemptAuthors"] = "exempt-authors";
22252239
})(Option || (exports.Option = Option = {}));
22262240

22272241

@@ -2567,7 +2581,8 @@ function _getAndValidateArgs() {
25672581
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
25682582
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
25692583
closeIssueReason: core.getInput('close-issue-reason'),
2570-
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
2584+
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true',
2585+
exemptAuthors: core.getInput('exempt-authors'),
25712586
};
25722587
for (const numberInput of ['days-before-stale']) {
25732588
if (isNaN(parseFloat(core.getInput(numberInput)))) {

src/classes/issue.spec.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,16 @@ describe('Issue', (): void => {
6464
ignorePrUpdates: undefined,
6565
exemptDraftPr: false,
6666
closeIssueReason: '',
67-
includeOnlyAssigned: false
67+
includeOnlyAssigned: false,
68+
exemptAuthors: ''
6869
};
6970
issueInterface = {
7071
title: 'dummy-title',
7172
number: 8,
73+
user: {
74+
login: 'dummy-author',
75+
type: 'User'
76+
},
7277
created_at: 'dummy-created-at',
7378
updated_at: 'dummy-updated-at',
7479
draft: false,
@@ -106,6 +111,12 @@ describe('Issue', (): void => {
106111
expect(issue.number).toStrictEqual(8);
107112
});
108113

114+
it('should set the author with the given issue author', (): void => {
115+
expect.assertions(1);
116+
117+
expect(issue.user.login).toStrictEqual('dummy-author');
118+
});
119+
109120
it('should set the created_at with the given issue created_at', (): void => {
110121
expect.assertions(1);
111122

src/classes/issue.ts

+7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {IIssue, OctokitIssue} from '../interfaces/issue';
55
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
66
import {ILabel} from '../interfaces/label';
77
import {IMilestone} from '../interfaces/milestone';
8+
import { IUser } from '../interfaces/user';
89
import {IsoDateString} from '../types/iso-date-string';
910
import {Operations} from './operations';
1011

1112
export class Issue implements IIssue {
1213
readonly title: string;
1314
readonly number: number;
15+
readonly user: IUser;
1416
created_at: IsoDateString;
1517
updated_at: IsoDateString;
1618
readonly draft: boolean;
@@ -32,6 +34,11 @@ export class Issue implements IIssue {
3234
this._options = options;
3335
this.title = issue.title;
3436
this.number = issue.number;
37+
//this.user = issue.user;
38+
this.user = {
39+
login: issue.user?.login ?? "",
40+
type: issue.user?.type ?? "User"
41+
};
3542
this.created_at = issue.created_at;
3643
this.updated_at = issue.updated_at;
3744
this.draft = Boolean(issue.draft);

src/classes/issues-processor.ts

+16
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,22 @@ export class IssuesProcessor {
368368
return; // Don't process exempt issues
369369
}
370370

371+
const exemptAuthors: string[] = wordsToList(this.options.exemptAuthors);
372+
373+
const hasExemptAuthors = exemptAuthors.some((exemptAuthor: Readonly<string>) =>
374+
issue.user.login == exemptAuthor
375+
);
376+
377+
if (hasExemptAuthors) {
378+
issueLogger.info(
379+
`Skipping this $$type because it contains an exempt author, see ${issueLogger.createOptionLink(
380+
Option.ExemptAuthors
381+
)} for more details`
382+
);
383+
IssuesProcessor._endIssueProcessing(issue);
384+
return; // Don't process exempt issues
385+
}
386+
371387
const anyOfLabels: string[] = wordsToList(this._getAnyOfLabels(issue));
372388

373389
if (anyOfLabels.length > 0) {

src/classes/user.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {IUser} from '../interfaces/user';
2+
3+
class User implements IUser {
4+
type: string;
5+
login: string;
6+
7+
constructor(user: IUser) {
8+
this.type = user.type;
9+
this.login = user.login;
10+
}
11+
}

src/enums/option.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ export enum Option {
4848
IgnoreIssueUpdates = 'ignore-issue-updates',
4949
IgnorePrUpdates = 'ignore-pr-updates',
5050
ExemptDraftPr = 'exempt-draft-pr',
51-
CloseIssueReason = 'close-issue-reason'
51+
CloseIssueReason = 'close-issue-reason',
52+
ExemptAuthors = 'exempt-authors',
5253
}

src/interfaces/issue.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {Assignee} from './assignee';
33
import {ILabel} from './label';
44
import {IMilestone} from './milestone';
55
import {components} from '@octokit/openapi-types';
6+
import { IUser } from './user';
67
export interface IIssue {
78
title: string;
89
number: number;
10+
user: IUser;
911
created_at: IsoDateString;
1012
updated_at: IsoDateString;
1113
draft: boolean;
@@ -17,4 +19,4 @@ export interface IIssue {
1719
assignees?: Assignee[] | null;
1820
}
1921

20-
export type OctokitIssue = components['schemas']['issue'];
22+
export type OctokitIssue = components['schemas']['issue'];

src/interfaces/issues-processor-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ export interface IIssuesProcessorOptions {
5454
exemptDraftPr: boolean;
5555
closeIssueReason: string;
5656
includeOnlyAssigned: boolean;
57+
exemptAuthors: string;
5758
}

src/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
123123
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
124124
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
125125
closeIssueReason: core.getInput('close-issue-reason'),
126-
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
126+
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true',
127+
exemptAuthors: core.getInput('exempt-authors'),
127128
};
128129

129130
for (const numberInput of ['days-before-stale']) {

0 commit comments

Comments
 (0)