Skip to content

Commit db5d06a

Browse files
authored
Enhancement: ignore stale labeling events (#1311)
* Ignore updates if only stale label changed after marking * Initial tests * test hasOnlyStaleLabelUpdateSince itself * Actually we should only ignore label events, not unlabel * Add logger * Fix test * Remove trailing whitespace * Add test for non-label event * Add boundary test * Add error handling to hasOnlyStaleLabelAddedSince * Add comment noting intentional event before boundary * Remove unneeded optional chaining * Limit pagination to max 3 calls / 300 events * Rename method * Handle invalid timestamps * Fallback when limit reached * Oh wow, just realized we already got events... * Refactor * Lint * Build * Remove events "cache" * Update index.js
1 parent b5d41d4 commit db5d06a

5 files changed

Lines changed: 427 additions & 12 deletions

File tree

__tests__/classes/issues-processor-mock.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {IComment} from '../../src/interfaces/comment';
44
import {IIssuesProcessorOptions} from '../../src/interfaces/issues-processor-options';
55
import {IPullRequest} from '../../src/interfaces/pull-request';
66
import {IState} from '../../src/interfaces/state/state';
7+
import {IIssueEvent} from '../../src/interfaces/issue-event';
78

89
export class IssuesProcessorMock extends IssuesProcessor {
910
constructor(
@@ -17,7 +18,15 @@ export class IssuesProcessorMock extends IssuesProcessor {
1718
getLabelCreationDate?: (
1819
issue: Issue,
1920
label: string
20-
) => Promise<string | undefined>,
21+
) =>
22+
| Promise<string | undefined>
23+
| Promise<{creationDate?: string; events: IIssueEvent[]}>,
24+
hasOnlyStaleLabelingEventsSince?: (
25+
issue: Issue,
26+
sinceDate: string,
27+
staleLabel: string,
28+
events: IIssueEvent[]
29+
) => Promise<boolean>,
2130
getPullRequest?: (issue: Issue) => Promise<IPullRequest | undefined | void>
2231
) {
2332
super(options, state);
@@ -31,7 +40,21 @@ export class IssuesProcessorMock extends IssuesProcessor {
3140
}
3241

3342
if (getLabelCreationDate) {
34-
this.getLabelCreationDate = getLabelCreationDate;
43+
this.getLabelCreationDate = async (
44+
issue: Issue,
45+
label: string
46+
): Promise<{creationDate?: string; events: IIssueEvent[]}> => {
47+
const result = await getLabelCreationDate(issue, label);
48+
if (typeof result === 'string' || typeof result === 'undefined') {
49+
return {creationDate: result, events: []};
50+
}
51+
52+
return result;
53+
};
54+
}
55+
56+
if (hasOnlyStaleLabelingEventsSince) {
57+
this.hasOnlyStaleLabelingEventsSince = hasOnlyStaleLabelingEventsSince;
3558
}
3659

3760
if (getPullRequest) {

__tests__/exempt-draft-pr.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class IssuesProcessorBuilder {
129129
async p => (p === 1 ? this._issues : []),
130130
async () => [],
131131
async () => new Date().toDateString(),
132+
undefined,
132133
async (): Promise<IPullRequest> => {
133134
return Promise.resolve({
134135
number: 0,
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import {Issue} from '../src/classes/issue';
2+
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
3+
import {IssuesProcessorMock} from './classes/issues-processor-mock';
4+
import {DefaultProcessorOptions} from './constants/default-processor-options';
5+
import {generateIssue} from './functions/generate-issue';
6+
import {alwaysFalseStateMock} from './classes/state-mock';
7+
import {IState} from '../src/interfaces/state/state';
8+
import {IIssueEvent} from '../src/interfaces/issue-event';
9+
import {IssuesProcessor} from '../src/classes/issues-processor';
10+
11+
describe('remove-stale-when-updated with stale label events', (): void => {
12+
const markedStaleOn = '2025-01-01T00:00:00Z';
13+
const updatedAt = '2025-01-01T00:01:00Z';
14+
15+
let options: IIssuesProcessorOptions;
16+
17+
beforeEach((): void => {
18+
options = {
19+
...DefaultProcessorOptions,
20+
removeStaleWhenUpdated: true
21+
};
22+
});
23+
24+
const buildIssue = (): Issue =>
25+
generateIssue(
26+
options,
27+
1,
28+
'dummy-title',
29+
updatedAt,
30+
markedStaleOn,
31+
false,
32+
false,
33+
['Stale']
34+
);
35+
36+
const buildEvents = (): IIssueEvent[] => [
37+
{
38+
event: 'labeled',
39+
created_at: markedStaleOn,
40+
label: {name: 'Stale'}
41+
}
42+
];
43+
44+
test('does not remove stale label when only stale label events occurred', async (): Promise<void> => {
45+
expect.assertions(1);
46+
const issue = buildIssue();
47+
48+
const processor = new IssuesProcessorMock(
49+
options,
50+
alwaysFalseStateMock,
51+
async p => (p === 1 ? [issue] : []),
52+
async () => [],
53+
async () => ({creationDate: markedStaleOn, events: buildEvents()}),
54+
async () => true
55+
);
56+
57+
await processor.processIssues();
58+
59+
expect(processor.removedLabelIssues).toHaveLength(0);
60+
});
61+
62+
test('removes stale label when updates are not just stale label events', async (): Promise<void> => {
63+
expect.assertions(1);
64+
const issue = buildIssue();
65+
66+
const processor = new IssuesProcessorMock(
67+
options,
68+
alwaysFalseStateMock,
69+
async p => (p === 1 ? [issue] : []),
70+
async () => [],
71+
async () => ({creationDate: markedStaleOn, events: buildEvents()}),
72+
async () => false
73+
);
74+
75+
await processor.processIssues();
76+
77+
expect(processor.removedLabelIssues).toHaveLength(1);
78+
});
79+
});
80+
81+
class TestIssuesProcessor extends IssuesProcessor {
82+
constructor(
83+
options: IIssuesProcessorOptions,
84+
state: IState,
85+
events: IIssueEvent[]
86+
) {
87+
super(options, state);
88+
const client = {
89+
rest: {
90+
issues: {
91+
listEvents: {
92+
endpoint: {
93+
merge: () => ({})
94+
}
95+
}
96+
}
97+
},
98+
paginate: {
99+
iterator: async function* () {
100+
yield {data: events};
101+
}
102+
}
103+
};
104+
(this as any).client = client;
105+
}
106+
107+
async callhasOnlyStaleLabelingEventsSince(
108+
issue: Issue,
109+
sinceDate: string,
110+
staleLabel: string,
111+
events: IIssueEvent[]
112+
): Promise<boolean> {
113+
return this.hasOnlyStaleLabelingEventsSince(
114+
issue,
115+
sinceDate,
116+
staleLabel,
117+
events
118+
);
119+
}
120+
}
121+
122+
describe('hasOnlyStaleLabelingEventsSince', (): void => {
123+
const staleLabel = 'Stale';
124+
const sinceDate = '2025-01-01T00:00:00Z';
125+
const originalRepo = process.env.GITHUB_REPOSITORY;
126+
127+
let options: IIssuesProcessorOptions;
128+
129+
beforeEach((): void => {
130+
process.env.GITHUB_REPOSITORY = 'owner/repo';
131+
options = {
132+
...DefaultProcessorOptions,
133+
staleIssueLabel: staleLabel,
134+
removeStaleWhenUpdated: true
135+
};
136+
});
137+
138+
afterEach((): void => {
139+
if (originalRepo === undefined) {
140+
delete process.env.GITHUB_REPOSITORY;
141+
} else {
142+
process.env.GITHUB_REPOSITORY = originalRepo;
143+
}
144+
});
145+
146+
const buildIssue = (): Issue =>
147+
generateIssue(
148+
options,
149+
1,
150+
'dummy-title',
151+
'2025-01-01T00:02:00Z',
152+
sinceDate,
153+
false,
154+
false,
155+
[staleLabel]
156+
);
157+
158+
test('returns true when only stale label events exist after the since date', async (): Promise<void> => {
159+
expect.assertions(1);
160+
const issue = buildIssue();
161+
const events: IIssueEvent[] = [
162+
// Event before the sinceDate should be ignored.
163+
{
164+
event: 'labeled',
165+
created_at: '2024-12-31T23:59:00Z',
166+
label: {name: staleLabel}
167+
},
168+
{
169+
event: 'labeled',
170+
created_at: '2025-01-01T00:00:10Z',
171+
label: {name: staleLabel}
172+
}
173+
];
174+
const processor = new TestIssuesProcessor(
175+
options,
176+
alwaysFalseStateMock,
177+
events
178+
);
179+
const result = await processor.callhasOnlyStaleLabelingEventsSince(
180+
issue,
181+
sinceDate,
182+
staleLabel,
183+
events
184+
);
185+
186+
expect(result).toBe(true);
187+
});
188+
189+
test('returns false when a non-stale label event exists after the since date', async (): Promise<void> => {
190+
expect.assertions(1);
191+
const issue = buildIssue();
192+
const events: IIssueEvent[] = [
193+
{
194+
event: 'labeled',
195+
created_at: '2025-01-01T00:00:10Z',
196+
label: {name: 'other-label'}
197+
}
198+
];
199+
const processor = new TestIssuesProcessor(
200+
options,
201+
alwaysFalseStateMock,
202+
events
203+
);
204+
const result = await processor.callhasOnlyStaleLabelingEventsSince(
205+
issue,
206+
sinceDate,
207+
staleLabel,
208+
events
209+
);
210+
211+
expect(result).toBe(false);
212+
});
213+
214+
test('returns false when stale label is removed after the since date', async (): Promise<void> => {
215+
expect.assertions(1);
216+
const issue = buildIssue();
217+
const events: IIssueEvent[] = [
218+
{
219+
event: 'unlabeled',
220+
created_at: '2025-01-01T00:00:10Z',
221+
label: {name: staleLabel}
222+
}
223+
];
224+
const processor = new TestIssuesProcessor(
225+
options,
226+
alwaysFalseStateMock,
227+
events
228+
);
229+
const result = await processor.callhasOnlyStaleLabelingEventsSince(
230+
issue,
231+
sinceDate,
232+
staleLabel,
233+
events
234+
);
235+
236+
expect(result).toBe(false);
237+
});
238+
239+
test('returns false when a non-label event exists after the since date', async (): Promise<void> => {
240+
expect.assertions(1);
241+
const issue = buildIssue();
242+
const events: IIssueEvent[] = [
243+
{
244+
event: 'commented',
245+
created_at: '2025-01-01T00:00:10Z',
246+
label: {name: staleLabel}
247+
}
248+
];
249+
const processor = new TestIssuesProcessor(
250+
options,
251+
alwaysFalseStateMock,
252+
events
253+
);
254+
const result = await processor.callhasOnlyStaleLabelingEventsSince(
255+
issue,
256+
sinceDate,
257+
staleLabel,
258+
events
259+
);
260+
261+
expect(result).toBe(false);
262+
});
263+
264+
test('includes events that occur exactly at the since date boundary', async (): Promise<void> => {
265+
expect.assertions(1);
266+
const issue = buildIssue();
267+
const events: IIssueEvent[] = [
268+
{
269+
event: 'labeled',
270+
created_at: sinceDate,
271+
label: {name: staleLabel}
272+
}
273+
];
274+
const processor = new TestIssuesProcessor(
275+
options,
276+
alwaysFalseStateMock,
277+
events
278+
);
279+
const result = await processor.callhasOnlyStaleLabelingEventsSince(
280+
issue,
281+
sinceDate,
282+
staleLabel,
283+
events
284+
);
285+
286+
expect(result).toBe(true);
287+
});
288+
});

0 commit comments

Comments
 (0)