From c565207b1b9460691c3ebd4ae7055da06e5a08b0 Mon Sep 17 00:00:00 2001 From: Sumit Anvekar <2293969+sumitanvekar@users.noreply.github.com> Date: Thu, 8 Dec 2022 08:06:29 +0000 Subject: [PATCH] feature: add dynamic messages support --- __tests__/functions/generate-issue.ts | 9 ++- __tests__/main.spec.ts | 101 ++++++++++++++++++++++++++ src/classes/issue.ts | 3 + src/classes/issues-processor.ts | 26 +++++-- src/interfaces/issue.ts | 3 + src/interfaces/user.ts | 4 + 6 files changed, 137 insertions(+), 9 deletions(-) diff --git a/__tests__/functions/generate-issue.ts b/__tests__/functions/generate-issue.ts index ad27de12..c645d555 100644 --- a/__tests__/functions/generate-issue.ts +++ b/__tests__/functions/generate-issue.ts @@ -14,7 +14,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, @@ -37,6 +38,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 21882547..cae5d7cf 100644 --- a/__tests__/main.spec.ts +++ b/__tests__/main.spec.ts @@ -5,6 +5,7 @@ import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-option import {IssuesProcessorMock} from './classes/issues-processor-mock'; import {DefaultProcessorOptions} from './constants/default-processor-options'; import {generateIssue} from './functions/generate-issue'; +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 = { @@ -2595,3 +2596,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 not be created + 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 + 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 207bc182..765bffe7 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; @@ -23,6 +24,7 @@ export class Issue implements IIssue { markedStaleThisRun: boolean; operations = new Operations(); private readonly _options: IIssuesProcessorOptions; + readonly user?: IUser | null; constructor( options: Readonly, @@ -41,6 +43,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 70e3bf24..9c6772d2 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -190,12 +190,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; @@ -1227,4 +1233,10 @@ export class IssuesProcessor { return Option.RemoveStaleWhenUpdated; } -} + + private _interpolatePlaceholders(issue: Issue, message: string) { + return issue.user + ? message.replace('{author}', `@${issue.user?.login}`) + : message; + } +} \ No newline at end of file diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts index 4ff2d014..be71ac0e 100644 --- a/src/interfaces/issue.ts +++ b/src/interfaces/issue.ts @@ -3,6 +3,8 @@ 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; @@ -14,6 +16,7 @@ export interface IIssue { 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..65e4f0fb 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']; \ No newline at end of file