Merge cffda38cb3c3024034c5cfef5422bba4e603658c into 3f3b0175e8c66fb49b9a6d5a0cd1f8436d4c3ab6

This commit is contained in:
M Viswanath Sai 2024-03-05 18:30:56 +00:00 committed by GitHub
commit 9af1c21c59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1469 additions and 176 deletions

View File

@ -6,18 +6,25 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
repoToken: 'none',
staleIssueMessage: 'This issue is stale',
stalePrMessage: 'This PR is stale',
rottenIssueMessage: 'This issue is rotten',
rottenPrMessage: 'This PR is rotten',
closeIssueMessage: 'This issue is being closed',
closePrMessage: 'This PR is being closed',
daysBeforeStale: 1,
daysBeforeRotten: -1,
daysBeforeIssueStale: NaN,
daysBeforePrStale: NaN,
daysBeforeIssueRotten: NaN,
daysBeforePrRotten: NaN,
daysBeforeClose: 30,
daysBeforeIssueClose: NaN,
daysBeforePrClose: NaN,
staleIssueLabel: 'Stale',
rottenIssueLabel: 'Rotten',
closeIssueLabel: '',
exemptIssueLabels: '',
stalePrLabel: 'Stale',
rottenPrLabel: 'Rotten',
closePrLabel: '',
exemptPrLabels: '',
onlyLabels: '',
@ -31,6 +38,9 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
removeStaleWhenUpdated: false,
removeIssueStaleWhenUpdated: undefined,
removePrStaleWhenUpdated: undefined,
removeRottenWhenUpdated: false,
removeIssueRottenWhenUpdated: undefined,
removePrRottenWhenUpdated: undefined,
ascending: false,
deleteBranch: false,
startDate: '',
@ -50,6 +60,9 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
labelsToRemoveWhenStale: '',
labelsToRemoveWhenUnstale: '',
labelsToAddWhenUnstale: '',
labelsToRemoveWhenRotten: '',
labelsToRemoveWhenUnrotten: '',
labelsToAddWhenUnrotten: '',
ignoreUpdates: false,
ignoreIssueUpdates: undefined,
ignorePrUpdates: undefined,

View File

