diff --git a/aws/lambda/clickhouse-replicator-dynamo/lambda_function.py b/aws/lambda/clickhouse-replicator-dynamo/lambda_function.py
index 792c2da148..37fc0bb755 100644
--- a/aws/lambda/clickhouse-replicator-dynamo/lambda_function.py
+++ b/aws/lambda/clickhouse-replicator-dynamo/lambda_function.py
@@ -33,6 +33,7 @@
"vllm-buildkite-agent-events": "vllm.vllm_buildkite_agents",
"vllm-buildkite-build-events": "vllm.vllm_buildkite_builds",
"vllm-buildkite-job-events": "vllm.vllm_buildkite_jobs",
+ "torchci-oot-workflow-job": "default.oot_workflow_job",
}
diff --git a/torchci/pages/[repoOwner]/[repoName]/pull/[prNumber].tsx b/torchci/pages/[repoOwner]/[repoName]/pull/[prNumber].tsx
index b1830bcd15..79dfcf08ef 100644
--- a/torchci/pages/[repoOwner]/[repoName]/pull/[prNumber].tsx
+++ b/torchci/pages/[repoOwner]/[repoName]/pull/[prNumber].tsx
@@ -3,6 +3,7 @@ import { CommitInfo } from "components/commit/CommitInfo";
import DrCIButton from "components/common/DrCIButton";
import ErrorBoundary from "components/common/ErrorBoundary";
import { useSetTitle } from "components/layout/DynamicTitle";
+import OotPrSection from "components/oot/OotPrSection";
import { fetcher } from "lib/GeneralUtils";
import { PRData } from "lib/types";
import { useRouter } from "next/router";
@@ -122,6 +123,11 @@ function Page() {
/>
)}
+
+ {prNumber && repoOwner === "pytorch" && repoName === "pytorch" && (
+
+ )}
+
);
}
diff --git a/torchci/pages/api/oot/results.ts b/torchci/pages/api/oot/results.ts
new file mode 100644
index 0000000000..8bcfa6f62b
--- /dev/null
+++ b/torchci/pages/api/oot/results.ts
@@ -0,0 +1,67 @@
+import { timingSafeEqual } from "crypto";
+import {
+ ApiError,
+ extractDynamoRecord,
+ validatePayloadSize,
+ writeToDynamo,
+} from "lib/oot/ootUtils";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+export const config = {
+ api: {
+ bodyParser: {
+ sizeLimit: "2mb",
+ },
+ },
+};
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== "POST") {
+ return res.status(405).json({ error: "Method not allowed" });
+ }
+
+ try {
+ // 1. Auth: dedicated X-OOT-Relay-Token header (timing-safe comparison)
+ const expected = process.env.OOT_RELAY_TOKEN;
+ if (!expected) {
+ return res.status(500).json({ error: "Server misconfigured" });
+ }
+ const raw = req.headers["x-oot-relay-token"];
+ if (typeof raw !== "string") {
+ return res.status(401).json({ error: "Unauthorized" });
+ }
+ const a = new Uint8Array(Buffer.from(raw));
+ const b = new Uint8Array(Buffer.from(expected));
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
+ return res.status(401).json({ error: "Unauthorized" });
+ }
+
+ // 2. Payload size cap (safety net — relay should also enforce this)
+ const rawBody =
+ typeof req.body === "string" ? req.body : JSON.stringify(req.body);
+ validatePayloadSize(rawBody);
+
+ // 3. Extract and write to DynamoDB via UpdateItem
+ // Schema validation is done by the relay before forwarding.
+ const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
+ const record = extractDynamoRecord(body);
+ await writeToDynamo(record);
+
+ return res.status(200).json({
+ ok: true,
+ status: record.status,
+ dynamoKey: record.dynamoKey,
+ });
+ } catch (err: any) {
+ if (err instanceof ApiError) {
+ return res.status(err.statusCode).json({ error: err.message });
+ }
+ console.error("OOT results handler error:", err);
+ return res
+ .status(500)
+ .json({ error: "Internal error writing to DynamoDB" });
+ }
+}
diff --git a/torchci/test/ootResults.test.ts b/torchci/test/ootResults.test.ts
new file mode 100644
index 0000000000..968277aaf5
--- /dev/null
+++ b/torchci/test/ootResults.test.ts
@@ -0,0 +1,167 @@
+import { NextApiRequest, NextApiResponse } from "next";
+import * as ootUtils from "../lib/oot/ootUtils";
+import handler from "../pages/api/oot/results";
+
+jest.mock("../lib/oot/ootUtils", () => {
+ const actual = jest.requireActual("../lib/oot/ootUtils");
+ return {
+ ...actual,
+ writeToDynamo: jest.fn().mockResolvedValue(undefined),
+ };
+});
+
+const VALID_TOKEN = "test-relay-token-abc123";
+
+function mockReq(overrides: Partial = {}): NextApiRequest {
+ return {
+ method: "POST",
+ headers: { "x-oot-relay-token": VALID_TOKEN },
+ body: {
+ trusted: {
+ verified_repo: "Ascend/pytorch",
+ downstream_repo_level: "L2",
+ ci_metrics: { queue_time: 5.0, execution_time: null },
+ },
+ untrusted: {
+ callback_payload: {
+ event_type: "workflow_job",
+ delivery_id: "del-001",
+ payload: {
+ pull_request: { number: 100, head: { sha: "abc123" } },
+ repository: { full_name: "pytorch/pytorch" },
+ },
+ workflow: {
+ status: "in_progress",
+ name: "npu-ci",
+ url: "https://github.com/Ascend/pytorch/actions/runs/1",
+ job_name: "build",
+ check_run_id: "9001",
+ run_id: "555",
+ run_attempt: 1,
+ started_at: "2026-05-20T10:00:00Z",
+ },
+ },
+ },
+ },
+ ...overrides,
+ } as unknown as NextApiRequest;
+}
+
+function mockRes(): NextApiResponse & { _status: number; _json: any } {
+ const res: any = {
+ _status: 0,
+ _json: null,
+ status(code: number) {
+ res._status = code;
+ return res;
+ },
+ json(data: any) {
+ res._json = data;
+ return res;
+ },
+ };
+ return res;
+}
+
+describe("POST /api/oot/results", () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ process.env = { ...originalEnv, OOT_RELAY_TOKEN: VALID_TOKEN };
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ process.env = originalEnv;
+ });
+
+ test("rejects non-POST methods with 405", async () => {
+ const res = mockRes();
+ await handler(mockReq({ method: "GET" }), res);
+ expect(res._status).toBe(405);
+ expect(res._json.error).toBe("Method not allowed");
+ });
+
+ test("returns 500 when OOT_RELAY_TOKEN env is not set", async () => {
+ delete process.env.OOT_RELAY_TOKEN;
+ const res = mockRes();
+ await handler(mockReq(), res);
+ expect(res._status).toBe(500);
+ expect(res._json.error).toBe("Server misconfigured");
+ });
+
+ test("returns 401 when token header is missing", async () => {
+ const res = mockRes();
+ await handler(mockReq({ headers: {} }), res);
+ expect(res._status).toBe(401);
+ expect(res._json.error).toBe("Unauthorized");
+ });
+
+ test("returns 401 when token header is wrong", async () => {
+ const res = mockRes();
+ await handler(
+ mockReq({ headers: { "x-oot-relay-token": "wrong-token" } }),
+ res
+ );
+ expect(res._status).toBe(401);
+ expect(res._json.error).toBe("Unauthorized");
+ });
+
+ test("returns 401 when token has different length", async () => {
+ const res = mockRes();
+ await handler(mockReq({ headers: { "x-oot-relay-token": "short" } }), res);
+ expect(res._status).toBe(401);
+ expect(res._json.error).toBe("Unauthorized");
+ });
+
+ test("returns 200 and writes to DynamoDB on valid request", async () => {
+ const res = mockRes();
+ await handler(mockReq(), res);
+ expect(res._status).toBe(200);
+ expect(res._json.ok).toBe(true);
+ expect(res._json.status).toBe("in_progress");
+ expect(res._json.dynamoKey).toContain("Ascend/pytorch");
+ expect(ootUtils.writeToDynamo).toHaveBeenCalledTimes(1);
+ });
+
+ test("returns 400 when required fields are missing", async () => {
+ const body = {
+ trusted: { verified_repo: "test/repo" },
+ untrusted: {
+ callback_payload: {
+ event_type: "workflow_job",
+ delivery_id: "del-002",
+ payload: {},
+ workflow: {
+ status: "in_progress",
+ name: "ci",
+ url: "https://example.com",
+ // job_name intentionally missing
+ },
+ },
+ },
+ };
+ const res = mockRes();
+ await handler(mockReq({ body }), res);
+ expect(res._status).toBe(400);
+ expect(res._json.error).toContain("job_name");
+ });
+
+ test("returns 500 when DynamoDB write fails", async () => {
+ (ootUtils.writeToDynamo as jest.Mock).mockRejectedValueOnce(
+ new Error("DynamoDB connection failed")
+ );
+ const res = mockRes();
+ await handler(mockReq(), res);
+ expect(res._status).toBe(500);
+ expect(res._json.error).toContain("Internal error");
+ });
+
+ test("handles string body by parsing JSON", async () => {
+ const body = JSON.stringify(mockReq().body);
+ const res = mockRes();
+ await handler(mockReq({ body }), res);
+ expect(res._status).toBe(200);
+ expect(res._json.ok).toBe(true);
+ });
+});