Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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-bot-updates](#ignore-bot-updates) | Bot activity does not reset the stale clock or block closing | `false` |
| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` |
| [sort-by](#sort-by) | What to sort issues and PRs by | `created` |
| [only-issue-types](#only-issue-types) | Only issues with a matching type are processed as stale/closed. | |
Expand Down Expand Up @@ -555,6 +556,17 @@ Useful to override [ignore-updates](#ignore-updates) but only to ignore the upda

Default value: unset

#### ignore-bot-updates

If set to `true`, activity caused by bot accounts (users of type `Bot`) does not remove the stale label and does not block closing.
This covers bot comments, bot label changes, and other bot-caused events.
Human activity behaves as usual: a comment, label change, or (for pull requests) a pushed commit removes the stale label and restarts the cycle.
An `updated_at` change that cannot be attributed (for example a body edit, which leaves no event) is treated as human activity, so the action errs on the side of keeping items open.

Useful for repositories where automation (label syncs, preview deployments, generated comments) updates issues and pull requests so often that the stale cycle never completes.

Default value: `false`

#### include-only-assigned

If set to `true`, only the issues or the pull requests with an assignee will be marked as stale automatically.
Expand Down
1 change: 1 addition & 0 deletions __tests__/constants/default-processor-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
labelsToRemoveWhenUnstale: '',
labelsToAddWhenUnstale: '',
ignoreUpdates: false,
ignoreBotUpdates: false,
ignoreIssueUpdates: undefined,
ignorePrUpdates: undefined,
exemptDraftPr: false,
Expand Down
241 changes: 241 additions & 0 deletions __tests__/ignore-bot-updates.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import {expect, test} from '@jest/globals';
import {Issue} from '../src/classes/issue.js';
import {IIssueEvent} from '../src/interfaces/issue-event.js';
import {IssuesProcessorMock} from './classes/issues-processor-mock.js';
import {DefaultProcessorOptions} from './constants/default-processor-options.js';
import {generateIssue} from './functions/generate-issue.js';
import {alwaysFalseStateMock} from './classes/state-mock.js';

const millisPerDay = 1000 * 60 * 60 * 24;

function daysAgo(days: number): string {
return new Date(Date.now() - days * millisPerDay).toISOString();
}

const botLabeledEvent = (createdAt: string, label: string): IIssueEvent => ({
created_at: createdAt,
event: 'labeled',
label: {name: label},
actor: {login: 'some-app[bot]', type: 'Bot'}
});

const botCommentedEvent = (createdAt: string): IIssueEvent => ({
created_at: createdAt,
event: 'commented',
label: {name: ''},
actor: {login: 'some-app[bot]', type: 'Bot'}
});

const humanLabeledEvent = (createdAt: string, label: string): IIssueEvent => ({
created_at: createdAt,
event: 'labeled',
label: {name: label},
actor: {login: 'octocat', type: 'User'}
});

test('bot activity does not remove the stale label when ignore-bot-updates is enabled', async () => {
expect.assertions(2);
const opts = {
...DefaultProcessorOptions,
ignoreBotUpdates: true,
daysBeforeClose: 7,
removeStaleWhenUpdated: true
};
const markedStaleOn = daysAgo(2);
// A bot commented after the stale marking: updated_at is fresh.
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale issue touched only by bots',
daysAgo(1), // updated_at bumped by the bot
daysAgo(40),
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => ({
creationDate: markedStaleOn,
events: [
botLabeledEvent(markedStaleOn, 'Stale'),
botCommentedEvent(daysAgo(1))
]
})
);

await processor.processIssues(1);

expect(processor.removedLabelIssues).toHaveLength(0);
expect(processor.closedIssues).toHaveLength(0); // warned 2d ago; close window is 7d
});

test('stale issue closes despite recent bot activity when ignore-bot-updates is enabled', async () => {
expect.assertions(1);
const opts = {
...DefaultProcessorOptions,
ignoreBotUpdates: true,
daysBeforeClose: 7,
removeStaleWhenUpdated: true
};
const markedStaleOn = daysAgo(10); // warned 10d ago > 7d close window
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale issue that bots keep touching',
daysAgo(1), // bots keep updated_at perpetually fresh
daysAgo(60),
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => ({
creationDate: markedStaleOn,
events: [
botLabeledEvent(markedStaleOn, 'Stale'),
botCommentedEvent(daysAgo(3)),
botCommentedEvent(daysAgo(1))
]
})
);

await processor.processIssues(1);

expect(processor.closedIssues).toHaveLength(1);
});

test('human labeling still removes the stale label when ignore-bot-updates is enabled', async () => {
expect.assertions(2);
const opts = {
...DefaultProcessorOptions,
ignoreBotUpdates: true,
daysBeforeClose: 7,
removeStaleWhenUpdated: true
};
const markedStaleOn = daysAgo(10);
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale issue a human labeled after the marking',
daysAgo(1),
daysAgo(60),
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => ({
creationDate: markedStaleOn,
events: [
botLabeledEvent(markedStaleOn, 'Stale'),
humanLabeledEvent(daysAgo(1), 'priority')
]
})
);

await processor.processIssues(1);

expect(processor.removedLabelIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});

test('a commit pushed after the stale marking blocks closing a PR when ignore-bot-updates is enabled', async () => {
expect.assertions(2);
const opts = {
...DefaultProcessorOptions,
ignoreBotUpdates: true,
daysBeforeClose: 7,
removeStaleWhenUpdated: true
};
const markedStaleOn = daysAgo(10);
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale PR whose author pushed yesterday',
daysAgo(1),
daysAgo(60),
false,
true, // pull request
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => ({
creationDate: markedStaleOn,
events: [botLabeledEvent(markedStaleOn, 'Stale')]
})
);
processor.getPullRequestLastCommitDate = async () => daysAgo(1);

await processor.processIssues(1);

// The push counts as a human update: un-stale, don't close.
expect(processor.removedLabelIssues).toHaveLength(1);
expect(processor.closedIssues).toHaveLength(0);
});

test('default behavior is unchanged when ignore-bot-updates is disabled', async () => {
expect.assertions(2);
const opts = {
...DefaultProcessorOptions,
ignoreBotUpdates: false,
daysBeforeClose: 7,
removeStaleWhenUpdated: true
};
const markedStaleOn = daysAgo(10);
// Same shape as the "closes despite bots" test above — but with the option
// off, the bot comment resets everything, exactly as before.
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'A stale issue that bots keep touching',
daysAgo(1),
daysAgo(60),
false,
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
alwaysFalseStateMock,
async p => (p === 1 ? TestIssueList : []),
async () => [],
async () => ({
creationDate: markedStaleOn,
events: [
botLabeledEvent(markedStaleOn, 'Stale'),
botCommentedEvent(daysAgo(1))
]
})
);

await processor.processIssues(1);

expect(processor.removedLabelIssues).toHaveLength(1); // updated_at bump un-stales
expect(processor.closedIssues).toHaveLength(0);
});
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,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-bot-updates:
description: 'Updates, comments, and label changes made by bot accounts (user type "Bot") do not remove the stale label and do not block closing. Human activity behaves as usual.'
default: 'false'
required: false
include-only-assigned:
description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.'
default: 'false'
Expand Down
Loading