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

Add anyOfAuthors allowlist option #1181

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ Every argument is optional.
| Input | Description | Default |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------- |
| [repo-token](#repo-token) | PAT for GitHub API authentication | `${{ github.token }}` |
| [author-allow-list](#author-allow-list) | Allow list for Issue or PR authors to consider | |
| [days-before-stale](#days-before-stale) | Idle number of days before marking issues/PRs stale | `60` |
| [days-before-issue-stale](#days-before-issue-stale) | Override [days-before-stale](#days-before-stale) for issues only | |
| [days-before-pr-stale](#days-before-pr-stale) | Override [days-before-stale](#days-before-stale) for PRs only | |
@@ -464,6 +465,15 @@ Override [exempt-all-milestones](#exempt-all-milestones) but only to exempt the

Default value: unset

#### any-of-authors

An allow-list of author(s) to only process the issues or the pull requests for.
It can be a comma separated list of usernames (e.g: `marco,polo`).

If unset (or an empty string), this option will not alter the stale workflow.

Default value: unset

#### exempt-assignees

An allow-list of assignee(s) to only process the issues or the pull requests that does not contain one of these assignee(s).
75 changes: 75 additions & 0 deletions src/classes/author.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { DefaultProcessorOptions } from "../../__tests__/constants/default-processor-options";
import { generateIIssue } from "../../__tests__/functions/generate-iissue";
import { IIssue } from "../interfaces/issue";
import { IIssuesProcessorOptions } from "../interfaces/issues-processor-options";
import { Author } from "./author";
import { Issue } from "./issue";

describe("Authors", (): void => {
let author: Author;
let optionsInterface: IIssuesProcessorOptions;
let issue: Issue;
let issueInterface: IIssue;

beforeEach((): void => {
optionsInterface = {
...DefaultProcessorOptions,
};
issueInterface = generateIIssue();
});

describe("should exempt", (): void => {
it("because issue.user is one of options.anyOfAuthors", (): void => {
optionsInterface.anyOfAuthors = "foo,bar,foobar123";
issueInterface.user = { type: "User", login: "foobar123" };

expect.assertions(1);
issue = new Issue(optionsInterface, issueInterface);
author = new Author(optionsInterface, issue);

const result = author.shouldExemptAuthor();

expect(result).toStrictEqual(true);
});
});

describe("should not exempt", (): void => {
it("because options.anyOfAuthors is not set", (): void => {
optionsInterface.anyOfAuthors = "";
issueInterface.user = null;
expect.assertions(1);
issue = new Issue(optionsInterface, issueInterface);
author = new Author(optionsInterface, issue);

const result = author.shouldExemptAuthor();

expect(result).toStrictEqual(false);
});

it("because issue.user is not set", (): void => {
optionsInterface.anyOfAuthors = "foo,bar";
issueInterface.user = null;

expect.assertions(1);
issue = new Issue(optionsInterface, issueInterface);
author = new Author(optionsInterface, issue);

const result = author.shouldExemptAuthor();

expect(result).toStrictEqual(false);
});

it("because issue.user is not one of options.anyOfAuthors", (): void => {
optionsInterface.anyOfAuthors = "foo,bar";
issueInterface.user = { type: "User", login: "foobar123" };

expect.assertions(1);
issue = new Issue(optionsInterface, issueInterface);
author = new Author(optionsInterface, issue);

const result = author.shouldExemptAuthor();

expect(result).toStrictEqual(false);
});
});
});
42 changes: 42 additions & 0 deletions src/classes/author.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import deburr from 'lodash.deburr';
import {Option} from '../enums/option';
import {wordsToList} from '../functions/words-to-list';
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
import {Issue} from './issue';
import {IssueLogger} from './loggers/issue-logger';
import {LoggerService} from '../services/logger.service';

export class Author {
private readonly _options: IIssuesProcessorOptions;
private readonly _issue: Issue;
private readonly _issueLogger: IssueLogger;

private readonly _anyOfAuthors: string[];

constructor(options: Readonly<IIssuesProcessorOptions>, issue: Issue) {
this._options = options;
this._issue = issue;
this._issueLogger = new IssueLogger(issue);

// allow-list of authors that should only be processed
this._anyOfAuthors = wordsToList(options.anyOfAuthors);
}

shouldExemptAuthor(): boolean {
if(this._issue.user === null) {
return false;
}

if(this._anyOfAuthors.length > 0) {
// if author is in the allow-list, return false to not skip processing this issue
if(this._anyOfAuthors.indexOf(this._issue.user.login) > -1) {
return false;
}

// else, return true to skip this issue because the user is not in the allow-list
return true;
}

return false;
}
}
3 changes: 3 additions & 0 deletions src/classes/issue.ts
Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@ 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 user: IUser | null;
readonly title: string;
readonly number: number;
created_at: IsoDateString;
@@ -30,6 +32,7 @@ export class Issue implements IIssue {
issue: Readonly<OctokitIssue> | Readonly<IIssue>
) {
this._options = options;
this.user = issue.user;
this.title = issue.title;
this.number = issue.number;
this.created_at = issue.created_at;
8 changes: 8 additions & 0 deletions src/classes/issues-processor.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import {ExemptDraftPullRequest} from './exempt-draft-pull-request';
import {Issue} from './issue';
import {IssueLogger} from './loggers/issue-logger';
import {Logger} from './loggers/logger';
import {Author} from './author';
import {Milestones} from './milestones';
import {StaleOperations} from './stale-operations';
import {Statistics} from './statistics';
@@ -414,6 +415,13 @@ export class IssuesProcessor {
);
}

const author: Author = new Author(this.options, issue);

if(author.shouldExemptAuthor()) {
IssuesProcessor._endIssueProcessing(issue);
return; // Don't process exempt author
}

const milestones: Milestones = new Milestones(this.options, issue);

if (milestones.shouldExemptMilestones()) {
2 changes: 2 additions & 0 deletions src/interfaces/issue.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {IsoDateString} from '../types/iso-date-string';
import {Assignee} from './assignee';
import {IUser} from './user';
import {ILabel} from './label';
import {IMilestone} from './milestone';
import {components} from '@octokit/openapi-types';
export interface IIssue {
user: IUser | null;
title: string;
number: number;
created_at: IsoDateString;
1 change: 1 addition & 0 deletions src/interfaces/issues-processor-options.ts
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ export interface IIssuesProcessorOptions {
exemptAllMilestones: boolean;
exemptAllIssueMilestones: boolean | undefined;
exemptAllPrMilestones: boolean | undefined;
anyOfAuthors: string;
exemptAssignees: string;
exemptIssueAssignees: string;
exemptPrAssignees: string;
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
exemptAllMilestones: core.getInput('exempt-all-milestones') === 'true',
exemptAllIssueMilestones: _toOptionalBoolean('exempt-all-issue-milestones'),
exemptAllPrMilestones: _toOptionalBoolean('exempt-all-pr-milestones'),
anyOfAuthors: core.getInput('any-of-authors'),
exemptAssignees: core.getInput('exempt-assignees'),
exemptIssueAssignees: core.getInput('exempt-issue-assignees'),
exemptPrAssignees: core.getInput('exempt-pr-assignees'),