Skip to content

Commit

Permalink
Exempt issue/pr stale for specific author names
Browse files Browse the repository at this point in the history
This allows excluding specific authors' issues/PRs. Useful for those created by project owners, for example.

Fixes #933
  • Loading branch information
kzu committed Aug 27, 2024
1 parent 3f3b017 commit c845031
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 7 deletions.
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

0 comments on commit c845031

Please sign in to comment.