Skip to content
Merged
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
5 changes: 4 additions & 1 deletion aws/lambda/cross_repo_ci_relay/callback/callback_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def _create_upstream_check_run(
workflow_name: str,
job_name: str | None,
details_url: str,
pr_number: str = "",
) -> None:
"""Create a new upstream check run mirroring the downstream job's status.

Expand Down Expand Up @@ -233,7 +234,7 @@ def _create_upstream_check_run(
details_url=details_url,
# Store the downstream run_id so a check-run rerequest can re-run
# the failed jobs of that workflow run.
external_id=str(run_id),
external_id=f"{run_id}:{pr_number}" if pr_number else str(run_id),
output=output,
)
logger.info(
Expand Down Expand Up @@ -369,6 +370,7 @@ def handle(config: RelayConfig, body: dict, verified_repo: str) -> dict:
if repo_level.value >= AllowlistLevel.L3.value:
pr_field = (body.get("payload") or {}).get("pull_request") or {}
head_sha = (pr_field.get("head") or {}).get("sha", "")
pr_number = str(pr_field.get("number") or "")
if head_sha:
conclusion = (body.get("workflow") or {}).get("conclusion")
details_url = f"https://github.com/{verified_repo}/actions/runs/{run_id}"
Expand Down Expand Up @@ -408,6 +410,7 @@ def handle(config: RelayConfig, body: dict, verified_repo: str) -> dict:
workflow_name=workflow_name,
job_name=job_name,
details_url=details_url,
pr_number=pr_number,
)

if status == "in_progress":
Expand Down
3 changes: 2 additions & 1 deletion aws/lambda/cross_repo_ci_relay/callback/cleanup_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def _finalize_timed_out_check_run(

pr_field = (body.get("payload") or {}).get("pull_request") or {}
head_sha = (pr_field.get("head") or {}).get("sha", "")
pr_number = str(pr_field.get("number") or "")
if not head_sha:
return

Expand Down Expand Up @@ -117,7 +118,7 @@ def _finalize_timed_out_check_run(
status="completed",
conclusion="timed_out",
details_url=details_url,
external_id=run_id,
external_id=f"{run_id}:{pr_number}" if pr_number else run_id,
output=output,
)
logger.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def test_check_run_external_id_is_run_id(self):
)

kw = self.mock_gh.create_check_run.call_args[1]
self.assertEqual(kw["external_id"], "99999") # run_id from _body
self.assertEqual(kw["external_id"], "99999:42") # run_id:pr_number

def test_in_progress_callback_creates_check_run(self):
self.mock_gh.create_check_run.return_value = 999
Expand Down
4 changes: 2 additions & 2 deletions aws/lambda/cross_repo_ci_relay/tests/test_event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def test_scenario2_in_progress_job_creates_in_progress_check_run(self):
self.assertEqual(kw["head_sha"], "abc123")
self.assertEqual(kw["status"], "in_progress")
self.assertIsNone(kw["conclusion"])
self.assertEqual(kw["external_id"], "99999") # run_id
self.assertEqual(kw["external_id"], "99999:42") # run_id:pr_number

def test_scenario2_backfills_every_job_not_just_one(self):
"""Multi-job workflow: a mid-run label backfills a check run for EVERY
Expand All @@ -215,7 +215,7 @@ def test_scenario2_backfills_every_job_not_just_one(self):
c.kwargs["external_id"]
for c in self.mock_gh.create_check_run.call_args_list
}
self.assertEqual(external_ids, {"99999"}) # run_id
self.assertEqual(external_ids, {"99999:42"}) # run_id:pr_number

def test_scenario3_completed_job_creates_completed_check_run(self):
"""Scenario 3: label arrives after workflow completed → create completed CR directly."""
Expand Down
13 changes: 8 additions & 5 deletions aws/lambda/cross_repo_ci_relay/webhook/event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def _handle_pr_labeled(config: RelayConfig, payload: dict) -> dict:
return {"ok": True, "created_check_runs": []}

