diff --git a/README.md b/README.md index eb65b46b9..c54498532 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/src/classes/author.spec.ts b/src/classes/author.spec.ts new file mode 100644 index 000000000..1ff06218c --- /dev/null +++ b/src/classes/author.spec.ts @@ -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); + }); + }); +}); diff --git a/src/classes/author.ts b/src/classes/author.ts new file mode 100644 index 000000000..266a1dc6e --- /dev/null +++ b/src/classes/author.ts @@ -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, 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; + } +} diff --git a/src/classes/issue.ts b/src/classes/issue.ts index b90631835..c06921faf 100644 --- a/src/classes/issue.ts +++ b/src/classes/issue.ts @@ -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 | Readonly ) { this._options = options; + this.user = issue.user; this.title = issue.title; this.number = issue.number; this.created_at = issue.created_at; diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 486c6a78a..9936dabf0 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -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()) { diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts index defdb75d6..3094bf9e0 100644 --- a/src/interfaces/issue.ts +++ b/src/interfaces/issue.ts @@ -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; diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 930992284..10eb92923 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -38,6 +38,7 @@ export interface IIssuesProcessorOptions { exemptAllMilestones: boolean; exemptAllIssueMilestones: boolean | undefined; exemptAllPrMilestones: boolean | undefined; + anyOfAuthors: string; exemptAssignees: string; exemptIssueAssignees: string; exemptPrAssignees: string; diff --git a/src/main.ts b/src/main.ts index a7836c160..081df6ada 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'),