Skip to content

Commit 9f2ef5f

Browse files
authored
[PyTorchBot] Remove irrelevant labels (#7463)
- `release notes:`/`topic:`/`ciflow/` should only be added to PR - `module:`/`oncall:` should only be added to labels Make bot do that and add tests (coded by Claude)
1 parent 9571290 commit 9f2ef5f

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

torchci/lib/bot/autoLabelBot.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,28 @@ function myBot(app: Probot): void {
429429

430430
const filtered = await filterCIFlowLabels(true, newLabels);
431431
await addNewLabels(existingLabels, filtered, context);
432+
433+
// Remove PR-only labels from issues
434+
if (
435+
addedLabel.startsWith("release notes:") ||
436+
addedLabel.startsWith("ciflow/")
437+
) {
438+
context.log(
439+
`Removing PR-only label "${addedLabel}" from issue ${context.payload.issue.html_url}`
440+
);
441+
await context.octokit.issues.removeLabel(
442+
context.repo({
443+
issue_number: context.payload.issue.number,
444+
name: addedLabel,
445+
})
446+
);
447+
await context.octokit.issues.createComment(
448+
context.repo({
449+
issue_number: context.payload.issue.number,
450+
body: `The label \`${addedLabel}\` is only applicable to pull requests and has been removed. Please only use this label on PRs.`,
451+
})
452+
);
453+
}
432454
});
433455

434456
app.on(["issues.opened", "issues.edited"], async (context) => {
@@ -542,6 +564,36 @@ function myBot(app: Probot): void {
542564
}
543565
await addNewLabels(existingLabels, labelsToAdd, context);
544566
});
567+
568+
app.on("pull_request.labeled", async (context) => {
569+
const owner = context.payload.repository.owner.login;
570+
if (!isPyTorchbotSupportedOrg(owner)) {
571+
context.log(`${__filename} isn't enabled on ${owner}'s repos`);
572+
return;
573+
}
574+
575+
const addedLabel = context.payload.label!.name;
576+
context.log({ addedLabel });
577+
578+
// Remove issue-only labels from PRs
579+
if (addedLabel.startsWith("module:") || addedLabel.startsWith("oncall:")) {
580+
context.log(
581+
`Removing issue-only label "${addedLabel}" from PR ${context.payload.pull_request.html_url}`
582+
);
583+
await context.octokit.issues.removeLabel(
584+
context.repo({
585+
issue_number: context.payload.pull_request.number,
586+
name: addedLabel,
587+
})
588+
);
589+
await context.octokit.issues.createComment(
590+
context.repo({
591+
issue_number: context.payload.pull_request.number,
592+
body: `The label \`${addedLabel}\` is only applicable to issues and has been removed. Please only use this label on issues.`,
593+
})
594+
);
595+
}
596+
});
545597
}
546598

547599
export default myBot;

