From 6a567ac541a45dac43e12bdbf6b89cc94e7c81eb Mon Sep 17 00:00:00 2001 From: Catherine Lee Date: Fri, 19 Sep 2025 19:50:28 -0700 Subject: [PATCH 1/2] tc --- torchci/lib/bot/index.ts | 4 - torchci/lib/bot/triggerCircleCIWorkflows.ts | 267 ------------------ torchci/lib/bot/triggerInductorTestsBot.ts | 71 ----- torchci/test/triggerCircleCIWorkflows.test.ts | 211 -------------- torchci/test/triggerInductorTestsBot.test.ts | 137 --------- 5 files changed, 690 deletions(-) delete mode 100644 torchci/lib/bot/triggerCircleCIWorkflows.ts delete mode 100644 torchci/lib/bot/triggerInductorTestsBot.ts delete mode 100644 torchci/test/triggerCircleCIWorkflows.test.ts delete mode 100644 torchci/test/triggerInductorTestsBot.test.ts diff --git a/torchci/lib/bot/index.ts b/torchci/lib/bot/index.ts index f1131bf8aa..537b4116e5 100644 --- a/torchci/lib/bot/index.ts +++ b/torchci/lib/bot/index.ts @@ -9,8 +9,6 @@ import drciBot from "./drciBot"; import pytorchBot from "./pytorchBot"; import retryBot from "./retryBot"; import stripApprovalBot from "./stripApprovalBot"; -import triggerCircleCIWorkflows from "./triggerCircleCIWorkflows"; -import triggerInductorTestsBot from "./triggerInductorTestsBot"; import verifyDisableTestIssueBot from "./verifyDisableTestIssueBot"; import webhookToDynamo from "./webhookToDynamo"; @@ -25,8 +23,6 @@ export default function bot(app: Probot) { pytorchBot(app); retryBot(app); stripApprovalBot(app); - triggerCircleCIWorkflows(app); verifyDisableTestIssueBot(app); webhookToDynamo(app); - triggerInductorTestsBot(app); } diff --git a/torchci/lib/bot/triggerCircleCIWorkflows.ts b/torchci/lib/bot/triggerCircleCIWorkflows.ts deleted file mode 100644 index c711ac6a6f..0000000000 --- a/torchci/lib/bot/triggerCircleCIWorkflows.ts +++ /dev/null @@ -1,267 +0,0 @@ -// @ts-nocheck -import axios from "axios"; -import { Context, Probot } from "probot"; -import * as utils from "./utils"; - -interface Params { - [param: string]: boolean; -} - -interface LabelParams { - parameter?: string; - default_true_on?: { - branches?: string[]; - tags?: string[]; - pull_request?: null; - }; - set_to_false?: boolean; -} - -interface Config { - default_params?: Params; - labels_to_circle_params: { - [label: string]: LabelParams; - }; -} - -export const configName = "pytorch-circleci-labels.yml"; -export const circleAPIUrl = "https://circleci.com"; -const circleToken = process.env.CIRCLE_TOKEN; -const repoMap = new Map(); - -async function loadConfig(context: Context): Promise { - const repoKey = utils.repoKey(context); - let configObj = repoMap.get(repoKey); - if (configObj === undefined) { - context.log.info({ repoKey }, "loadConfig"); - configObj = (await context.config(configName)) as Config | {}; - if (configObj === null || !configObj["labels_to_circle_params"]) { - return {}; - } - context.log.info({ configObj }, "loadConfig"); - repoMap.set(repoKey, configObj); - } - return repoMap.get(repoKey); -} - -function isValidConfig( - context: Context, - config: Config | {} -): config is Config { - if (Object.keys(config).length === 0 || !config["labels_to_circle_params"]) { - context.log.info( - `No valid configuration found for repository ${utils.repoKey(context)}` - ); - return false; - } - return true; -} - -function stripReference(reference: string): string { - return reference.replace(/refs\/(heads|tags)\//, ""); -} - -async function getAppliedLabels(context: Context): Promise { - const appliedLabels = new Array(); - // Check if we already have the applied labels in our context payload - if (context.payload["pull_request"]["labels"]) { - for (const label of context.payload["pull_request"]["labels"]) { - appliedLabels.push(label["name"]); - } - } - context.log.info({ appliedLabels }, "getAppliedLabels"); - return appliedLabels; -} - -async function triggerCircleCI(context: Context, data: object): Promise { - const repoKey = utils.repoKey(context); - context.log.info({ repoKey, data }, "triggerCircleCI"); - const resp = await axios.post( - `${circleAPIUrl}${circlePipelineEndpoint(repoKey)}`, - data, - { - validateStatus: () => { - return true; - }, - auth: { - username: circleToken, - password: "", - }, - } - ); - - if (resp.status !== 201) { - throw Error( - `Error triggering downstream circleci workflow (${ - resp.status - }): ${JSON.stringify(resp.data)}` - ); - } - context.log.info({ data }, `Build triggered successfully for ${repoKey}`); -} - -export function circlePipelineEndpoint(repoKey: string): string { - return `/api/v2/project/github/${repoKey}/pipeline`; -} - -function invert(label: string): string { - return label.replace(/^ci\//, "ci/no-"); -} - -export function genCircleParametersForPR( - config: Config, - context: Context, - appliedLabels: string[] -): Params { - context.log.info({ config, appliedLabels }, "genParametersForPR"); - const { - default_params: parameters = {}, - labels_to_circle_params: labelsToParams, - } = config; - context.log.info({ parameters }, "genCircleParametersForPR (default_params)"); - for (const label of Object.keys(labelsToParams)) { - const defaultTrueOn = labelsToParams[label].default_true_on || {}; - if ( - appliedLabels.includes(label) || - (defaultTrueOn.pull_request !== undefined && - !appliedLabels.includes(invert(label))) - ) { - const { parameter } = labelsToParams[label]; - if (parameter !== undefined) { - context.log.info( - { parameter }, - "genCircleParametersForPR setting parameter to true" - ); - parameters[parameter] = true; - } - if (labelsToParams[label].set_to_false) { - const falseParams = labelsToParams[label].set_to_false; - // There's potential for labels to override each other which we should - // probably be careful of - for (const falseLabel of Object.keys(falseParams)) { - const param = falseParams[falseLabel]; - context.log.info( - { param }, - "genCircleParametersForPR (set_to_false) setting param to false" - ); - parameters[param] = false; - } - } - } - } - return parameters; -} - -function genCircleParametersForPush(config: Config, context: Context): Params { - const { - default_params: parameters = {}, - labels_to_circle_params: labelsToParams, - } = config; - context.log.info( - { parameters }, - "genCircleParametersForPush (default_params)" - ); - const onTag: boolean = context.payload["ref"].startsWith("refs/tags"); - const strippedRef: string = stripReference(context.payload["ref"]); - for (const label of Object.keys(labelsToParams)) { - context.log.info({ label }, "genParametersForPush"); - const defaultTrueOn = labelsToParams[label].default_true_on; - if (!defaultTrueOn) { - context.log.info( - { label }, - "genParametersForPush (no default_true_on found)" - ); - continue; - } - const refsToMatch = onTag ? "tags" : "branches"; - context.log.info({ defaultTrueOn, refsToMatch, strippedRef }); - for (const pattern of defaultTrueOn[refsToMatch] || []) { - context.log.info({ pattern }, "genParametersForPush"); - if (strippedRef.match(pattern)) { - const { parameter } = labelsToParams[label]; - if (parameter !== undefined) { - context.log.info( - { parameter }, - "genParametersForPush setting parameter to true" - ); - parameters[parameter] = true; - } - if (labelsToParams[label].set_to_false) { - const falseParams = labelsToParams[label].set_to_false; - // There's potential for labels to override each other which we should - // probably be careful of - for (const falseLabel of Object.keys(falseParams)) { - const param = falseParams[falseLabel]; - context.log.info( - { param }, - "genParametersForPush (set_to_false) setting param to false" - ); - parameters[param] = false; - } - } - } - } - } - return parameters; -} - -async function runBotForPR(context: Context): Promise { - try { - let triggerBranch = context.payload["pull_request"]["head"]["ref"]; - if (context.payload["pull_request"]["head"]["repo"]["fork"]) { - triggerBranch = `pull/${context.payload["pull_request"]["number"]}/head`; - } - context.log.info({ triggerBranch }, "runBotForPR"); - const config = await loadConfig(context); - if (!isValidConfig(context, config)) { - return; - } - const labels = await getAppliedLabels(context); - const parameters = genCircleParametersForPR(config, context, labels); - context.log.info({ config, labels, parameters }, "runBot"); - if (Object.keys(parameters).length !== 0) { - await triggerCircleCI(context, { - branch: triggerBranch, - parameters, - }); - } else { - context.log.info( - `No labels applied for ${context.payload["number"]}, not triggering an extra CircleCI build` - ); - } - } catch (err) { - context.log.error({ err }, "runBotForPR"); - } -} - -async function runBotForPush(context: Context): Promise { - try { - const rawRef = context.payload["ref"]; - const onTag: boolean = rawRef.startsWith("refs/tags"); - const ref = stripReference(rawRef); - context.log.info({ rawRef, onTag, ref }, "runBotForPush"); - const config = await loadConfig(context); - if (!isValidConfig(context, config)) { - return; - } - const parameters = genCircleParametersForPush(config, context); - const refKey: string = onTag ? "tag" : "branch"; - context.log.info({ parameters }, "runBot"); - if (Object.keys(parameters).length !== 0) { - await triggerCircleCI(context, { - [refKey]: ref, - parameters, - }); - } - } catch (err) { - context.log.error({ err }, "runBotForPush"); - } -} - -export function myBot(app: Probot): void { - app.on("pull_request.labeled", runBotForPR); - app.on("pull_request.synchronize", runBotForPR); - app.on("push", runBotForPush); -} - -export default myBot; diff --git a/torchci/lib/bot/triggerInductorTestsBot.ts b/torchci/lib/bot/triggerInductorTestsBot.ts deleted file mode 100644 index 133c4fe216..0000000000 --- a/torchci/lib/bot/triggerInductorTestsBot.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Context, Probot } from "probot"; -import { reactOnComment } from "./utils"; - -export default function triggerInductorTestsBot(app: Probot): void { - const preapprovedUsers = ["pytorchbot", "PaliC", "huydhn"]; // List of preapproved users - // const tritonRepo = "triton-lang/triton"; // uncomment once this is good enough for triton - const tritonRepo = "triton-lang-test/triton"; // delete once this is good enough for triton - const preapprovedRepos = ["malfet/deleteme", tritonRepo]; // List of preapproved orgs/repos - // const preapprovedRepos = ["malfet/deleteme", "triton-lang/triton"];// uncomment once this is good enough for triton and delete line above - - app.on( - ["issue_comment.created"], - async (ctx: Context<"issue_comment.created">) => { - const commentBody = ctx.payload.comment.body.toLowerCase(); - const commenter = ctx.payload.comment.user.login; - const orgRepo = `${ctx.payload.repository.owner.login}/${ctx.payload.repository.name}`; - if ( - commentBody.includes("@pytorch run pytorch tests") && - preapprovedUsers.includes(commenter) && - preapprovedRepos.includes(orgRepo) - ) { - const workflow_owner = "pytorch"; - const workflow_repo = "pytorch-integration-testing"; - const workflow_id = "inductor.yml"; - const pytorchCommit = "viable/strict"; - - let tritonCommit = "main"; - - reactOnComment(ctx, "+1"); - - // if on the triton repo, get the commit of the pr - if (orgRepo === tritonRepo) { - const pr = await ctx.octokit.pulls.get({ - owner: ctx.payload.repository.owner.login, - repo: ctx.payload.repository.name, - pull_number: ctx.payload.issue.number, - }); - tritonCommit = pr.data.head.sha; - } - - try { - await ctx.octokit.actions.createWorkflowDispatch({ - owner: workflow_owner, - repo: workflow_repo, - workflow_id, - ref: "main", - inputs: { - triton_commit: tritonCommit, - pytorch_commit: pytorchCommit, - }, - }); - - await ctx.octokit.issues.createComment({ - owner: ctx.payload.repository.owner.login, - repo: ctx.payload.repository.name, - issue_number: ctx.payload.issue.number, - body: `Inductor tests triggered successfully with pytorch commit: ${pytorchCommit} and triton commit: ${tritonCommit}`, - }); - } catch (error) { - console.error("Error triggering workflow:", error); - await ctx.octokit.issues.createComment({ - owner: ctx.payload.repository.owner.login, - repo: ctx.payload.repository.name, - issue_number: ctx.payload.issue.number, - body: `Failed to trigger Inductor tests. Please check the logs. Failed with error ${error}`, - }); - } - } - } - ); -} diff --git a/torchci/test/triggerCircleCIWorkflows.test.ts b/torchci/test/triggerCircleCIWorkflows.test.ts deleted file mode 100644 index 89da25679b..0000000000 --- a/torchci/test/triggerCircleCIWorkflows.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { promises as fs } from "fs"; -import nock from "nock"; -import { Probot } from "probot"; -import * as triggerCircleBot from "../lib/bot/triggerCircleCIWorkflows"; -import * as utils from "./utils"; - -nock.disableNetConnect(); - -const EXAMPLE_CONFIG = ` -labels_to_circle_params: - ci/binaries: - parameter: run_binaries_tests - default_true_on: - branches: - - nightly - - ci-all/.* - tags: - - v[0-9]+(\.[0-9]+)*-rc[0-9]+ - ci/default: - parameter: default - default_true_on: - branches: - - master - pull_request: - ci/bleh: - parameter: run_bleh_tests - ci/foo: - parameter: run_foo_tests -`; - -interface Example { - payload: object; - endpoint: string; -} - -// Prior to the existence of this `prepare` function, the tests in this suite -// were failing in very strange ways when run together (but not when run in -// isolation). This seemed to be caused by the fact that all the tests were -// `nock`ing the same endpoint of the same CircleCI URL, so one test would -// receive the CircleCI parameters that corresponded to a different test. No -// idea how CI was previously passing on `master`. Anyway, this fixes the issue -// by enforcing that every test rename the example repo to a unique name, -// resulting in a unique CircleCI endpoint. -const usedNames: Set = new Set(); -async function prepare(fixture: string, repoName: string): Promise { - expect(usedNames.has(repoName)).toBe(false); - usedNames.add(repoName); - const repoFullName = `seemethere/${repoName}`; - utils.mockConfig(triggerCircleBot.configName, EXAMPLE_CONFIG, repoFullName); - const payload = JSON.parse( - (await fs.readFile(`test/fixtures/${fixture}.json`, "utf8")).replace( - /test-repo/g, - repoName - ) - ); - const endpoint = triggerCircleBot.circlePipelineEndpoint(repoFullName); - return { payload, endpoint }; -} - -describe("trigger-circleci-workflows", () => { - let probot: Probot; - let payload: object; - - beforeEach(() => { - probot = utils.testProbot(); - probot.load(triggerCircleBot.myBot); - process.env.CIRCLE_TOKEN = "dummy_token"; - utils.mockAccessToken(); - }); - - afterEach(() => { - // Cleanup environment variables after the fact - delete process.env.CIRCLE_TOKEN; - }); - - test("test with pull_request.labeled (specific labels)", async () => { - const { payload, endpoint } = await prepare( - "pull_request.labeled", - "pr-labeled-specific" - ); - // @ts-ignore - payload["pull_request"]["number"] = 1; - // @ts-ignore - payload["pull_request"]["head"]["ref"] = "test_branch"; - // @ts-ignore - payload["pull_request"]["labels"] = [ - { name: "ci/binaries" }, - { name: "ci/bleh" }, - ]; - const scope = nock(`${triggerCircleBot.circleAPIUrl}`) - .post(endpoint, (body: any) => { - expect(body).toStrictEqual({ - branch: "test_branch", - parameters: { - run_binaries_tests: true, - run_bleh_tests: true, - default: true, - }, - }); - return true; - }) - .reply(201); - - // @ts-ignore - await probot.receive({ name: "pull_request", payload, id: "2" }); - - expect(scope.isDone()).toBe(true); - }); - - test("test with pull_request.labeled (fork) (specific labels)", async () => { - const { payload, endpoint } = await prepare( - "pull_request.labeled", - "pr-labeled-fork-specific" - ); - // @ts-ignore - payload["pull_request"]["head"]["repo"]["fork"] = true; - // @ts-ignore - payload["pull_request"]["number"] = 1; - // @ts-ignore - payload["pull_request"]["head"]["ref"] = "test_branch"; - // @ts-ignore - payload["pull_request"]["labels"] = [ - { name: "ci/binaries" }, - { name: "ci/no-default" }, - { name: "ci/bleh" }, - ]; - const scope = nock(`${triggerCircleBot.circleAPIUrl}`) - .post(endpoint, (body: any) => { - expect(body).toStrictEqual({ - branch: "pull/1/head", - parameters: { - run_binaries_tests: true, - run_bleh_tests: true, - }, - }); - return true; - }) - .reply(201); - - // @ts-ignore - await probot.receive({ name: "pull_request", payload, id: "2" }); - - expect(scope.isDone()).toBe(true); - }); - - test("test with push (refs/heads/nightly)", async () => { - const { payload, endpoint } = await prepare("push", "push-nightly"); - // @ts-ignore - payload["ref"] = "refs/heads/nightly"; - const scope = nock(`${triggerCircleBot.circleAPIUrl}`) - .post(endpoint, (body: any) => { - expect(body).toStrictEqual({ - branch: "nightly", - parameters: { - run_binaries_tests: true, - }, - }); - return true; - }) - .reply(201); - - // @ts-ignore - await probot.receive({ name: "push", payload, id: "2" }); - - scope.done(); - }); - - test("test with push (refs/heads/ci-all/bleh)", async () => { - const { payload, endpoint } = await prepare("push", "push-all-bleh"); - // @ts-ignore - payload["ref"] = "refs/heads/ci-all/bleh"; - const scope = nock(`${triggerCircleBot.circleAPIUrl}`) - .post(endpoint, (body: any) => { - expect(body).toStrictEqual({ - branch: "ci-all/bleh", - parameters: { - run_binaries_tests: true, - }, - }); - return true; - }) - .reply(201); - - // @ts-ignore - await probot.receive({ name: "push", payload, id: "2" }); - - scope.done(); - }); - - test("test with push (/refs/tags/v1.5.0-rc1)", async () => { - const { payload, endpoint } = await prepare("push", "push-tag-rc"); - // @ts-ignore - payload["ref"] = "refs/tags/v1.5.0-rc1"; - const scope = nock(`${triggerCircleBot.circleAPIUrl}`) - .post(endpoint, (body: any) => { - expect(body).toStrictEqual({ - tag: "v1.5.0-rc1", - parameters: { - run_binaries_tests: true, - }, - }); - return true; - }) - .reply(201); - - // @ts-ignore - await probot.receive({ name: "push", payload, id: "2" }); - - scope.done(); - }); -}); diff --git a/torchci/test/triggerInductorTestsBot.test.ts b/torchci/test/triggerInductorTestsBot.test.ts deleted file mode 100644 index f12882c44c..0000000000 --- a/torchci/test/triggerInductorTestsBot.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import nock from "nock"; -import { Probot } from "probot"; -import triggerInductorTestsBot from "../lib/bot/triggerInductorTestsBot"; -import { requireDeepCopy } from "./common"; -import * as utils from "./utils"; - -nock.disableNetConnect(); - -describe("trigger-inductor-tests-bot", () => { - let probot: Probot; - - beforeEach(() => { - probot = utils.testProbot(); - probot.load(triggerInductorTestsBot); - }); - - afterEach(() => { - nock.cleanAll(); - jest.restoreAllMocks(); - }); - - test("triggers inductor tests for preapproved user and repo", async () => { - const event = requireDeepCopy("./fixtures/pull_request_comment.json"); - event.payload.comment.body = "@pytorch run pytorch tests"; - event.payload.comment.user.login = "pytorchbot"; - event.payload.repository.owner.login = "malfet"; - event.payload.repository.name = "deleteme"; - - const scope = nock("https://api.github.com") - .post("/repos/malfet/deleteme/issues/comments/890173751/reactions") - .reply(200, {}) - .post( - "/repos/pytorch/pytorch-integration-testing/actions/workflows/inductor.yml/dispatches", - (body) => { - expect(JSON.stringify(body)).toContain( - `{"ref":"main","inputs\":{"triton_commit":"main","pytorch_commit":"viable/strict"}}` - ); - return true; - } - ) - .reply(200, {}) - .post("/repos/malfet/deleteme/issues/31/comments", (body) => { - expect(body.body).toBe( - `Inductor tests triggered successfully with pytorch commit: viable/strict and triton commit: main` - ); - return true; - }) - .reply(200); - - await probot.receive(event); - - scope.done(); - }); - - test("triggers inductor tests for triton repo", async () => { - const event = requireDeepCopy("./fixtures/pull_request_comment.json"); - event.payload.comment.body = "@pytorch run pytorch tests"; - event.payload.comment.user.login = "pytorchbot"; - event.payload.repository.owner.login = "triton-lang-test"; - event.payload.repository.name = "triton"; - - const scope = nock("https://api.github.com") - .post( - "/repos/triton-lang-test/triton/issues/comments/890173751/reactions" - ) - .reply(200, {}) - .get("/repos/triton-lang-test/triton/pulls/31") - .reply(200, { - head: { - sha: "custom_triton_sha", - }, - }) - .post( - "/repos/pytorch/pytorch-integration-testing/actions/workflows/inductor.yml/dispatches", - (body) => { - expect(JSON.stringify(body)).toContain( - `{"ref":"main","inputs\":{"triton_commit":"custom_triton_sha","pytorch_commit":"viable/strict"}}` - ); - return true; - } - ) - .reply(200, {}) - .post("/repos/triton-lang-test/triton/issues/31/comments", (body) => { - expect(body.body).toBe( - `Inductor tests triggered successfully with pytorch commit: viable/strict and triton commit: custom_triton_sha` - ); - return true; - }) - .reply(200); - - await probot.receive(event); - - scope.done(); - }); - - test("does not trigger for non-preapproved user", async () => { - const event = requireDeepCopy("./fixtures/pull_request_comment.json"); - event.payload.comment.body = "@pytorch run pytorch tests"; - event.payload.comment.user.login = "nonApprovedUser"; - event.payload.repository.owner.login = "malfet"; - event.payload.repository.name = "deleteme"; - - const scope = nock("https://api.github.com"); - - await probot.receive(event); - - scope.done(); - }); - - test("does not trigger for non-preapproved repo", async () => { - const event = requireDeepCopy("./fixtures/pull_request_comment.json"); - event.payload.comment.body = "@pytorch run pytorch tests"; - event.payload.comment.user.login = "pytorchbot"; - event.payload.repository.owner.login = "fakeorg"; - event.payload.repository.name = "fakerepo"; - - const scope = nock("https://api.github.com"); - - await probot.receive(event); - - scope.done(); - }); - - test("does not trigger for irrelevant comment", async () => { - const event = requireDeepCopy("./fixtures/pull_request_comment.json"); - event.payload.comment.body = "random comment"; - event.payload.comment.user.login = "pytorchbot"; - event.payload.repository.owner.login = "malfet"; - event.payload.repository.name = "deleteme"; - - const scope = nock("https://api.github.com"); - - await probot.receive(event); - - scope.done(); - }); -}); From 5e9721de4afeda71df8b6b3a43e21a5fe72901f0 Mon Sep 17 00:00:00 2001 From: Catherine Lee Date: Fri, 19 Sep 2025 19:54:31 -0700 Subject: [PATCH 2/2] tc --- torchci/test/utils.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/torchci/test/utils.test.ts b/torchci/test/utils.test.ts index c21c8e2940..63da2e1161 100644 --- a/torchci/test/utils.test.ts +++ b/torchci/test/utils.test.ts @@ -1,7 +1,6 @@ import { hasApprovedPullRuns } from "lib/bot/utils"; import nock from "nock"; import { Probot } from "probot"; -import triggerInductorTestsBot from "../lib/bot/triggerInductorTestsBot"; import * as utils from "./utils"; nock.disableNetConnect(); @@ -14,7 +13,6 @@ describe("utils: hasApprovedPullRuns", () => { beforeEach(() => { probot = utils.testProbot(); - probot.load(triggerInductorTestsBot); }); function mockRuns(