pr = payload.get("pull_request") or {}
pr_number = str(pr.get("number", ""))
pr_number = str(pr.get("number") or "")
head_sha = (pr.get("head") or {}).get("sha", "")
if not pr_number or not head_sha:
return {"ignored": True, "reason": "missing pr context"}
Expand Down Expand Up @@ -205,9 +205,12 @@ def _handle_pr_labeled(config: RelayConfig, payload: dict) -> dict:
status=job_status,
conclusion=(job_conclusion if job_status == "completed" else None),
details_url=details_url,
external_id=str(run_id),
external_id=f"{run_id}:{pr_number}" if pr_number else str(run_id),
output=gh_helper.build_check_run_output(
job_status, job_conclusion, details_url, downstream_repo
job_status,
job_conclusion,
details_url,
downstream_repo,
),
)
created.append(f"{downstream_repo}/{job_name}")
Expand Down Expand Up @@ -263,7 +266,7 @@ def _handle_check_run_rerequested(config: RelayConfig, payload: dict) -> dict:
"""
check_run = payload.get("check_run") or {}
name = check_run.get("name", "")
run_id = check_run.get("external_id") or ""
run_id = (check_run.get("external_id") or "").split(":")[0]
downstream_repo = _downstream_repo_from_check_run(name)
if not downstream_repo or not run_id:
return {"ignored": True, "reason": "not a crcr check run"}
Expand Down Expand Up @@ -346,7 +349,7 @@ def _handle_check_suite_rerequested(config: RelayConfig, payload: dict) -> dict:
rerun: list[str] = []
for check_run in check_runs:
downstream_repo = _downstream_repo_from_check_run(check_run.get("name", ""))
run_id = check_run.get("external_id") or ""
run_id = (check_run.get("external_id") or "").split(":")[0]
if not downstream_repo or not run_id:
continue
if (downstream_repo, run_id) in seen:
Expand Down
22 changes: 4 additions & 18 deletions torchci/lib/bot/crcrOncallBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,13 @@ export default function crcrOncallBot(app: Probot): void {
}

// Get the PRs this check run belongs to.
// checkRun.pull_requests is empty for cross-fork PRs (most pytorch
// contributions), so fall back to the commits API to resolve PRs
// from the head SHA — the same strategy used by the merge-blocking path.
let prNumbers: number[] = [];
if (checkRun.pull_requests && checkRun.pull_requests.length > 0) {
prNumbers = checkRun.pull_requests.map((pr) => pr.number);
} else if (checkRun.head_sha) {
try {
const result =
await ctx.octokit.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: checkRun.head_sha,
});
prNumbers = result.data.map((pr: any) => pr.number);
} catch (err) {
ctx.log(
{ err },
`crcrOncall: failed to resolve PRs for commit ${checkRun.head_sha}, skipping`
);
return;
} else if (checkRun.external_id) {
const parts = checkRun.external_id.split(":");
if (parts.length === 2 && parts[1]) {
prNumbers = [parseInt(parts[1], 10)];
}
}

Expand Down
32 changes: 31 additions & 1 deletion torchci/test/crcrOncallBot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,46 @@ L3:
});
});

test("does not comment when no PR is associated", async () => {
test("does not comment when external_id has no PR number", async () => {
// external_id carries bare run_id (no ":" separator) when pr_number
// is unavailable — the bot returns early without any API call.
await probot.receive({
name: "check_run" as any,
payload: checkRunPayload({
pull_requests: [],
external_id: "12345",
}) as any,
id: "8",
});
});

test("posts comment when external_id contains PR number for cross-fork PR", async () => {
const scope = nock("https://api.github.com")
.get(`/repos/${OWNER}/${REPO}/issues/${PR_NUMBER}/comments`)
.reply(200, [])
.post(
`/repos/${OWNER}/${REPO}/issues/${PR_NUMBER}/comments`,
(body: any) => {
expect(body.body).toContain(
"<!-- crcr-oncall:intel/torch-xpu-ops -->"
);
expect(body.body).toContain("@oncall_xpu");
return true;
}
)
.reply(200);

await probot.receive({
name: "check_run" as any,
payload: checkRunPayload({
pull_requests: [], // empty — simulates cross-fork PR
external_id: `12345:${PR_NUMBER}`,
}) as any,
id: "10",
});
handleScope(scope);
});

test("does nothing for unsupported org", async () => {
const payload = checkRunPayload();
payload.repository = {
Expand Down
Loading