This repository serves as the reference implementation and test bed for the Cross-Repository CI Relay (CRCR) system. It demonstrates how an out-of-tree (OOT) backend can receive upstream PyTorch events and run its own CI in response.
Use this guide to onboard your downstream repository into the CRCR pipeline.
When a PR is opened or code is pushed to pytorch/pytorch, the pytorch-fdn-cross-repo-ci-relay GitHub App dispatches repository_dispatch events to all approved downstream repositories. Your repository receives these events and can trigger builds, tests, or any workflow in response.
pytorch/pytorch (PR / push)
│
▼
CRCR Relay Bot
(pytorch-fdn-cross-repo-ci-relay)
│
▼
repository_dispatch
│
├──► your-org/your-repo (your workflow runs)
├──► Ascend/pytorch
├──► riseproject-dev/pytorch-ci
└──► pytorch/crcr-test
Each downstream repository is assigned a trust level that determines how deeply it integrates with PyTorch CI:
| Level | Name | Description |
|---|---|---|
| L1 | Onboarding | Events are forwarded to downstream, but upstream receives no feedback. |
| L2 | Observation | Downstream CI results are displayed on the HUD page, but not on PRs. |
| L3 | Stable | Adds a non-blocking check run on PRs when ciflow/oot/<name> label is applied. |
| L4 | Mature | Adds a blocking check run on every PR; reserved for critical accelerators. |
All new repositories start at L1. Promotion to higher levels is based on demonstrated stability and reliability.
Install the pytorch-fdn-cross-repo-ci-relay GitHub App on your downstream repository.
For installation approval, contact @albanD or @atalman.
Open a PR against pytorch/pytorch to add your repository to the allowlist file:
File: .github/allowlist.yml
Add your repository under the appropriate level (new repos start at L1):
L1:
- pytorch/crcr-test
- Ascend/pytorch
- riseproject-dev/pytorch-ci
- your-org/your-repo # ← add your repo hereCreate a GitHub Actions workflow in your repository that listens for repository_dispatch events. The relay sends two event types:
push— Triggered when code is pushed tomainor ciflow tags are created (e.g.,refs/tags/ciflow/trunk/<pr_number>)pull_request— Triggered when a PR is opened, reopened, synchronized, or closed
Create .github/workflows/out-of-tree-ci.yml in your repository:
name: PyTorch Out-of-Tree Dispatch
on:
repository_dispatch:
types:
- pull_request
- push
run-name: >-
Dispatch -
${{
github.event.client_payload.event_type == 'pull_request' &&
format(
'PR #{0} ({1})',
github.event.client_payload.payload.pull_request.number,
github.event.client_payload.payload.action
) ||
github.event.client_payload.payload.ref
}}
concurrency:
group: >-
oot-${{ github.event.client_payload.payload.repository.full_name }}-${{
github.event.client_payload.payload.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
actions: write
id-token: write # required for L2+ callback authentication (OIDC)
jobs:
cancel-workflow:
if: ${{ github.event.client_payload.payload.action == 'closed' }}
runs-on: ubuntu-latest
steps:
- run: echo "PR closed, canceling older runs in the same concurrency group"
build-and-test:
if: ${{ github.event.client_payload.payload.action != 'closed' }} # listen to the specific action types you need (opened, reopened, synchronize)
runs-on: ubuntu-latest
steps:
- name: Checkout your repository
uses: actions/checkout@v4
- name: Checkout PyTorch at dispatched SHA
uses: actions/checkout@v4
with:
repository: pytorch/pytorch
ref: ${{ github.event.client_payload.payload.pull_request.head.sha || github.event.client_payload.payload.after }}
path: pytorch
# Add your build and test steps here
- name: Build and test
run: |
echo "Building against PyTorch SHA: ${{ github.event.client_payload.payload.pull_request.head.sha || github.event.client_payload.payload.after }}"
# your build commands here
- name: Log event to step summary
if: always()
run: |
cat <<'SUMMARY' >> $GITHUB_STEP_SUMMARY
```json
${{ toJson(github.event) }}
```
SUMMARYOnce Steps 1–3 are complete, your workflow will start receiving dispatches. You can verify this by:
- Checking the Actions tab of your repository for
repository_dispatchevents - Looking for runs triggered by
pytorch-fdn-cross-repo-ci-relay[bot] - Inspecting the step summary for the full event payload
The relay wraps the original GitHub event inside client_payload:
{
"event_type": "push",
"client_payload": {
"event_type": "push",
"payload": {
"ref": "refs/tags/ciflow/trunk/184534",
"after": "abc123...",
"deleted": false,
"base_ref": "refs/heads/main",
"repository": {
"full_name": "pytorch/pytorch"
}
}
}
}For pull_request events:
{
"event_type": "pull_request",
"client_payload": {
"event_type": "pull_request",
"payload": {
"action": "opened",
"pull_request": {
"number": 184442,
"title": "My PR title",
"head": {
"sha": "abc123...",
"repo": {
"full_name": "user/pytorch"
}
}
},
"repository": {
"full_name": "pytorch/pytorch"
}
}
}
}| Field | Path | Description |
|---|---|---|
| Event type | github.event.client_payload.event_type |
push or pull_request |
| Upstream repo | github.event.client_payload.payload.repository.full_name |
Always pytorch/pytorch |
| Push ref | github.event.client_payload.payload.ref |
Git ref (e.g., refs/tags/ciflow/trunk/12345) |
| Push SHA | github.event.client_payload.payload.after |
Commit SHA for push events |
| PR number | github.event.client_payload.payload.pull_request.number |
PR number for pull_request events |
| PR action | github.event.client_payload.payload.action |
opened, reopened, synchronize, closed |
| PR head SHA | github.event.client_payload.payload.pull_request.head.sha |
Head commit SHA of the PR |
| PR head repo | github.event.client_payload.payload.pull_request.head.repo.full_name |
Fork repo (if applicable) |
The CRCR relay Lambda enforces server-side rate limiting to protect backend infrastructure (Redis, ClickHouse) from callback bursts. The current limit is 60 requests per minute across all downstream repositories. When multiple downstream repositories send callbacks concurrently — for example, during a large upstream PR push that fans out to many repos — the relay may respond with HTTP 429 (Too Many Requests).
The default callback action (cross-repo-ci-relay-callback) uses curl --fail-with-body, which treats any non-2xx response as a hard failure. A single transient 429 will fail the callback step and mark the job as errored, even though the rate limit is temporary.
As discussed in #8, the preferred mitigation is client-side retry with exponential backoff rather than raising the server-side threshold. The server-side limiter is a protection that should be preserved — no fixed limit can guarantee a concurrent burst never crosses it.
Why retry over raising limits (per @can-gaa-hou):
429 is inherently transient, so the right place to handle it is the client. Adding retry with backoff (ideally honoring a
Retry-Afterheader from the relay) makes the callbacks resilient to bursts without weakening the server-side guard.
- Add jitter to workflow steps — If your workflow has multiple jobs sending callbacks, stagger them with random delays (
sleep $((RANDOM % 10))) to avoid synchronized bursts. - Anticipate 429 responses — Design your callback steps to tolerate transient failures. A retry wrapper around the
curlcall can prevent unnecessary job failures. - Honor
Retry-Afterheaders — If the relay includes aRetry-Afterheader in 429 responses, wait at least that long before retrying. - Limit concurrent callback jobs — Use GitHub Actions concurrency groups to avoid overwhelming the relay with parallel callbacks from the same repository.
- Client-side retry with backoff in the
cross-repo-ci-relay-callbackaction (pytorch/test-infra) Retry-Afterheader support in the relay Lambda
- RFC-0050: Cross-Repository CI Relay — Full CRCR design specification
- Allowlist — Current list of approved downstream repositories
- CRCR Relay GitHub App — The relay bot
- Tracking Issue — CRCR tracking issue
For questions about onboarding, installation approval, or promotion to higher trust levels:
- @albanD — PyTorch Core Maintainer
- @atalman — PyTorch Dev Infra
- @groenenboomj
- @jewelkm89
- @subinz1
- @fffrog