feat(stale-and-close): add new options to change the days before close (#224)

* docs(readme): add new options in the documentation

* chore: update the action schema

* chore: parse the new arguments

* feat(stale-and-close): add new options to change the days before close

to avoid a breaking change and simplify the configuration the old options 'daysBeforeStale' and 'daysBeforePrClose' are kept and new options are available to override them with 'daysBeforeIssueStale', 'daysBeforePrStale', 'daysBeforeIssueClose' and 'daysBeforePrClose'

* chore: rename the issue type enum to remove the enum suffix

* chore: add missing dependency for eslint and typescript

also upgrade the parser

* chore: fix an issue with the linter for the shadow rules

it was not configured properly for TypeScript

* chore: use camelCase for constants

* chore: use camelCase for enum members

* chore: fix the tests

* chore: enhance prettier to also lint other kind of files

it was configured to only work with ts and it was not working well to be honest
also now the lint scripts will also run prettier
This commit is contained in:
Geoffrey Testelin
2021-01-16 14:28:29 +01:00
committed by GitHub
parent b12dccced8
commit 552e4c60f0
19 changed files with 767 additions and 203 deletions

View File

@@ -2,8 +2,12 @@ import * as core from '@actions/core';
import {context, getOctokit} from '@actions/github';
import {GitHub} from '@actions/github/lib/utils';
import {GetResponseTypeFromEndpointMethod} from '@octokit/types';
import {IssueType} from './enums/issue-type.enum';
import {getIssueType} from './functions/get-issue-type';
import {isLabeled} from './functions/is-labeled';
import {isPullRequest} from './functions/is-pull-request';
import {labelsToList} from './functions/labels-to-list';
import {shouldMarkWhenStale} from './functions/should-mark-when-stale';
export interface Issue {
title: string;
@@ -48,7 +52,11 @@ export interface IssueProcessorOptions {
closeIssueMessage: string;
closePrMessage: string;
daysBeforeStale: number;
daysBeforeIssueStale: number; // Could be NaN
daysBeforePrStale: number; // Could be NaN
daysBeforeClose: number;
daysBeforeIssueClose: number; // Could be NaN
daysBeforePrClose: number; // Could be NaN
staleIssueLabel: string;
closeIssueLabel: string;
exemptIssueLabels: string;
@@ -69,14 +77,21 @@ export interface IssueProcessorOptions {
* Handle processing of issues for staleness/closure.
*/
export class IssueProcessor {
private static updatedSince(timestamp: string, num_days: number): boolean {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
const millisSinceLastUpdated =
new Date().getTime() - new Date(timestamp).getTime();
return millisSinceLastUpdated <= daysInMillis;
}
readonly client: InstanceType<typeof GitHub>;
readonly options: IssueProcessorOptions;
private operationsLeft = 0;
readonly staleIssues: Issue[] = [];
readonly closedIssues: Issue[] = [];
readonly deletedBranchIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
private operationsLeft = 0;
constructor(
options: IssueProcessorOptions,
@@ -131,7 +146,7 @@ export class IssueProcessor {
}
for (const issue of issues.values()) {
const isPr = !!issue.pull_request;
const isPr = isPullRequest(issue);
core.info(
`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})`
@@ -156,10 +171,20 @@ export class IssueProcessor {
const skipMessage = isPr
? this.options.skipStalePrMessage
: this.options.skipStaleIssueMessage;
const issueType: string = isPr ? 'pr' : 'issue';
const shouldMarkWhenStale = this.options.daysBeforeStale > -1;
const issueType: IssueType = getIssueType(isPr);
const daysBeforeStale: number = isPr
? this._getDaysBeforePrStale()
: this._getDaysBeforeIssueStale();
if (!staleMessage && shouldMarkWhenStale) {
if (isPr) {
core.info(`Days before pull request stale: ${daysBeforeStale}`);
} else {
core.info(`Days before issue stale: ${daysBeforeStale}`);
}
const shouldMarkAsStale: boolean = shouldMarkWhenStale(daysBeforeStale);
if (!staleMessage && shouldMarkAsStale) {
core.info(`Skipping ${issueType} due to empty stale message`);
continue;
}
@@ -199,7 +224,7 @@ export class IssueProcessor {
);
// determine if this issue needs to be marked stale first
if (!isStale && shouldBeStale && shouldMarkWhenStale) {
if (!isStale && shouldBeStale && shouldMarkAsStale) {
core.info(
`Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label`
);
@@ -233,7 +258,7 @@ export class IssueProcessor {
// handle all of the stale issue logic when we find a stale issue
private async processStaleIssue(
issue: Issue,
issueType: string,
issueType: IssueType,
staleLabel: string,
actor: string,
closeMessage?: string,
@@ -252,9 +277,20 @@ export class IssueProcessor {
`Issue #${issue.number} has been commented on: ${issueHasComments}`
);
const isPr: boolean = isPullRequest(issue);
const daysBeforeClose: number = isPr
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
if (isPr) {
core.info(`Days before pull request close: ${daysBeforeClose}`);
} else {
core.info(`Days before issue close: ${daysBeforeClose}`);
}
const issueHasUpdate: boolean = IssueProcessor.updatedSince(
issue.updated_at,
this.options.daysBeforeClose
daysBeforeClose
);
core.info(`Issue #${issue.number} has been updated: ${issueHasUpdate}`);
@@ -267,7 +303,7 @@ export class IssueProcessor {
}
// now start closing logic
if (this.options.daysBeforeClose < 0) {
if (daysBeforeClose < 0) {
return; // nothing to do because we aren't closing stale issues
}
@@ -590,11 +626,27 @@ export class IssueProcessor {
return staleLabeledEvent.created_at;
}
private static updatedSince(timestamp: string, num_days: number): boolean {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
const millisSinceLastUpdated =
new Date().getTime() - new Date(timestamp).getTime();
private _getDaysBeforeIssueStale(): number {
return isNaN(this.options.daysBeforeIssueStale)
? this.options.daysBeforeStale
: this.options.daysBeforeIssueStale;
}
return millisSinceLastUpdated <= daysInMillis;
private _getDaysBeforePrStale(): number {
return isNaN(this.options.daysBeforePrStale)
? this.options.daysBeforeStale
: this.options.daysBeforePrStale;
}
private _getDaysBeforeIssueClose(): number {
return isNaN(this.options.daysBeforeIssueClose)
? this.options.daysBeforeClose
: this.options.daysBeforeIssueClose;
}
private _getDaysBeforePrClose(): number {
return isNaN(this.options.daysBeforePrClose)
? this.options.daysBeforeClose
: this.options.daysBeforePrClose;
}
}

View File

@@ -0,0 +1,4 @@
export enum IssueType {
Issue = 'issue',
PullRequest = 'pr'
}

View File

@@ -0,0 +1,33 @@
import {getIssueType} from './get-issue-type';
describe('getIssueType()', (): void => {
let isPullRequest: boolean;
describe('when the issue is a not pull request', (): void => {
beforeEach((): void => {
isPullRequest = false;
});
it('should return that the issue is really an issue', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('issue');
});
});
describe('when the issue is a pull request', (): void => {
beforeEach((): void => {
isPullRequest = true;
});
it('should return that the issue is a pull request', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('pr');
});
});
});

View File

@@ -0,0 +1,5 @@
import {IssueType} from '../enums/issue-type.enum';
export function getIssueType(isPullRequest: Readonly<boolean>): IssueType {
return isPullRequest ? IssueType.PullRequest : IssueType.Issue;
}

View File

@@ -0,0 +1,57 @@
import {Issue} from '../IssueProcessor';
import {isPullRequest} from './is-pull-request';
describe('isPullRequest()', (): void => {
let issue: Issue;
describe('when the given issue has an undefined pull request', (): void => {
beforeEach((): void => {
issue = {
pull_request: undefined
} as Issue;
});
it('should return false', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(false);
});
});
describe('when the given issue has a null pull request', (): void => {
beforeEach((): void => {
issue = {
pull_request: null
} as Issue;
});
it('should return false', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(false);
});
});
describe.each([{}, true])(
'when the given issue has pull request',
(value): void => {
beforeEach((): void => {
issue = {
pull_request: value
} as Issue;
});
it('should return true', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(true);
});
}
);
});

View File

@@ -0,0 +1,5 @@
import {Issue} from '../IssueProcessor';
export function isPullRequest(issue: Readonly<Issue>): boolean {
return !!issue.pull_request;
}

View File

@@ -0,0 +1,47 @@
import {shouldMarkWhenStale} from './should-mark-when-stale';
describe('shouldMarkWhenStale()', (): void => {
let daysBeforeStale: number;
describe('when the given number of days indicate that it should be stalled', (): void => {
beforeEach((): void => {
daysBeforeStale = -1;
});
it('should return false', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(false);
});
});
describe('when the given number of days indicate that it should be stalled today', (): void => {
beforeEach((): void => {
daysBeforeStale = 0;
});
it('should return true', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(true);
});
});
describe('when the given number of days indicate that it should be stalled tomorrow', (): void => {
beforeEach((): void => {
daysBeforeStale = 1;
});
it('should return true', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(true);
});
});
});

View File

@@ -0,0 +1,5 @@
export function shouldMarkWhenStale(
daysBeforeStale: Readonly<number>
): boolean {
return daysBeforeStale >= 0;
}

View File

@@ -23,9 +23,13 @@ function getAndValidateArgs(): IssueProcessorOptions {
daysBeforeStale: parseInt(
core.getInput('days-before-stale', {required: true})
),
daysBeforeIssueStale: parseInt(core.getInput('days-before-issue-stale')),
daysBeforePrStale: parseInt(core.getInput('days-before-pr-stale')),
daysBeforeClose: parseInt(
core.getInput('days-before-close', {required: true})
),
daysBeforeIssueClose: parseInt(core.getInput('days-before-issue-close')),
daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')),
staleIssueLabel: core.getInput('stale-issue-label', {required: true}),
closeIssueLabel: core.getInput('close-issue-label'),
exemptIssueLabels: core.getInput('exempt-issue-labels'),
@@ -48,7 +52,11 @@ function getAndValidateArgs(): IssueProcessorOptions {
for (const numberInput of [
'days-before-stale',
'days-before-issue-stale',
'days-before-pr-stale',
'days-before-close',
'days-before-issue-close',
'days-before-pr-close',
'operations-per-run'
]) {
if (isNaN(parseInt(core.getInput(numberInput)))) {