Skip to content
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

Exempt issue/pr stale for specific author names #1176

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,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` |
| [exempt-authors](#exempt-authors) | Skip issues or pull requests by these authors | |

### List of output options

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

Default value: `false`

#### exempt-authors

Comma separated list of authors to exclude from being marked as stale (e.g: issue, pull request).

Default value: unset

### Usage

See also [action.yml](./action.yml) for a comprehensive list of all the options.
Expand Down
3 changes: 2 additions & 1 deletion __tests__/constants/default-processor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
ignorePrUpdates: undefined,
exemptDraftPr: false,
closeIssueReason: 'not_planned',
includeOnlyAssigned: false
includeOnlyAssigned: false,
exemptAuthors: ''
});
4 changes: 4 additions & 0 deletions __tests__/functions/generate-iissue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function generateIIssue(
title: 'dummy-title',
locked: false,
state: 'dummy-state',
user: {
login: 'dummy-login',
type: 'User'
},
...partialIssue
};
}
7 changes: 6 additions & 1 deletion __tests__/functions/generate-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ export function generateIssue(
isClosed = false,
isLocked = false,
milestone: string | undefined = undefined,
assignees: string[] = []
assignees: string[] = [],
author: string = 'author'
): Issue {
return new Issue(options, {
number: id,
user: {
login: author,
type: 'User'
},
labels: labels.map(l => {
return {name: l};
}),
Expand Down
61 changes: 61 additions & 0 deletions __tests__/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,67 @@ test('stale locked prs will not be closed', async () => {
expect(processor.closedIssues).toHaveLength(0);
});

test('exempt issue authors will not be marked stale', async () => {
const opts = {...DefaultProcessorOptions};
opts.exemptAuthors = 'author';
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Exempt'],
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);

// process our fake issue list
await processor.processIssues(1);

expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
expect(processor.removedLabelIssues.length).toStrictEqual(0);
});

test('non exempt issue authors will be marked stale', async () => {
const opts = {...DefaultProcessorOptions};
opts.exemptAuthors = 'dummy1,dummy2';
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Exempt'],
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);

// process our fake issue list
await processor.processIssues(1);

expect(processor.staleIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});

test('exempt issue labels will not be marked stale', async () => {
expect.assertions(3);
const opts = {...DefaultProcessorOptions};
Expand Down
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
exempt-authors:
description: 'Skip issues or pull requests by these authors.'
default: ''
required: false
outputs:
closed-issues-prs:
description: 'List of all closed issues and pull requests.'
Expand Down
17 changes: 16 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,16 @@ const is_pull_request_1 = __nccwpck_require__(5400);
const operations_1 = __nccwpck_require__(7957);
class Issue {
constructor(options, issue) {
var _a, _b, _c, _d;
this.operations = new operations_1.Operations();
this._options = options;
this.title = issue.title;
this.number = issue.number;
//this.user = issue.user;
this.user = {
login: (_b = (_a = issue.user) === null || _a === void 0 ? void 0 : _a.login) !== null && _b !== void 0 ? _b : "",
type: (_d = (_c = issue.user) === null || _c === void 0 ? void 0 : _c.type) !== null && _d !== void 0 ? _d : "User"
};
this.created_at = issue.created_at;
this.updated_at = issue.updated_at;
this.draft = Boolean(issue.draft);
Expand Down Expand Up @@ -561,6 +567,13 @@ class IssuesProcessor {
IssuesProcessor._endIssueProcessing(issue);
return; // Don't process exempt issues
}
const exemptAuthors = (0, words_to_list_1.wordsToList)(this.options.exemptAuthors);
const hasExemptAuthors = exemptAuthors.some((exemptAuthor) => issue.user.login == exemptAuthor);
if (hasExemptAuthors) {
issueLogger.info(`Skipping this $$type because it contains an exempt author, see ${issueLogger.createOptionLink(option_1.Option.ExemptAuthors)} for more details`);
IssuesProcessor._endIssueProcessing(issue);
return; // Don't process exempt issues
}
const anyOfLabels = (0, words_to_list_1.wordsToList)(this._getAnyOfLabels(issue));
if (anyOfLabels.length > 0) {
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)})`);
Expand Down Expand Up @@ -2222,6 +2235,7 @@ var Option;
Option["IgnorePrUpdates"] = "ignore-pr-updates";
Option["ExemptDraftPr"] = "exempt-draft-pr";
Option["CloseIssueReason"] = "close-issue-reason";
Option["ExemptAuthors"] = "exempt-authors";
})(Option || (exports.Option = Option = {}));


