Skip to content
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
7 changes: 6 additions & 1 deletion __tests__/functions/generate-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export function generateIssue(
isLocked = false,
milestone: string | undefined = undefined,
assignees: string[] = [],
issue_type?: string
issue_type?: string,
userLogin: string | undefined = undefined
): Issue {
return new Issue(options, {
number: id,
Expand All @@ -41,6 +42,10 @@ export function generateIssue(
type: 'User'
};
}),
user: {
login: userLogin ? userLogin : 'dummy-test-user',
type: 'User'
},
...(issue_type ? {type: {name: issue_type}} : {})
});
}
101 changes: 101 additions & 0 deletions __tests__/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
);
});
3 changes: 3 additions & 0 deletions src/classes/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +26,7 @@ export class Issue implements IIssue {
operations = new Operations();
private readonly _options: IIssuesProcessorOptions;
readonly issue_type?: string;
readonly user?: IUser | null;

constructor(
options: Readonly<IIssuesProcessorOptions>,
Expand Down Expand Up @@ -53,6 +55,7 @@ export class Issue implements IIssue {
} else {
this.issue_type = undefined;
}
this.user = issue.user
}

get isPullRequest(): boolean {
Expand Down
24 changes: 18 additions & 6 deletions src/classes/issues-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,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;
Expand Down Expand Up @@ -1316,4 +1322,10 @@ export class IssuesProcessor {

return Option.RemoveStaleWhenUpdated;
}

private _interpolatePlaceholders(issue: Issue, message: string) {
return issue.user
? message.replace('{author}', `@${issue.user?.login}`)
: message;
}
}
45 changes: 24 additions & 21 deletions src/interfaces/issue.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
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;
issue_type?: string;
}

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;
draft: boolean;
labels: ILabel[];
pull_request?: object | null;
state: string;
locked: boolean;
milestone?: IMilestone | null;
assignees?: Assignee[] | null;
issue_type?: string;
user?: IUser | null;
}

export type OctokitIssue = components['schemas']['issue'];
4 changes: 4 additions & 0 deletions src/interfaces/user.ts
Original file line number Diff line number Diff line change
@@ -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'];