diff --git a/__tests__/functions/generate-issue.ts b/__tests__/functions/generate-issue.ts index 0198c42c..08c4fbb5 100644 --- a/__tests__/functions/generate-issue.ts +++ b/__tests__/functions/generate-issue.ts @@ -15,7 +15,8 @@ export function generateIssue( isClosed = false, isLocked = false, milestone: string | undefined = undefined, - assignees: string[] = [] + assignees: string[] = [], + userLogin: string | undefined = undefined ): Issue { return new Issue(options, { number: id, @@ -39,6 +40,10 @@ export function generateIssue( login: assignee, type: 'User' }; - }) + }), + user: { + login: userLogin ? userLogin : 'dummy-test-user', + type: 'User' + } }); } diff --git a/__tests__/main.spec.ts b/__tests__/main.spec.ts index 80d660e8..4814d675 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -6,6 +6,7 @@ import {IssuesProcessorMock} from './classes/issues-processor-mock'; import {DefaultProcessorOptions} from './constants/default-processor-options'; import {generateIssue} from './functions/generate-issue'; import {alwaysFalseStateMock} from './classes/state-mock'; +import {isPullRequest} from '../src/functions/is-pull-request'; test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to 0', async () => { const opts: IIssuesProcessorOptions = { @@ -2743,3 +2744,103 @@ test('processing an issue with the "includeOnlyAssigned" option set and no assig expect(processor.staleIssues).toHaveLength(0); expect(processor.closedIssues).toHaveLength(0); }); + +test('interpolate stale message on prs when there is placeholder', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.stalePrMessage = 'Hello {author}, Please take care of this pr!'; + const lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const loginUser = 'dummy-user'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'An issue that should be marked stale but not closed.', + lastUpdate.toString(), + lastUpdate.toString(), + true, + [], + false, + false, + undefined, + [], + loginUser + ) + ]; + const processor = new IssuesProcessorMock( + opts, + async p => (p === 1 ? TestIssueList : []), + async () => [], + async () => new Date().toDateString() + ); + + // for sake of testing, mocking private function + const markSpy = jest.spyOn(processor as any, '_markStale'); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); + + // comment should be created with placeholder replaced. + expect(markSpy).toHaveBeenCalledWith( + TestIssueList[0], + 'Hello @dummy-user, Please take care of this pr!', + opts.stalePrLabel, + false + ); +}); + +test('interpolate stale message on issues when there is placeholder', async () => { + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.staleIssueMessage = 'Hello {author}, Please take care of this issue!'; + const lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const loginUser = 'dummy-user'; + const TestIssueList: Issue[] = [ + generateIssue( + opts, + 1, + 'An issue that should be marked stale but not closed', + lastUpdate.toString(), + lastUpdate.toString(), + false, + [], + false, + false, + undefined, + [], + loginUser + ) + ]; + const processor = new IssuesProcessorMock( + opts, + async p => (p === 1 ? TestIssueList : []), + async () => [], + async () => new Date().toDateString() + ); + + // for sake of testing, mocking private function + const markSpy = jest.spyOn(processor as any, '_markStale'); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues).toHaveLength(0); + expect(processor.removedLabelIssues).toHaveLength(0); + expect(processor.staleIssues).toHaveLength(1); + + // comment should be created with placeholder replaced. + expect(markSpy).toHaveBeenCalledWith( + TestIssueList[0], + 'Hello @dummy-user, Please take care of this issue!', + opts.staleIssueLabel, + false + ); +}); diff --git a/src/classes/issue.ts b/src/classes/issue.ts index b9063183..e1025724 100644 --- a/src/classes/issue.ts +++ b/src/classes/issue.ts @@ -7,6 +7,7 @@ import {ILabel} from '../interfaces/label'; import {IMilestone} from '../interfaces/milestone'; import {IsoDateString} from '../types/iso-date-string'; import {Operations} from './operations'; +import {IUser} from '../interfaces/user'; export class Issue implements IIssue { readonly title: string; @@ -24,6 +25,7 @@ export class Issue implements IIssue { markedStaleThisRun: boolean; operations = new Operations(); private readonly _options: IIssuesProcessorOptions; + readonly user?: IUser | null; constructor( options: Readonly, @@ -43,6 +45,7 @@ export class Issue implements IIssue { this.assignees = issue.assignees || []; this.isStale = isLabeled(this, this.staleLabel); this.markedStaleThisRun = false; + this.user = issue.user } get isPullRequest(): boolean { diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 486c6a78..998cd6b2 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -212,12 +212,18 @@ export class IssuesProcessor { ); // calculate string based messages for this issue - const staleMessage: string = issue.isPullRequest - ? this.options.stalePrMessage - : this.options.staleIssueMessage; - const closeMessage: string = issue.isPullRequest - ? this.options.closePrMessage - : this.options.closeIssueMessage; + const staleMessage: string = this._interpolatePlaceholders( + issue, + issue.isPullRequest + ? this.options.stalePrMessage + : this.options.staleIssueMessage + ); + const closeMessage: string = this._interpolatePlaceholders( + issue, + issue.isPullRequest + ? this.options.closePrMessage + : this.options.closeIssueMessage + ); const staleLabel: string = issue.isPullRequest ? this.options.stalePrLabel : this.options.staleIssueLabel; @@ -1286,4 +1292,10 @@ export class IssuesProcessor { return Option.RemoveStaleWhenUpdated; } + + private _interpolatePlaceholders(issue: Issue, message: string) { + return issue.user + ? message.replace('{author}', `@${issue.user?.login}`) + : message; + } } diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts index defdb75d..67d2b5f4 100644 --- a/src/interfaces/issue.ts +++ b/src/interfaces/issue.ts @@ -1,20 +1,22 @@ -import {IsoDateString} from '../types/iso-date-string'; -import {Assignee} from './assignee'; -import {ILabel} from './label'; -import {IMilestone} from './milestone'; -import {components} from '@octokit/openapi-types'; -export interface IIssue { - title: string; - number: number; - created_at: IsoDateString; - updated_at: IsoDateString; - draft: boolean; - labels: ILabel[]; - pull_request?: object | null; - state: string; - locked: boolean; - milestone?: IMilestone | null; - assignees?: Assignee[] | null; -} - -export type OctokitIssue = components['schemas']['issue']; +import {IsoDateString} from '../types/iso-date-string'; +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; + created_at: IsoDateString; + updated_at: IsoDateString; + labels: ILabel[]; + pull_request?: object | null; + state: string; + locked: boolean; + milestone?: IMilestone | null; + assignees?: Assignee[] | null; + user?: IUser | null; +} + +export type OctokitIssue = components['schemas']['issue']; diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 4e454f6d..31980428 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,4 +1,8 @@ +import {components} from '@octokit/openapi-types'; + export interface IUser { type: string | 'User'; login: string; } + +export type OctokitUser = components['schemas']['nullable-simple-user'];