ci(sgl-router): add PR test workflow (pre-positioned for feature PR) (#25854)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kangyan-Zhou
2026-05-20 15:20:03 +08:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 8131641bc6
commit a1843524a5
+347
View File
@@ -0,0 +1,347 @@
name: PR Test (sgl-router)
on:
push:
branches: [main]
paths:
- "experimental/sgl-router/**"
- ".github/workflows/pr-test-sgl-router.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
paths:
- "experimental/sgl-router/**"
- ".github/workflows/pr-test-sgl-router.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
workflow_dispatch:
concurrency:
group: sgl-router-${{ github.ref }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
jobs:
sgl-router-lint:
name: tier-1 — lint
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-latest
env:
# Scoped per-job (not workflow-wide) to avoid SMG's documented breakage
# of self-hosted-runner pip installs that compile Rust extensions.
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
# Supply-chain guard: deny.toml allowlists the dynamo git source but
# cannot pin a SHA. Bumps to this constant must be a deliberate PR.
DYNAMO_TOKENIZERS_EXPECTED_REV: "1efdd4dcb901caeae636131321094090d252c8d6"
steps:
- uses: actions/checkout@v4
- name: Verify dynamo-* SHA pin
run: |
set -euo pipefail
revs=$(grep -E '^dynamo-[a-z]+[[:space:]]*=.*\brev[[:space:]]*=' experimental/sgl-router/Cargo.toml \
| sed -E 's/.*rev[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
if [ -z "${revs:-}" ]; then
echo "::error::no dynamo-* git deps with rev= found in experimental/sgl-router/Cargo.toml"
exit 1
fi
count=0
while IFS= read -r r; do
count=$((count + 1))
if [ "$r" != "$DYNAMO_TOKENIZERS_EXPECTED_REV" ]; then
echo "::error::dynamo-* SHA pin mismatch in experimental/sgl-router/Cargo.toml"
echo " expected: $DYNAMO_TOKENIZERS_EXPECTED_REV"
echo " actual: $r"
echo "All revs found:"
echo "$revs" | sed 's/^/ /'
exit 1
fi
done <<< "$revs"
echo "dynamo-* SHA pin OK: $count entries all at $DYNAMO_TOKENIZERS_EXPECTED_REV"
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo check
working-directory: experimental/sgl-router
run: cargo check --all-targets
- name: cargo clippy
working-directory: experimental/sgl-router
run: cargo clippy --all-targets -- -D warnings
- name: cargo fmt
working-directory: experimental/sgl-router
run: |
rustup toolchain install nightly --profile minimal --component rustfmt
cargo +nightly fmt -- --check
# cargo-deny 0.19.6+ required for CVSS 4.0 parsing.
- name: Install cargo-deny
run: cargo install --locked cargo-deny@0.19.6
- name: cargo deny check
working-directory: experimental/sgl-router
run: cargo deny check licenses bans sources advisories
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
sgl-router-build-and-test:
name: tier-2 — build + test
needs: sgl-router-lint
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install OS deps (libssl, pkg-config)
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev pkg-config
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo build (record wall time)
working-directory: experimental/sgl-router
run: |
START=$(date +%s)
cargo build --release --all-targets
END=$(date +%s)
BUILD_SECONDS=$((END - START))
echo "tier_2_build_duration_seconds=${BUILD_SECONDS}" >> "$GITHUB_ENV"
echo "::notice title=Tier-2 build duration::${BUILD_SECONDS}s"
- name: cargo test
working-directory: experimental/sgl-router
# Skip tokenizer_parity here: ubuntu-latest has no HuggingFace cache,
# so the matrix would hard-fail (see the test docstring). The e2e job
# runs it after pytest populates the Qwen3-0.6B tokenizer.json.
run: cargo test --release -- --skip tokenizer_parity
# Regenerate the cross-impl block-hash parity fixture and fail if it
# differs from the committed file. The Python script replicates
# SGLang's `radix_cache.hash_page` algorithm verbatim (no
# `import sglang`); a diff here means EITHER the Rust port's algorithm
# drifted from the script, OR the script drifted from SGLang's actual
# code — in both cases the operator must investigate and re-commit
# the fixture in the same PR as the algorithm change.
#
# `set -euo pipefail` makes a Python-side failure (syntax error,
# missing import) cause this step to fail instead of falling
# through to a no-diff "pass" when no fixture was actually
# regenerated.
- name: kv_events hash parity fixture (drift check)
working-directory: experimental/sgl-router
run: |
set -euo pipefail
python3 tests/scripts/generate_kv_events_hash_parity.py
if ! git diff --exit-code -- tests/fixtures/kv_events_hash_parity.json; then
echo "::error::kv_events hash parity fixture drifted." \
"Re-run experimental/sgl-router/tests/scripts/generate_kv_events_hash_parity.py" \
"and commit the updated tests/fixtures/kv_events_hash_parity.json."
exit 1
fi
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
- name: Summary
if: always()
run: |
echo "## Tier-2 results" >> "$GITHUB_STEP_SUMMARY"
echo "- Build duration: \`${tier_2_build_duration_seconds:-(not recorded)}\` s" >> "$GITHUB_STEP_SUMMARY"
sgl-router-docker-build-test:
name: tier-3 — docker (placeholder)
needs: sgl-router-build-and-test
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Placeholder
run: |
echo "docker-build-test not implemented yet."
exit 0
sgl-router-k8s-integration:
name: tier-3 — k8s integration
needs: sgl-router-build-and-test
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Install kind
run: |
curl -Lo /tmp/kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x /tmp/kind && sudo mv /tmp/kind /usr/local/bin/kind
- name: Install kubectl
run: |
curl -Lo /tmp/kubectl https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl
chmod +x /tmp/kubectl && sudo mv /tmp/kubectl /usr/local/bin/kubectl
- name: Build router image
run: docker build -t sgl-router:e2e -f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.router .
- name: Build fake_worker image
run: |
docker build -t sgl-router-fake-worker:e2e \
-f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.fake_worker \
experimental/sgl-router/tests/e2e/k8s_integration/
- name: Bootstrap kind + deploy
run: bash experimental/sgl-router/tests/e2e/k8s_integration/setup.sh
- name: Set up Python
run: |
python3 -m venv /tmp/e2e-venv
/tmp/e2e-venv/bin/pip install -r experimental/sgl-router/tests/e2e/k8s_integration/requirements.txt
- name: Run E2E
run: /tmp/e2e-venv/bin/pytest experimental/sgl-router/tests/e2e/k8s_integration/ -v --tb=short
- name: Dump router logs on failure
if: failure()
run: kubectl -n sgl-router-test logs deploy/sgl-router --tail=200 || true
sgl-router-e2e:
name: tier-3 — e2e
needs: sgl-router-build-and-test
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: 2-gpu-h100
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
- name: cargo build (release)
working-directory: experimental/sgl-router
run: cargo build --release
# Install SGLang from the local checkout in editable mode (no PyPI
# version pin) — mirrors `pr-test-rust.yml` so the router e2e runs
# against whatever SGLang version is on the branch. Replaces the
# previous `pip install sglang[srt]==X.Y.Z` pin, which forced the
# e2e to test against a stale wheel and would silently miss any
# new ServerArgs field (e.g. `disaggregation_mode` /
# `disaggregation_bootstrap_port`) added in the branch.
- name: Install SGLang dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Install e2e test dependencies
run: |
python3 -m pip install -r experimental/sgl-router/tests/e2e/requirements.txt
- name: Run e2e
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: python3 -m pytest tests/e2e/ -v -s --tb=short
- name: Tokenizer parity matrix (uses HF cache populated by e2e)
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
# Runs after pytest so the Qwen3-0.6B tokenizer.json is cached by the
# SGLang worker. Cells without a cached snapshot are skipped; if no
# cells could be checked, the test hard-fails under SGLANG_IS_IN_CI.
run: cargo test --release --test component tokenizer::parity
sgl-router-finish:
name: finish
needs:
- sgl-router-lint
- sgl-router-build-and-test
- sgl-router-docker-build-test
- sgl-router-k8s-integration
- sgl-router-e2e
# Gate finish on the same label as upstream jobs to avoid false-green on
# unlabeled PRs (skipped needs => success), and fail when any upstream
# job actually failed or was cancelled.
if: |
always() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled') &&
(
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
)
runs-on: ubuntu-latest
steps:
- name: All required checks completed
run: echo "OK"