Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Fix websocket poison connection and leaks on failed requests #725

Fix websocket poison connection and leaks on failed requests

Fix websocket poison connection and leaks on failed requests #725

Workflow file for this run

name: Bittensor SDK Test
permissions:
pull-requests: write
contents: read
concurrency:
group: e2e-sdk-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
branches:
- staging
- master
types: [ opened, synchronize, reopened, labeled, unlabeled ]
workflow_dispatch:
inputs:
bittensor_branch:
description: "Optional: Bittensor branch to use instead of staging"
required: false
default: "staging"
env:
CARGO_TERM_COLOR: always
VERBOSE: ${{ github.event.inputs.verbose }}
BITTENSOR_BRANCH: ${{ github.event.inputs.bittensor_branch || 'staging' }}
jobs:
apply-label-to-new-pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.draft == false }}
outputs:
should_continue_sdk: ${{ steps.check.outputs.should_continue_sdk }}
steps:
- name: Check
id: check
run: |
ACTION="${{ github.event.action }}"
if [[ "$ACTION" == "opened" || "$ACTION" == "reopened" ]]; then
echo "should_continue_sdk=true" >> $GITHUB_OUTPUT
else
echo "should_continue_sdk=false" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Add label
if: steps.check.outputs.should_continue_sdk == 'true'
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: run-bittensor-sdk-tests
check-labels:
needs: apply-label-to-new-pr
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
outputs:
run-sdk-tests: ${{ steps.check-manual.outputs.run-sdk-tests || steps.get-labels-pr.outputs.run-sdk-tests }}
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Skip label check for manual runs
id: check-manual
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
echo "Manual workflow dispatch detected, skipping PR label check."
echo "run-sdk-tests=true" >> $GITHUB_OUTPUT
- name: Get labels from PR
id: get-labels-pr
if: ${{ github.event_name == 'pull_request' }}
run: |
sleep 5
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name')
echo "Current labels: $LABELS"
if echo "$LABELS" | grep -q "run-bittensor-sdk-tests"; then
echo "run-sdk-tests=true" >> $GITHUB_ENV
echo "run-sdk-tests=true" >> $GITHUB_OUTPUT
else
echo "run-sdk-tests=false" >> $GITHUB_ENV
echo "run-sdk-tests=false" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
find-e2e-tests:
needs: check-labels
if: ${{ !cancelled() && needs.check-labels.outputs.run-sdk-tests == 'true' }}
runs-on: ubuntu-latest
outputs:
test-cases: ${{ steps.get-tests.outputs.test-cases }}
steps:
- name: Research preparation
working-directory: ${{ github.workspace }}
run: git clone https://github.com/latent-to/bittensor.git
- name: Verify and checkout Bittensor branch
working-directory: ${{ github.workspace }}/bittensor
run: |
if ! git fetch origin $BITTENSOR_BRANCH; then
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in latent-to/bittensor."
exit 1
fi
git checkout FETCH_HEAD
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
- name: Install dependencies
run: sudo apt-get install -y jq
- name: Find e2e test cases
id: get-tests
run: |
# Emit one matrix entry per individual test function (as a `path::test_name`
# node id) rather than per file, so each test gets its own concurrent job.
# The e2e suite has no test classes, so grepping top-level `def test_*` is
# sufficient and keeps this job dependency-free (no pytest collection needed).
# Node ids are relative to the bittensor repo root so they resolve inside the
# prebuilt image (where bittensor lives at /opt/bittensor), not the workspace.
cd "${{ github.workspace }}/bittensor"
test_cases=$(
while IFS= read -r f; do
grep -oE '^(async[[:space:]]+)?def[[:space:]]+test_[A-Za-z0-9_]+' "$f" \
| grep -oE 'test_[A-Za-z0-9_]+' \
| while IFS= read -r name; do printf '%s::%s\n' "$f" "$name"; done
done < <(find tests/e2e_tests -name "test*.py") \
| jq -R -s -c 'split("\n") | map(select(. != ""))'
)
echo "Collected $(echo "$test_cases" | jq 'length') test cases"
echo "test-cases=$test_cases" >> $GITHUB_OUTPUT
shell: bash
pull-docker-image:
needs:
- check-labels
- find-e2e-tests
runs-on: ubuntu-latest
if: ${{ !cancelled() && needs.check-labels.outputs.run-sdk-tests == 'true' }}
steps:
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
- name: Pull Docker Image
run: docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready
- name: Save Docker Image to Cache
run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready
- name: Upload Docker Image as Artifact
uses: actions/upload-artifact@v7
with:
name: subtensor-localnet
path: subtensor-localnet.tar
build-e2e-image:
# Build the test environment (system deps + bittensor[dev] + this PR's
# async-substrate-interface) into a single image ONCE per run, so the e2e
# matrix jobs below just pull it and run pytest — no per-test setup.
needs:
- check-labels
if: ${{ !cancelled() && needs.check-labels.outputs.run-sdk-tests == 'true' }}
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- name: Clone Bittensor SDK repo
run: git clone https://github.com/latent-to/bittensor.git
- name: Checkout Bittensor branch
working-directory: ${{ github.workspace }}/bittensor
run: |
if ! git fetch origin $BITTENSOR_BRANCH; then
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in latent-to/bittensor."
exit 1
fi
git checkout FETCH_HEAD
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
- name: Clone async-substrate-interface repo
run: git clone https://github.com/latent-to/async-substrate-interface.git
- name: Checkout PR branch in async-substrate-interface
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
BRANCH="${{ github.event.pull_request.head.ref || github.ref_name }}"
REPO_URL="${{ github.event.pull_request.head.repo.clone_url || 'https://github.com/latent-to/async-substrate-interface.git' }}"
git fetch $REPO_URL $BRANCH
git checkout FETCH_HEAD
echo "Current SHA: $(git rev-parse HEAD)"
- name: Write Dockerfile
run: |
# Exclude the (large) .git dirs from the build context.
printf '**/.git\n' > .dockerignore
cat > Dockerfile <<'DOCKERFILE'
FROM python:3.13-slim
# System deps: build toolchain for any source wheels, curl for the docker CLI
# download below, and git for any VCS-based installs.
RUN apt-get update && apt-get install -y --no-install-recommends \
clang curl libssl-dev llvm libudev-dev protobuf-compiler git \
&& rm -rf /var/lib/apt/lists/*
# Static Docker CLI (client only). The e2e fixtures shell out to `docker` to
# launch the localnet via the mounted host socket; installing the static binary
# to /usr/local/bin guarantees it is on PATH regardless of the base distro's
# packaging (the `docker.io` apt package did not reliably provide it). The final
# `docker --version` fails the build early if the CLI is ever missing.
ARG DOCKER_CLI_VERSION=27.5.1
RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz" \
| tar -xz -C /usr/local/bin --strip-components=1 docker/docker \
&& docker --version
RUN pip install --no-cache-dir uv
COPY bittensor /opt/bittensor
COPY async-substrate-interface /opt/async-substrate-interface
# Install the SDK with dev extras, then overlay this PR's build of
# async-substrate-interface so the tests exercise the PR code.
RUN uv pip install --system '/opt/bittensor[dev]' \
&& (uv pip uninstall --system async-substrate-interface || true) \
&& uv pip install --system /opt/async-substrate-interface
# pytest is invoked from here so the relative `tests/e2e_tests/...` node ids resolve.
WORKDIR /opt/bittensor
DOCKERFILE
- name: Set image ref
id: meta
# A plain local tag (no registry): the image is shipped to the matrix jobs as a
# `docker save` tarball artifact, not via a registry push.
run: echo "image=async-substrate-interface-e2e:${{ github.run_id }}-${{ github.run_attempt }}" >> $GITHUB_OUTPUT
- name: Build image
run: docker build -t "${{ steps.meta.outputs.image }}" .
- name: Save image to tarball
# Distribute via a GitHub artifact (fast internal transport, same as the localnet
# image) instead of GHCR, which avoids slow registry pulls across all matrix jobs,
# registry rate-limits, and the fork-PR push-permission problem.
run: docker save -o e2e-image.tar "${{ steps.meta.outputs.image }}"
- name: Upload image as artifact
uses: actions/upload-artifact@v7
with:
name: e2e-image
path: e2e-image.tar
run-e2e-tests:
needs:
- check-labels
- find-e2e-tests
- pull-docker-image
- build-e2e-image
# Only fan out the matrix if every input-producing dependency succeeded — otherwise
# all 139 jobs would spawn only to fail (missing image/localnet artifact, or no
# test list). !cancelled() alone would let them run on upstream failure.
if: >-
${{ !cancelled()
&& needs.check-labels.outputs.run-sdk-tests == 'true'
&& needs.find-e2e-tests.result == 'success'
&& needs.pull-docker-image.result == 'success'
&& needs.build-e2e-image.result == 'success' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 64
matrix:
test-case: ${{ fromJson(needs.find-e2e-tests.outputs.test-cases) }}
timeout-minutes: 60
name: "e2e tests: ${{ matrix.test-case }}"
steps:
- name: Download prebuilt test image
uses: actions/download-artifact@v8
with:
name: e2e-image
- name: Load prebuilt test image
run: docker load -i e2e-image.tar
- name: Download Cached Docker Image
uses: actions/download-artifact@v8
with:
name: subtensor-localnet
- name: Load Docker Image
run: docker load -i subtensor-localnet.tar
# The job stays on the bare runner (no `container:`), so we can launch the test
# container ourselves with `--network host` — GitHub injects its own network into
# job containers, which conflicts with host mode. Host networking + the mounted
# docker socket let the e2e fixtures start the localnet (published on the host's
# :9944) and reach it at ws://localhost:9944. All deps are baked into the image,
# so there is no per-test apt/clone/install.
- name: Run e2e tests
# --reruns retries a flaky test (e.g. when the localnet is listening but its RPC
# isn't ready yet, surfacing as a websocket InvalidMessage during connect). The
# whole test reruns, re-spinning the localnet fixture, so by the retry the node
# is ready. pytest-rerunfailures ships with bittensor[dev].
run: |
docker run --rm \
--network host \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /opt/bittensor \
-e SKIP_PULL=1 \
"${{ needs.build-e2e-image.outputs.image }}" \
pytest "${{ matrix.test-case }}" -s --reruns 2 --reruns-delay 5
run-integration-and-unit-test:
needs:
- check-labels
if: ${{ !cancelled() && needs.check-labels.outputs.run-sdk-tests == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Check-out repository
uses: actions/checkout@v6
- name: Install system dependencies
run: |
sudo apt-get update &&
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
- name: Clone Bittensor SDK repo
run: git clone https://github.com/latent-to/bittensor.git
- name: Checkout Bittensor branch
working-directory: ${{ github.workspace }}/bittensor
run: |
if ! git fetch origin $BITTENSOR_BRANCH; then
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in latent-to/bittensor."
exit 1
fi
git checkout FETCH_HEAD
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
- name: Install Bittensor SDK dependencies
working-directory: ${{ github.workspace }}/bittensor
run: uv pip install --system '.[dev]' torch
- name: Clone async-substrate-interface repo
run: git clone https://github.com/latent-to/async-substrate-interface.git
- name: Checkout PR branch in async-substrate-interface
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
git fetch ${{ github.event.pull_request.head.repo.clone_url }} ${{ github.event.pull_request.head.ref }}
git checkout FETCH_HEAD
echo "Current SHA: $(git rev-parse HEAD)"
- name: Install async-substrate-interface with dev dependencies
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
uv pip uninstall --system async-substrate-interface || true
uv pip install --system '.[dev]'
- name: Run SDK integration tests
run: pytest ${{ github.workspace }}/bittensor/tests/integration_tests
- name: Run Bittensor SDK unit tests
run: pytest ${{ github.workspace }}/bittensor/tests/unit_tests