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
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down Expand Up @@ -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).
Expand Down
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
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/classes/issues-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()) {
Expand Down
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;
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/issues-processor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface IIssuesProcessorOptions {
exemptAllMilestones: boolean;
exemptAllIssueMilestones: boolean | undefined;
exemptAllPrMilestones: boolean | undefined;
anyOfAuthors: string;
exemptAssignees: string;
exemptIssueAssignees: string;
exemptPrAssignees: string;
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down