Expand Down Expand Up @@ -2567,7 +2581,8 @@ function _getAndValidateArgs() {
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
closeIssueReason: core.getInput('close-issue-reason'),
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true',
exemptAuthors: core.getInput('exempt-authors'),
};
for (const numberInput of ['days-before-stale']) {
if (isNaN(parseFloat(core.getInput(numberInput)))) {
Expand Down
13 changes: 12 additions & 1 deletion src/classes/issue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,16 @@ describe('Issue', (): void => {
ignorePrUpdates: undefined,
exemptDraftPr: false,
closeIssueReason: '',
includeOnlyAssigned: false
includeOnlyAssigned: false,
exemptAuthors: ''
};
issueInterface = {
title: 'dummy-title',
number: 8,
user: {
login: 'dummy-author',
type: 'User'
},
created_at: 'dummy-created-at',
updated_at: 'dummy-updated-at',
draft: false,
Expand Down Expand Up @@ -106,6 +111,12 @@ describe('Issue', (): void => {
expect(issue.number).toStrictEqual(8);
});

it('should set the author with the given issue author', (): void => {
expect.assertions(1);

expect(issue.user.login).toStrictEqual('dummy-author');
});

it('should set the created_at with the given issue created_at', (): void => {
expect.assertions(1);

Expand Down
7 changes: 7 additions & 0 deletions src/classes/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {IIssue, OctokitIssue} from '../interfaces/issue';
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
import {ILabel} from '../interfaces/label';
import {IMilestone} from '../interfaces/milestone';
import { IUser } from '../interfaces/user';
import {IsoDateString} from '../types/iso-date-string';
import {Operations} from './operations';

export class Issue implements IIssue {
readonly title: string;
readonly number: number;
readonly user: IUser;
created_at: IsoDateString;
updated_at: IsoDateString;
readonly draft: boolean;
Expand All @@ -32,6 +34,11 @@ export class Issue implements IIssue {
this._options = options;
this.title = issue.title;
this.number = issue.number;
//this.user = issue.user;
this.user = {
login: issue.user?.login ?? "",
type: issue.user?.type ?? "User"
};
this.created_at = issue.created_at;
this.updated_at = issue.updated_at;
this.draft = Boolean(issue.draft);
Expand Down
16 changes: 16 additions & 0 deletions src/classes/issues-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,22 @@ export class IssuesProcessor {
return; // Don't process exempt issues
}

const exemptAuthors: string[] = wordsToList(this.options.exemptAuthors);

const hasExemptAuthors = exemptAuthors.some((exemptAuthor: Readonly<string>) =>
issue.user.login == exemptAuthor
);

if (hasExemptAuthors) {
issueLogger.info(
`Skipping this $$type because it contains an exempt author, see ${issueLogger.createOptionLink(
Option.ExemptAuthors
)} for more details`
);
IssuesProcessor._endIssueProcessing(issue);
return; // Don't process exempt issues
}

const anyOfLabels: string[] = wordsToList(this._getAnyOfLabels(issue));

if (anyOfLabels.length > 0) {
Expand Down
11 changes: 11 additions & 0 deletions src/classes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {IUser} from '../interfaces/user';

class User implements IUser {
type: string;
login: string;

constructor(user: IUser) {
this.type = user.type;
this.login = user.login;
}
}
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',
ExemptAuthors = 'exempt-authors',
}
4 changes: 3 additions & 1 deletion src/interfaces/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {Assignee} from './assignee';
import {ILabel} from './label';
import {IMilestone} from './milestone';
import {components} from '@octokit/openapi-types';
import { IUser } from './user';
export interface IIssue {
title: string;
number: number;
user: IUser;
created_at: IsoDateString;
updated_at: IsoDateString;
draft: boolean;
Expand All @@ -17,4 +19,4 @@ export interface IIssue {
assignees?: Assignee[] | null;
}

export type OctokitIssue = components['schemas']['issue'];
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;
exemptAuthors: string;
}
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
closeIssueReason: core.getInput('close-issue-reason'),
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true',
exemptAuthors: core.getInput('exempt-authors'),
};

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