torchci/test/autoLabelBot.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,140 @@ adfadsfasd
12781278
await probot.receive(event);
12791279
});
12801280
});
1281+
1282+
describe("auto-label-bot: label restrictions", () => {
1283+
let probot: Probot;
1284+
1285+
function emptyMockConfig(repoFullName: string) {
1286+
utils.mockConfig("pytorch-probot.yml", "", repoFullName);
1287+
}
1288+
1289+
beforeEach(() => {
1290+
probot = utils.testProbot();
1291+
probot.load(myProbotApp);
1292+
const mock = jest.spyOn(botUtils, "isPyTorchPyTorch");
1293+
mock.mockReturnValue(true);
1294+
const mockbotSupportedOrg = jest.spyOn(
1295+
botUtils,
1296+
"isPyTorchbotSupportedOrg"
1297+
);
1298+
mockbotSupportedOrg.mockReturnValue(true);
1299+
});
1300+
1301+
afterEach(() => {
1302+
jest.restoreAllMocks();
1303+
nock.cleanAll();
1304+
});
1305+
1306+
test("remove release notes label from issue", async () => {
1307+
nock("https://api.github.com")
1308+
.post("/app/installations/2/access_tokens")
1309+
.reply(200, { token: "test" });
1310+
1311+
const payload = requireDeepCopy("./fixtures/issues.labeled");
1312+
payload["label"] = { name: "release notes: nn" };
1313+
payload["issue"]["labels"] = [{ name: "release notes: nn" }];
1314+
emptyMockConfig(payload.repository.full_name);
1315+
1316+
const scope = nock("https://api.github.com")
1317+
.delete(
1318+
"/repos/ezyang/testing-ideal-computing-machine/issues/5/labels/release%20notes%3A%20nn"
1319+
)
1320+
.reply(200)
1321+
.post(
1322+
"/repos/ezyang/testing-ideal-computing-machine/issues/5/comments",
1323+
(body) => {
1324+
expect(body.body).toContain("release notes: nn");
1325+
expect(body.body).toContain("only applicable to pull requests");
1326+
return true;
1327+
}
1328+
)
1329+
.reply(200);
1330+
1331+
await probot.receive({ name: "issues", payload, id: "2" });
1332+
1333+
handleScope(scope);
1334+
});
1335+
1336+
test("remove ciflow label from issue", async () => {
1337+
nock("https://api.github.com")
1338+
.post("/app/installations/2/access_tokens")
1339+
.reply(200, { token: "test" });
1340+
1341+
const payload = requireDeepCopy("./fixtures/issues.labeled");
1342+
payload["label"] = { name: "ciflow/rocm" };
1343+
payload["issue"]["labels"] = [{ name: "ciflow/rocm" }];
1344+
emptyMockConfig(payload.repository.full_name);
1345+
1346+
const scope = nock("https://api.github.com")
1347+
.delete(
1348+
"/repos/ezyang/testing-ideal-computing-machine/issues/5/labels/ciflow%2Frocm"
1349+
)
1350+
.reply(200)
1351+
.post(
1352+
"/repos/ezyang/testing-ideal-computing-machine/issues/5/comments",
1353+
(body) => {
1354+
expect(body.body).toContain("ciflow/rocm");
1355+
expect(body.body).toContain("only applicable to pull requests");
1356+
return true;
1357+
}
1358+
)
1359+
.reply(200);
1360+
1361+
await probot.receive({ name: "issues", payload, id: "2" });
1362+
1363+
handleScope(scope);
1364+
});
1365+
1366+
test("remove module label from pull request", async () => {
1367+
nock("https://api.github.com")
1368+
.post("/app/installations/2/access_tokens")
1369+
.reply(200, { token: "test" });
1370+
1371+
const payload = requireDeepCopy("./fixtures/pull_request.labeled");
1372+
payload["label"] = { name: "module: ci" };
1373+
payload["pull_request"]["labels"] = [{ name: "module: ci" }];
1374+
emptyMockConfig(payload.repository.full_name);
1375+
1376+
const scope = nock("https://api.github.com")
1377+
.delete("/repos/seemethere/test-repo/issues/20/labels/module%3A%20ci")
1378+
.reply(200)
1379+
.post("/repos/seemethere/test-repo/issues/20/comments", (body) => {
1380+
expect(body.body).toContain("module: ci");
1381+
expect(body.body).toContain("only applicable to issues");
1382+
return true;
1383+
})
1384+
.reply(200);
1385+
1386+
await probot.receive({ name: "pull_request", payload, id: "2" });
1387+
1388+
handleScope(scope);
1389+
});
1390+
1391+
test("remove oncall label from pull request", async () => {
1392+
nock("https://api.github.com")
1393+
.post("/app/installations/2/access_tokens")
1394+
.reply(200, { token: "test" });
1395+
1396+
const payload = requireDeepCopy("./fixtures/pull_request.labeled");
1397+
payload["label"] = { name: "oncall: distributed" };
1398+
payload["pull_request"]["labels"] = [{ name: "oncall: distributed" }];
1399+
emptyMockConfig(payload.repository.full_name);
1400+
1401+
const scope = nock("https://api.github.com")
1402+
.delete(
1403+
"/repos/seemethere/test-repo/issues/20/labels/oncall%3A%20distributed"
1404+
)
1405+
.reply(200)
1406+
.post("/repos/seemethere/test-repo/issues/20/comments", (body) => {
1407+
expect(body.body).toContain("oncall: distributed");
1408+
expect(body.body).toContain("only applicable to issues");
1409+
return true;
1410+
})
1411+
.reply(200);
1412+
1413+
await probot.receive({ name: "pull_request", payload, id: "2" });
1414+
1415+
handleScope(scope);
1416+
});
1417+
});

0 commit comments

Comments
 (0)