[CI] Report torch-nightly results to PyTorch CRCR (#51830)

Signed-off-by: Andrey Talman <atalman@fb.com>
This commit is contained in:
Andrey Talman
2026-08-24 18:00:13 -07:00
committed by GitHub
parent 7ca336929c
commit e3dde1ee9b
3 changed files with 239 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# Report this build's per-job results to the PyTorch Cross-Repo CI Relay (CRCR).
#
# Only runs in the torch-nightly lane on main. CRCR's nightly path is a
# self-report: unlike PR-triggered callbacks there is no upstream dispatch to
# correlate with, so the relay accepts a single "completed" callback per job and
# forwards it to HUD (hud.pytorch.org/crcr).
#
# Reporting is best-effort. A relay outage, an expired mapping or a missing
# token must never fail the nightly build, so every failure path here exits 0
# after logging. The step is also marked soft_fail in the pipeline.
set -uo pipefail
# --- Gating -----------------------------------------------------------------
# Matches the image_build.sh convention: the lane is selected inside the script
# rather than in pipeline YAML.
if [[ "${TORCH_NIGHTLY:-0}" != "1" ]]; then
echo "TORCH_NIGHTLY != 1 -- not the nightly lane, nothing to report"
exit 0
fi
# Defence in depth. The relay decides what a Buildkite pipeline may claim via
# ci_providers.yml, and a pipeline that builds fork PRs should constrain
# build_branch there. Refusing to even mint a token off main means a fork PR
# cannot report as vllm-project/vllm even if that mapping is ever relaxed.
if [[ "${BUILDKITE_BRANCH:-}" != "main" ]]; then
echo "branch '${BUILDKITE_BRANCH:-}' is not main -- refusing to report"
exit 0
fi
CALLBACK_URL="${CRCR_CALLBACK_URL:-}"
if [[ -z "${CALLBACK_URL}" ]]; then
echo "CRCR_CALLBACK_URL unset -- skipping report"
exit 0
fi
# read_builds only. Needed because a job cannot see its siblings' outcomes:
# the agent exposes only its own step, so the job list comes from the REST API.
BK_TOKEN="${BUILDKITE_API_TOKEN:-}"
if [[ -z "${BK_TOKEN}" ]]; then
echo "BUILDKITE_API_TOKEN unset -- skipping report"
exit 0
fi
AUDIENCE="pytorch-cross-repo-ci-relay"
OIDC_TOKEN="$(buildkite-agent oidc request-token --audience "${AUDIENCE}" 2>/dev/null)"
if [[ -z "${OIDC_TOKEN}" ]]; then
echo "could not mint a Buildkite OIDC token -- skipping report"
exit 0
fi
BUILD_JSON="$(mktemp)"
trap 'rm -f "${BUILD_JSON}"' EXIT
http_code="$(curl -sS -w '%{http_code}' -o "${BUILD_JSON}" \
-H "Authorization: Bearer ${BK_TOKEN}" \
"https://api.buildkite.com/v2/organizations/${BUILDKITE_ORGANIZATION_SLUG}/pipelines/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_NUMBER}")"
if [[ "${http_code}" != "200" ]]; then
echo "buildkite API returned ${http_code} -- skipping report"
exit 0
fi
# One callback per job, matching HUD's per-job crcr_workflow_job schema.
# delivery_id is synthetic: the nightly path has no upstream dispatch to borrow
# one from, so build+job is used as the idempotency key.
python3 .buildkite/scripts/crcr_report.py \
--build-json "${BUILD_JSON}" \
--callback-url "${CALLBACK_URL}" \
--oidc-token "${OIDC_TOKEN}" \
|| echo "crcr report failed -- continuing (reporting is best-effort)"
exit 0
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Post one CRCR nightly callback per Buildkite job.
Separated from crcr-report.sh because the payload needs real JSON encoding: job
labels routinely contain quotes, colons and emoji, and hand-building JSON in
shell corrupts them silently.
CRCR's nightly contract (aws/lambda/cross_repo_ci_relay/callback/callback_handler.py):
{"delivery_id": str,
"event_type": "nightly",
"workflow": {"status": "completed", # nightly rejects any other status
"conclusion": str,
"run_id": int,
"run_attempt": int,
"name": str,
"job_name": str}}
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.request
# Buildkite job states -> the conclusion vocabulary HUD already stores for
# GitHub Actions callbacks, so nightly rows aggregate with the rest.
_CONCLUSION = {
"passed": "success",
"failed": "failure",
"broken": "failure",
"canceled": "cancelled",
"canceling": "cancelled",
"timed_out": "timed_out",
"timing_out": "timed_out",
"skipped": "skipped",
"not_run": "skipped",
}
def post(url: str, token: str, body: dict, timeout: int = 30) -> tuple[int, str]:
req = urllib.request.Request(
url,
data=json.dumps(body).encode(),
method="POST",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", "replace")[:200]
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", "replace")[:200]
except Exception as exc: # network, DNS, TLS
return 0, f"{type(exc).__name__}: {exc}"
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--build-json", required=True)
p.add_argument("--callback-url", required=True)
p.add_argument("--oidc-token", required=True)
p.add_argument("--dry-run", action="store_true")
args = p.parse_args()
with open(args.build_json) as f:
build = json.load(f)
build_number = build.get("number")
commit = build.get("commit", "")
# Only script jobs are real work. Buildkite also returns waiter/trigger/
# manual pseudo-jobs, which would otherwise show up in HUD as phantom
# zero-duration entries.
jobs = [
j
for j in build.get("jobs") or []
if j.get("type") == "script" and j.get("state") in _CONCLUSION
]
if not jobs:
print("no terminal script jobs in this build; nothing to report")
return 0
ok = failed = 0
for job in jobs:
name = job.get("name") or job.get("command") or "<unnamed>"
body = {
# Stable per (build, job) so a re-run of this reporting step is
# idempotent from HUD's point of view rather than double-counting.
"delivery_id": f"buildkite-{build_number}-{job['id']}",
"event_type": "nightly",
"pytorch_head_sha": commit,
"workflow": {
"status": "completed",
"conclusion": _CONCLUSION[job["state"]],
"run_id": int(build_number),
"run_attempt": 1,
"name": build.get("pipeline", {}).get("slug", "ci"),
"job_name": name,
"url": job.get("web_url", ""),
"started_at": job.get("started_at"),
"completed_at": job.get("finished_at"),
},
}
if args.dry_run:
print(f"[dry-run] {_CONCLUSION[job['state']]:<10} {name[:70]}")
ok += 1
continue
status, detail = post(args.callback_url, args.oidc_token, body)
if 200 <= status < 300:
ok += 1
else:
failed += 1
# Printed once per job on purpose: a 403 means the pipeline mapping
# is missing, and a 200 {"status":"ignored"} means the repo is not
# allowlisted -- two failures that otherwise look identical.
print(f" {status} for {name[:60]}: {detail}", file=sys.stderr)
print(f"reported {ok} job(s), {failed} failed")
return 0
if __name__ == "__main__":
sys.exit(main())
+37
View File
@@ -0,0 +1,37 @@
group: CRCR Report
# Runs only in the torch-nightly lane; the script exits 0 immediately otherwise,
# following the same in-script gating as image_build.sh.
#
# ORDERING (needs confirmation against the pipeline generator): this must be the
# last step to run, because it reads final job states from the Buildkite REST
# API and silently skips any job still in a non-terminal state. On build 83159
# four jobs were still `scheduled` when the build was already 320 jobs deep, so
# running early under-reports rather than erroring.
#
# depends_on across every group is brittle to maintain. If the generator emits a
# `wait` between groups, placing this group last is enough. If not, the more
# robust option is an agent pre-exit hook or a follow-up scheduled step, since
# `CONTINUE_ON_FAILURE=1` means failures must not stop the report either way.
steps:
- label: ":satellite: Report nightly results to CRCR"
key: crcr-report
# Belt and braces with the script's own TORCH_NIGHTLY check: this keeps the
# step out of non-nightly builds entirely, while the script gate still holds
# if the generator ever drops this key.
if: build.env("TORCH_NIGHTLY") == "1"
# Reporting is observability, never a gate: a relay outage, an expired
# pipeline mapping or a missing secret must not turn the nightly red.
soft_fail: true
allow_dependency_failure: true
timeout_in_minutes: 15
# Reporting is a couple of REST calls and a curl: no GPU needed, so keep it
# off the accelerator agents the test groups queue for.
device: cpu-small
# No source_file_dependencies: this step is lane-gated, not change-gated,
# so it must not be skipped by the file-based step selection.
commands:
- bash .buildkite/scripts/crcr-report.sh
# CRCR_CALLBACK_URL and BUILDKITE_API_TOKEN are deliberately not declared
# here. Buildkite interpolates a step-level env block at pipeline-upload
# time, so declaring them would substitute empty strings and shadow the
# values the agent already has from the pipeline/schedule environment.