diff --git a/.github/actions/try-runtime/action.yml b/.github/actions/try-runtime/action.yml deleted file mode 100644 index ce02c4d3f8..0000000000 --- a/.github/actions/try-runtime/action.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: "Subtensor Try Runtime" -description: "Check Subtensor runtime migrations using try-runtime-cli" -author: "Rao Foundation" -branding: - icon: "shield" - color: "green" - -inputs: - runtime-package: - description: "The runtime package name" - required: true - node-uri: - description: "URI of a node to scrape state" - required: true - checks: - description: "Types of checks to run." - default: "all" - blocktime: - description: "Block time in milliseconds to use for try-runtime checks." - default: "12000" - extra-args: - description: "Extra args to pass to the on-runtime-upgrade subcommand." - default: "" - cache-on-failure: - description: "Whether to save the Rust cache even if the job fails" - default: "false" - -runs: - using: "composite" - steps: - - name: Download try-runtime-cli v0.10.1 - run: | - TRY_RUNTIME_BIN="$RUNNER_TEMP/try-runtime" - curl --fail --silent --show-error --location \ - "https://github.com/paritytech/try-runtime-cli/releases/download/v0.10.1/try-runtime-x86_64-unknown-linux-musl" \ - --output "$TRY_RUNTIME_BIN" - chmod +x "$TRY_RUNTIME_BIN" - echo "TRY_RUNTIME_BIN=$TRY_RUNTIME_BIN" >> "$GITHUB_ENV" - shell: bash - - - name: Install Protoc - uses: arduino/setup-protoc@v1.3.0 - with: - version: "3.6.1" - repo-token: ${{ github.token }} - - - name: Add wasm32-unknown-unknown target - run: rustup target add wasm32-unknown-unknown - shell: bash - - - name: Add rust-src component - run: rustup component add rust-src - shell: bash - - - name: Fetch cache - uses: Swatinem/rust-cache@v2.7.0 - with: - shared-key: try-runtime - cache-on-failure: ${{ inputs.cache-on-failure }} - - - name: Build ${{ inputs.runtime-package }} - run: cargo build --profile production -p "$RUNTIME_PACKAGE" --features try-runtime -q --locked - shell: bash - env: - RUNTIME_PACKAGE: ${{ inputs.runtime-package }} - - - name: Check migrations - run: | - RUNTIME_BLOB_NAME="${RUNTIME_PACKAGE//-/_}.compact.compressed.wasm" - RUNTIME_BLOB_PATH="./target/production/wbuild/$RUNTIME_PACKAGE/$RUNTIME_BLOB_NAME" - export RUST_LOG=remote-ext=debug,runtime=debug - - command=( - "$TRY_RUNTIME_BIN" - --runtime "$RUNTIME_BLOB_PATH" - on-runtime-upgrade - "--checks=$CHECKS" - --blocktime - "$BLOCKTIME" - ) - - if [[ -n "$EXTRA_ARGS" ]]; then - read -r -a extra_args <<< "$EXTRA_ARGS" - command+=("${extra_args[@]}") - fi - - command+=(live --uri "$NODE_URI") - - echo "Running command:" - printf '%q ' "${command[@]}" - printf '\n' - "${command[@]}" - shell: bash - env: - RUNTIME_PACKAGE: ${{ inputs.runtime-package }} - NODE_URI: ${{ inputs.node-uri }} - CHECKS: ${{ inputs.checks }} - BLOCKTIME: ${{ inputs.blocktime }} - EXTRA_ARGS: ${{ inputs.extra-args }} diff --git a/.github/scripts/check-skip-label.sh b/.github/scripts/check-skip-label.sh deleted file mode 100755 index 32820fec55..0000000000 --- a/.github/scripts/check-skip-label.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Check if the "skip-validate-benchmarks" label is present on a PR. -# Usage: check-skip-label.sh -# Always exits 0. Writes skip=true to $GITHUB_OUTPUT when the label is -# found so the consuming job can skip its benchmark steps. - -set -euo pipefail - -PR_NUMBER="${1:-}" -[[ -z "$PR_NUMBER" ]] && exit 0 - -REPO="${GITHUB_REPOSITORY:-}" -[[ -z "$REPO" ]] && exit 0 - -labels=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name' 2>/dev/null || true) - -if echo "$labels" | grep -q "skip-validate-benchmarks"; then - echo "skip-validate-benchmarks label found — skipping benchmark validation." - echo "skip=true" >> "$GITHUB_OUTPUT" -fi diff --git a/.github/workflows/check-bittensor-e2e-tests.yml b/.github/workflows/check-bittensor-e2e-tests.yml index e061bd200f..8e872fea53 100644 --- a/.github/workflows/check-bittensor-e2e-tests.yml +++ b/.github/workflows/check-bittensor-e2e-tests.yml @@ -8,10 +8,34 @@ concurrency: group: e2e-cli-${{ github.ref }} cancel-in-progress: true +# Path-filtered: none of this workflow's checks are required for merge, so a +# workflow-level `paths:` filter is safe. If any check here is ever made +# required, this filter MUST move to job-level `if:` gating (a required check +# whose workflow never triggers is stuck "Expected" and blocks the merge). on: pull_request: branches: ["*"] types: [opened, synchronize, reopened, labeled, unlabeled] + paths: + # Anything that changes the node/runtime the e2e suites run against. + - "common/**" + - "node/**" + - "pallets/**" + - "precompiles/**" + - "primitives/**" + - "runtime/**" + - "support/**" + - "chain-extensions/**" + - "src/**" + - "vendor/**" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + - "rust-toolchain.toml" + - "Dockerfile-localnet" + # The SDK/CLI code under test. + - "sdk/**" + - ".github/workflows/check-bittensor-e2e-tests.yml" workflow_dispatch: inputs: diff --git a/.github/workflows/check-clone-upgrade.yml b/.github/workflows/check-clone-upgrade.yml deleted file mode 100644 index 279bd68836..0000000000 --- a/.github/workflows/check-clone-upgrade.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: Clone Upgrade Check - -# For every PR: build the proposed runtime, sudo-upgrade a local clone of -# mainnet with it, then run the clone regression suite and the Python SDK -# suite against the upgraded chain. The docs/website build validates generated -# docs even when clone-upgrade is skipped via label. - -on: - pull_request: - types: [labeled, unlabeled, synchronize, opened] - -concurrency: - group: check-clone-upgrade-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - WS_ENDPOINT: ws://127.0.0.1:9944 - -jobs: - trusted-pr: - name: Trusted PR source (non-fork) - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-latest - steps: - - run: echo "Non-fork PR; self-hosted runners may execute checkout." - - clone-upgrade: - name: Sudo-upgrade mainnet clone and test - needs: trusted-pr - runs-on: [self-hosted, fireactions-heavy] - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} - timeout-minutes: 180 - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/rust-setup - with: - cache-key: clone-upgrade - - - name: Build node and runtime - run: cargo build --release -p node-subtensor - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Create mainnet clone - run: ./clones/scripts/clone-mainnet.sh - - - name: Start local clone - run: | - nohup ./clones/scripts/start-local-clone.sh > clone-node.log 2>&1 & - # Genesis init from the ~2 GB mainnet-clone chainspec takes several - # minutes before the RPC server comes up, so allow up to 15 minutes. - for i in $(seq 1 450); do - if curl -sf -H "Content-Type: application/json" \ - -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ - http://127.0.0.1:9944 > /dev/null; then - echo "Clone node is up." - exit 0 - fi - sleep 2 - done - echo "Clone node failed to start:" - cat clone-node.log - exit 1 - - - name: Sudo-upgrade clone with proposed runtime - working-directory: clones/js-tests - run: | - npm ci - npm run runtime:update:alice - - - name: Run clone smoke tests - working-directory: clones/js-tests - run: npm test - - - name: Run clone regression tests - working-directory: clones/js-tests - run: npm run test:clone-regressions - - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH - - - name: SDK offline checks (same as just check) - working-directory: sdk/python - run: | - uv sync --locked --all-extras --dev - uv run ruff check . - uv run ruff format --check . - uv run ty check --exit-zero-on-warning bittensor - uv run pytest - uv run python -m codegen.check --coverage - uv run python -m codegen.check --names - - - name: Metadata drift gate (committed _generated vs upgraded clone) - working-directory: sdk/python - run: uv run python -m codegen.check --drift ${{ env.WS_ENDPOINT }} - - - name: Docs drift gate (generated tx/query/errors pages) - working-directory: sdk/python - run: uv run python ../../website/apps/bittensor-website/scripts/generate.py --check - - - name: SDK e2e tests against upgraded clone - working-directory: sdk/python - env: - E2E_ENDPOINT: ws://127.0.0.1:9944 - run: uv run pytest -m e2e - - - name: Dump clone node and harness logs - if: failure() - run: | - echo "===== clone-node.log (last 200 lines) =====" - tail -n 200 clone-node.log 2>/dev/null || echo "(missing)" - for f in clones/js-tests/temp/*.log; do - [ -f "$f" ] || continue - echo "===== $f =====" - cat "$f" - done - - - name: Stop local clone - if: always() - run: ./clones/scripts/stop-local-clone.sh - - docs-website: - name: Docs and website build - needs: trusted-pr - runs-on: [self-hosted, fireactions-heavy] - steps: - - uses: actions/checkout@v4 - - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH - - - name: Docs drift gate (generated tx/query/errors pages) - working-directory: sdk/python - run: | - uv sync --locked --all-extras --dev - uv run python ../../website/apps/bittensor-website/scripts/generate.py --check - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable corepack (yarn 4) - run: corepack enable - - - name: Build docs site - working-directory: website - run: | - yarn install --immutable - yarn turbo run build --filter=@raofoundation/bittensor-website diff --git a/.github/workflows/check-docker.yml b/.github/workflows/check-docker.yml index 49cff89b58..3685801c77 100644 --- a/.github/workflows/check-docker.yml +++ b/.github/workflows/check-docker.yml @@ -1,7 +1,31 @@ name: Docker Build Check +# Path-filtered: `docker build` is not a required merge check, so a +# workflow-level `paths:` filter is safe. If it is ever made required, this +# filter MUST move to job-level `if:` gating (a required check whose workflow +# never triggers is stuck "Expected" and blocks the merge). on: pull_request: + paths: + # Anything the Dockerfile's cargo build consumes. + - "common/**" + - "node/**" + - "pallets/**" + - "precompiles/**" + - "primitives/**" + - "runtime/**" + - "sdk/bittensor-core/**" + - "sdk/bittensor-core-py/**" + - "support/**" + - "chain-extensions/**" + - "src/**" + - "vendor/**" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + - "rust-toolchain.toml" + - "Dockerfile" + - ".github/workflows/check-docker.yml" concurrency: group: check-docker-${{ github.ref }} diff --git a/.github/workflows/check-rust.yml b/.github/workflows/check-rust.yml index da9d3cfb88..aca8a95efa 100644 --- a/.github/workflows/check-rust.yml +++ b/.github/workflows/check-rust.yml @@ -25,9 +25,50 @@ jobs: steps: - run: echo "Non-fork PR; self-hosted runners may execute checkout." + # Every check in this workflow is a required merge check, so the workflow + # itself must always trigger (a required check whose workflow never runs is + # stuck "Expected" and blocks the merge). Instead, jobs skip themselves when + # the PR touches no Rust-relevant paths; a `skipped` conclusion satisfies + # branch protection. No checkout: for pull_request events the filter reads + # the changed-file list from the API, so no untrusted code is executed. + changes: + name: detect rust changes + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + rust: ${{ steps.filter.outputs.rust }} + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + rust: + - 'common/**' + - 'node/**' + - 'pallets/**' + - 'precompiles/**' + - 'primitives/**' + - 'runtime/**' + - 'sdk/bittensor-core/**' + - 'sdk/bittensor-core-py/**' + - 'support/**' + - 'chain-extensions/**' + - 'src/**' + - 'vendor/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build.rs' + - 'rust-toolchain.toml' + - 'zepter.yaml' + - '.github/workflows/check-rust.yml' + - '.github/actions/rust-setup/**' + fmt: name: cargo fmt - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-light] steps: - uses: actions/checkout@v4 @@ -47,7 +88,8 @@ jobs: clippy: name: cargo clippy (${{ matrix.features.name }}) - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-light] env: SKIP_WASM_BUILD: 1 @@ -70,7 +112,8 @@ jobs: warnings: name: no cargo check warnings - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-light] env: RUSTFLAGS: -D warnings @@ -89,7 +132,8 @@ jobs: test: name: cargo test - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-heavy] env: SKIP_WASM_BUILD: 1 @@ -103,7 +147,8 @@ jobs: fix: name: cargo fix leaves no diff - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-light] env: SKIP_WASM_BUILD: 1 @@ -119,7 +164,8 @@ jobs: zepter: name: zepter feature propagation - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-light] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/check-spec-version.yml b/.github/workflows/check-spec-version.yml deleted file mode 100644 index 5b33a03824..0000000000 --- a/.github/workflows/check-spec-version.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Spec Version Check - -# Runtime-affecting PRs must carry a spec_version newer than what mainnet is -# running — otherwise the release train will build the merge and then no-op -# at every deploy guard. Label a PR `no-spec-version-bump` when the change -# deliberately ships without a runtime release. - -on: - pull_request: - types: [labeled, unlabeled, synchronize, opened] - paths: - - "runtime/**" - - "pallets/**" - - "node/**" - - "primitives/**" - - "precompiles/**" - - "common/**" - - "Cargo.lock" - -concurrency: - group: check-spec-version-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - MAINNET_WS: wss://entrypoint-finney.opentensor.ai:443 - -jobs: - trusted-pr: - name: Trusted PR source (non-fork) - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-latest - steps: - - run: echo "Non-fork PR; self-hosted runners may execute checkout." - - check-spec-version: - name: spec_version newer than mainnet - needs: trusted-pr - runs-on: [self-hosted, fireactions-tryruntime-finney] - if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-spec-version-bump') }} - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/rust-setup - with: - cache-key: spec-version - - - name: Install substrate-spec-version - run: cargo install substrate-spec-version - - - name: Compare against mainnet - run: | - chain_spec=$(PATH=$PATH:$HOME/.cargo/bin substrate-spec-version ${{ env.MAINNET_WS }} | tr -d '\n') - : ${chain_spec:?could not fetch mainnet spec_version} - local_spec=$(cargo run -p subtensor-tools --bin spec-version | tr -d '\n') - echo "mainnet spec_version: $chain_spec" - echo "local spec_version: $local_spec" - if [ "$local_spec" -le "$chain_spec" ]; then - echo "spec_version must be bumped above mainnet's, or label the PR 'no-spec-version-bump'." - exit 1 - fi diff --git a/.github/workflows/eco-tests.yml b/.github/workflows/eco-tests.yml index 84fb20a485..5a419c2565 100644 --- a/.github/workflows/eco-tests.yml +++ b/.github/workflows/eco-tests.yml @@ -22,9 +22,47 @@ jobs: steps: - run: echo "Non-fork PR; self-hosted runners may execute checkout." + # "cargo test (eco-tests)" is a required merge check, so the workflow must + # always trigger (a required check whose workflow never runs is stuck + # "Expected" and blocks the merge). The job skips itself when the PR touches + # nothing the eco-tests compile against; `skipped` satisfies branch + # protection. + changes: + name: detect eco-tests changes + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + rust: ${{ steps.filter.outputs.rust }} + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + rust: + - 'eco-tests/**' + - 'common/**' + - 'node/**' + - 'pallets/**' + - 'precompiles/**' + - 'primitives/**' + - 'runtime/**' + - 'support/**' + - 'chain-extensions/**' + - 'src/**' + - 'vendor/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build.rs' + - 'rust-toolchain.toml' + - '.github/workflows/eco-tests.yml' + - '.github/actions/rust-setup/**' + eco-tests: name: cargo test (eco-tests) - needs: trusted-pr + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' runs-on: [self-hosted, fireactions-heavy] env: RUST_BACKTRACE: full diff --git a/.github/workflows/run-benchmarks.yml b/.github/workflows/run-benchmarks.yml index ec31eed098..27a6dbeffd 100644 --- a/.github/workflows/run-benchmarks.yml +++ b/.github/workflows/run-benchmarks.yml @@ -1,9 +1,27 @@ # .github/workflows/run-benchmarks.yml name: Validate-Benchmarks +# The gate lives in a cheap `decide` job on a free GitHub runner so the one +# persistent Benchmarking machine is only ever occupied by runs that matter: +# - PRs touching benchmark-relevant paths run automatically (no "someone +# forgot the label and weights drifted into main" hole); +# - PRs whose diff stays inside pallets/ benchmark only the changed pallets +# (PALLET_DIRS); anything wider runs the full suite; +# - `run-benchmarks` label forces a full run, `skip-validate-benchmarks` +# forces a skip; +# - the nightly schedule reruns the full suite against the default branch as +# the drift backstop (a dependency bump that changes codegen surfaces +# within a day; on failure, apply the bench-patch artifact via the +# apply-benchmark-patch label on a fix PR). +# validate-benchmarks is not a required merge check today. If that changes, +# this decide-job shape keeps working (the workflow always triggers and the +# skipped job satisfies branch protection), unlike a `paths:` trigger filter. + on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, labeled, unlabeled] + schedule: + - cron: "0 3 * * *" # nightly full-suite drift check on the default branch workflow_dispatch: # This workflow only builds the node and validates benchmark weights; it never @@ -27,33 +45,80 @@ jobs: steps: - run: echo "Non-fork PR; self-hosted runners may execute checkout." + decide: + name: decide whether to benchmark + runs-on: ubuntu-latest + outputs: + run: ${{ steps.gate.outputs.run }} + pallet_dirs: ${{ steps.gate.outputs.pallet_dirs }} + steps: + - name: Gate on event, labels, and changed paths + id: gate + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + run="" + dirs="" + if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then + echo "$GITHUB_EVENT_NAME event: full run" + run=true + else + labels=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") + if grep -qx 'skip-validate-benchmarks' <<< "$labels"; then + echo "skip-validate-benchmarks label present: skipping" + run=false + elif grep -qx 'run-benchmarks' <<< "$labels"; then + echo "run-benchmarks label present: forcing full run" + run=true + else + files=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \ + --paginate --jq '.[].filename') + relevant=$(grep -E '^(pallets|runtime|node|primitives|precompiles|common|support|chain-extensions|src|vendor)/|^Cargo\.(toml|lock)$|^build\.rs$|^rust-toolchain\.toml$|^\.maintain/|^scripts/(benchmark_action|discover_pallets)\.sh$|^\.github/workflows/run-benchmarks\.yml$' \ + <<< "$files" || true) + if [[ -z "$relevant" ]]; then + echo "no benchmark-relevant paths changed: skipping" + run=false + else + run=true + nonpallet=$(grep -vE '^pallets/' <<< "$relevant" || true) + if [[ -z "$nonpallet" ]]; then + # Weights can also shift via runtime/ or dependency changes, + # but those hit the non-pallet branch; pallet-only diffs are + # safe to benchmark selectively. Nightly covers the rest. + dirs=$(grep -oE '^pallets/[^/]+' <<< "$relevant" | sort -u \ + | sed 's|^pallets/||' | tr '\n' ' ') + echo "pallet-only change: restricting to: $dirs" + else + echo "change reaches outside pallets/: full run" + fi + fi + fi + fi + { + echo "run=$run" + echo "pallet_dirs=$dirs" + } >> "$GITHUB_OUTPUT" + validate-benchmarks: - needs: trusted-pr + needs: [trusted-pr, decide] + if: needs.decide.outputs.run == 'true' runs-on: Benchmarking steps: - name: Check out PR branch uses: actions/checkout@v4 with: - # Fall back to the current repo/ref so workflow_dispatch runs work too. + # Fall back to the current repo/ref so workflow_dispatch and + # scheduled runs work too. repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.event.pull_request.head.ref || github.ref_name }} fetch-depth: 0 - - name: Install GitHub CLI - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" gh - - - name: Check for skip-validate-benchmarks label - id: skip-label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: .github/scripts/check-skip-label.sh ${{ github.event.pull_request.number }} - - name: Install system dependencies run: | + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ build-essential clang curl libssl-dev llvm libudev-dev protobuf-compiler pkg-config @@ -68,14 +133,14 @@ jobs: cache-on-failure: true - name: Build node with benchmarks + weight tools - if: steps.skip-label.outputs.skip != 'true' run: | cargo build --profile production -p node-subtensor --features runtime-benchmarks cargo build --profile production -p subtensor-weight-tools --bin weight-compare - name: Run & validate benchmarks - if: steps.skip-label.outputs.skip != 'true' timeout-minutes: 180 + env: + PALLET_DIRS: ${{ needs.decide.outputs.pallet_dirs }} run: | mkdir -p .bench_patch chmod +x scripts/benchmark_action.sh diff --git a/.github/workflows/runtime-checks.yml b/.github/workflows/runtime-checks.yml new file mode 100644 index 0000000000..6203ba6193 --- /dev/null +++ b/.github/workflows/runtime-checks.yml @@ -0,0 +1,371 @@ +name: Runtime Checks + +# Build-once, fan-out gate for every PR: one job compiles the runtime wasm and +# the release node binary, then try-runtime (devnet/testnet/mainnet), the +# spec-version comparison, and the mainnet clone-upgrade suite all consume the +# artifacts instead of rebuilding. Consolidates the former try-runtime.yml, +# check-spec-version.yml, and check-clone-upgrade.yml (artifacts are scoped to +# a single workflow run, so the fan-out jobs must live in one file). + +on: + pull_request: + types: [labeled, unlabeled, synchronize, opened, reopened] + workflow_dispatch: + +concurrency: + group: runtime-checks-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + WS_ENDPOINT: ws://127.0.0.1:9944 + MAINNET_RPC: https://entrypoint-finney.opentensor.ai + +jobs: + # Fork PRs must not run on self-hosted runners — checkout executes untrusted + # code from the fork head (RCE risk on persistent org hardware). + trusted-pr: + name: Trusted PR source (non-fork) + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + runs-on: ubuntu-latest + steps: + - run: echo "Non-fork PR; self-hosted runners may execute checkout." + + # "Sudo-upgrade mainnet clone and test" and "Docs and website build" are + # required merge checks, so this workflow must always trigger (a required + # check whose workflow never runs is stuck "Expected" and blocks the merge). + # Jobs skip themselves when the PR touches nothing they exercise; a + # `skipped` conclusion satisfies branch protection. + changes: + name: detect relevant changes + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + runtime: ${{ steps.filter.outputs.runtime }} + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + runtime: + - 'common/**' + - 'node/**' + - 'pallets/**' + - 'precompiles/**' + - 'primitives/**' + - 'runtime/**' + - 'support/**' + - 'chain-extensions/**' + - 'src/**' + - 'vendor/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build.rs' + - 'rust-toolchain.toml' + - 'clones/**' + - 'sdk/**' + - 'website/apps/bittensor-website/scripts/**' + - '.github/workflows/runtime-checks.yml' + - '.github/actions/rust-setup/**' + docs: + - 'website/**' + - 'sdk/python/**' + - '.github/workflows/runtime-checks.yml' + + # The single compile of this workflow. Two artifacts: + # try-runtime-wasm — production profile + try-runtime feature, used by the + # three try-runtime network checks. + # node-release — release node binary + its wbuild runtime wasm, used by + # the clone-upgrade suite (start-local-clone.sh expects the binary at + # target/release/node-subtensor; update-runtime-with-alice.ts expects the + # wasm at target/release/wbuild/node-subtensor-runtime/). + build: + name: build runtime artifacts + needs: [trusted-pr, changes] + if: github.event_name != 'pull_request' || needs.changes.outputs.runtime == 'true' + runs-on: [self-hosted, fireactions-heavy] + timeout-minutes: 60 + env: + RUST_BACKTRACE: full + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/rust-setup + with: + cache-key: runtime-checks + + - name: Add wasm target and rust-src + run: | + rustup target add wasm32-unknown-unknown + rustup component add rust-src + + - name: Build runtime wasm (production, try-runtime feature) + run: cargo build --profile production -p node-subtensor-runtime --features try-runtime -q --locked + + - name: Build node-subtensor (release) + run: cargo build --release -p node-subtensor + + - name: Upload try-runtime wasm + uses: actions/upload-artifact@v4 + with: + name: try-runtime-wasm + path: target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm + if-no-files-found: error + retention-days: 3 + + - name: Upload release node + runtime wasm + uses: actions/upload-artifact@v4 + with: + name: node-release + path: | + target/release/node-subtensor + target/release/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm + if-no-files-found: error + retention-days: 3 + + # For every PR: run try-runtime's on-runtime-upgrade checks against the + # current state of each live network, using the shared wasm artifact and the + # prebuilt try-runtime-cli — no Rust toolchain on these runners. + try-runtime: + name: try-runtime ${{ matrix.network.name }} + needs: build + runs-on: ${{ matrix.network.runner }} + strategy: + fail-fast: false + matrix: + network: + # NOTE: the hostnames are misleading — archive.dev.opentensor.ai + # serves the TESTNET archive on :8443 and the MAINNET archive on + # :443 (verified by genesis hash; try-runtime wants archive nodes, + # which is why we don't use the entrypoint hostnames). The + # "Verify endpoint" step below enforces each URI's identity. + - name: devnet + uri: wss://dev.chain.opentensor.ai:443 + genesis: "0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5" + runner: [self-hosted, fireactions-tryruntime] + - name: testnet + uri: wss://archive.dev.opentensor.ai:8443 + genesis: "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" + runner: [self-hosted, fireactions-tryruntime] + - name: mainnet + uri: wss://archive.dev.opentensor.ai:443 + genesis: "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03" + runner: [self-hosted, fireactions-tryruntime-finney] + steps: + - name: Verify endpoint serves the expected network + run: | + http_uri="${{ matrix.network.uri }}" + http_uri="${http_uri/wss:/https:}" + genesis=$(curl -sS -m 30 -H 'Content-Type: application/json' \ + -d '{"id":1,"jsonrpc":"2.0","method":"chain_getBlockHash","params":[0]}' \ + "$http_uri" | sed -n 's/.*"result":"\(0x[0-9a-f]*\)".*/\1/p') + echo "endpoint genesis: $genesis" + if [ "$genesis" != "${{ matrix.network.genesis }}" ]; then + echo "::error::${{ matrix.network.name }} endpoint $http_uri serves genesis $genesis, expected ${{ matrix.network.genesis }}" + exit 1 + fi + + - name: Download runtime wasm + uses: actions/download-artifact@v4 + with: + name: try-runtime-wasm + path: runtime-blob + + - name: Download try-runtime-cli v0.10.1 + run: | + TRY_RUNTIME_BIN="$RUNNER_TEMP/try-runtime" + curl --fail --silent --show-error --location \ + "https://github.com/paritytech/try-runtime-cli/releases/download/v0.10.1/try-runtime-x86_64-unknown-linux-musl" \ + --output "$TRY_RUNTIME_BIN" + chmod +x "$TRY_RUNTIME_BIN" + echo "TRY_RUNTIME_BIN=$TRY_RUNTIME_BIN" >> "$GITHUB_ENV" + + - name: Check migrations + env: + NODE_URI: ${{ matrix.network.uri }} + run: | + export RUST_LOG=remote-ext=debug,runtime=debug + "$TRY_RUNTIME_BIN" \ + --runtime runtime-blob/node_subtensor_runtime.compact.compressed.wasm \ + on-runtime-upgrade \ + --checks=all \ + --blocktime 12000 \ + --disable-spec-version-check --no-weight-warnings \ + live --uri "$NODE_URI" + + # Runtime-affecting PRs must carry a spec_version newer than what mainnet is + # running — otherwise the release train will build the merge and then no-op + # at every deploy guard. Label a PR `no-spec-version-bump` when the change + # deliberately ships without a runtime release. No compilation: the local + # value is the literal in runtime/src/lib.rs (grep fails loudly unless it + # matches exactly once) and mainnet's comes from one RPC call. + spec-version: + name: spec_version newer than mainnet + needs: [trusted-pr, changes] + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-spec-version-bump') && (github.event_name != 'pull_request' || needs.changes.outputs.runtime == 'true') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Compare against mainnet + run: | + set -euo pipefail + matches=$(grep -cE '^\s*spec_version: [0-9]+,' runtime/src/lib.rs) + if [ "$matches" -ne 1 ]; then + echo "::error::expected exactly one 'spec_version: ,' literal in runtime/src/lib.rs, found $matches" + exit 1 + fi + local_spec=$(grep -oE '^\s*spec_version: [0-9]+,' runtime/src/lib.rs | grep -oE '[0-9]+') + chain_spec=$(curl -sS -m 30 -H 'Content-Type: application/json' \ + -d '{"id":1,"jsonrpc":"2.0","method":"state_getRuntimeVersion","params":[]}' \ + "$MAINNET_RPC" | jq -er '.result.specVersion') + echo "mainnet spec_version: $chain_spec" + echo "local spec_version: $local_spec" + if [ "$local_spec" -le "$chain_spec" ]; then + echo "spec_version must be bumped above mainnet's, or label the PR 'no-spec-version-bump'." + exit 1 + fi + + # For every PR: sudo-upgrade a local clone of mainnet with the proposed + # runtime, then run the clone regression suite and the Python SDK suite + # against the upgraded chain. Consumes the node-release artifact instead of + # building. Auto-skips (via the `build` need) when the PR touches nothing + # runtime-relevant. + clone-upgrade: + name: Sudo-upgrade mainnet clone and test + needs: [trusted-pr, changes, build] + runs-on: [self-hosted, fireactions-heavy] + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clone-upgrade') }} + timeout-minutes: 180 + steps: + - uses: actions/checkout@v4 + + - name: Download release node + runtime wasm + uses: actions/download-artifact@v4 + with: + name: node-release + path: target/release + + - name: Make node binary executable + run: chmod +x target/release/node-subtensor + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Create mainnet clone + run: ./clones/scripts/clone-mainnet.sh + + - name: Start local clone + run: | + nohup ./clones/scripts/start-local-clone.sh > clone-node.log 2>&1 & + # Genesis init from the ~2 GB mainnet-clone chainspec takes several + # minutes before the RPC server comes up, so allow up to 15 minutes. + for i in $(seq 1 450); do + if curl -sf -H "Content-Type: application/json" \ + -d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \ + http://127.0.0.1:9944 > /dev/null; then + echo "Clone node is up." + exit 0 + fi + sleep 2 + done + echo "Clone node failed to start:" + cat clone-node.log + exit 1 + + - name: Sudo-upgrade clone with proposed runtime + working-directory: clones/js-tests + run: | + npm ci + npm run runtime:update:alice + + - name: Run clone smoke tests + working-directory: clones/js-tests + run: npm test + + - name: Run clone regression tests + working-directory: clones/js-tests + run: npm run test:clone-regressions + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: SDK offline checks (same as just check) + working-directory: sdk/python + run: | + uv sync --locked --all-extras --dev + uv run ruff check . + uv run ruff format --check . + uv run ty check --exit-zero-on-warning bittensor + uv run pytest + uv run python -m codegen.check --coverage + uv run python -m codegen.check --names + + - name: Metadata drift gate (committed _generated vs upgraded clone) + working-directory: sdk/python + run: uv run python -m codegen.check --drift ${{ env.WS_ENDPOINT }} + + - name: Docs drift gate (generated tx/query/errors pages) + working-directory: sdk/python + run: uv run python ../../website/apps/bittensor-website/scripts/generate.py --check + + - name: SDK e2e tests against upgraded clone + working-directory: sdk/python + env: + E2E_ENDPOINT: ws://127.0.0.1:9944 + run: uv run pytest -m e2e + + - name: Dump clone node and harness logs + if: failure() + run: | + echo "===== clone-node.log (last 200 lines) =====" + tail -n 200 clone-node.log 2>/dev/null || echo "(missing)" + for f in clones/js-tests/temp/*.log; do + [ -f "$f" ] || continue + echo "===== $f =====" + cat "$f" + done + + - name: Stop local clone + if: always() + run: ./clones/scripts/stop-local-clone.sh + + docs-website: + name: Docs and website build + needs: [trusted-pr, changes] + if: needs.changes.outputs.docs == 'true' + runs-on: [self-hosted, fireactions-heavy] + steps: + - uses: actions/checkout@v4 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/0.11.28/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Docs drift gate (generated tx/query/errors pages) + working-directory: sdk/python + run: | + uv sync --locked --all-extras --dev + uv run python ../../website/apps/bittensor-website/scripts/generate.py --check + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Enable corepack (yarn 4) + run: corepack enable + + - name: Build docs site + working-directory: website + run: | + yarn install --immutable + yarn turbo run build --filter=@raofoundation/bittensor-website diff --git a/.github/workflows/try-runtime.yml b/.github/workflows/try-runtime.yml deleted file mode 100644 index 129dd8ad28..0000000000 --- a/.github/workflows/try-runtime.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Try Runtime - -# For every PR: run try-runtime's on-runtime-upgrade checks against the -# current state of each live network. This is the "will this runtime migrate -# cleanly from what's actually deployed" gate. - -on: - pull_request: - workflow_dispatch: - -concurrency: - group: try-runtime-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - trusted-pr: - name: Trusted PR source (non-fork) - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-latest - steps: - - run: echo "Non-fork PR; self-hosted runners may execute checkout." - - try-runtime: - name: try-runtime ${{ matrix.network.name }} - needs: trusted-pr - runs-on: ${{ matrix.network.runner }} - strategy: - fail-fast: false - matrix: - network: - # NOTE: the hostnames are misleading — archive.dev.opentensor.ai - # serves the TESTNET archive on :8443 and the MAINNET archive on - # :443 (verified by genesis hash; try-runtime wants archive nodes, - # which is why we don't use the entrypoint hostnames). The - # "Verify endpoint" step below enforces each URI's identity. - - name: devnet - uri: wss://dev.chain.opentensor.ai:443 - genesis: "0x077899043eb684c5277b6814a39161f4ce072b45e782e12c81a521c63fb4f3e5" - runner: [self-hosted, fireactions-tryruntime] - - name: testnet - uri: wss://archive.dev.opentensor.ai:8443 - genesis: "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105" - runner: [self-hosted, fireactions-tryruntime] - - name: mainnet - uri: wss://archive.dev.opentensor.ai:443 - genesis: "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03" - runner: [self-hosted, fireactions-tryruntime-finney] - steps: - - name: Verify endpoint serves the expected network - run: | - http_uri="${{ matrix.network.uri }}" - http_uri="${http_uri/wss:/https:}" - genesis=$(curl -sS -m 30 -H 'Content-Type: application/json' \ - -d '{"id":1,"jsonrpc":"2.0","method":"chain_getBlockHash","params":[0]}' \ - "$http_uri" | sed -n 's/.*"result":"\(0x[0-9a-f]*\)".*/\1/p') - echo "endpoint genesis: $genesis" - if [ "$genesis" != "${{ matrix.network.genesis }}" ]; then - echo "::error::${{ matrix.network.name }} endpoint $http_uri serves genesis $genesis, expected ${{ matrix.network.genesis }}" - exit 1 - fi - - - uses: actions/checkout@v4 - - - name: Install system dependencies - run: | - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --no-install-recommends \ - -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ - build-essential clang curl git make libssl-dev llvm libudev-dev protobuf-compiler pkg-config unzip - - - name: Install Rust (stable) - uses: dtolnay/rust-toolchain@stable - - - name: Run try-runtime checks - uses: ./.github/actions/try-runtime - with: - runtime-package: "node-subtensor-runtime" - node-uri: ${{ matrix.network.uri }} - checks: "all" - blocktime: "12000" - extra-args: "--disable-spec-version-check --no-weight-warnings" - cache-on-failure: "true" diff --git a/.github/workflows/typescript-e2e.yml b/.github/workflows/typescript-e2e.yml index 1843c94f0d..22a09e6cec 100644 --- a/.github/workflows/typescript-e2e.yml +++ b/.github/workflows/typescript-e2e.yml @@ -21,6 +21,40 @@ jobs: steps: - run: echo "Non-fork PR; self-hosted runners may execute checkout." + # typescript-formatting is a required merge check, so the workflow must + # always trigger and that job always runs (it is cheap). The expensive node + # builds and zombienet suites skip themselves when the PR touches nothing + # they exercise; `skipped` satisfies branch protection. + changes: + name: detect e2e-relevant changes + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + e2e: ${{ steps.filter.outputs.e2e }} + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + e2e: + - 'ts-tests/**' + - 'common/**' + - 'node/**' + - 'pallets/**' + - 'precompiles/**' + - 'primitives/**' + - 'runtime/**' + - 'support/**' + - 'chain-extensions/**' + - 'src/**' + - 'vendor/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build.rs' + - 'rust-toolchain.toml' + - '.github/workflows/typescript-e2e.yml' + typescript-formatting: runs-on: ubuntu-latest steps: @@ -56,7 +90,8 @@ jobs: # Build the node binary in both variants and share as artifacts. build: runs-on: [self-hosted, fireactions-heavy] - needs: [trusted-pr, typescript-formatting] + needs: [trusted-pr, typescript-formatting, changes] + if: needs.changes.outputs.e2e == 'true' timeout-minutes: 60 strategy: matrix: diff --git a/scripts/benchmark_action.sh b/scripts/benchmark_action.sh index 2497956a84..6babef20b6 100755 --- a/scripts/benchmark_action.sh +++ b/scripts/benchmark_action.sh @@ -27,6 +27,26 @@ done < <("$SCRIPT_DIR/discover_pallets.sh") (( ${#OUTPUTS[@]} > 0 )) || die "no benchmarked pallets found" +# PALLET_DIRS (space-separated directory names under pallets/, set by CI for +# pallet-only diffs) restricts the run to the changed pallets. Unset/empty +# means the full suite. +if [[ -n "${PALLET_DIRS:-}" ]]; then + declare -A KEEP + for dir in $PALLET_DIRS; do + for pallet in "${!OUTPUTS[@]}"; do + [[ "${OUTPUTS[$pallet]}" == "pallets/$dir/"* ]] && KEEP[$pallet]="${OUTPUTS[$pallet]}" + done + done + if (( ${#KEEP[@]} == 0 )); then + echo "Changed pallets ($PALLET_DIRS) have no registered benchmarks; nothing to validate." + exit 0 + fi + echo "Restricting to changed pallets: ${!KEEP[*]}" + unset OUTPUTS + declare -A OUTPUTS + for pallet in "${!KEEP[@]}"; do OUTPUTS[$pallet]="${KEEP[$pallet]}"; done +fi + mkdir -p "$PATCH_DIR" # Build if needed