@ -159,11 +159,12 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
});
test('processing an issue with no label and a start date as ISO 8601 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
expect.assertions(3);
const january2000 = '2000-01-01T00:00:00Z';
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
daysBeforeRotten: 0,
startDate: january2000.toString()
};
const TestIssueList: Issue[] = [
@ -187,6 +188,7 @@ test('processing an issue with no label and a start date as ISO 8601 being befor
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(1);
expect(processor.rottenIssues.length).toStrictEqual(1);
expect(processor.closedIssues.length).toStrictEqual(1);
});
@ -222,6 +224,39 @@ test('processing an issue with no label and a start date as ISO 8601 being after
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as ISO 8601 being after the issue creation date will not make it stale , rotten or close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(3);
const january2021 = '2021-01-01T00:00:00Z';
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2021.toString()
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.rottenIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as RFC 2822 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const january2000 = 'January 1, 2000 00:00:00';
@ -290,6 +325,7 @@ test('processing an issue with no label will make it stale and close it, if it i
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 1,
daysBeforeRotten: 0,
daysBeforeIssueClose: 0
};
const TestIssueList: Issue[] = [
@ -307,6 +343,7 @@ test('processing an issue with no label will make it stale and close it, if it i
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.deletedBranchIssues).toHaveLength(0);
});
@ -459,10 +496,11 @@ test('processing an issue with no label will make it stale but not close it', as
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue will close it', async () => {
test('processing a stale issue will rot it but not close it, given days before rotten is > -1', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30
daysBeforeClose: 30,
daysBeforeRotten: 0
};
const TestIssueList: Issue[] = [
generateIssue(
@ -488,13 +526,15 @@ test('processing a stale issue will close it', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue containing a space in the label will close it', async () => {
test('processing a stale issue containing a space in the label will rotten it but not close it, given days before rotten is > -1', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
staleIssueLabel: 'state: stale'
staleIssueLabel: 'state: stale',
daysBeforeRotten: 0
};
const TestIssueList: Issue[] = [
generateIssue(
@ -520,13 +560,15 @@ test('processing a stale issue containing a space in the label will close it', a
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue containing a slash in the label will close it', async () => {
test('processing a stale issue containing a slash in the label will rotten it but not close it', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
staleIssueLabel: 'lifecycle/stale'
staleIssueLabel: 'lifecycle/stale',
daysBeforeRotten: 0
};
const TestIssueList: Issue[] = [
generateIssue(
@ -552,20 +594,21 @@ test('processing a stale issue containing a slash in the label will close it', a
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => {
test('processing a stale issue will rotten it but not close it when days-before-issue-rotten override days-before-rotten', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30,
daysBeforeIssueStale: 30
daysBeforeRotten: -1,
daysBeforeIssueRotten: 30
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale issue that should be closed',
'A stale issue that should be rotten',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
@ -585,13 +628,14 @@ test('processing a stale issue will close it when days-before-issue-stale overri
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale PR will close it', async () => {
test('processing a stale PR will rotten it but not close it', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30
daysBeforePrRotten: 30
};
const TestIssueList: Issue[] = [
generateIssue(
@ -617,13 +661,49 @@ test('processing a stale PR will close it', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => {
test('processing a stale PR will rotten it it when days-before-pr-rotten override days-before-rotten', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeRotten: 30,
daysBeforePrRotten: 30
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale PR that should be closed',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
true,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale PR will rotten it but not close it when days-before-pr-stale override days-before-stale', async () => {
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30,
daysBeforeRotten: 0,
daysBeforePrClose: 30
};
const TestIssueList: Issue[] = [
@ -650,13 +730,15 @@ test('processing a stale PR will close it when days-before-pr-stale override day
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue will close it even if configured not to mark as stale', async () => {
test('processing a stale issue will rotten it even if configured not to mark as stale', async () => {
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: -1,
daysBeforeRotten: 0,
staleIssueMessage: ''
};
const TestIssueList: Issue[] = [
@ -683,13 +765,16 @@ test('processing a stale issue will close it even if configured not to mark as s
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => {
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: 0,
daysBeforeRotten: 0,
daysBeforeIssueStale: -1,
staleIssueMessage: ''
};
@ -717,13 +802,15 @@ test('processing a stale issue will close it even if configured not to mark as s
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale PR will close it even if configured not to mark as stale', async () => {
test('processing a stale PR will rotten it even if configured not to mark as stale', async () => {
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: -1,
daysBeforeRotten: 0,
stalePrMessage: ''
};
const TestIssueList: Issue[] = [
@ -750,14 +837,52 @@ test('processing a stale PR will close it even if configured not to mark as stal
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a stale PR will close it even if configured not to mark as stale or rotten', async () => {
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: -1,
stalePrMessage: '',
daysBeforeRotten: -1
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
true,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
});
test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => {
test('processing a stale PR will rotten it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => {
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: 0,
daysBeforePrStale: -1,
daysBeforeRotten: 0,
stalePrMessage: ''
};
const TestIssueList: Issue[] = [
@ -784,10 +909,11 @@ test('processing a stale PR will close it even if configured not to mark as stal
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});
test('closed issues will not be marked stale', async () => {
test('closed issues will not be marked stale or rotten', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -812,10 +938,41 @@ test('closed issues will not be marked stale', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('stale closed issues will not be closed', async () => {
test('rotten closed issues will not be closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
1,
'A rotten closed issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Rotten'],
true
)
];
const processor = new IssuesProcessorMock(
DefaultProcessorOptions,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('stale closed issues will not be closed or rotten', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -841,10 +998,11 @@ test('stale closed issues will not be closed', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('closed prs will not be marked stale', async () => {
test('closed prs will not be marked stale or rotten', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -870,6 +1028,7 @@ test('closed prs will not be marked stale', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
@ -902,7 +1061,7 @@ test('stale closed prs will not be closed', async () => {
expect(processor.closedIssues).toHaveLength(0);
});
test('locked issues will not be marked stale', async () => {
test('locked issues will not be marked stale or rotten', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -927,10 +1086,11 @@ test('locked issues will not be marked stale', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('stale locked issues will not be closed', async () => {
test('stale locked issues will not be rotten or closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -957,10 +1117,42 @@ test('stale locked issues will not be closed', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('locked prs will not be marked stale', async () => {
test('rotten locked issues will not be rotten or closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
1,
'A stale locked issue that will not be closed',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Rotten'],
false,
true
)
];
const processor = new IssuesProcessorMock(
DefaultProcessorOptions,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('locked prs will not be marked stale or rotten', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -985,10 +1177,11 @@ test('locked prs will not be marked stale', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('stale locked prs will not be closed', async () => {
test('stale locked prs will not be rotten or closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
DefaultProcessorOptions,
@ -1015,11 +1208,12 @@ test('stale locked prs will not be closed', async () => {
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
test('exempt issue labels will not be marked stale', async () => {
expect.assertions(3);
test('exempt issue labels will not be marked stale or rotten', async () => {
expect.assertions(4);
const opts = {...DefaultProcessorOptions};
opts.exemptIssueLabels = 'Exempt';
const TestIssueList: Issue[] = [
@ -1046,11 +1240,12 @@ test('exempt issue labels will not be marked stale', async () => {
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues.length).toStrictEqual(0);
expect(processor.removedLabelIssues.length).toStrictEqual(0);
});
test('exempt issue labels will not be marked stale (multi issue label with spaces)', async () => {
test('exempt issue labels will not be marked stale or rotten (multi issue label with spaces)', async () => {
const opts = {...DefaultProcessorOptions};
opts.exemptIssueLabels = 'Exempt, Cool, None';
const TestIssueList: Issue[] = [
@ -1077,6 +1272,7 @@ test('exempt issue labels will not be marked stale (multi issue label with space
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
});
@ -1210,7 +1406,11 @@ test('stale issues should not be closed if days is set to -1', async () => {
});
test('stale label should be removed if a comment was added to a stale issue', async () => {
const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true};
const opts = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true,
daysBeforeRotten: 0
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
@ -1337,8 +1537,12 @@ test('when the option "labelsToRemoveWhenStale" is set, the labels should be rem
expect(processor.removedLabelIssues).toHaveLength(1);
});
test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => {
const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true};
test('stale label should not be removed if a comment was added by the bot, given that it does not get rotten', async () => {
const opts = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true,
daysBeforeRotten: -1
};
github.context.actor = 'abot';
const TestIssueList: Issue[] = [
generateIssue(
@ -1372,6 +1576,7 @@ test('stale label should not be removed if a comment was added by the bot (and t
await processor.processIssues(1);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.rottenIssues).toHaveLength(0);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.removedLabelIssues).toHaveLength(0);
});
@ -1442,10 +1647,10 @@ test('stale issues should not be closed until after the closed number of days',
expect(processor.staleIssues).toHaveLength(1);
});
test('stale issues should be closed if the closed nubmer of days (additive) is also passed', async () => {
test('stale issues should be rotten if the rotten nubmer of days (additive) is also passed', async () => {
const opts = {...DefaultProcessorOptions};
opts.daysBeforeStale = 5; // stale after 5 days
opts.daysBeforeClose = 1; // closes after 6 days
opts.daysBeforeRotten = 1; // rotten after 6 days
const lastUpdate = new Date();
lastUpdate.setDate(lastUpdate.getDate() - 7);
const TestIssueList: Issue[] = [
@ -1471,8 +1676,9 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.removedLabelIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0);
expect(processor.rottenIssues).toHaveLength(1);
expect(processor.removedLabelIssues).toHaveLength(1); // the stale label should be removed on rotten label being added
expect(processor.staleIssues).toHaveLength(0);
});
@ -1690,8 +1896,12 @@ test('send stale message on prs when stale-pr-message is not empty', async () =>
);
});
test('git branch is deleted when option is enabled', async () => {
const opts = {...DefaultProcessorOptions, deleteBranch: true};
test('git branch is deleted when option is enabled and days before rotten is set to -1', async () => {
const opts = {
...DefaultProcessorOptions,
deleteBranch: true,
daysBeforeRotten: -1
};
const isPullRequest = true;
const TestIssueList: Issue[] = [
generateIssue(
@ -1721,8 +1931,12 @@ test('git branch is deleted when option is enabled', async () => {
expect(processor.deletedBranchIssues).toHaveLength(1);
});
test('git branch is not deleted when issue is not pull request', async () => {
const opts = {...DefaultProcessorOptions, deleteBranch: true};
test('git branch is not deleted when issue is not pull request and days before rotten is set to -1', async () => {
const opts = {
...DefaultProcessorOptions,
deleteBranch: true,
daysBeforeRotten: -1
};
const isPullRequest = false;
const TestIssueList: Issue[] = [
generateIssue(
@ -2516,13 +2730,14 @@ test('processing a locked issue with a close label will not remove the close lab
expect(processor.removedLabelIssues).toHaveLength(0);
});
test('processing an issue stale since less than the daysBeforeStale with a stale label created after daysBeforeClose should close the issue', async () => {
expect.assertions(3);
test('processing an issue stale since less than the daysBeforeStale with a stale label created after daysBeforeRotten should rotten the issue', async () => {
expect.assertions(4);
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
staleIssueLabel: 'stale-label',
daysBeforeStale: 30,
daysBeforeClose: 7,
daysBeforeRotten: 0,
closeIssueMessage: 'close message',
removeStaleWhenUpdated: false
};
@ -2554,9 +2769,10 @@ test('processing an issue stale since less than the daysBeforeStale with a stale
// process our fake issue list
await processor.processIssues(1);
expect(processor.removedLabelIssues).toHaveLength(0);
expect(processor.removedLabelIssues).toHaveLength(1); // The stale label should be removed on adding the rotten label
expect(processor.rottenIssues).toHaveLength(1); // Expected at 0 by the user
expect(processor.deletedBranchIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(1); // Expected at 0 by the user
expect(processor.closedIssues).toHaveLength(0);
});
test('processing an issue stale since less than the daysBeforeStale without a stale label should close the issue', async () => {
@ -2566,6 +2782,8 @@ test('processing an issue stale since less than the daysBeforeStale without a st
staleIssueLabel: 'stale-label',
daysBeforeStale: 30,
daysBeforeClose: 7,
daysBeforeRotten: 0,
closeIssueMessage: 'close message',
removeStaleWhenUpdated: false
};
@ -2601,13 +2819,14 @@ test('processing an issue stale since less than the daysBeforeStale without a st
expect(processor.closedIssues).toHaveLength(0);
});
test('processing a pull request to be stale with the "stalePrMessage" option set will send a PR comment', async () => {
test('processing a pull request to be stale with the "stalePrMessage" option set will send a PR comment, given that days before rotten is set to -1', async () => {
expect.assertions(3);
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
stalePrMessage: 'This PR is stale',
daysBeforeStale: 10,
daysBeforePrStale: 1
daysBeforePrStale: 1,
daysBeforeRotten: -1
};
const issueDate = new Date();
issueDate.setDate(issueDate.getDate() - 2);
@ -2638,12 +2857,52 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(1);
});
test('processing a pull request to be stale with the "stalePrMessage" option set to empty will not send a PR comment', async () => {
test('processing a pull request to be stale with the "stalePrMessage" option set will send two PR comments, given that days before rotten is set to 0', async () => {
expect.assertions(3);
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
stalePrMessage: 'This PR is stale',
daysBeforeStale: 10,
daysBeforePrStale: 1,
daysBeforeRotten: 0
};
const issueDate = new Date();
issueDate.setDate(issueDate.getDate() - 2);
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A pull request with no label and a stale message',
issueDate.toDateString(),
issueDate.toDateString(),
false,
true
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(2);
});
test('processing a pull request to be stale with the "stalePrMessage" option set to empty will not send a PR comment, given that "rottenPRMessage" is also an empty string and days before rotten is not -1', async () => {
expect.assertions(3);
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
stalePrMessage: '',
rottenPrMessage: '',
daysBeforeStale: 10,
daysBeforeRotten: 0,
daysBeforePrStale: 1
};
const issueDate = new Date();
@ -2675,6 +2934,45 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(0);
});
test('processing a pull request to be stale with the "stalePrMessage" option set to empty will send a PR comment from "rottenPRMessage" given that it is also an empty string', async () => {
expect.assertions(3);
const opts: IIssuesProcessorOptions = {
...DefaultProcessorOptions,
stalePrMessage: '',
daysBeforeStale: 10,
daysBeforeRotten: 0,
daysBeforePrStale: 1
};
const issueDate = new Date();
issueDate.setDate(issueDate.getDate() - 2);
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A pull request with no label and a stale message',
issueDate.toDateString(),
issueDate.toDateString(),
false,
true
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(1);
});
test('processing an issue with the "includeOnlyAssigned" option and nonempty assignee list will stale the issue', async () => {
const issueDate = new Date();
issueDate.setDate(issueDate.getDate() - 2);

View File

@ -13,7 +13,7 @@ describe('operations-per-run option', (): void => {
sut = new SUT();
});
describe('when one issue should be stale within 10 days and updated 20 days ago', (): void => {
describe('when one issue should be stale within 10 days and updated 20 days ago and days before rotten is -1', (): void => {
beforeEach((): void => {
sut.staleIn(10).newIssue().updated(20);
});

View File

@ -202,7 +202,7 @@ describe('state', (): void => {
await processor.processIssues(1);
// make sure all issues are proceeded
expect(infoSpy.mock.calls[71][0]).toContain(
expect(infoSpy.mock.calls[77][0]).toContain(
'No more issues found to process. Exiting...'
);

View File

@ -1,6 +1,6 @@
name: 'Close Stale Issues'
name: 'Close, Rotten and Stale Issues'
description: 'Close issues and pull requests with no recent activity'
author: 'GitHub'
author: 'M Viswanath Sai'
inputs:
repo-token:
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.'
@ -12,6 +12,12 @@ inputs:
stale-pr-message:
description: 'The message to post on the pull request when tagging it. If none provided, will not mark pull requests stale.'
required: false
rotten-issue-message:
description: 'The message to post on the issue when tagging it. If none provided, will not mark issues rotten.'
required: false
rotten-pr-message:
description: 'The message to post on the pull request when tagging it. If none provided, will not mark pull requests rotten.'
required: false
close-issue-message:
description: 'The message to post on the issue when closing it. If none provided, will not comment when closing an issue.'
required: false
@ -21,17 +27,27 @@ inputs:
days-before-stale:
description: 'The number of days old an issue or a pull request can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.'
required: false
default: '60'
default: '90'
days-before-issue-stale:
description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding only the issues.'
required: false
days-before-pr-stale:
description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding only the pull requests.'
required: false
days-before-rotten:
description: 'The number of days old an issue or a pull request can be before marking it rotten. Set to -1 to never mark issues or pull requests as rotten automatically.'
required: false
default: '30'
days-before-issue-rotten:
description: 'The number of days old an issue can be before marking it rotten. Set to -1 to never mark issues as rotten automatically. Override "days-before-rotten" option regarding only the issues.'
required: false
days-before-pr-rotten:
description: 'The number of days old a pull request can be before marking it rotten. Set to -1 to never mark pull requests as rotten automatically. Override "days-before-rotten" option regarding only the pull requests.'
required: false
days-before-close:
description: 'The number of days to wait to close an issue or a pull request after it being marked stale. Set to -1 to never close stale issues or pull requests.'
required: false
default: '7'
default: '30'
days-before-issue-close:
description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding only the issues.'
required: false
@ -42,6 +58,10 @@ inputs:
description: 'The label to apply when an issue is stale.'
required: false
default: 'Stale'
rotten-issue-label:
description: 'The label to apply when an issue is rotten.'
required: false
default: 'Rotten'
close-issue-label:
description: 'The label to apply when an issue is closed.'
required: false
@ -57,6 +77,10 @@ inputs:
description: 'The label to apply when a pull request is stale.'
default: 'Stale'
required: false
rotten-pr-label:
description: 'The label to apply when a pull request is rotten.'
default: 'Rotten'
required: false
close-pr-label:
description: 'The label to apply when a pull request is closed.'
required: false
@ -128,6 +152,18 @@ inputs:
description: 'Remove stale labels from pull requests when they are updated or commented on. Override "remove-stale-when-updated" option regarding only the pull requests.'
default: ''
required: false
remove-rotten-when-updated:
description: 'Remove rotten labels from issues and pull requests when they are updated or commented on.'
default: 'true'
required: false
remove-issue-rotten-when-updated:
description: 'Remove rotten labels from issues when they are updated or commented on. Override "remove-rotten-when-updated" option regarding only the issues.'
default: ''
required: false
remove-pr-rotten-when-updated:
description: 'Remove rotten labels from pull requests when they are updated or commented on. Override "remove-rotten-when-updated" option regarding only the pull requests.'
default: ''
required: false
debug-only:
description: 'Run the processor in debug mode without actually performing any operations on live issues.'
default: 'false'
@ -188,6 +224,18 @@ inputs:
description: 'A comma delimited list of labels to remove when an issue or pull request becomes unstale.'
default: ''
required: false
labels-to-add-when-unrotten:
description: 'A comma delimited list of labels to add when an issue or pull request becomes unrotten.'
default: ''
required: false
labels-to-remove-when-rotten:
description: 'A comma delimited list of labels to remove when an issue or pull request becomes rotten.'
default: ''
required: false
labels-to-remove-when-unrotten:
description: 'A comma delimited list of labels to remove when an issue or pull request becomes unrotten.'
default: ''
required: false
ignore-updates:
description: 'Any update (update/comment) can reset the stale idle time on the issues and pull requests.'
default: 'false'

418
dist/index.js vendored
View File

@ -288,7 +288,9 @@ class Issue {
this.milestone = issue.milestone;
this.assignees = issue.assignees || [];
this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel);
this.isRotten = (0, is_labeled_1.isLabeled)(this, this.rottenLabel);
this.markedStaleThisRun = false;
this.markedRottenThisRun = false;
}
get isPullRequest() {
return (0, is_pull_request_1.isPullRequest)(this);
@ -296,6 +298,9 @@ class Issue {
get staleLabel() {
return this._getStaleLabel();
}
get rottenLabel() {
return this._getRottenLabel();
}
get hasAssignees() {
return this.assignees.length > 0;
}
@ -304,6 +309,11 @@ class Issue {
? this._options.stalePrLabel
: this._options.staleIssueLabel;
}
_getRottenLabel() {
return this.isPullRequest
? this._options.rottenPrLabel
: this._options.rottenIssueLabel;
}
}
exports.Issue = Issue;
function mapLabels(labels) {
@ -403,6 +413,7 @@ class IssuesProcessor {
}
constructor(options, state) {
this.staleIssues = [];
this.rottenIssues = [];
this.closedIssues = [];
this.deletedBranchIssues = [];
this.removedLabelIssues = [];
@ -439,6 +450,9 @@ class IssuesProcessor {
const labelsToRemoveWhenStale = (0, words_to_list_1.wordsToList)(this.options.labelsToRemoveWhenStale);
const labelsToAddWhenUnstale = (0, words_to_list_1.wordsToList)(this.options.labelsToAddWhenUnstale);
const labelsToRemoveWhenUnstale = (0, words_to_list_1.wordsToList)(this.options.labelsToRemoveWhenUnstale);
const labelsToRemoveWhenRotten = (0, words_to_list_1.wordsToList)(this.options.labelsToRemoveWhenRotten);
const labelsToAddWhenUnrotten = (0, words_to_list_1.wordsToList)(this.options.labelsToAddWhenUnrotten);
const labelsToRemoveWhenUnrotten = (0, words_to_list_1.wordsToList)(this.options.labelsToRemoveWhenUnrotten);
for (const issue of issues.values()) {
// Stop the processing if no more operations remains
if (!this.operations.hasRemainingOperations()) {
@ -450,7 +464,7 @@ class IssuesProcessor {
continue;
}
yield issueLogger.grouping(`$$type #${issue.number}`, () => __awaiter(this, void 0, void 0, function* () {
yield this.processIssue(issue, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale);
yield this.processIssue(issue, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten);
}));
this.state.addIssueToProcessed(issue);
}
@ -465,7 +479,7 @@ class IssuesProcessor {
return this.processIssues(page + 1);
});
}
processIssue(issue, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale) {
processIssue(issue, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementProcessedItemsCount(issue);
@ -475,12 +489,21 @@ class IssuesProcessor {
const staleMessage = issue.isPullRequest
? this.options.stalePrMessage
: this.options.staleIssueMessage;
const rottenMessage = issue.isPullRequest
? this.options.rottenPrMessage
: this.options.rottenIssueMessage;
const closeMessage = issue.isPullRequest
? this.options.closePrMessage
: this.options.closeIssueMessage;
const skipRottenMessage = issue.isPullRequest
? this.options.rottenPrMessage.length === 0
: this.options.rottenIssueMessage.length === 0;
const staleLabel = issue.isPullRequest
? this.options.stalePrLabel
: this.options.staleIssueLabel;
const rottenLabel = issue.isPullRequest
? this.options.rottenPrLabel
: this.options.rottenIssueLabel;
const closeLabel = issue.isPullRequest
? this.options.closePrLabel
: this.options.closeIssueLabel;
@ -546,11 +569,18 @@ class IssuesProcessor {
return; // Don't process issues which were created before the start date
}
}
// Check if the issue is stale, if not, check if it is rotten and then log the findings.
if (issue.isStale) {
issueLogger.info(`This $$type includes a stale label`);
}
else {
issueLogger.info(`This $$type does not include a stale label`);
if (issue.isRotten) {
issueLogger.info(`This $$type includes a rotten label`);
}
else {
issueLogger.info(`This $$type does not include a rotten label`);
}
}
const exemptLabels = (0, words_to_list_1.wordsToList)(issue.isPullRequest
? this.options.exemptPrLabels
@ -601,51 +631,57 @@ class IssuesProcessor {
IssuesProcessor._endIssueProcessing(issue);
return; // Don't process draft PR
}
// Here we are looking into if the issue is stale or not, and then adding the label. This same code will also be used for the rotten label.
// Determine if this issue needs to be marked stale first
if (!issue.isStale) {
issueLogger.info(`This $$type is not stale`);
const shouldIgnoreUpdates = new ignore_updates_1.IgnoreUpdates(this.options, issue).shouldIgnoreUpdates();
// Should this issue be marked as stale?
let shouldBeStale;
// Ignore the last update and only use the creation date
if (shouldIgnoreUpdates) {
shouldBeStale = !IssuesProcessor._updatedSince(issue.created_at, daysBeforeStale);
}
// Use the last update to check if we need to stale
else {
shouldBeStale = !IssuesProcessor._updatedSince(issue.updated_at, daysBeforeStale);
}
if (shouldBeStale) {
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type should be stale based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
}
else {
issueLogger.info(`This $$type should be stale based on the last update date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
}
if (shouldMarkAsStale) {
issueLogger.info(`This $$type should be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeStale)})`);
yield this._markStale(issue, staleMessage, staleLabel, skipMessage);
issue.isStale = true; // This issue is now considered stale
issue.markedStaleThisRun = true;
issueLogger.info(`This $$type is now stale`);
}
else {
issueLogger.info(`This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeStale)})`);
}
if (issue.isRotten) {
yield this._processRottenIssue(issue, rottenLabel, rottenMessage, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten, closeMessage, closeLabel);
}
else {
const shouldIgnoreUpdates = new ignore_updates_1.IgnoreUpdates(this.options, issue).shouldIgnoreUpdates();
// Should this issue be marked as stale?
let shouldBeStale;
// Ignore the last update and only use the creation date
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type should not be stale based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
shouldBeStale = !IssuesProcessor._updatedSince(issue.created_at, daysBeforeStale);
}
// Use the last update to check if we need to stale
else {
shouldBeStale = !IssuesProcessor._updatedSince(issue.updated_at, daysBeforeStale);
}
if (shouldBeStale) {
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type should be stale based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
}
else {
issueLogger.info(`This $$type should be stale based on the last update date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
}
if (shouldMarkAsStale) {
issueLogger.info(`This $$type should be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeStale)})`);
yield this._markStale(issue, staleMessage, staleLabel, skipMessage);
issue.isStale = true; // This issue is now considered stale
issue.markedStaleThisRun = true;
issueLogger.info(`This $$type is now stale`);
}
else {
issueLogger.info(`This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeStale)})`);
}
}
else {
issueLogger.info(`This $$type should not be stale based on the last update date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type should not be stale based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
}
else {
issueLogger.info(`This $$type should not be stale based on the last update date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
}
}
}
}
// Process the issue if it was marked stale
if (issue.isStale) {
issueLogger.info(`This $$type is already stale`);
yield this._processStaleIssue(issue, staleLabel, staleMessage, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, closeMessage, closeLabel);
yield this._processStaleIssue(issue, staleLabel, staleMessage, rottenLabel, rottenMessage, closeLabel, closeMessage, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten, skipRottenMessage);
}
IssuesProcessor._endIssueProcessing(issue);
});
@ -752,17 +788,23 @@ class IssuesProcessor {
});
}
// handle all of the stale issue logic when we find a stale issue
_processStaleIssue(issue, staleLabel, staleMessage, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, closeMessage, closeLabel) {
// This whole thing needs to be altered, to be calculated based on the days to rotten, rather than days to close or whatever
_processStaleIssue(issue, staleLabel, staleMessage, rottenLabel, rottenMessage, closeLabel, closeMessage, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten, skipMessage) {
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
let issueHasClosed = false;
// We can get the label creation date from the getLableCreationDate function
const markedStaleOn = (yield this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
issueLogger.info(`$$type marked stale on: ${logger_service_1.LoggerService.cyan(markedStaleOn)}`);
const issueHasCommentsSinceStale = yield this._hasCommentsSince(issue, markedStaleOn, staleMessage);
issueLogger.info(`$$type has been commented on: ${logger_service_1.LoggerService.cyan(issueHasCommentsSinceStale)}`);
const daysBeforeRotten = issue.isPullRequest
? this._getDaysBeforePrRotten()
: this._getDaysBeforeIssueRotten();
const daysBeforeClose = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
issueLogger.info(`Days before $$type close: ${logger_service_1.LoggerService.cyan(daysBeforeClose)}`);
issueLogger.info(`Days before $$type rotten: ${logger_service_1.LoggerService.cyan(daysBeforeRotten)}`);
const shouldRemoveStaleWhenUpdated = this._shouldRemoveStaleWhenUpdated(issue);
issueLogger.info(`The option ${issueLogger.createOptionLink(this._getRemoveStaleWhenUpdatedUsedOptionName(issue))} is: ${logger_service_1.LoggerService.cyan(shouldRemoveStaleWhenUpdated)}`);
if (shouldRemoveStaleWhenUpdated) {
@ -771,6 +813,7 @@ class IssuesProcessor {
else {
issueLogger.info(`The stale label should be removed if all conditions met`);
}
// we will need to use a variation of this for the rotten state
if (issue.markedStaleThisRun) {
issueLogger.info(`marked stale this run, so don't check for updates`);
yield this._removeLabelsOnStatusTransition(issue, labelsToRemoveWhenStale, option_1.Option.LabelsToRemoveWhenStale);
@ -791,13 +834,124 @@ class IssuesProcessor {
issueLogger.info(`Skipping the process since the $$type is now un-stale`);
return; // Nothing to do because it is no longer stale
}
if (daysBeforeRotten < 0) {
if (daysBeforeClose < 0) {
issueLogger.info(`Stale $$type cannot be rotten or closed because days before rotten: ${daysBeforeRotten}, and days before close: ${daysBeforeClose}`);
return;
}
else {
issueLogger.info(`Closing issue without rottening it because days before $$type rotten: ${logger_service_1.LoggerService.cyan(daysBeforeRotten)}`);
const issueHasUpdateInCloseWindow = IssuesProcessor._updatedSince(issue.updated_at, daysBeforeClose);
issueLogger.info(`$$type has been updated in the last ${daysBeforeClose} days: ${logger_service_1.LoggerService.cyan(issueHasUpdateInCloseWindow)}`);
if (!issueHasUpdateInCloseWindow && !issueHasCommentsSinceStale) {
issueLogger.info(`Closing $$type because it was last updated on: ${logger_service_1.LoggerService.cyan(issue.updated_at)}`);
yield this._closeIssue(issue, closeMessage, closeLabel);
issueHasClosed = true;
if (this.options.deleteBranch && issue.pull_request) {
issueLogger.info(`Deleting the branch since the option ${issueLogger.createOptionLink(option_1.Option.DeleteBranch)} is enabled`);
yield this._deleteBranch(issue);
this.deletedBranchIssues.push(issue);
}
}
else {
issueLogger.info(`Stale $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceStale}, hasUpdate? ${issueHasUpdateInCloseWindow})`);
}
}
}
// TODO: make a function for shouldMarkWhenRotten
const shouldMarkAsRotten = (0, should_mark_when_stale_1.shouldMarkWhenStale)(daysBeforeRotten);
if (issueHasClosed) {
issueLogger.info(`Issue $$type has been closed, no need to process it further.`);
return;
}
if (!issue.isRotten) {
issueLogger.info(`This $$type is not rotten`);
const shouldIgnoreUpdates = new ignore_updates_1.IgnoreUpdates(this.options, issue).shouldIgnoreUpdates();
const shouldBeRotten = !IssuesProcessor._updatedSince(issue.updated_at, daysBeforeRotten);
if (shouldBeRotten) {
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type should be rotten based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
}
else {
issueLogger.info(`This $$type should be rotten based on the last update date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
}
if (shouldMarkAsRotten) {
issueLogger.info(`This $$type should be marked as rotten based on the option ${issueLogger.createOptionLink(this._getDaysBeforeRottenUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeRotten)})`);
// remove the stale label before marking the issue as rotten
yield this._removeStaleLabel(issue, staleLabel);
yield this._markRotten(issue, rottenMessage, rottenLabel, skipMessage);
issue.isRotten = true; // This issue is now considered rotten
issue.markedRottenThisRun = true;
issueLogger.info(`This $$type is now rotten`);
}
else {
issueLogger.info(`This $$type should not be marked as rotten based on the option ${issueLogger.createOptionLink(this._getDaysBeforeStaleUsedOptionName(issue))} (${logger_service_1.LoggerService.cyan(daysBeforeRotten)})`);
}
}
else {
if (shouldIgnoreUpdates) {
issueLogger.info(`This $$type is not old enough to be rotten based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.created_at))} (${logger_service_1.LoggerService.cyan(issue.created_at)})`);
}
else {
issueLogger.info(`This $$type is not old enough to be rotten based on the creation date the ${(0, get_humanized_date_1.getHumanizedDate)(new Date(issue.updated_at))} (${logger_service_1.LoggerService.cyan(issue.updated_at)})`);
}
}
}
if (issue.isRotten) {
issueLogger.info(`This $$type is already rotten`);
// process the rotten issues
this._processRottenIssue(issue, rottenLabel, rottenMessage, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten, closeMessage, closeLabel);
}
});
}
_processRottenIssue(issue, rottenLabel, rottenMessage, labelsToAddWhenUnrotten, labelsToRemoveWhenUnrotten, labelsToRemoveWhenRotten, closeMessage, closeLabel) {
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
// We can get the label creation date from the getLableCreationDate function
const markedRottenOn = (yield this.getLabelCreationDate(issue, rottenLabel)) || issue.updated_at;
issueLogger.info(`$$type marked rotten on: ${logger_service_1.LoggerService.cyan(markedRottenOn)}`);
const issueHasCommentsSinceRotten = yield this._hasCommentsSince(issue, markedRottenOn, rottenMessage);
issueLogger.info(`$$type has been commented on: ${logger_service_1.LoggerService.cyan(issueHasCommentsSinceRotten)}`);
const daysBeforeClose = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
issueLogger.info(`Days before $$type close: ${logger_service_1.LoggerService.cyan(daysBeforeClose)}`);
const shouldRemoveRottenWhenUpdated = this._shouldRemoveRottenWhenUpdated(issue);
issueLogger.info(`The option ${issueLogger.createOptionLink(this._getRemoveRottenWhenUpdatedUsedOptionName(issue))} is: ${logger_service_1.LoggerService.cyan(shouldRemoveRottenWhenUpdated)}`);
if (shouldRemoveRottenWhenUpdated) {
issueLogger.info(`The rotten label should not be removed`);
}
else {
issueLogger.info(`The rotten label should be removed if all conditions met`);
}
if (issue.markedRottenThisRun) {
issueLogger.info(`marked rotten this run, so don't check for updates`);
yield this._removeLabelsOnStatusTransition(issue, labelsToRemoveWhenRotten, option_1.Option.LabelsToRemoveWhenRotten);
}
// The issue.updated_at and markedRottenOn are not always exactly in sync (they can be off by a second or 2)
// isDateMoreRecentThan makes sure they are not the same date within a certain tolerance (15 seconds in this case)
const issueHasUpdateSinceRotten = (0, is_date_more_recent_than_1.isDateMoreRecentThan)(new Date(issue.updated_at), new Date(markedRottenOn), 15);
issueLogger.info(`$$type has been updated since it was marked rotten: ${logger_service_1.LoggerService.cyan(issueHasUpdateSinceRotten)}`);
// Should we un-rotten this issue?
if (shouldRemoveRottenWhenUpdated &&
(issueHasUpdateSinceRotten || issueHasCommentsSinceRotten) &&
!issue.markedRottenThisRun) {
issueLogger.info(`Remove the rotten label since the $$type has been updated and the workflow should remove the stale label when updated`);
yield this._removeRottenLabel(issue, rottenLabel);
// Are there labels to remove or add when an issue is no longer rotten?
// This logic takes care of removing labels when unrotten
yield this._removeLabelsOnStatusTransition(issue, labelsToRemoveWhenUnrotten, option_1.Option.LabelsToRemoveWhenUnrotten);
yield this._addLabelsWhenUnrotten(issue, labelsToAddWhenUnrotten);
issueLogger.info(`Skipping the process since the $$type is now un-rotten`);
return; // Nothing to do because it is no longer rotten
}
// Now start closing logic
if (daysBeforeClose < 0) {
return; // Nothing to do because we aren't closing stale issues
return; // Nothing to do because we aren't closing rotten issues
}
const issueHasUpdateInCloseWindow = IssuesProcessor._updatedSince(issue.updated_at, daysBeforeClose);
issueLogger.info(`$$type has been updated in the last ${daysBeforeClose} days: ${logger_service_1.LoggerService.cyan(issueHasUpdateInCloseWindow)}`);
if (!issueHasCommentsSinceStale && !issueHasUpdateInCloseWindow) {
if (!issueHasCommentsSinceRotten && !issueHasUpdateInCloseWindow) {
issueLogger.info(`Closing $$type because it was last updated on: ${logger_service_1.LoggerService.cyan(issue.updated_at)}`);
yield this._closeIssue(issue, closeMessage, closeLabel);
if (this.options.deleteBranch && issue.pull_request) {
@ -807,7 +961,7 @@ class IssuesProcessor {
}
}
else {
issueLogger.info(`Stale $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceStale}, hasUpdate? ${issueHasUpdateInCloseWindow})`);
issueLogger.info(`Rotten $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceRotten}, hasUpdate? ${issueHasUpdateInCloseWindow})`);
}
});
}
@ -877,12 +1031,57 @@ class IssuesProcessor {
}
});
}
_markRotten(issue, rottenMessage, rottenLabel, skipMessage) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Marking this $$type as rotten`);
this.rottenIssues.push(issue);
// if the issue is being marked rotten, the updated date should be changed to right now
// so that close calculations work correctly
const newUpdatedAtDate = new Date();
issue.updated_at = newUpdatedAtDate.toString();
if (!skipMessage) {
try {
this._consumeIssueOperation(issue);
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsComment(issue);
if (!this.options.debugOnly) {
yield this.client.rest.issues.createComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issue.number,
body: rottenMessage
});
}
}
catch (error) {
issueLogger.error(`Error when creating a comment: ${error.message}`);
}
}
try {
this._consumeIssueOperation(issue);
(_b = this.statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedItemsLabel(issue);
(_c = this.statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleItemsCount(issue);
if (!this.options.debugOnly) {
yield this.client.rest.issues.addLabels({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issue.number,
labels: [rottenLabel]
});
}
}
catch (error) {
issueLogger.error(`Error when adding a label: ${error.message}`);
}
});
}
// Close an issue based on staleness
_closeIssue(issue, closeMessage, closeLabel) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Closing $$type for being stale`);
issueLogger.info(`Closing $$type for being stale/rotten`);
this.closedIssues.push(issue);
if (closeMessage) {
try {
@ -1012,6 +1211,16 @@ class IssuesProcessor {
? this.options.daysBeforeStale
: this.options.daysBeforePrStale;
}
_getDaysBeforeIssueRotten() {
return isNaN(this.options.daysBeforeIssueRotten)
? this.options.daysBeforeRotten
: this.options.daysBeforeIssueRotten;
}
_getDaysBeforePrRotten() {
return isNaN(this.options.daysBeforePrRotten)
? this.options.daysBeforeRotten
: this.options.daysBeforePrRotten;
}
_getDaysBeforeIssueClose() {
return isNaN(this.options.daysBeforeIssueClose)
? this.options.daysBeforeClose
@ -1063,6 +1272,18 @@ class IssuesProcessor {
}
return this.options.removeStaleWhenUpdated;
}
_shouldRemoveRottenWhenUpdated(issue) {
if (issue.isPullRequest) {
if ((0, is_boolean_1.isBoolean)(this.options.removePrRottenWhenUpdated)) {
return this.options.removePrRottenWhenUpdated;
}
return this.options.removeRottenWhenUpdated;
}
if ((0, is_boolean_1.isBoolean)(this.options.removeIssueRottenWhenUpdated)) {
return this.options.removeIssueRottenWhenUpdated;
}
return this.options.removeRottenWhenUpdated;
}
_removeLabelsOnStatusTransition(issue, removeLabels, staleStatus) {
return __awaiter(this, void 0, void 0, function* () {
if (!removeLabels.length) {
@ -1101,6 +1322,33 @@ class IssuesProcessor {
}
});
}
_addLabelsWhenUnrotten(issue, labelsToAdd) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!labelsToAdd.length) {
return;
}
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Adding all the labels specified via the ${this._logger.createOptionLink(option_1.Option.LabelsToAddWhenUnrotten)} option.`);
// TODO: this might need to be changed to a set to avoiod repetition
this.addedLabelIssues.push(issue);
try {
this._consumeIssueOperation(issue);
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsLabel(issue);
if (!this.options.debugOnly) {
yield this.client.rest.issues.addLabels({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
}
}
catch (error) {
this._logger.error(`Error when adding labels after updated from rotten: ${error.message}`);
}
});
}
_removeStaleLabel(issue, staleLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
@ -1110,6 +1358,15 @@ class IssuesProcessor {
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementUndoStaleItemsCount(issue);
});
}
_removeRottenLabel(issue, rottenLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`The $$type is no longer rotten. Removing the rotten label...`);
yield this._removeLabel(issue, rottenLabel);
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementUndoRottenItemsCount(issue);
});
}
_removeCloseLabel(issue, closeLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
@ -1150,6 +1407,21 @@ class IssuesProcessor {
? option_1.Option.DaysBeforeStale
: option_1.Option.DaysBeforePrStale;
}
_getDaysBeforeRottenUsedOptionName(issue) {
return issue.isPullRequest
? this._getDaysBeforePrRottenUsedOptionName()
: this._getDaysBeforeIssueRottenUsedOptionName();
}
_getDaysBeforeIssueRottenUsedOptionName() {
return isNaN(this.options.daysBeforeIssueRotten)
? option_1.Option.DaysBeforeRotten
: option_1.Option.DaysBeforeIssueRotten;
}
_getDaysBeforePrRottenUsedOptionName() {
return isNaN(this.options.daysBeforePrRotten)
? option_1.Option.DaysBeforeRotten
: option_1.Option.DaysBeforePrRotten;
}
_getRemoveStaleWhenUpdatedUsedOptionName(issue) {
if (issue.isPullRequest) {
if ((0, is_boolean_1.isBoolean)(this.options.removePrStaleWhenUpdated)) {
@ -1162,6 +1434,18 @@ class IssuesProcessor {
}
return option_1.Option.RemoveStaleWhenUpdated;
}
_getRemoveRottenWhenUpdatedUsedOptionName(issue) {
if (issue.isPullRequest) {
if ((0, is_boolean_1.isBoolean)(this.options.removePrRottenWhenUpdated)) {
return option_1.Option.RemovePrRottenWhenUpdated;
}
return option_1.Option.RemoveRottenWhenUpdated;
}
if ((0, is_boolean_1.isBoolean)(this.options.removeIssueRottenWhenUpdated)) {
return option_1.Option.RemoveIssueRottenWhenUpdated;
}
return option_1.Option.RemoveRottenWhenUpdated;
}
}
exports.IssuesProcessor = IssuesProcessor;
@ -1815,6 +2099,10 @@ class Statistics {
this.stalePullRequestsCount = 0;
this.undoStaleIssuesCount = 0;
this.undoStalePullRequestsCount = 0;
this.rottenIssuesCount = 0;
this.rottenPullRequestsCount = 0;
this.undoRottenIssuesCount = 0;
this.undoRottenPullRequestsCount = 0;
this.operationsCount = 0;
this.closedIssuesCount = 0;
this.closedPullRequestsCount = 0;
@ -1850,6 +2138,12 @@ class Statistics {
}
return this._incrementUndoStaleIssuesCount(increment);
}
incrementUndoRottenItemsCount(issue, increment = 1) {
if (issue.isPullRequest) {
return this._incrementUndoRottenPullRequestsCount(increment);
}
return this._incrementUndoRottenIssuesCount(increment);
}
setOperationsCount(operationsCount) {
this.operationsCount = operationsCount;
return this;
@ -1942,6 +2236,14 @@ class Statistics {
this.undoStaleIssuesCount += increment;
return this;
}
_incrementUndoRottenPullRequestsCount(increment = 1) {
this.undoRottenPullRequestsCount += increment;
return this;
}
_incrementUndoRottenIssuesCount(increment = 1) {
this.undoRottenIssuesCount += increment;
return this;
}
_incrementUndoStalePullRequestsCount(increment = 1) {
this.undoStalePullRequestsCount += increment;
return this;
@ -2175,18 +2477,25 @@ var Option;
Option["RepoToken"] = "repo-token";
Option["StaleIssueMessage"] = "stale-issue-message";
Option["StalePrMessage"] = "stale-pr-message";
Option["RottenIssueMessage"] = "rotten-issue-message";
Option["RottenPrMessage"] = "rotten-pr-message";
Option["CloseIssueMessage"] = "close-issue-message";
Option["ClosePrMessage"] = "close-pr-message";
Option["DaysBeforeStale"] = "days-before-stale";
Option["DaysBeforeIssueStale"] = "days-before-issue-stale";
Option["DaysBeforePrStale"] = "days-before-pr-stale";
Option["DaysBeforeRotten"] = "days-before-rotten";
Option["DaysBeforeIssueRotten"] = "days-before-issue-rotten";
Option["DaysBeforePrRotten"] = "days-before-pr-rotten";
Option["DaysBeforeClose"] = "days-before-close";
Option["DaysBeforeIssueClose"] = "days-before-issue-close";
Option["DaysBeforePrClose"] = "days-before-pr-close";
Option["StaleIssueLabel"] = "stale-issue-label";
Option["RottenIssueLabel"] = "rotten-issue-label";
Option["CloseIssueLabel"] = "close-issue-label";
Option["ExemptIssueLabels"] = "exempt-issue-labels";
Option["StalePrLabel"] = "stale-pr-label";
Option["RottenPrLabel"] = "rotten-pr-label";
Option["ClosePrLabel"] = "close-pr-label";
Option["ExemptPrLabels"] = "exempt-pr-labels";
Option["OnlyLabels"] = "only-labels";
@ -2197,6 +2506,9 @@ var Option;
Option["RemoveStaleWhenUpdated"] = "remove-stale-when-updated";
Option["RemoveIssueStaleWhenUpdated"] = "remove-issue-stale-when-updated";
Option["RemovePrStaleWhenUpdated"] = "remove-pr-stale-when-updated";
Option["RemoveRottenWhenUpdated"] = "remove-rotten-when-updated";
Option["RemoveIssueRottenWhenUpdated"] = "remove-issue-rotten-when-updated";
Option["RemovePrRottenWhenUpdated"] = "remove-pr-rotten-when-updated";
Option["DebugOnly"] = "debug-only";
Option["Ascending"] = "ascending";
Option["DeleteBranch"] = "delete-branch";
@ -2217,6 +2529,9 @@ var Option;
Option["LabelsToRemoveWhenStale"] = "labels-to-remove-when-stale";
Option["LabelsToRemoveWhenUnstale"] = "labels-to-remove-when-unstale";
Option["LabelsToAddWhenUnstale"] = "labels-to-add-when-unstale";
Option["LabelsToRemoveWhenRotten"] = "labels-to-remove-when-rotten";
Option["LabelsToRemoveWhenUnrotten"] = "labels-to-remove-when-unrotten";
Option["LabelsToAddWhenUnrotten"] = "labels-to-add-when-unrotten";
Option["IgnoreUpdates"] = "ignore-updates";
Option["IgnoreIssueUpdates"] = "ignore-issue-updates";
Option["IgnorePrUpdates"] = "ignore-pr-updates";
@ -2503,7 +2818,7 @@ function _run() {
core.info(`Github API rate remaining: ${rateLimitAtEnd.remaining}; reset at: ${rateLimitAtEnd.reset}`);
}
yield state.persist();
yield processOutput(issueProcessor.staleIssues, issueProcessor.closedIssues);
yield processOutput(issueProcessor.staleIssues, issueProcessor.rottenIssues, issueProcessor.closedIssues);
}
catch (error) {
core.error(error);
@ -2516,18 +2831,25 @@ function _getAndValidateArgs() {
repoToken: core.getInput('repo-token'),
staleIssueMessage: core.getInput('stale-issue-message'),
stalePrMessage: core.getInput('stale-pr-message'),
rottenIssueMessage: core.getInput('rotten-issue-message'),
rottenPrMessage: core.getInput('rotten-pr-message'),
closeIssueMessage: core.getInput('close-issue-message'),
closePrMessage: core.getInput('close-pr-message'),
daysBeforeStale: parseFloat(core.getInput('days-before-stale', { required: true })),
daysBeforeRotten: parseFloat(core.getInput('days-before-rotten', { required: true })),
daysBeforeIssueStale: parseFloat(core.getInput('days-before-issue-stale')),
daysBeforePrStale: parseFloat(core.getInput('days-before-pr-stale')),
daysBeforeIssueRotten: parseFloat(core.getInput('days-before-issue-rotten')),
daysBeforePrRotten: parseFloat(core.getInput('days-before-pr-rotten')),
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 }),
rottenIssueLabel: core.getInput('rotten-issue-label', { required: true }),
closeIssueLabel: core.getInput('close-issue-label'),
exemptIssueLabels: core.getInput('exempt-issue-labels'),
stalePrLabel: core.getInput('stale-pr-label', { required: true }),
rottenPrLabel: core.getInput('rotten-pr-label', { required: true }),
closePrLabel: core.getInput('close-pr-label'),
exemptPrLabels: core.getInput('exempt-pr-labels'),
onlyLabels: core.getInput('only-labels'),
@ -2540,6 +2862,9 @@ function _getAndValidateArgs() {
removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'),
removeIssueStaleWhenUpdated: _toOptionalBoolean('remove-issue-stale-when-updated'),
removePrStaleWhenUpdated: _toOptionalBoolean('remove-pr-stale-when-updated'),
removeRottenWhenUpdated: !(core.getInput('remove-rotten-when-updated') === 'false'),
removeIssueRottenWhenUpdated: _toOptionalBoolean('remove-issue-rotten-when-updated'),
removePrRottenWhenUpdated: _toOptionalBoolean('remove-pr-rotten-when-updated'),
debugOnly: core.getInput('debug-only') === 'true',
ascending: core.getInput('ascending') === 'true',
deleteBranch: core.getInput('delete-branch') === 'true',
@ -2562,6 +2887,9 @@ function _getAndValidateArgs() {
labelsToRemoveWhenStale: core.getInput('labels-to-remove-when-stale'),
labelsToRemoveWhenUnstale: core.getInput('labels-to-remove-when-unstale'),
labelsToAddWhenUnstale: core.getInput('labels-to-add-when-unstale'),
labelsToRemoveWhenRotten: core.getInput('labels-to-remove-when-rotten'),
labelsToRemoveWhenUnrotten: core.getInput('labels-to-remove-when-unrotten'),
labelsToAddWhenUnrotten: core.getInput('labels-to-add-when-unrotten'),
ignoreUpdates: core.getInput('ignore-updates') === 'true',
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
@ -2576,6 +2904,13 @@ function _getAndValidateArgs() {
throw new Error(errorMessage);
}
}
for (const numberInput of ['days-before-rotten']) {
if (isNaN(parseFloat(core.getInput(numberInput)))) {
const errorMessage = `Option "${numberInput}" did not parse to a valid float`;
core.setFailed(errorMessage);
throw new Error(errorMessage);
}
}
for (const numberInput of ['days-before-close', 'operations-per-run']) {
if (isNaN(parseInt(core.getInput(numberInput)))) {
const errorMessage = `Option "${numberInput}" did not parse to a valid integer`;
@ -2601,9 +2936,10 @@ function _getAndValidateArgs() {
}
return args;
}
function processOutput(staledIssues, closedIssues) {
function processOutput(staledIssues, rottenIssues, closedIssues) {
return __awaiter(this, void 0, void 0, function* () {
core.setOutput('staled-issues-prs', JSON.stringify(staledIssues));
core.setOutput('rotten-issues-prs', JSON.stringify(rottenIssues));
core.setOutput('closed-issues-prs', JSON.stringify(closedIssues));
});
}

View File

@ -20,9 +20,12 @@ describe('Issue', (): void => {
daysBeforeClose: 0,
daysBeforeIssueClose: 0,
daysBeforeIssueStale: 0,
daysBeforeIssueRotten: 0,
daysBeforePrClose: 0,
daysBeforePrStale: 0,
daysBeforePrRotten: 0,
daysBeforeStale: 0,
daysBeforeRotten: 0,
debugOnly: false,
deleteBranch: false,
exemptIssueLabels: '',
@ -37,12 +40,19 @@ describe('Issue', (): void => {
removeStaleWhenUpdated: false,
removeIssueStaleWhenUpdated: undefined,
removePrStaleWhenUpdated: undefined,
removeRottenWhenUpdated: false,
removeIssueRottenWhenUpdated: undefined,
removePrRottenWhenUpdated: undefined,
repoToken: '',
staleIssueMessage: '',
stalePrMessage: '',
rottenIssueMessage: '',
rottenPrMessage: '',
startDate: undefined,
stalePrLabel: 'dummy-stale-pr-label',
staleIssueLabel: 'dummy-stale-issue-label',
rottenPrLabel: 'dummy-rotten-pr-label',
rottenIssueLabel: 'dummy-rotten-issue-label',
exemptMilestones: '',
exemptIssueMilestones: '',
exemptPrMilestones: '',
@ -59,6 +69,9 @@ describe('Issue', (): void => {
labelsToRemoveWhenStale: '',
labelsToRemoveWhenUnstale: '',
labelsToAddWhenUnstale: '',
labelsToRemoveWhenRotten: '',
labelsToRemoveWhenUnrotten: '',
labelsToAddWhenUnrotten: '',
ignoreUpdates: false,
ignoreIssueUpdates: undefined,
ignorePrUpdates: undefined,

View File

@ -21,7 +21,9 @@ export class Issue implements IIssue {
readonly milestone?: IMilestone | null;
readonly assignees: Assignee[];
isStale: boolean;
isRotten: boolean;
markedStaleThisRun: boolean;
markedRottenThisRun: boolean;
operations = new Operations();
private readonly _options: IIssuesProcessorOptions;
@ -42,7 +44,9 @@ export class Issue implements IIssue {
this.milestone = issue.milestone;
this.assignees = issue.assignees || [];
this.isStale = isLabeled(this, this.staleLabel);
this.isRotten = isLabeled(this, this.rottenLabel);
this.markedStaleThisRun = false;
this.markedRottenThisRun = false;
}
get isPullRequest(): boolean {
@ -52,6 +56,9 @@ export class Issue implements IIssue {
get staleLabel(): string {
return this._getStaleLabel();
}
get rottenLabel(): string {
return this._getRottenLabel();
}
get hasAssignees(): boolean {
return this.assignees.length > 0;
@ -62,6 +69,11 @@ export class Issue implements IIssue {
? this._options.stalePrLabel
: this._options.staleIssueLabel;
}
private _getRottenLabel(): string {
return this.isPullRequest
? this._options.rottenPrLabel
: this._options.rottenIssueLabel;
}
}
function mapLabels(labels: (string | ILabel)[] | ILabel[]): ILabel[] {

View File

@ -69,6 +69,7 @@ export class IssuesProcessor {
readonly client: InstanceType<typeof GitHub>;
readonly options: IIssuesProcessorOptions;
readonly staleIssues: Issue[] = [];
readonly rottenIssues: Issue[] = [];
readonly closedIssues: Issue[] = [];
readonly deletedBranchIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
@ -141,6 +142,16 @@ export class IssuesProcessor {
const labelsToRemoveWhenUnstale: string[] = wordsToList(
this.options.labelsToRemoveWhenUnstale
);
const labelsToRemoveWhenRotten: string[] = wordsToList(
this.options.labelsToRemoveWhenRotten
);
const labelsToAddWhenUnrotten: string[] = wordsToList(
this.options.labelsToAddWhenUnrotten
);
const labelsToRemoveWhenUnrotten: string[] = wordsToList(
this.options.labelsToRemoveWhenUnrotten
);
for (const issue of issues.values()) {
// Stop the processing if no more operations remains
@ -160,7 +171,10 @@ export class IssuesProcessor {
issue,
labelsToAddWhenUnstale,
labelsToRemoveWhenUnstale,
labelsToRemoveWhenStale
labelsToRemoveWhenStale,
labelsToAddWhenUnrotten,
labelsToRemoveWhenUnrotten,
labelsToRemoveWhenRotten
);
});
this.state.addIssueToProcessed(issue);
@ -200,7 +214,10 @@ export class IssuesProcessor {
issue: Issue,
labelsToAddWhenUnstale: Readonly<string>[],
labelsToRemoveWhenUnstale: Readonly<string>[],
labelsToRemoveWhenStale: Readonly<string>[]
labelsToRemoveWhenStale: Readonly<string>[],
labelsToAddWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenRotten: Readonly<string>[]
): Promise<void> {
this.statistics?.incrementProcessedItemsCount(issue);
@ -215,12 +232,21 @@ export class IssuesProcessor {
const staleMessage: string = issue.isPullRequest
? this.options.stalePrMessage
: this.options.staleIssueMessage;
const rottenMessage: string = issue.isPullRequest
? this.options.rottenPrMessage
: this.options.rottenIssueMessage;
const closeMessage: string = issue.isPullRequest
? this.options.closePrMessage
: this.options.closeIssueMessage;
const skipRottenMessage = issue.isPullRequest
? this.options.rottenPrMessage.length === 0
: this.options.rottenIssueMessage.length === 0;
const staleLabel: string = issue.isPullRequest
? this.options.stalePrLabel
: this.options.staleIssueLabel;
const rottenLabel: string = issue.isPullRequest
? this.options.rottenPrLabel
: this.options.rottenIssueLabel;
const closeLabel: string = issue.isPullRequest
? this.options.closePrLabel
: this.options.closeIssueLabel;
@ -342,10 +368,16 @@ export class IssuesProcessor {
}
}
// Check if the issue is stale, if not, check if it is rotten and then log the findings.
if (issue.isStale) {
issueLogger.info(`This $$type includes a stale label`);
} else {
issueLogger.info(`This $$type does not include a stale label`);
if (issue.isRotten) {
issueLogger.info(`This $$type includes a rotten label`);
} else {
issueLogger.info(`This $$type does not include a rotten label`);
}
}
const exemptLabels: string[] = wordsToList(
@ -445,78 +477,92 @@ export class IssuesProcessor {
return; // Don't process draft PR
}
// Here we are looking into if the issue is stale or not, and then adding the label. This same code will also be used for the rotten label.
// Determine if this issue needs to be marked stale first
if (!issue.isStale) {
issueLogger.info(`This $$type is not stale`);
const shouldIgnoreUpdates: boolean = new IgnoreUpdates(
this.options,
issue
).shouldIgnoreUpdates();
// Should this issue be marked as stale?
let shouldBeStale: boolean;
// Ignore the last update and only use the creation date
if (shouldIgnoreUpdates) {
shouldBeStale = !IssuesProcessor._updatedSince(
issue.created_at,
daysBeforeStale
if (issue.isRotten) {
await this._processRottenIssue(
issue,
rottenLabel,
rottenMessage,
labelsToAddWhenUnrotten,
labelsToRemoveWhenUnrotten,
labelsToRemoveWhenRotten,
closeMessage,
closeLabel
);
}
// Use the last update to check if we need to stale
else {
shouldBeStale = !IssuesProcessor._updatedSince(
issue.updated_at,
daysBeforeStale
);
}
if (shouldBeStale) {
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type should be stale based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
);
} else {
issueLogger.info(
`This $$type should be stale based on the last update date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
}
if (shouldMarkAsStale) {
issueLogger.info(
`This $$type should be marked as stale based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeStaleUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeStale)})`
);
await this._markStale(issue, staleMessage, staleLabel, skipMessage);
issue.isStale = true; // This issue is now considered stale
issue.markedStaleThisRun = true;
issueLogger.info(`This $$type is now stale`);
} else {
issueLogger.info(
`This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeStaleUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeStale)})`
);
}
} else {
const shouldIgnoreUpdates: boolean = new IgnoreUpdates(
this.options,
issue
).shouldIgnoreUpdates();
// Should this issue be marked as stale?
let shouldBeStale: boolean;
// Ignore the last update and only use the creation date
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type should not be stale based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
shouldBeStale = !IssuesProcessor._updatedSince(
issue.created_at,
daysBeforeStale
);
}
// Use the last update to check if we need to stale
else {
shouldBeStale = !IssuesProcessor._updatedSince(
issue.updated_at,
daysBeforeStale
);
}
if (shouldBeStale) {
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type should be stale based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
);
} else {
issueLogger.info(
`This $$type should be stale based on the last update date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
}
if (shouldMarkAsStale) {
issueLogger.info(
`This $$type should be marked as stale based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeStaleUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeStale)})`
);
await this._markStale(issue, staleMessage, staleLabel, skipMessage);
issue.isStale = true; // This issue is now considered stale
issue.markedStaleThisRun = true;
issueLogger.info(`This $$type is now stale`);
} else {
issueLogger.info(
`This $$type should not be marked as stale based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeStaleUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeStale)})`
);
}
} else {
issueLogger.info(
`This $$type should not be stale based on the last update date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type should not be stale based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
);
} else {
issueLogger.info(
`This $$type should not be stale based on the last update date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
}
}
}
}
@ -528,11 +574,17 @@ export class IssuesProcessor {
issue,
staleLabel,
staleMessage,
rottenLabel,
rottenMessage,
closeLabel,
closeMessage,
labelsToAddWhenUnstale,
labelsToRemoveWhenUnstale,
labelsToRemoveWhenStale,
closeMessage,
closeLabel
labelsToAddWhenUnrotten,
labelsToRemoveWhenUnrotten,
labelsToRemoveWhenRotten,
skipRottenMessage
);
}
@ -650,17 +702,28 @@ export class IssuesProcessor {
}
// handle all of the stale issue logic when we find a stale issue
// This whole thing needs to be altered, to be calculated based on the days to rotten, rather than days to close or whatever
private async _processStaleIssue(
issue: Issue,
staleLabel: string,
staleMessage: string,
rottenLabel: string,
rottenMessage: string,
closeLabel: string,
closeMessage: string,
labelsToAddWhenUnstale: Readonly<string>[],
labelsToRemoveWhenUnstale: Readonly<string>[],
labelsToRemoveWhenStale: Readonly<string>[],
closeMessage?: string,
closeLabel?: string
labelsToAddWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenRotten: Readonly<string>[],
skipMessage: boolean
) {
const issueLogger: IssueLogger = new IssueLogger(issue);
let issueHasClosed: boolean = false;
// We can get the label creation date from the getLableCreationDate function
const markedStaleOn: string =
(await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
issueLogger.info(
@ -678,12 +741,15 @@ export class IssuesProcessor {
)}`
);
const daysBeforeRotten: number = issue.isPullRequest
? this._getDaysBeforePrRotten()
: this._getDaysBeforeIssueRotten();
const daysBeforeClose: number = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
issueLogger.info(
`Days before $$type close: ${LoggerService.cyan(daysBeforeClose)}`
`Days before $$type rotten: ${LoggerService.cyan(daysBeforeRotten)}`
);
const shouldRemoveStaleWhenUpdated: boolean =
@ -703,6 +769,7 @@ export class IssuesProcessor {
);
}
// we will need to use a variation of this for the rotten state
if (issue.markedStaleThisRun) {
issueLogger.info(`marked stale this run, so don't check for updates`);
await this._removeLabelsOnStatusTransition(
@ -750,9 +817,254 @@ export class IssuesProcessor {
return; // Nothing to do because it is no longer stale
}
if (daysBeforeRotten < 0) {
if (daysBeforeClose < 0) {
issueLogger.info(
`Stale $$type cannot be rotten or closed because days before rotten: ${daysBeforeRotten}, and days before close: ${daysBeforeClose}`
);
return;
} else {
issueLogger.info(
`Closing issue without rottening it because days before $$type rotten: ${LoggerService.cyan(
daysBeforeRotten
)}`
);
const issueHasUpdateInCloseWindow: boolean =
IssuesProcessor._updatedSince(issue.updated_at, daysBeforeClose);
issueLogger.info(
`$$type has been updated in the last ${daysBeforeClose} days: ${LoggerService.cyan(
issueHasUpdateInCloseWindow
)}`
);
if (!issueHasUpdateInCloseWindow && !issueHasCommentsSinceStale) {
issueLogger.info(
`Closing $$type because it was last updated on: ${LoggerService.cyan(
issue.updated_at
)}`
);
await this._closeIssue(issue, closeMessage, closeLabel);
issueHasClosed = true;
if (this.options.deleteBranch && issue.pull_request) {
issueLogger.info(
`Deleting the branch since the option ${issueLogger.createOptionLink(
Option.DeleteBranch
)} is enabled`
);
await this._deleteBranch(issue);
this.deletedBranchIssues.push(issue);
}
} else {
issueLogger.info(
`Stale $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceStale}, hasUpdate? ${issueHasUpdateInCloseWindow})`
);
}
}
}
// TODO: make a function for shouldMarkWhenRotten
const shouldMarkAsRotten: boolean = shouldMarkWhenStale(daysBeforeRotten);
if (issueHasClosed) {
issueLogger.info(
`Issue $$type has been closed, no need to process it further.`
);
return;
}
if (!issue.isRotten) {
issueLogger.info(`This $$type is not rotten`);
const shouldIgnoreUpdates: boolean = new IgnoreUpdates(
this.options,
issue
).shouldIgnoreUpdates();
const shouldBeRotten: boolean = !IssuesProcessor._updatedSince(
issue.updated_at,
daysBeforeRotten
);
if (shouldBeRotten) {
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type should be rotten based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
);
} else {
issueLogger.info(
`This $$type should be rotten based on the last update date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
}
if (shouldMarkAsRotten) {
issueLogger.info(
`This $$type should be marked as rotten based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeRottenUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeRotten)})`
);
// remove the stale label before marking the issue as rotten
await this._removeStaleLabel(issue, staleLabel);
await this._markRotten(
issue,
rottenMessage,
rottenLabel,
skipMessage
);
issue.isRotten = true; // This issue is now considered rotten
issue.markedRottenThisRun = true;
issueLogger.info(`This $$type is now rotten`);
} else {
issueLogger.info(
`This $$type should not be marked as rotten based on the option ${issueLogger.createOptionLink(
this._getDaysBeforeStaleUsedOptionName(issue)
)} (${LoggerService.cyan(daysBeforeRotten)})`
);
}
} else {
if (shouldIgnoreUpdates) {
issueLogger.info(
`This $$type is not old enough to be rotten based on the creation date the ${getHumanizedDate(
new Date(issue.created_at)
)} (${LoggerService.cyan(issue.created_at)})`
);
} else {
issueLogger.info(
`This $$type is not old enough to be rotten based on the creation date the ${getHumanizedDate(
new Date(issue.updated_at)
)} (${LoggerService.cyan(issue.updated_at)})`
);
}
}
}
if (issue.isRotten) {
issueLogger.info(`This $$type is already rotten`);
// process the rotten issues
this._processRottenIssue(
issue,
rottenLabel,
rottenMessage,
labelsToAddWhenUnrotten,
labelsToRemoveWhenUnrotten,
labelsToRemoveWhenRotten,
closeMessage,
closeLabel
);
}
}
private async _processRottenIssue(
issue: Issue,
rottenLabel: string,
rottenMessage: string,
labelsToAddWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenUnrotten: Readonly<string>[],
labelsToRemoveWhenRotten: Readonly<string>[],
closeMessage?: string,
closeLabel?: string
) {
const issueLogger: IssueLogger = new IssueLogger(issue);
// We can get the label creation date from the getLableCreationDate function
const markedRottenOn: string =
(await this.getLabelCreationDate(issue, rottenLabel)) || issue.updated_at;
issueLogger.info(
`$$type marked rotten on: ${LoggerService.cyan(markedRottenOn)}`
);
const issueHasCommentsSinceRotten: boolean = await this._hasCommentsSince(
issue,
markedRottenOn,
rottenMessage
);
issueLogger.info(
`$$type has been commented on: ${LoggerService.cyan(
issueHasCommentsSinceRotten
)}`
);
const daysBeforeClose: number = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
issueLogger.info(
`Days before $$type close: ${LoggerService.cyan(daysBeforeClose)}`
);
const shouldRemoveRottenWhenUpdated: boolean =
this._shouldRemoveRottenWhenUpdated(issue);
issueLogger.info(
`The option ${issueLogger.createOptionLink(
this._getRemoveRottenWhenUpdatedUsedOptionName(issue)
)} is: ${LoggerService.cyan(shouldRemoveRottenWhenUpdated)}`
);
if (shouldRemoveRottenWhenUpdated) {
issueLogger.info(`The rotten label should not be removed`);
} else {
issueLogger.info(
`The rotten label should be removed if all conditions met`
);
}
if (issue.markedRottenThisRun) {
issueLogger.info(`marked rotten this run, so don't check for updates`);
await this._removeLabelsOnStatusTransition(
issue,
labelsToRemoveWhenRotten,
Option.LabelsToRemoveWhenRotten
);
}
// The issue.updated_at and markedRottenOn are not always exactly in sync (they can be off by a second or 2)
// isDateMoreRecentThan makes sure they are not the same date within a certain tolerance (15 seconds in this case)
const issueHasUpdateSinceRotten = isDateMoreRecentThan(
new Date(issue.updated_at),
new Date(markedRottenOn),
15
);
issueLogger.info(
`$$type has been updated since it was marked rotten: ${LoggerService.cyan(
issueHasUpdateSinceRotten
)}`
);
// Should we un-rotten this issue?
if (
shouldRemoveRottenWhenUpdated &&
(issueHasUpdateSinceRotten || issueHasCommentsSinceRotten) &&
!issue.markedRottenThisRun
) {
issueLogger.info(
`Remove the rotten label since the $$type has been updated and the workflow should remove the stale label when updated`
);
await this._removeRottenLabel(issue, rottenLabel);
// Are there labels to remove or add when an issue is no longer rotten?
// This logic takes care of removing labels when unrotten
await this._removeLabelsOnStatusTransition(
issue,
labelsToRemoveWhenUnrotten,
Option.LabelsToRemoveWhenUnrotten
);
await this._addLabelsWhenUnrotten(issue, labelsToAddWhenUnrotten);
issueLogger.info(
`Skipping the process since the $$type is now un-rotten`
);
return; // Nothing to do because it is no longer rotten
}
// Now start closing logic
if (daysBeforeClose < 0) {
return; // Nothing to do because we aren't closing stale issues
return; // Nothing to do because we aren't closing rotten issues
}
const issueHasUpdateInCloseWindow: boolean = IssuesProcessor._updatedSince(
@ -765,7 +1077,7 @@ export class IssuesProcessor {
)}`
);
if (!issueHasCommentsSinceStale && !issueHasUpdateInCloseWindow) {
if (!issueHasCommentsSinceRotten && !issueHasUpdateInCloseWindow) {
issueLogger.info(
`Closing $$type because it was last updated on: ${LoggerService.cyan(
issue.updated_at
@ -784,7 +1096,7 @@ export class IssuesProcessor {
}
} else {
issueLogger.info(
`Stale $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceStale}, hasUpdate? ${issueHasUpdateInCloseWindow})`
`Rotten $$type is not old enough to close yet (hasComments? ${issueHasCommentsSinceRotten}, hasUpdate? ${issueHasUpdateInCloseWindow})`
);
}
}
@ -876,6 +1188,57 @@ export class IssuesProcessor {
issueLogger.error(`Error when adding a label: ${error.message}`);
}
}
private async _markRotten(
issue: Issue,
rottenMessage: string,
rottenLabel: string,
skipMessage: boolean
): Promise<void> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Marking this $$type as rotten`);
this.rottenIssues.push(issue);
// if the issue is being marked rotten, the updated date should be changed to right now
// so that close calculations work correctly
const newUpdatedAtDate: Date = new Date();
issue.updated_at = newUpdatedAtDate.toString();
if (!skipMessage) {
try {
this._consumeIssueOperation(issue);
this.statistics?.incrementAddedItemsComment(issue);
if (!this.options.debugOnly) {
await this.client.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: rottenMessage
});
}
} catch (error) {
issueLogger.error(`Error when creating a comment: ${error.message}`);
}
}
try {
this._consumeIssueOperation(issue);
this.statistics?.incrementAddedItemsLabel(issue);
this.statistics?.incrementStaleItemsCount(issue);
if (!this.options.debugOnly) {
await this.client.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [rottenLabel]
});
}
} catch (error) {
issueLogger.error(`Error when adding a label: ${error.message}`);
}
}
// Close an issue based on staleness
private async _closeIssue(
@ -885,7 +1248,7 @@ export class IssuesProcessor {
): Promise<void> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Closing $$type for being stale`);
issueLogger.info(`Closing $$type for being stale/rotten`);
this.closedIssues.push(issue);
if (closeMessage) {
@ -1056,6 +1419,17 @@ export class IssuesProcessor {
? this.options.daysBeforeStale
: this.options.daysBeforePrStale;
}
private _getDaysBeforeIssueRotten(): number {
return isNaN(this.options.daysBeforeIssueRotten)
? this.options.daysBeforeRotten
: this.options.daysBeforeIssueRotten;
}
private _getDaysBeforePrRotten(): number {
return isNaN(this.options.daysBeforePrRotten)
? this.options.daysBeforeRotten
: this.options.daysBeforePrRotten;
}
private _getDaysBeforeIssueClose(): number {
return isNaN(this.options.daysBeforeIssueClose)
@ -1116,6 +1490,21 @@ export class IssuesProcessor {
return this.options.removeStaleWhenUpdated;
}
private _shouldRemoveRottenWhenUpdated(issue: Issue): boolean {
if (issue.isPullRequest) {
if (isBoolean(this.options.removePrRottenWhenUpdated)) {
return this.options.removePrRottenWhenUpdated;
}
return this.options.removeRottenWhenUpdated;
}
if (isBoolean(this.options.removeIssueRottenWhenUpdated)) {
return this.options.removeIssueRottenWhenUpdated;
}
return this.options.removeRottenWhenUpdated;
}
private async _removeLabelsOnStatusTransition(
issue: Issue,
@ -1175,6 +1564,42 @@ export class IssuesProcessor {
}
}
private async _addLabelsWhenUnrotten(
issue: Issue,
labelsToAdd: Readonly<string>[]
): Promise<void> {
if (!labelsToAdd.length) {
return;
}
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(
`Adding all the labels specified via the ${this._logger.createOptionLink(
Option.LabelsToAddWhenUnrotten
)} option.`
);
// TODO: this might need to be changed to a set to avoiod repetition
this.addedLabelIssues.push(issue);
try {
this._consumeIssueOperation(issue);
this.statistics?.incrementAddedItemsLabel(issue);
if (!this.options.debugOnly) {
await this.client.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
}
} catch (error) {
this._logger.error(
`Error when adding labels after updated from rotten: ${error.message}`
);
}
}
private async _removeStaleLabel(
issue: Issue,
staleLabel: Readonly<string>
@ -1188,6 +1613,19 @@ export class IssuesProcessor {
await this._removeLabel(issue, staleLabel);
this.statistics?.incrementUndoStaleItemsCount(issue);
}
private async _removeRottenLabel(
issue: Issue,
rottenLabel: Readonly<string>
): Promise<void> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(
`The $$type is no longer rotten. Removing the rotten label...`
);
await this._removeLabel(issue, rottenLabel);
this.statistics?.incrementUndoRottenItemsCount(issue);
}
private async _removeCloseLabel(
issue: Issue,
@ -1266,6 +1704,32 @@ export class IssuesProcessor {
: Option.DaysBeforePrStale;
}
private _getDaysBeforeRottenUsedOptionName(
issue: Readonly<Issue>
):
| Option.DaysBeforeRotten
| Option.DaysBeforeIssueRotten
| Option.DaysBeforePrRotten {
return issue.isPullRequest
? this._getDaysBeforePrRottenUsedOptionName()
: this._getDaysBeforeIssueRottenUsedOptionName();
}
private _getDaysBeforeIssueRottenUsedOptionName():
| Option.DaysBeforeRotten
| Option.DaysBeforeIssueRotten {
return isNaN(this.options.daysBeforeIssueRotten)
? Option.DaysBeforeRotten
: Option.DaysBeforeIssueRotten;
}
private _getDaysBeforePrRottenUsedOptionName():
| Option.DaysBeforeRotten
| Option.DaysBeforePrRotten {
return isNaN(this.options.daysBeforePrRotten)
? Option.DaysBeforeRotten
: Option.DaysBeforePrRotten;
}
private _getRemoveStaleWhenUpdatedUsedOptionName(
issue: Readonly<Issue>
):
@ -1286,4 +1750,24 @@ export class IssuesProcessor {
return Option.RemoveStaleWhenUpdated;
}
private _getRemoveRottenWhenUpdatedUsedOptionName(
issue: Readonly<Issue>
):
| Option.RemovePrRottenWhenUpdated
| Option.RemoveRottenWhenUpdated
| Option.RemoveIssueRottenWhenUpdated {
if (issue.isPullRequest) {
if (isBoolean(this.options.removePrRottenWhenUpdated)) {
return Option.RemovePrRottenWhenUpdated;
}
return Option.RemoveRottenWhenUpdated;
}
if (isBoolean(this.options.removeIssueRottenWhenUpdated)) {
return Option.RemoveIssueRottenWhenUpdated;
}
return Option.RemoveRottenWhenUpdated;
}
}

View File

@ -15,6 +15,10 @@ export class Statistics {
stalePullRequestsCount = 0;
undoStaleIssuesCount = 0;
undoStalePullRequestsCount = 0;
rottenIssuesCount = 0;
rottenPullRequestsCount = 0;
undoRottenIssuesCount = 0;
undoRottenPullRequestsCount = 0;
operationsCount = 0;
closedIssuesCount = 0;
closedPullRequestsCount = 0;
@ -65,6 +69,17 @@ export class Statistics {
return this._incrementUndoStaleIssuesCount(increment);
}
incrementUndoRottenItemsCount(
issue: Readonly<Issue>,
increment: Readonly<number> = 1
): Statistics {
if (issue.isPullRequest) {
return this._incrementUndoRottenPullRequestsCount(increment);
}
return this._incrementUndoRottenIssuesCount(increment);
}
setOperationsCount(operationsCount: Readonly<number>): Statistics {
this.operationsCount = operationsCount;
@ -222,6 +237,21 @@ export class Statistics {
return this;
}
private _incrementUndoRottenPullRequestsCount(
increment: Readonly<number> = 1
): Statistics {
this.undoRottenPullRequestsCount += increment;
return this;
}
private _incrementUndoRottenIssuesCount(
increment: Readonly<number> = 1
): Statistics {
this.undoRottenIssuesCount += increment;
return this;
}
private _incrementUndoStalePullRequestsCount(
increment: Readonly<number> = 1
): Statistics {

View File

@ -2,18 +2,25 @@ export enum Option {
RepoToken = 'repo-token',
StaleIssueMessage = 'stale-issue-message',
StalePrMessage = 'stale-pr-message',
RottenIssueMessage = 'rotten-issue-message',
RottenPrMessage = 'rotten-pr-message',
CloseIssueMessage = 'close-issue-message',
ClosePrMessage = 'close-pr-message',
DaysBeforeStale = 'days-before-stale',
DaysBeforeIssueStale = 'days-before-issue-stale',
DaysBeforePrStale = 'days-before-pr-stale',
DaysBeforeRotten = 'days-before-rotten',
DaysBeforeIssueRotten = 'days-before-issue-rotten',
DaysBeforePrRotten = 'days-before-pr-rotten',
DaysBeforeClose = 'days-before-close',
DaysBeforeIssueClose = 'days-before-issue-close',
DaysBeforePrClose = 'days-before-pr-close',
StaleIssueLabel = 'stale-issue-label',
RottenIssueLabel = 'rotten-issue-label',
CloseIssueLabel = 'close-issue-label',
ExemptIssueLabels = 'exempt-issue-labels',
StalePrLabel = 'stale-pr-label',
RottenPrLabel = 'rotten-pr-label',
ClosePrLabel = 'close-pr-label',
ExemptPrLabels = 'exempt-pr-labels',
OnlyLabels = 'only-labels',
@ -24,6 +31,9 @@ export enum Option {
RemoveStaleWhenUpdated = 'remove-stale-when-updated',
RemoveIssueStaleWhenUpdated = 'remove-issue-stale-when-updated',
RemovePrStaleWhenUpdated = 'remove-pr-stale-when-updated',
RemoveRottenWhenUpdated = 'remove-rotten-when-updated',
RemoveIssueRottenWhenUpdated = 'remove-issue-rotten-when-updated',
RemovePrRottenWhenUpdated = 'remove-pr-rotten-when-updated',
DebugOnly = 'debug-only',
Ascending = 'ascending',
DeleteBranch = 'delete-branch',
@ -44,6 +54,9 @@ export enum Option {
LabelsToRemoveWhenStale = 'labels-to-remove-when-stale',
LabelsToRemoveWhenUnstale = 'labels-to-remove-when-unstale',
LabelsToAddWhenUnstale = 'labels-to-add-when-unstale',
LabelsToRemoveWhenRotten = 'labels-to-remove-when-rotten',
LabelsToRemoveWhenUnrotten = 'labels-to-remove-when-unrotten',
LabelsToAddWhenUnrotten = 'labels-to-add-when-unrotten',
IgnoreUpdates = 'ignore-updates',
IgnoreIssueUpdates = 'ignore-issue-updates',
IgnorePrUpdates = 'ignore-pr-updates',

View File

@ -4,18 +4,25 @@ export interface IIssuesProcessorOptions {
repoToken: string;
staleIssueMessage: string;
stalePrMessage: string;
rottenIssueMessage: string;
rottenPrMessage: string;
closeIssueMessage: string;
closePrMessage: string;
daysBeforeStale: number;
daysBeforeIssueStale: number; // Could be NaN
daysBeforePrStale: number; // Could be NaN
daysBeforeRotten: number;
daysBeforeIssueRotten: number; // Could be NaN
daysBeforePrRotten: number; // Could be NaN
daysBeforeClose: number;
daysBeforeIssueClose: number; // Could be NaN
daysBeforePrClose: number; // Could be NaN
staleIssueLabel: string;
rottenIssueLabel: string;
closeIssueLabel: string;
exemptIssueLabels: string;
stalePrLabel: string;
rottenPrLabel: string;
closePrLabel: string;
exemptPrLabels: string;
onlyLabels: string;
@ -28,6 +35,9 @@ export interface IIssuesProcessorOptions {
removeStaleWhenUpdated: boolean;
removeIssueStaleWhenUpdated: boolean | undefined;
removePrStaleWhenUpdated: boolean | undefined;
removeRottenWhenUpdated: boolean;
removeIssueRottenWhenUpdated: boolean | undefined;
removePrRottenWhenUpdated: boolean | undefined;
debugOnly: boolean;
ascending: boolean;
deleteBranch: boolean;
@ -48,6 +58,9 @@ export interface IIssuesProcessorOptions {
labelsToRemoveWhenStale: string;
labelsToRemoveWhenUnstale: string;
labelsToAddWhenUnstale: string;
labelsToRemoveWhenRotten: string;
labelsToRemoveWhenUnrotten: string;
labelsToAddWhenUnrotten: string;
ignoreUpdates: boolean;
ignoreIssueUpdates: boolean | undefined;
ignorePrUpdates: boolean | undefined;

View File

@ -46,6 +46,7 @@ async function _run(): Promise<void> {
await processOutput(
issueProcessor.staleIssues,
issueProcessor.rottenIssues,
issueProcessor.closedIssues
);
} catch (error) {
@ -59,22 +60,33 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
repoToken: core.getInput('repo-token'),
staleIssueMessage: core.getInput('stale-issue-message'),
stalePrMessage: core.getInput('stale-pr-message'),
rottenIssueMessage: core.getInput('rotten-issue-message'),
rottenPrMessage: core.getInput('rotten-pr-message'),
closeIssueMessage: core.getInput('close-issue-message'),
closePrMessage: core.getInput('close-pr-message'),
daysBeforeStale: parseFloat(
core.getInput('days-before-stale', {required: true})
),
daysBeforeRotten: parseFloat(
core.getInput('days-before-rotten', {required: true})
),
daysBeforeIssueStale: parseFloat(core.getInput('days-before-issue-stale')),
daysBeforePrStale: parseFloat(core.getInput('days-before-pr-stale')),
daysBeforeIssueRotten: parseFloat(
core.getInput('days-before-issue-rotten')
),
daysBeforePrRotten: parseFloat(core.getInput('days-before-pr-rotten')),
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}),
rottenIssueLabel: core.getInput('rotten-issue-label', {required: true}),
closeIssueLabel: core.getInput('close-issue-label'),
exemptIssueLabels: core.getInput('exempt-issue-labels'),
stalePrLabel: core.getInput('stale-pr-label', {required: true}),
rottenPrLabel: core.getInput('rotten-pr-label', {required: true}),
closePrLabel: core.getInput('close-pr-label'),
exemptPrLabels: core.getInput('exempt-pr-labels'),
onlyLabels: core.getInput('only-labels'),
@ -95,6 +107,15 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
removePrStaleWhenUpdated: _toOptionalBoolean(
'remove-pr-stale-when-updated'
),
removeRottenWhenUpdated: !(
core.getInput('remove-rotten-when-updated') === 'false'
),
removeIssueRottenWhenUpdated: _toOptionalBoolean(
'remove-issue-rotten-when-updated'
),
removePrRottenWhenUpdated: _toOptionalBoolean(
'remove-pr-rotten-when-updated'
),
debugOnly: core.getInput('debug-only') === 'true',
ascending: core.getInput('ascending') === 'true',
deleteBranch: core.getInput('delete-branch') === 'true',
@ -118,6 +139,9 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
labelsToRemoveWhenStale: core.getInput('labels-to-remove-when-stale'),
labelsToRemoveWhenUnstale: core.getInput('labels-to-remove-when-unstale'),
labelsToAddWhenUnstale: core.getInput('labels-to-add-when-unstale'),
labelsToRemoveWhenRotten: core.getInput('labels-to-remove-when-rotten'),
labelsToRemoveWhenUnrotten: core.getInput('labels-to-remove-when-unrotten'),
labelsToAddWhenUnrotten: core.getInput('labels-to-add-when-unrotten'),
ignoreUpdates: core.getInput('ignore-updates') === 'true',
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
@ -133,6 +157,13 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
throw new Error(errorMessage);
}
}
for (const numberInput of ['days-before-rotten']) {
if (isNaN(parseFloat(core.getInput(numberInput)))) {
const errorMessage = `Option "${numberInput}" did not parse to a valid float`;
core.setFailed(errorMessage);
throw new Error(errorMessage);
}
}
for (const numberInput of ['days-before-close', 'operations-per-run']) {
if (isNaN(parseInt(core.getInput(numberInput)))) {
@ -167,9 +198,11 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
async function processOutput(
staledIssues: Issue[],
rottenIssues: Issue[],
closedIssues: Issue[]
): Promise<void> {
core.setOutput('staled-issues-prs', JSON.stringify(staledIssues));
core.setOutput('rotten-issues-prs', JSON.stringify(rottenIssues));
core.setOutput('closed-issues-prs', JSON.stringify(closedIssues));
}