Merge ac20e355748a11b818619bf1692eabf8b946223a into 3f3b0175e8c66fb49b9a6d5a0cd1f8436d4c3ab6

This commit is contained in:
Mateusz Gorzeliński 2023-12-15 17:29:53 +03:00 committed by GitHub
commit 8182a0d5e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 407 additions and 5 deletions

View File

@ -96,6 +96,7 @@ Every argument is optional.
| [ignore-updates](#ignore-updates) | Any update (update/comment) can reset the stale idle time on the issues/PRs | `false` |
| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | |
| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | |
| [ignore-reactions](#ignore-reactions) | Any reaction can reset the stale idle time on the issues/PRs | |
| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` |
### List of output options
@ -541,6 +542,12 @@ Useful to override [ignore-updates](#ignore-updates) but only to ignore the upda
Default value: unset
#### ignore-reactions
If set to `false`, any reaction to an issue will be considered an update.
Default value: unset
#### include-only-assigned
If set to `true`, only the issues or the pull requests with an assignee will be marked as stale automatically.

View File

@ -1144,6 +1144,7 @@ class IssuesProcessorBuilder {
new StateMock(),
async p => (p === 1 ? this._issues : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
}

View File

@ -53,6 +53,7 @@ describe('assignees options', (): void => {
alwaysFalseStateMock,
async p => (p === 1 ? testIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
};

View File

@ -1,6 +1,7 @@
import {Issue} from '../../src/classes/issue';
import {IssuesProcessor} from '../../src/classes/issues-processor';
import {IComment} from '../../src/interfaces/comment';
import {IReaction} from '../../src/interfaces/reaction';
import {IIssuesProcessorOptions} from '../../src/interfaces/issues-processor-options';
import {IPullRequest} from '../../src/interfaces/pull-request';
import {IState} from '../../src/interfaces/state/state';
@ -14,6 +15,10 @@ export class IssuesProcessorMock extends IssuesProcessor {
issue: Issue,
sinceDate: string
) => Promise<IComment[]>,
listIssueReactions?: (
issue: Issue,
sinceDate: string
) => Promise<IReaction[]>,
getLabelCreationDate?: (
issue: Issue,
label: string
@ -30,6 +35,10 @@ export class IssuesProcessorMock extends IssuesProcessor {
this.listIssueComments = listIssueComments;
}
if (listIssueReactions) {
this.listIssueReactions = listIssueReactions;
}
if (getLabelCreationDate) {
this.getLabelCreationDate = getLabelCreationDate;
}

View File

@ -53,6 +53,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
ignoreUpdates: false,
ignoreIssueUpdates: undefined,
ignorePrUpdates: undefined,
ignoreReactions: undefined,
exemptDraftPr: false,
closeIssueReason: 'not_planned',
includeOnlyAssigned: false

View File

@ -128,6 +128,7 @@ class IssuesProcessorBuilder {
alwaysFalseStateMock,
async p => (p === 1 ? this._issues : []),
async () => [],
async () => [],
async () => new Date().toDateString(),
async (): Promise<IPullRequest> => {
return Promise.resolve({

View File

@ -20,6 +20,7 @@ test('processing an issue with no label will make it stale and close it, if it i
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -52,6 +53,7 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -84,6 +86,7 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -116,6 +119,7 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -148,6 +152,7 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -180,6 +185,7 @@ test('processing an issue with no label and a start date as ISO 8601 being befor
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -212,6 +218,7 @@ test('processing an issue with no label and a start date as ISO 8601 being after
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -244,6 +251,7 @@ test('processing an issue with no label and a start date as RFC 2822 being befor
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -276,6 +284,7 @@ test('processing an issue with no label and a start date as RFC 2822 being after
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -300,6 +309,7 @@ test('processing an issue with no label will make it stale and close it, if it i
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -325,6 +335,7 @@ test('processing an issue with no label will make it stale and not close it, if
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -348,6 +359,7 @@ test('processing an issue with no label will make it stale and not close it if d
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -372,6 +384,7 @@ test('processing an issue with no label will make it stale and not close it if d
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -396,6 +409,7 @@ test('processing an issue with no label will not make it stale if days-before-st
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -421,6 +435,7 @@ test('processing an issue with no label will not make it stale if days-before-st
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -449,6 +464,7 @@ test('processing an issue with no label will make it stale but not close it', as
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -481,6 +497,7 @@ test('processing a stale issue will close it', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -513,6 +530,7 @@ test('processing a stale issue containing a space in the label will close it', a
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -545,6 +563,7 @@ test('processing a stale issue containing a slash in the label will close it', a
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -578,6 +597,7 @@ test('processing a stale issue will close it when days-before-issue-stale overri
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -610,6 +630,7 @@ test('processing a stale PR will close it', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -643,6 +664,7 @@ test('processing a stale PR will close it when days-before-pr-stale override day
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -676,6 +698,7 @@ test('processing a stale issue will close it even if configured not to mark as s
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -710,6 +733,7 @@ test('processing a stale issue will close it even if configured not to mark as s
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -743,6 +767,7 @@ test('processing a stale PR will close it even if configured not to mark as stal
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -777,6 +802,7 @@ test('processing a stale PR will close it even if configured not to mark as stal
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -834,6 +860,7 @@ test('stale closed issues will not be closed', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -863,6 +890,7 @@ test('closed prs will not be marked stale', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -892,6 +920,7 @@ test('stale closed prs will not be closed', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -950,6 +979,7 @@ test('stale locked issues will not be closed', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1008,6 +1038,7 @@ test('stale locked prs will not be closed', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1039,6 +1070,7 @@ test('exempt issue labels will not be marked stale', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1070,6 +1102,7 @@ test('exempt issue labels will not be marked stale (multi issue label with space
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1100,6 +1133,7 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1150,6 +1184,7 @@ test('exempt pr labels will not be marked stale', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1199,6 +1234,7 @@ test('stale issues should not be closed if days is set to -1', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1236,6 +1272,7 @@ test('stale label should be removed if a comment was added to a stale issue', as
body: 'Body'
}
], // return a fake comment to indicate there was an update
async () => [],
async () => new Date().toDateString()
);
@ -1247,6 +1284,122 @@ test('stale label should be removed if a comment was added to a stale issue', as
expect(processor.removedLabelIssues).toHaveLength(1);
});
test('when the option "ignoreReactions" is set to false, stale label should be removed if a reaction was added to a stale issue', async () => {
const opts = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true,
ignoreReactions: false
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue that should un-stale',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [
{
content: '+1',
created_at: '2020-01-07T17:00:00Z'
}
], // return a fake reaction to indicate there was an update
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues).toHaveLength(0);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.removedLabelIssues).toHaveLength(1);
});
test('when the option "ignoreReactions" is not set, stale label should not be removed if a reaction was added to a stale issue', async () => {
const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue that should un-stale',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [
{
content: '+1',
created_at: '2020-01-07T17:00:00Z'
}
], // return a fake reaction to indicate there was an update
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.removedLabelIssues).toHaveLength(0);
});
test('when the option "ignoreReactions" is set to true, stale label should not be removed if a reaction was added to a stale issue', async () => {
const opts = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true,
ignoreReactions: true
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue that should un-stale',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [
{
content: '+1',
created_at: '2020-01-07T17:00:00Z'
}
], // return a fake reaction to indicate there was an update
async () => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues).toHaveLength(1);
expect(processor.staleIssues).toHaveLength(0);
expect(processor.removedLabelIssues).toHaveLength(0);
});
test('when the option "labelsToAddWhenUnstale" is set, the labels should be added when unstale', async () => {
expect.assertions(4);
const opts = {
@ -1279,6 +1432,7 @@ test('when the option "labelsToAddWhenUnstale" is set, the labels should be adde
body: 'Body'
}
], // return a fake comment to indicate there was an update
async () => [],
async () => new Date().toDateString()
);
@ -1325,6 +1479,7 @@ test('when the option "labelsToRemoveWhenStale" is set, the labels should be rem
body: 'Body'
}
], // return a fake comment to indicate there was an update
async () => [],
async () => new Date().toDateString()
);
@ -1365,6 +1520,7 @@ test('stale label should not be removed if a comment was added by the bot (and t
body: 'This issue is stale'
}
], // return a fake comment to indicate there was an update by the bot
async () => [],
async () => new Date().toDateString()
);
@ -1399,6 +1555,7 @@ test('stale label containing a space should be removed if a comment was added to
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [{user: {login: 'notme', type: 'User'}, body: 'Body'}], // return a fake comment to indicate there was an update
async () => [],
async () => new Date().toDateString()
);
@ -1431,6 +1588,7 @@ test('stale issues should not be closed until after the closed number of days',
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1465,6 +1623,7 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1497,6 +1656,7 @@ test('stale issues should not be closed until after the closed number of days (l
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1530,6 +1690,7 @@ test('skips stale message on issues when stale-issue-message is empty', async ()
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1575,6 +1736,7 @@ test('send stale message on issues when stale-issue-message is not empty', async
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1621,6 +1783,7 @@ test('skips stale message on prs when stale-pr-message is empty', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1667,6 +1830,7 @@ test('send stale message on prs when stale-pr-message is not empty', async () =>
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1710,6 +1874,7 @@ test('git branch is deleted when option is enabled', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1741,6 +1906,7 @@ test('git branch is not deleted when issue is not pull request', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1774,6 +1940,7 @@ test('an issue without a milestone will be marked as stale', async () => {
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1809,6 +1976,7 @@ test('an issue without an exempted milestone will be marked as stale', async ()
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1844,6 +2012,7 @@ test('an issue with an exempted milestone will not be marked as stale', async ()
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1879,6 +2048,7 @@ test('an issue with an exempted milestone will not be marked as stale (multi mil
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1914,6 +2084,7 @@ test('an issue with an exempted milestone will not be marked as stale (multi mil
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1950,6 +2121,7 @@ test('an issue with an exempted milestone but without an exempted issue mileston
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -1986,6 +2158,7 @@ test('an issue with an exempted milestone but with another exempted issue milest
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2022,6 +2195,7 @@ test('an issue with an exempted milestone and with an exempted issue milestone w
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2050,6 +2224,7 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2077,6 +2252,7 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2104,6 +2280,7 @@ test('processing an issue opened since 2 days and with the option "daysBeforeIss
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2131,6 +2308,7 @@ test('processing an issue opened since 1 hour and with the option "daysBeforeIss
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2158,6 +2336,7 @@ test('processing an issue opened since 4 hours and with the option "daysBeforeIs
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2185,6 +2364,7 @@ test('processing an issue opened since 5 hours and with the option "daysBeforeIs
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2220,6 +2400,7 @@ test('processing a pull request opened since 2 days and with the option "daysBef
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2255,6 +2436,7 @@ test('processing a pull request opened since 2 days and with the option "daysBef
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2290,6 +2472,7 @@ test('processing a pull request opened since 2 days and with the option "daysBef
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2325,6 +2508,7 @@ test('processing a pull request opened since 1 hour and with the option "daysBef
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2360,6 +2544,7 @@ test('processing a pull request opened since 4 hours and with the option "daysBe
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2395,6 +2580,7 @@ test('processing a pull request opened since 5 hours and with the option "daysBe
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toISOString()
);
@ -2433,6 +2619,7 @@ test('processing a previously closed issue with a close label will remove the cl
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2470,6 +2657,7 @@ test('processing a closed issue with a close label will not remove the close lab
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2507,6 +2695,7 @@ test('processing a locked issue with a close label will not remove the close lab
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2548,6 +2737,7 @@ test('processing an issue stale since less than the daysBeforeStale with a stale
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async (): Promise<IComment[]> => Promise.resolve([]),
async () => [],
async () => labelCreatedAt.toDateString()
);
@ -2590,6 +2780,7 @@ test('processing an issue stale since less than the daysBeforeStale without a st
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async (): Promise<IComment[]> => Promise.resolve([]),
async () => [],
async () => new Date().toDateString()
);
@ -2627,6 +2818,7 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2664,6 +2856,7 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2706,6 +2899,7 @@ test('processing an issue with the "includeOnlyAssigned" option and nonempty ass
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -2734,6 +2928,7 @@ test('processing an issue with the "includeOnlyAssigned" option set and no assig
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);

View File

@ -44,6 +44,7 @@ describe('milestones options', (): void => {
alwaysFalseStateMock,
async p => (p === 1 ? testIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
};

View File

@ -1144,6 +1144,7 @@ class IssuesProcessorBuilder {
alwaysFalseStateMock,
async p => (p === 1 ? this._issues : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
}

View File

@ -209,6 +209,7 @@ class SUT {
alwaysFalseStateMock,
async p => (p === 1 ? this._testIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);

View File

@ -555,6 +555,7 @@ class IssuesProcessorBuilder {
body: 'body'
}
],
async () => [],
async () => new Date().toDateString()
);
}

View File

@ -51,6 +51,7 @@ describe('state', (): void => {
state,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -78,6 +79,7 @@ describe('state', (): void => {
state,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -117,6 +119,7 @@ describe('state', (): void => {
state,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -159,6 +162,7 @@ describe('state', (): void => {
state,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
@ -197,12 +201,13 @@ describe('state', (): void => {
state,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);
await processor.processIssues(1);
// make sure all issues are proceeded
expect(infoSpy.mock.calls[71][0]).toContain(
expect(infoSpy.mock.calls[73][0]).toContain(
'No more issues found to process. Exiting...'
);

View File

@ -691,6 +691,7 @@ class SUT {
alwaysFalseStateMock,
async p => (p === 1 ? this._testIssueList : []),
async () => [],
async () => [],
async () => new Date().toDateString()
);

View File

@ -200,6 +200,10 @@ inputs:
description: 'Any update (update/comment) can reset the stale idle time on the pull requests. Override "ignore-updates" option regarding only the pull requests.'
default: ''
required: false
ignore-reactions:
description: 'Any reaction can reset the stale idle time on the issues/PRs.'
default: ''
required: false
include-only-assigned:
description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.'
default: 'false'

72
dist/index.js vendored
View File

@ -672,6 +672,38 @@ class IssuesProcessor {
}
});
}
// Grab reactions for an issue since a given date
listIssueReactions(issue, sinceDate) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
try {
this._consumeIssueOperation(issue);
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsReactionsCount();
let reactions = [];
let currentReactions = [];
let iterator = 1;
const daysSinceLastUpdated = (new Date().getTime() - new Date(sinceDate).getTime()) /
(1000 * 60 * 60 * 24);
do {
const response = yield this.client.rest.reactions.listForIssue({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issue.number,
per_page: 100,
page: iterator
});
currentReactions = response.data;
reactions = [...reactions, ...currentReactions];
iterator++;
} while (currentReactions.length !== 0);
return reactions.filter(reaction => IssuesProcessor._updatedSince(reaction.created_at, daysSinceLastUpdated));
}
catch (error) {
this._logger.error(`List issue reactions error: ${error.message}`);
return Promise.resolve([]);
}
});
}
// grab issues from github in batches of 100
getIssues(page) {
var _a;
@ -759,6 +791,8 @@ class IssuesProcessor {
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 issueHasReactionsSinceStale = yield this._hasReactionsSince(issue, markedStaleOn, this.options.ignoreReactions);
issueLogger.info(`$$type had a reaction: ${logger_service_1.LoggerService.cyan(issueHasReactionsSinceStale)}`);
const daysBeforeClose = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
@ -781,7 +815,11 @@ class IssuesProcessor {
issueLogger.info(`$$type has been updated since it was marked stale: ${logger_service_1.LoggerService.cyan(issueHasUpdateSinceStale)}`);
// Should we un-stale this issue?
if (shouldRemoveStaleWhenUpdated &&
(issueHasUpdateSinceStale || issueHasCommentsSinceStale) &&
(issueHasUpdateSinceStale ||
issueHasCommentsSinceStale ||
(this.options.ignoreReactions === false
? issueHasReactionsSinceStale
: false)) &&
!issue.markedStaleThisRun) {
issueLogger.info(`Remove the stale label since the $$type has been updated and the workflow should remove the stale label when updated`);
yield this._removeStaleLabel(issue, staleLabel);
@ -797,7 +835,11 @@ class IssuesProcessor {
}
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 (!issueHasCommentsSinceStale &&
!issueHasUpdateInCloseWindow &&
(this.options.ignoreReactions === false
? !issueHasReactionsSinceStale
: true)) {
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) {
@ -831,6 +873,21 @@ class IssuesProcessor {
return filteredComments.length > 0;
});
}
// find any reactions since the date
_hasReactionsSince(issue, sinceDate, ignoreReactions) {
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
if (!sinceDate) {
return true;
}
if (ignoreReactions === true || ignoreReactions === undefined) {
return false;
}
issueLogger.info(`Checking for reactions on $$type since: ${logger_service_1.LoggerService.cyan(sinceDate)}`);
const reactions = yield this.listIssueReactions(issue, sinceDate);
return reactions.length > 0;
});
}
// Mark an issue as stale with a comment and a label
_markStale(issue, staleMessage, staleLabel, skipMessage) {
var _a, _b, _c;
@ -1830,6 +1887,7 @@ class Statistics {
this.fetchedItemsCount = 0;
this.fetchedItemsEventsCount = 0;
this.fetchedItemsCommentsCount = 0;
this.fetchedItemsReactionsCount = 0;
this.fetchedPullRequestsCount = 0;
}
incrementProcessedItemsCount(issue, increment = 1) {
@ -1900,6 +1958,10 @@ class Statistics {
this.fetchedItemsCommentsCount += increment;
return this;
}
incrementFetchedItemsReactionsCount(increment = 1) {
this.fetchedItemsReactionsCount += increment;
return this;
}
incrementFetchedPullRequestsCount(increment = 1) {
this.fetchedPullRequestsCount += increment;
return this;
@ -1918,6 +1980,7 @@ class Statistics {
this._logFetchedItemsCount();
this._logFetchedItemsEventsCount();
this._logFetchedItemsCommentsCount();
this._logFetchedItemsReactionsCount();
this._logFetchedPullRequestsCount();
this._logOperationsCount();
return this;
@ -2094,6 +2157,9 @@ class Statistics {
_logFetchedItemsCommentsCount() {
this._logCount('Fetched items comments', this.fetchedItemsCommentsCount);
}
_logFetchedItemsReactionsCount() {
this._logCount('Fetched items reactions', this.fetchedItemsReactionsCount);
}
_logFetchedPullRequestsCount() {
this._logCount('Fetched pull requests', this.fetchedPullRequestsCount);
}
@ -2220,6 +2286,7 @@ var Option;
Option["IgnoreUpdates"] = "ignore-updates";
Option["IgnoreIssueUpdates"] = "ignore-issue-updates";
Option["IgnorePrUpdates"] = "ignore-pr-updates";
Option["IgnoreReactions"] = "ignore-reactions";
Option["ExemptDraftPr"] = "exempt-draft-pr";
Option["CloseIssueReason"] = "close-issue-reason";
})(Option || (exports.Option = Option = {}));
@ -2565,6 +2632,7 @@ function _getAndValidateArgs() {
ignoreUpdates: core.getInput('ignore-updates') === 'true',
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
ignoreReactions: _toOptionalBoolean('ignore-reactions'),
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
closeIssueReason: core.getInput('close-issue-reason'),
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'

View File

@ -62,6 +62,7 @@ describe('Issue', (): void => {
ignoreUpdates: false,
ignoreIssueUpdates: undefined,
ignorePrUpdates: undefined,
ignoreReactions: undefined,
exemptDraftPr: false,
closeIssueReason: '',
includeOnlyAssigned: false

View File

@ -14,6 +14,7 @@ import {IComment} from '../interfaces/comment';
import {IIssueEvent} from '../interfaces/issue-event';
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
import {IPullRequest} from '../interfaces/pull-request';
import {IReaction} from '../interfaces/reaction';
import {Assignees} from './assignees';
import {IgnoreUpdates} from './ignore-updates';
import {ExemptDraftPullRequest} from './exempt-draft-pull-request';
@ -561,6 +562,41 @@ export class IssuesProcessor {
}
}
// Grab reactions for an issue since a given date
async listIssueReactions(
issue: Readonly<Issue>,
sinceDate: Readonly<string>
): Promise<IReaction[]> {
try {
this._consumeIssueOperation(issue);
this.statistics?.incrementFetchedItemsReactionsCount();
let reactions: IReaction[] = [];
let currentReactions: IReaction[] = [];
let iterator = 1;
const daysSinceLastUpdated =
(new Date().getTime() - new Date(sinceDate).getTime()) /
(1000 * 60 * 60 * 24);
do {
const response = await this.client.rest.reactions.listForIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
page: iterator
});
currentReactions = response.data;
reactions = [...reactions, ...currentReactions];
iterator++;
} while (currentReactions.length !== 0);
return reactions.filter(reaction =>
IssuesProcessor._updatedSince(reaction.created_at, daysSinceLastUpdated)
);
} catch (error) {
this._logger.error(`List issue reactions error: ${error.message}`);
return Promise.resolve([]);
}
}
// grab issues from github in batches of 100
async getIssues(page: number): Promise<Issue[]> {
try {
@ -678,6 +714,17 @@ export class IssuesProcessor {
)}`
);
const issueHasReactionsSinceStale: boolean = await this._hasReactionsSince(
issue,
markedStaleOn,
this.options.ignoreReactions
);
issueLogger.info(
`$$type had a reaction: ${LoggerService.cyan(
issueHasReactionsSinceStale
)}`
);
const daysBeforeClose: number = issue.isPullRequest
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
@ -729,7 +776,11 @@ export class IssuesProcessor {
// Should we un-stale this issue?
if (
shouldRemoveStaleWhenUpdated &&
(issueHasUpdateSinceStale || issueHasCommentsSinceStale) &&
(issueHasUpdateSinceStale ||
issueHasCommentsSinceStale ||
(this.options.ignoreReactions === false
? issueHasReactionsSinceStale
: false)) &&
!issue.markedStaleThisRun
) {
issueLogger.info(
@ -765,7 +816,13 @@ export class IssuesProcessor {
)}`
);
if (!issueHasCommentsSinceStale && !issueHasUpdateInCloseWindow) {
if (
!issueHasCommentsSinceStale &&
!issueHasUpdateInCloseWindow &&
(this.options.ignoreReactions === false
? !issueHasReactionsSinceStale
: true)
) {
issueLogger.info(
`Closing $$type because it was last updated on: ${LoggerService.cyan(
issue.updated_at
@ -824,6 +881,30 @@ export class IssuesProcessor {
return filteredComments.length > 0;
}
// find any reactions since the date
private async _hasReactionsSince(
issue: Issue,
sinceDate: string,
ignoreReactions: boolean | undefined
): Promise<boolean> {
const issueLogger: IssueLogger = new IssueLogger(issue);
if (!sinceDate) {
return true;
}
if (ignoreReactions === true || ignoreReactions === undefined) {
return false;
}
issueLogger.info(
`Checking for reactions on $$type since: ${LoggerService.cyan(sinceDate)}`
);
const reactions = await this.listIssueReactions(issue, sinceDate);
return reactions.length > 0;
}
// Mark an issue as stale with a comment and a label
private async _markStale(
issue: Issue,

View File

@ -30,6 +30,7 @@ export class Statistics {
fetchedItemsCount = 0;
fetchedItemsEventsCount = 0;
fetchedItemsCommentsCount = 0;
fetchedItemsReactionsCount = 0;
fetchedPullRequestsCount = 0;
incrementProcessedItemsCount(
@ -154,6 +155,14 @@ export class Statistics {
return this;
}
incrementFetchedItemsReactionsCount(
increment: Readonly<number> = 1
): Statistics {
this.fetchedItemsReactionsCount += increment;
return this;
}
incrementFetchedPullRequestsCount(
increment: Readonly<number> = 1
): Statistics {
@ -176,6 +185,7 @@ export class Statistics {
this._logFetchedItemsCount();
this._logFetchedItemsEventsCount();
this._logFetchedItemsCommentsCount();
this._logFetchedItemsReactionsCount();
this._logFetchedPullRequestsCount();
this._logOperationsCount();
@ -430,6 +440,10 @@ export class Statistics {
this._logCount('Fetched items comments', this.fetchedItemsCommentsCount);
}
private _logFetchedItemsReactionsCount(): void {
this._logCount('Fetched items reactions', this.fetchedItemsReactionsCount);
}
private _logFetchedPullRequestsCount(): void {
this._logCount('Fetched pull requests', this.fetchedPullRequestsCount);
}

View File

@ -47,6 +47,7 @@ export enum Option {
IgnoreUpdates = 'ignore-updates',
IgnoreIssueUpdates = 'ignore-issue-updates',
IgnorePrUpdates = 'ignore-pr-updates',
IgnoreReactions = 'ignore-reactions',
ExemptDraftPr = 'exempt-draft-pr',
CloseIssueReason = 'close-issue-reason'
}

View File

@ -2,6 +2,7 @@ import {IsoDateString} from '../types/iso-date-string';
import {Assignee} from './assignee';
import {ILabel} from './label';
import {IMilestone} from './milestone';
import {IReaction} from './reaction';
import {components} from '@octokit/openapi-types';
export interface IIssue {
title: string;
@ -15,6 +16,7 @@ export interface IIssue {
locked: boolean;
milestone?: IMilestone | null;
assignees?: Assignee[] | null;
reactions?: IReaction[] | null;
}
export type OctokitIssue = components['schemas']['issue'];

View File

@ -51,6 +51,7 @@ export interface IIssuesProcessorOptions {
ignoreUpdates: boolean;
ignoreIssueUpdates: boolean | undefined;
ignorePrUpdates: boolean | undefined;
ignoreReactions: boolean | undefined;
exemptDraftPr: boolean;
closeIssueReason: string;
includeOnlyAssigned: boolean;

View File

@ -0,0 +1,4 @@
export interface IReaction {
content: string;
created_at: string;
}

View File

@ -121,6 +121,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
ignoreUpdates: core.getInput('ignore-updates') === 'true',
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
ignoreReactions: _toOptionalBoolean('ignore-reactions'),
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
closeIssueReason: core.getInput('close-issue-reason'),
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'