Skip to content

Commit ea654cb

Browse files
bjjeongclaude
andcommitted
Fix check-for-reproducer never removing "Needs: Repro" once react-native-bot applied it
hasMaintainerChangedLabel treats every "Needs: Repro" label event by a non-Bot actor other than the author as a maintainer decision. react-native-bot, the account check-for-reproducer.yml runs as, is a regular user account (actor.type === 'User'), so the bot's own initial label satisfies that check and every later run exits before looking for a reproducer. Authors who edit a valid reproducer into the issue can therefore never get the label removed (e.g. #58427; all recently labeled issues show the same). Ignore react-native-bot's own label events alongside GitHub App events so that only human label changes count as maintainer intervention, and add a unit test for the script covering the regression and the existing behaviours. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 1cfc5f2 commit ea654cb

2 files changed

Lines changed: 160 additions & 3 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
const checkForReproducer = require('../checkForReproducer');
11+
12+
const AUTHOR = 'issue-author';
13+
const BOT = 'react-native-bot';
14+
const MAINTAINER = 'some-maintainer';
15+
16+
function labelEvent(event, login, type, name = 'Needs: Repro') {
17+
return {event, label: {name}, actor: {login, type}};
18+
}
19+
20+
function buildGithub({body, comments = [], timeline = []}) {
21+
return {
22+
rest: {
23+
issues: {
24+
get: jest.fn().mockResolvedValue({
25+
data: {
26+
user: {login: AUTHOR},
27+
created_at: '2026-01-01T00:00:00Z',
28+
body,
29+
},
30+
}),
31+
listComments: jest.fn().mockResolvedValue({data: comments}),
32+
listEventsForTimeline: jest.fn().mockResolvedValue({data: timeline}),
33+
removeLabel: jest.fn().mockResolvedValue({}),
34+
addLabels: jest.fn().mockResolvedValue({}),
35+
},
36+
},
37+
};
38+
}
39+
40+
const context = {
41+
payload: {issue: {number: 1}},
42+
repo: {owner: 'react', repo: 'react-native'},
43+
};
44+
45+
const REPRO_LINK = `Repro: https://github.com/${AUTHOR}/rn-repro`;
46+
47+
describe('checkForReproducer', () => {
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
});
51+
52+
it('adds "Needs: Repro" and "Needs: Author Feedback" when no reproducer is present', async () => {
53+
const github = buildGithub({body: 'It crashes.'});
54+
55+
await checkForReproducer(github, context);
56+
57+
expect(github.rest.issues.addLabels).toHaveBeenCalledWith(
58+
expect.objectContaining({
59+
labels: ['Needs: Repro', 'Needs: Author Feedback'],
60+
}),
61+
);
62+
expect(github.rest.issues.removeLabel).not.toHaveBeenCalled();
63+
});
64+
65+
it('removes "Needs: Repro" when the author links a repository they own', async () => {
66+
const github = buildGithub({body: REPRO_LINK});
67+
68+
await checkForReproducer(github, context);
69+
70+
expect(github.rest.issues.removeLabel).toHaveBeenCalledWith(
71+
expect.objectContaining({name: 'Needs: Repro'}),
72+
);
73+
expect(github.rest.issues.addLabels).not.toHaveBeenCalled();
74+
});
75+
76+
it('removes "Needs: Repro" after the author edits in a reproducer, even though react-native-bot applied the label', async () => {
77+
const github = buildGithub({
78+
body: REPRO_LINK,
79+
timeline: [
80+
labelEvent('labeled', BOT, 'User', 'Needs: Author Feedback'),
81+
labelEvent('labeled', BOT, 'User'),
82+
labelEvent(
83+
'unlabeled',
84+
'github-actions[bot]',
85+
'Bot',
86+
'Needs: Author Feedback',
87+
),
88+
],
89+
});
90+
91+
await checkForReproducer(github, context);
92+
93+
expect(github.rest.issues.removeLabel).toHaveBeenCalledWith(
94+
expect.objectContaining({name: 'Needs: Repro'}),
95+
);
96+
expect(github.rest.issues.addLabels).not.toHaveBeenCalled();
97+
});
98+
99+
it('does nothing when a maintainer has changed the "Needs: Repro" label', async () => {
100+
const github = buildGithub({
101+
body: REPRO_LINK,
102+
timeline: [
103+
labelEvent('labeled', BOT, 'User'),
104+
labelEvent('unlabeled', MAINTAINER, 'User'),
105+
labelEvent('labeled', MAINTAINER, 'User'),
106+
],
107+
});
108+
109+
await checkForReproducer(github, context);
110+
111+
expect(github.rest.issues.removeLabel).not.toHaveBeenCalled();
112+
expect(github.rest.issues.addLabels).not.toHaveBeenCalled();
113+
});
114+
115+
it('ignores label changes on other labels when deciding whether a maintainer intervened', async () => {
116+
const github = buildGithub({
117+
body: REPRO_LINK,
118+
timeline: [
119+
labelEvent('labeled', BOT, 'User'),
120+
labelEvent('labeled', MAINTAINER, 'User', 'Platform: iOS'),
121+
],
122+
});
123+
124+
await checkForReproducer(github, context);
125+
126+
expect(github.rest.issues.removeLabel).toHaveBeenCalledWith(
127+
expect.objectContaining({name: 'Needs: Repro'}),
128+
);
129+
});
130+
131+
it('accepts a reproducer link posted in a comment by its own author', async () => {
132+
const github = buildGithub({
133+
body: 'It crashes.',
134+
comments: [
135+
{
136+
user: {login: AUTHOR},
137+
body: `Here you go: https://github.com/${AUTHOR}/rn-repro`,
138+
},
139+
],
140+
timeline: [labelEvent('labeled', BOT, 'User')],
141+
});
142+
143+
await checkForReproducer(github, context);
144+
145+
expect(github.rest.issues.removeLabel).toHaveBeenCalledWith(
146+
expect.objectContaining({name: 'Needs: Repro'}),
147+
);
148+
});
149+
});

.github/workflow-scripts/checkForReproducer.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
const NEEDS_REPRO_LABEL = 'Needs: Repro';
1111
const NEEDS_AUTHOR_FEEDBACK_LABEL = 'Needs: Author Feedback';
1212
const SKIP_ISSUES_OLDER_THAN = '2023-07-01T00:00:00Z';
13+
// The account this workflow runs as; a user account, so actor.type is 'User'.
14+
const REACT_NATIVE_BOT_LOGIN = 'react-native-bot';
1315

1416
module.exports = async (github, context) => {
1517
const issueData = {
@@ -82,16 +84,22 @@ function containsPattern(body, pattern) {
8284
return body.search(regexp) !== -1;
8385
}
8486

85-
// Prevents the bot from responding when maintainer has changed the 'Needs: Repro' label
87+
// Prevents the bot from responding when a maintainer has changed the
88+
// 'Needs: Repro' label. Events from GitHub Apps and react-native-bot itself
89+
// are automation, not maintainer decisions.
8690
async function hasMaintainerChangedLabel(github, issueData, author) {
8791
const timeline = await github.rest.issues.listEventsForTimeline(issueData);
8892

8993
const labeledEvents = timeline.data.filter(
9094
event => event.event === 'labeled' || event.event === 'unlabeled',
9195
);
92-
const userEvents = labeledEvents.filter(event => event.actor.type !== 'Bot');
96+
const maintainerEvents = labeledEvents.filter(
97+
event =>
98+
event.actor.type !== 'Bot' &&
99+
event.actor.login !== REACT_NATIVE_BOT_LOGIN,
100+
);
93101

94-
return userEvents.some(
102+
return maintainerEvents.some(
95103
event =>
96104
event.actor.login !== author && event.label.name === NEEDS_REPRO_LABEL,
97105
);

0 commit comments

Comments
 (0)