name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [published]
repository_dispatch:
types: [build-chain, upstream-changed]
workflow_dispatch:
inputs:
skip_tests:
description: 'Skip test execution and coverage steps'
required: false
type: boolean
default: false
permissions:
contents: read
actions: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-C link-arg=-fuse-ld=lld'
BLVM_BUILD_TARGET: ${{ github.workspace }}/.build/gh-blvm-node-build/${{ github.run_id }}/target
jobs:
setup:
name: Setup
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]')))
outputs:
cache-key: ${{ steps.setup-cache.outputs.cache-key }}
consensus-cache-key: ${{ steps.setup-cache.outputs.consensus-cache-key }}
protocol-cache-key: ${{ steps.setup-cache.outputs.protocol-cache-key }}
steps:
- name: Emergency pre-flight disk cleanup
run: |
echo "=== disk BEFORE cleanup ==="
df -h /
# Nuke ALL stale run dirs (every dir except a currently-active run on THIS runner).
# We can't know other runners' run_ids, so we delete everything older than 1 hour.
# The teardown job deletes the current run dir at the end, so at steady-state
# nothing should survive here — but we clean defensively on every setup.
if [ -d ${{ github.workspace }}/.build/gh-blvm-node-build ]; then
find ${{ github.workspace }}/.build/gh-blvm-node-build -mindepth 1 -maxdepth 1 -type d \
-not -name "${{ github.run_id }}" \
-mmin +60 \
-exec rm -rf {} + 2>/dev/null || true
# If still >50 GB used by gh-blvm-node-build, evict everything except current run.
USED=$(du -sb ${{ github.workspace }}/.build/gh-blvm-node-build 2>/dev/null | cut -f1 || echo 0)
if [ "$USED" -gt 53687091200 ]; then
echo "⚠️ gh-blvm-node-build still >50 GB after 1h prune — force-evicting all stale dirs"
find ${{ github.workspace }}/.build/gh-blvm-node-build -mindepth 1 -maxdepth 1 -type d \
-not -name "${{ github.run_id }}" \
-exec rm -rf {} + 2>/dev/null || true
fi
fi
# Fuzz artifacts from previous runs
rm -rf ${{ github.workspace }}/.build/gh-fuzz-blvm-node 2>/dev/null || true
# Ensure TMPDIR exists (nested inside the per-run build dir — always writable)
mkdir -p "${{ github.workspace }}/.build/gh-blvm-node-build/${{ github.run_id }}/target"
echo "=== disk AFTER cleanup ==="
df -h /
- name: Disk check
uses: BTCDecoded/rust-ci/runner-disk-guard@main
with:
show-df: false
- name: Show disk usage (top 10 largest dirs)
if: always()
run: |
echo "=== df -h ==="
df -h /
echo ""
echo "=== Top 10 largest directories under / (excluding proc/sys/dev) ==="
du -x --max-depth=4 / 2>/dev/null \
| sort -rh \
| grep -v -e '^[0-9]*\s*/proc' -e '^[0-9]*\s*/sys' -e '^[0-9]*\s*/dev' \
| head -10 \
| awk '{printf "%-10s %s\n", $1, $2}'
echo ""
echo "=== /tmp usage ==="
du -sh /tmp/* 2>/dev/null | sort -rh | head -10
echo ""
echo "=== Runner cache ==="
du -sh ${{ github.workspace }}/.build/runner-cache 2>/dev/null || echo "(no runner-cache)"
du -sh ${{ github.workspace }}/.build/gh-blvm-node-build 2>/dev/null || echo "(no gh-blvm-node-build)"
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Checkout blvm-protocol dependency (for cache key generation)
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-protocol
path: _temp-blvm-protocol
- name: Checkout consensus (for cache key generation)
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-consensus
path: _temp-blvm-consensus
- name: Cache
id: setup-cache
run: |
CACHE_ROOT="${{ github.workspace }}/.build/runner-cache"
# Generate cache keys from Cargo.toml (sibling libraries do not commit Cargo.lock)
DEPS_KEY=$(sha256sum Cargo.toml | cut -d' ' -f1)
CONSENSUS_DEPS_KEY=$(sha256sum _temp-blvm-consensus/Cargo.toml | cut -d' ' -f1)
PROTOCOL_DEPS_KEY=$(sha256sum _temp-blvm-protocol/Cargo.toml | cut -d' ' -f1)
# Include toolchain version in cache key
TOOLCHAIN=$(grep -E '^channel|rust-version' rust-toolchain.toml Cargo.toml 2>/dev/null | head -1 | sha256sum | cut -d' ' -f1 || echo "1.88.0")
CACHE_KEY="${DEPS_KEY}-${TOOLCHAIN}"
CONSENSUS_CACHE_KEY="${CONSENSUS_DEPS_KEY}-${TOOLCHAIN}"
PROTOCOL_CACHE_KEY="${PROTOCOL_DEPS_KEY}-${TOOLCHAIN}"
# Set up cache directories
CARGO_CACHE_DIR="$CACHE_ROOT/cargo/$CACHE_KEY"
TARGET_CACHE_DIR="$CACHE_ROOT/target/$CACHE_KEY"
CONSENSUS_TARGET_DIR="$CACHE_ROOT/blvm-consensus-target/$CONSENSUS_CACHE_KEY"
PROTOCOL_TARGET_DIR="$CACHE_ROOT/blvm-protocol-target/$PROTOCOL_CACHE_KEY"
echo "CARGO_CACHE_DIR=$CARGO_CACHE_DIR" >> $GITHUB_ENV
echo "TARGET_CACHE_DIR=$TARGET_CACHE_DIR" >> $GITHUB_ENV
echo "CONSENSUS_TARGET_DIR=$CONSENSUS_TARGET_DIR" >> $GITHUB_ENV
echo "PROTOCOL_TARGET_DIR=$PROTOCOL_TARGET_DIR" >> $GITHUB_ENV
echo "cache-key=$CACHE_KEY" >> $GITHUB_OUTPUT
echo "consensus-cache-key=$CONSENSUS_CACHE_KEY" >> $GITHUB_OUTPUT
echo "protocol-cache-key=$PROTOCOL_CACHE_KEY" >> $GITHUB_OUTPUT
mkdir -p "$CARGO_CACHE_DIR"/{registry,git} "$TARGET_CACHE_DIR" "$CONSENSUS_TARGET_DIR" "$PROTOCOL_TARGET_DIR"
- name: Cleanup temp dependencies
if: always()
run: |
# Remove temp directories to prevent them from being used by other jobs
rm -rf _temp-blvm-protocol _temp-blvm-consensus
# Also clean up any existing parent directory dependencies (from previous runs)
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
echo " - Main: ${CACHE_KEY:0:16}..."
echo " - Consensus: ${CONSENSUS_CACHE_KEY:0:16}..."
echo " - Protocol: ${PROTOCOL_CACHE_KEY:0:16}..."
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
build-dev:
name: Build (dev, shared)
needs: setup
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- name: Show disk usage (top 10 largest dirs)
run: |
echo "=== df -h ==="
df -h /
echo ""
echo "=== Top 10 largest directories ==="
du -x --max-depth=4 / 2>/dev/null \
| sort -rh \
| grep -v -e '^[0-9]*\s*/proc' -e '^[0-9]*\s*/sys' -e '^[0-9]*\s*/dev' \
| head -10 \
| awk '{printf "%-10s %s\n", $1, $2}'
echo ""
echo "=== /tmp ==="
du -sh /tmp/* 2>/dev/null | sort -rh | head -10
du -sh ${{ github.workspace }}/.build/runner-cache 2>/dev/null || true
du -sh ${{ github.workspace }}/.build/gh-blvm-node-build 2>/dev/null || true
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Install tidesdb native dependencies
run: |
if pkg-config --exists liblz4 libzstd 2>/dev/null && command -v cmake >/dev/null 2>&1 && pkg-config --exists openssl 2>/dev/null; then
echo "✅ tidesdb native deps already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq liblz4-dev libzstd-dev libsnappy-dev cmake pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq lz4 zstd snappy cmake pkgconf openssl >/dev/null 2>&1 || sudo pacman -Sy --noconfirm lz4 zstd snappy cmake pkgconf openssl
fi
- name: Isolate CARGO_HOME for this runner
run: |
mkdir -p "${RUNNER_TEMP}/cargo"
echo "CARGO_HOME=${RUNNER_TEMP}/cargo" >> "$GITHUB_ENV"
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Compile (lib + bins + test binaries)
env:
CARGO_TARGET_DIR: ${{ env.BLVM_BUILD_TARGET }}
run: cargo test --no-run
- name: Dump disk usage on failure
if: failure()
run: |
echo "=== df -h ==="
df -h /
echo ""
echo "=== Top 10 largest directories ==="
du -x --max-depth=4 / 2>/dev/null \
| sort -rh \
| grep -v -e '^[0-9]*\s*/proc' -e '^[0-9]*\s*/sys' -e '^[0-9]*\s*/dev' \
| head -10 \
| awk '{printf "%-10s %s\n", $1, $2}'
echo ""
echo "=== /tmp ==="
du -sh /tmp/* 2>/dev/null | sort -rh | head -10
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
- name: Prune old runner caches
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: prune
test:
name: Test
needs: [setup, build-dev]
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Install tidesdb native dependencies
run: |
if pkg-config --exists liblz4 libzstd 2>/dev/null && command -v cmake >/dev/null 2>&1 && pkg-config --exists openssl 2>/dev/null; then
echo "✅ tidesdb native deps already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq liblz4-dev libzstd-dev libsnappy-dev cmake pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq lz4 zstd snappy cmake pkgconf openssl >/dev/null 2>&1 || sudo pacman -Sy --noconfirm lz4 zstd snappy cmake pkgconf openssl
fi
- name: Isolate CARGO_HOME for this runner
run: |
mkdir -p "${RUNNER_TEMP}/cargo"
echo "CARGO_HOME=${RUNNER_TEMP}/cargo" >> "$GITHUB_ENV"
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Run tests
env:
CARGO_TARGET_DIR: ${{ env.BLVM_BUILD_TARGET }}
run: cargo test
- name: Dump disk usage on failure
if: failure()
run: |
echo "=== df -h ==="
df -h /
echo ""
echo "=== Top 10 largest directories ==="
du -x --max-depth=4 / 2>/dev/null \
| sort -rh \
| grep -v -e '^[0-9]*\s*/proc' -e '^[0-9]*\s*/sys' -e '^[0-9]*\s*/dev' \
| head -10 \
| awk '{printf "%-10s %s\n", $1, $2}'
echo ""
echo "=== /tmp ==="
du -sh /tmp/* 2>/dev/null | sort -rh | head -10
- name: Clean test binaries and stale run dirs
if: always()
run: |
# Remove test binary executables from this run (largest artifacts, not needed after test).
# Rlibs stay — they speed up future compilations via the cache.
TARGET="${{ env.BLVM_BUILD_TARGET }}"
if [ -d "$TARGET/debug" ]; then
find "$TARGET/debug" -maxdepth 1 -type f -executable ! -name "*.so" ! -name "*.rlib" ! -name "*.d" \
-delete 2>/dev/null || true
fi
# Remove ALL run dirs except the current one — no mtime guard, stale dirs eat 10s of GB.
find ${{ github.workspace }}/.build/gh-blvm-node-build -mindepth 1 -maxdepth 1 -type d \
-not -name "${{ github.run_id }}" \
-exec rm -rf {} + 2>/dev/null || true
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
- name: Prune old runner caches
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: prune
clippy:
name: Clippy
needs: [setup, build-dev]
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Install OpenSSL development headers (openssl-sys / native-tls)
run: |
if pkg-config --exists openssl 2>/dev/null || command -v openssl >/dev/null 2>&1; then
echo "✅ openssl already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq openssl pkgconf >/dev/null 2>&1 || sudo pacman -Sy --noconfirm openssl pkgconf
fi
- name: Isolate CARGO_HOME for this runner
run: |
mkdir -p "${RUNNER_TEMP}/cargo"
echo "CARGO_HOME=${RUNNER_TEMP}/cargo" >> "$GITHUB_ENV"
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
include-target: false
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Run clippy
env:
CARGO_TARGET_DIR: ${{ env.BLVM_BUILD_TARGET }}
run: |
if ! cargo clippy --version >/dev/null 2>&1; then
echo "Skipping Clippy (cargo-clippy not installed for the active toolchain)"
exit 0
fi
cargo clippy --lib --bins -- -D warnings
verify:
name: Verify
needs: setup
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 60
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_verify]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Setup blvm-spec (Orange Paper for spec-lock verification)
uses: BTCDecoded/rust-ci/setup-blvm-spec@main
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Verify spec-lock build deps (Z3 / clang / jq)
run: |
pkg-config --exists z3 || { echo "❌ z3 not found; install it on the runner."; exit 1; }
command -v clang >/dev/null 2>&1 || { echo "❌ clang not found; install it on the runner."; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "❌ jq not found; install it on the runner."; exit 1; }
pkg-config --exists openssl 2>/dev/null || command -v openssl >/dev/null 2>&1 || { echo "❌ openssl not found; install it on the runner."; exit 1; }
echo "✅ Z3 + clang + jq + openssl present."
- name: Install cargo-spec-lock from crates.io
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo install blvm-spec-lock --locked --features z3
- name: Fetch blvm-consensus sources from crates.io
id: consensus_src
run: |
# crates.io returns 403 without a descriptive User-Agent (see https://crates.io/policies)
UA='BTCDecoded/blvm-node verify job (https://github.com/BTCDecoded/blvm-node)'
CONS_VER=$(curl -fsSL --retry 3 --retry-delay 2 -A "$UA" \
'https://crates.io/api/v1/crates/blvm-consensus' \
| jq -r '.crate.max_stable_version // .crate.max_version')
if [ -z "$CONS_VER" ] || [ "$CONS_VER" = "null" ]; then
echo "❌ Could not resolve blvm-consensus version from crates.io API"
exit 1
fi
echo "Using blvm-consensus $CONS_VER from crates.io"
mkdir -p "$GITHUB_WORKSPACE/_spec_verify"
curl -fsSL --retry 3 --retry-delay 2 -A "$UA" \
"https://crates.io/api/v1/crates/blvm-consensus/${CONS_VER}/download" \
| tar -xz -C "$GITHUB_WORKSPACE/_spec_verify"
echo "path=$GITHUB_WORKSPACE/_spec_verify/blvm-consensus-${CONS_VER}" >> "$GITHUB_OUTPUT"
- name: Run blvm-spec-lock verify (blvm-consensus)
run: |
export PATH="$HOME/.cargo/bin:$PATH"
echo "🔒 Running blvm-spec-lock verification on crates.io blvm-consensus..."
SPEC_ARGS="--crate-path ${{ steps.consensus_src.outputs.path }}"
if [ -f "../blvm-spec/PROTOCOL.md" ] && [ -f "../blvm-spec/ARCHITECTURE.md" ]; then
SPEC_ARGS="$SPEC_ARGS --spec-path ../blvm-spec/PROTOCOL.md ../blvm-spec/ARCHITECTURE.md"
echo "Using PROTOCOL.md + ARCHITECTURE.md for spec-derived contracts"
elif [ -f "../blvm-spec/THE_ORANGE_PAPER.md" ]; then
SPEC_ARGS="$SPEC_ARGS --spec-path ../blvm-spec/THE_ORANGE_PAPER.md"
echo "Using THE_ORANGE_PAPER.md for spec-derived contracts"
else
echo "❌ No spec at ../blvm-spec (run setup-blvm-spec step)"
exit 1
fi
export SPEC_LOCK_STRICT=1
cargo-spec-lock verify $SPEC_ARGS 2>&1 | tee "$GITHUB_WORKSPACE/spec_lock_consensus_output.txt"
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "❌ blvm-consensus spec-lock verification failed"
exit $EXIT_CODE
fi
echo "✅ blvm-consensus verification passed"
- name: Run blvm-spec-lock verify (blvm-node)
run: |
export PATH="$HOME/.cargo/bin:$PATH"
echo "🔒 Running blvm-spec-lock verification on blvm-node..."
cd "$GITHUB_WORKSPACE"
SPEC_ARGS="--crate-path ."
if [ -f "../blvm-spec/PROTOCOL.md" ] && [ -f "../blvm-spec/ARCHITECTURE.md" ]; then
SPEC_ARGS="$SPEC_ARGS --spec-path ../blvm-spec/PROTOCOL.md ../blvm-spec/ARCHITECTURE.md"
echo "Using PROTOCOL.md + ARCHITECTURE.md for spec-derived contracts"
elif [ -f "../blvm-spec/THE_ORANGE_PAPER.md" ]; then
SPEC_ARGS="$SPEC_ARGS --spec-path ../blvm-spec/THE_ORANGE_PAPER.md"
echo "Using THE_ORANGE_PAPER.md for spec-derived contracts"
else
echo "❌ No spec at ../blvm-spec"
exit 1
fi
export SPEC_LOCK_STRICT=1
cargo-spec-lock verify $SPEC_ARGS 2>&1 | tee "$GITHUB_WORKSPACE/spec_lock_node_output.txt"
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "❌ blvm-node spec-lock verification failed"
exit $EXIT_CODE
fi
echo "✅ blvm-node verification passed"
- name: Create spec-lock attestation data
run: |
CONSENSUS_FUNCS=$(grep -c "Status: PASSED" spec_lock_consensus_output.txt 2>/dev/null || echo "0")
NODE_FUNCS=$(grep -c "Status: PASSED" spec_lock_node_output.txt 2>/dev/null || echo "0")
TOTAL=$((CONSENSUS_FUNCS + NODE_FUNCS))
echo "spec_lock_consensus_functions=$CONSENSUS_FUNCS" >> $GITHUB_ENV
echo "spec_lock_node_functions=$NODE_FUNCS" >> $GITHUB_ENV
echo "spec_lock_total_functions=$TOTAL" >> $GITHUB_ENV
echo "spec_lock_verified=true" >> $GITHUB_ENV
HASH=$(sha256sum spec_lock_consensus_output.txt spec_lock_node_output.txt | sha256sum | cut -d' ' -f1)
echo "spec_lock_output_hash=$HASH" >> $GITHUB_ENV
echo "📋 Spec-lock: consensus=$CONSENSUS_FUNCS node=$NODE_FUNCS total=$TOTAL"
- name: Upload spec-lock output for attestation
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: spec-lock-attestation-data
path: |
spec_lock_consensus_output.txt
spec_lock_node_output.txt
retention-days: 90
fmt:
name: Format
needs: setup
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Check formatting
run: |
if ! cargo fmt --version >/dev/null 2>&1; then
echo "Skipping rustfmt (not installed for the active toolchain)"
exit 0
fi
cargo fmt -- --check
docs:
name: Docs
needs: [setup, build-dev]
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_docs]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Install OpenSSL development headers (openssl-sys / native-tls)
run: |
if pkg-config --exists openssl 2>/dev/null || command -v openssl >/dev/null 2>&1; then
echo "✅ openssl already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq openssl pkgconf >/dev/null 2>&1 || sudo pacman -Sy --noconfirm openssl pkgconf
fi
- name: Isolate CARGO_HOME for this runner
run: |
mkdir -p "${RUNNER_TEMP}/cargo"
echo "CARGO_HOME=${RUNNER_TEMP}/cargo" >> "$GITHUB_ENV"
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
include-target: false
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Docs
env:
CARGO_TARGET_DIR: ${{ env.BLVM_BUILD_TARGET }}
run: cargo doc --no-deps --document-private-items
security:
name: Security
needs: setup
runs-on: [self-hosted, Linux, X64, builds]
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Install OpenSSL development headers (openssl-sys / native-tls)
run: |
if pkg-config --exists openssl 2>/dev/null || command -v openssl >/dev/null 2>&1; then
echo "✅ openssl already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq openssl pkgconf >/dev/null 2>&1 || sudo pacman -Sy --noconfirm openssl pkgconf
fi
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Install cargo-audit
run: |
# Ensure cargo bin is in PATH (fixes exit code 127)
export PATH="$HOME/.cargo/bin:$PATH"
cargo install cargo-audit --version 0.22.1 --locked
- name: Run security audit
run: |
# Ignore vulnerabilities in optional iroh-net feature dependencies
# These are transitive dependencies that can't be easily fixed without
# updating iroh-net to 0.28+ (deprecated, requires API changes)
# See Cargo.toml security note for details
# hickory-proto/resolver via iroh (DNS): fixed upstream needs newer iroh / hickory stack
cargo audit --ignore RUSTSEC-2024-0421 --ignore RUSTSEC-2025-0009 --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2026-0007 --ignore RUSTSEC-2026-0037 --ignore RUSTSEC-2026-0009 --ignore RUSTSEC-2026-0118 --ignore RUSTSEC-2026-0119
fuzz:
name: Fuzz
needs: [setup]
runs-on: [self-hosted, Linux, X64, builds]
timeout-minutes: 30
if: |
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
(github.event_name != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_fuzz]')) &&
(github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Install tidesdb native dependencies
run: |
if pkg-config --exists liblz4 libzstd 2>/dev/null && command -v cmake >/dev/null 2>&1 && pkg-config --exists openssl 2>/dev/null; then
echo "✅ tidesdb native deps already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq liblz4-dev libzstd-dev libsnappy-dev cmake pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq lz4 zstd snappy cmake pkgconf openssl >/dev/null 2>&1 || sudo pacman -Sy --noconfirm lz4 zstd snappy cmake pkgconf openssl
fi
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Install nightly and cargo-fuzz
run: |
rustup toolchain install nightly
export PATH="$HOME/.cargo/bin:$PATH"
cargo install cargo-fuzz
- name: Initialize corpus dirs
working-directory: fuzz
run: |
chmod +x init_corpus.sh
./init_corpus.sh
- name: Clean stale fuzz build artifacts
run: rm -rf fuzz/target
- name: Build fuzz binaries
working-directory: fuzz
env:
RUSTFLAGS: ''
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo +nightly fuzz build
- name: Run all libFuzzer binaries in parallel
working-directory: fuzz
env:
ASAN_OPTIONS: detect_odr_violation=0
run: |
# Derive targets from what was actually built, not TARGETS.md.
# This avoids failures when TARGETS.md lists a target that didn't compile.
BINDIR="target/x86_64-unknown-linux-gnu/release"
mapfile -t bins < <(
grep -v '^#' TARGETS.md | grep -v '^$' | while read -r t; do
[ -x "$BINDIR/$t" ] && echo "$t" || echo "⚠️ skipping $t (binary not found)" >&2
done
)
if [ ${#bins[@]} -eq 0 ]; then
echo "❌ No fuzz binaries found in $BINDIR"; exit 1
fi
JOBS=$(nproc)
echo "Running ${#bins[@]} fuzz targets across $JOBS cores in parallel"
RUNSCRIPT=$(mktemp /tmp/fuzz-run-XXXXXX.sh)
cat > "$RUNSCRIPT" << 'SCRIPT'
#!/usr/bin/env bash
t=$1
bin="$PWD/target/x86_64-unknown-linux-gnu/release/$t"
echo "▶ $t"
timeout 210 "$bin" "corpus/$t" -- -max_total_time=60 -timeout=10 -max_len=100000 2>&1 | tail -3
echo "✓ $t"
SCRIPT
chmod +x "$RUNSCRIPT"
printf '%s\n' "${bins[@]}" | xargs -P"$JOBS" -I{} bash "$RUNSCRIPT" {}
EXIT=$?
rm -f "$RUNSCRIPT"
exit $EXIT
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
build:
name: Build
needs:
- setup
- build-dev
- test
- clippy
- fmt
- docs
- security
- verify
- fuzz
runs-on: [self-hosted, Linux, X64, builds]
env:
BLVM_FAST_BUILD: ${{ ((github.event_name != 'push' || github.ref != 'refs/heads/main') && (github.event_name != 'release')) && '1' || '' }}
if: |
always() &&
needs.setup.result == 'success' &&
(needs.build-dev.result == 'success' || needs.build-dev.result == 'skipped') &&
(github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch') &&
(github.event_name != 'push' || github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]'))) &&
((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true') ||
((needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.clippy.result == 'success' || needs.clippy.result == 'skipped') &&
(needs.fmt.result == 'success' || needs.fmt.result == 'skipped') &&
(needs.docs.result == 'success' || needs.docs.result == 'skipped') &&
(needs.security.result == 'success' || needs.security.result == 'skipped'))) &&
(needs.verify.result == 'skipped' || needs.verify.result == 'success') &&
(needs.fuzz.result == 'success' || needs.fuzz.result == 'skipped')
steps:
- uses: actions/checkout@v4
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Clean leftover path overrides
run: |
if [ -d "../blvm-protocol" ]; then rm -rf ../blvm-protocol; fi
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
if [ -d "../blvm-spec-lock" ]; then rm -rf ../blvm-spec-lock; fi
if [ -f ".cargo/config.toml" ]; then rm -f .cargo/config.toml; fi
- name: Install tidesdb native dependencies
run: |
if pkg-config --exists liblz4 libzstd 2>/dev/null && command -v cmake >/dev/null 2>&1 && pkg-config --exists openssl 2>/dev/null; then
echo "✅ tidesdb native deps already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq liblz4-dev libzstd-dev libsnappy-dev cmake pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq lz4 zstd snappy cmake pkgconf openssl >/dev/null 2>&1 || sudo pacman -Sy --noconfirm lz4 zstd snappy cmake pkgconf openssl
fi
- name: Isolate CARGO_HOME for this runner
run: |
mkdir -p "${RUNNER_TEMP}/cargo"
echo "CARGO_HOME=${RUNNER_TEMP}/cargo" >> "$GITHUB_ENV"
- name: Cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: bind-env
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Restore Cargo cache
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: restore
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Build
run: |
PROFILE_FLAG="--release"
[ -n "${BLVM_FAST_BUILD:-}" ] && PROFILE_FLAG="--profile release-fast"
cargo build $PROFILE_FLAG
release:
name: Release
needs: [build, build-dev, test, clippy, fmt, docs, security, verify, fuzz]
runs-on: [self-hosted, Linux, X64, builds]
permissions:
contents: write
id-token: write
attestations: write
if: |
always() &&
needs.build.result == 'success' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(github.event.head_commit == null ||
(!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[ci skip]') &&
!contains(github.event.head_commit.message, '[no ci]') &&
!contains(github.event.head_commit.message, '[skip release]'))) &&
((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true') ||
((needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.clippy.result == 'success' || needs.clippy.result == 'skipped') &&
(needs.fmt.result == 'success' || needs.fmt.result == 'skipped') &&
(needs.docs.result == 'success' || needs.docs.result == 'skipped') &&
(needs.security.result == 'success' || needs.security.result == 'skipped') &&
(needs.verify.result == 'success' || needs.verify.result == 'skipped'))) &&
(needs.fuzz.result == 'success' || needs.fuzz.result == 'skipped')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Strip [patch.crates-io] for crates.io-only CI
uses: BTCDecoded/rust-ci/strip-patch-crates-io@main
- name: Install OpenSSL development headers (openssl-sys / native-tls)
run: |
if pkg-config --exists openssl 2>/dev/null || command -v openssl >/dev/null 2>&1; then
echo "✅ openssl already present."
elif command -v apt-get >/dev/null 2>&1; then
bash scripts/apt-get-retry.sh update -qq
bash scripts/apt-get-retry.sh install -y -qq pkg-config libssl-dev
elif command -v pacman >/dev/null 2>&1; then
pacman -Qq openssl pkgconf >/dev/null 2>&1 || sudo pacman -Sy --noconfirm openssl pkgconf
fi
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Determine version
id: version
run: |
# Extract current version from [package] section
CURRENT=$(grep -A 10 '^\[package\]' Cargo.toml | grep '^version = ' | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
if [ -z "$CURRENT" ]; then
echo "❌ Could not determine current version from Cargo.toml"
grep -A 10 '^\[package\]' Cargo.toml | head -10
exit 1
fi
echo "Current version: ${CURRENT}"
if ! echo "$CURRENT" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "❌ Invalid version format: ${CURRENT} (expected X.Y.Z)"
exit 1
fi
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
CRATE_NAME=$(grep -A 5 '^\[package\]' Cargo.toml | grep '^name = ' | head -1 | sed -E 's/^name = "([^"]+)".*/\1/')
# Fetch all published versions from crates.io in one call (fast, no compilation).
echo "Fetching published versions for ${CRATE_NAME} from crates.io..."
API_RESPONSE=$(curl -sf \
-H "User-Agent: blvm-node-ci/1.0 (github.com/BTCDecoded/blvm-node)" \
"https://crates.io/api/v1/crates/${CRATE_NAME}/versions" 2>/dev/null || echo "")
PUBLISHED_VERSIONS=""
if [ -n "$API_RESPONSE" ] && ! echo "$API_RESPONSE" | grep -q '"detail"'; then
if command -v jq &>/dev/null; then
PUBLISHED_VERSIONS=$(echo "$API_RESPONSE" | jq -r '.versions[].num' 2>/dev/null || echo "")
else
PUBLISHED_VERSIONS=$(echo "$API_RESPONSE" | grep -oE '"num":"[^"]+"' | sed 's/"num":"//;s/"//')
fi
echo "Found $(echo "$PUBLISHED_VERSIONS" | wc -l | tr -d ' ') published versions."
else
echo "⚠️ crates.io API unavailable or crate not yet published — assuming version is free."
fi
# Find the lowest patch version >= PATCH+1 that is not already published.
PATCH=$((PATCH + 1))
MAX_ATTEMPTS=10
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
VERSION="${MAJOR}.${MINOR}.${PATCH}"
if echo "$PUBLISHED_VERSIONS" | grep -qxF "$VERSION"; then
echo "⚠️ ${VERSION} already published, trying next patch..."
PATCH=$((PATCH + 1))
ATTEMPT=$((ATTEMPT + 1))
else
echo "✅ Version ${VERSION} is available"
break
fi
done
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
echo "❌ Failed to find available version after ${MAX_ATTEMPTS} attempts"
exit 1
fi
# -unverified when manual run skipped tests
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
SKIP_TESTS="${{ github.event.inputs.skip_tests }}"
else
SKIP_TESTS="false"
fi
if [ "$SKIP_TESTS" = "true" ]; then
VERSION_TAG="v${VERSION}-unverified"
echo "⚠️ Releasing UNVERIFIED version: ${VERSION_TAG}"
else
VERSION_TAG="v${VERSION}"
echo "✅ Releasing version: ${VERSION_TAG}"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT
- name: Get latest blvm-protocol version from crates.io
id: protocol-version
run: |
PROTOCOL_VER=$(cargo search blvm-protocol --limit 1 2>/dev/null | head -1 | sed -E 's/^blvm-protocol = "([^"]+)".*/\1/' || echo "")
if [ -z "$PROTOCOL_VER" ]; then
echo "❌ blvm-protocol not found on crates.io. It must be published first."
exit 1
fi
echo "version=${PROTOCOL_VER}" >> $GITHUB_OUTPUT
echo "Using blvm-protocol version: ${PROTOCOL_VER}"
- name: Prepare release Cargo.toml
run: |
VERSION="${{ steps.version.outputs.version }}"
PROTOCOL_VER="${{ steps.protocol-version.outputs.version }}"
# Bump [package] version in place. The awk approach is safer than sed for version lines
# because Cargo.toml has many dependency version pins that naive regexes would corrupt.
awk -v ver="$VERSION" '
/^\[package\]/ { in_package = 1; print; next }
/^\[/ { in_package = 0 }
in_package && /^version = / {
print "version = \"" ver "\""
next
}
{ print }
' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml
# Verify the bump actually happened
ACTUAL_VER=$(grep -A 5 '^\[package\]' Cargo.toml | grep '^version = ' | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
if [ "$ACTUAL_VER" != "$VERSION" ]; then
echo "❌ Version bump failed: Cargo.toml has '$ACTUAL_VER', expected '$VERSION'"
grep -A 5 '^\[package\]' Cargo.toml
exit 1
fi
echo "✅ Cargo.toml version bumped to ${ACTUAL_VER}"
# Update blvm-protocol dependency to use latest crates.io version
sed -i 's|blvm-protocol = { path = "\.\./blvm-protocol"[^}]*}|blvm-protocol = "'"${PROTOCOL_VER}"'"|g' Cargo.toml
sed -i 's|^blvm-protocol = "[^"]*"|blvm-protocol = "'"${PROTOCOL_VER}"'"|g' Cargo.toml
# Also keep Cargo.toml.release as a copy for the smoke-test step
cp Cargo.toml Cargo.toml.release
echo "Release Cargo.toml prepared:"
echo " version: ${ACTUAL_VER}"
echo " blvm-protocol: ${PROTOCOL_VER}"
echo ""
echo "Dependencies section:"
grep -A 2 "^\[dependencies\]" Cargo.toml | head -5
echo ""
echo "Patch section check:"
grep -A 5 "^\[patch\.crates-io\]" Cargo.toml || echo " ✅ [patch.crates-io] removed - using published crates"
- name: Pre-release manifest smoke (crates.io Cargo.toml)
run: |
cargo metadata --format-version 1 --no-deps > /dev/null
echo "✅ Cargo.toml parses cleanly"
- name: Check for crates.io token
run: |
if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
echo "⚠️ CARGO_REGISTRY_TOKEN not set, skipping crate publication"
echo " Set CARGO_REGISTRY_TOKEN secret to enable crate publishing"
exit 0
fi
- name: Configure cargo for crates.io
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
cargo login "${CARGO_REGISTRY_TOKEN}"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Verify package
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
cargo package --list
- name: Dry-run publish
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
cargo publish --dry-run --no-verify --allow-dirty
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Check if version already exists
if: env.CARGO_REGISTRY_TOKEN != ''
id: check-version
run: |
VERSION="${{ steps.version.outputs.version }}"
CRATE_NAME=$(grep -A 5 '^\[package\]' Cargo.toml | grep '^name = ' | head -1 | sed -E 's/^name = "([^"]+)".*/\1/')
echo "Checking if ${CRATE_NAME} v${VERSION} already exists on crates.io..."
API_RESPONSE=$(curl -sf \
-H "User-Agent: blvm-node-ci/1.0 (github.com/BTCDecoded/blvm-node)" \
"https://crates.io/api/v1/crates/${CRATE_NAME}/versions" 2>/dev/null || echo "")
if [ -n "$API_RESPONSE" ] && ! echo "$API_RESPONSE" | grep -q '"detail"'; then
if command -v jq &>/dev/null; then
EXISTS=$(echo "$API_RESPONSE" | jq -r ".versions[] | select(.num == \"${VERSION}\") | .num" | head -1)
else
EXISTS=$(echo "$API_RESPONSE" | grep -oE "\"num\":\"${VERSION}\"" | head -1)
fi
if [ -n "$EXISTS" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "⚠️ Version ${VERSION} already published for ${CRATE_NAME}"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "✅ Version ${VERSION} not yet published for ${CRATE_NAME}"
fi
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "⚠️ Could not reach crates.io API — assuming not yet published"
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish to crates.io
if: env.CARGO_REGISTRY_TOKEN != ''
id: publish
run: |
set +e
VERSION="${{ steps.version.outputs.version }}"
CRATE_NAME=$(grep -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
# Fail if version already exists — the Determine version step should have picked a free one.
# A "skip" here means our API check was wrong (stale cache, pagination, etc.).
if [ "${{ steps.check-version.outputs.exists }}" = "true" ]; then
echo "❌ Version ${VERSION} already exists on crates.io — version determination picked a taken version"
echo "published=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "Publishing ${CRATE_NAME} v${VERSION} to crates.io..."
# Final sanity: confirm Cargo.toml on disk actually has the bumped version
ACTUAL_VER=$(grep -A 5 '^\[package\]' Cargo.toml | grep '^version = ' | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
if [ "$ACTUAL_VER" != "$VERSION" ]; then
echo "❌ Cargo.toml version mismatch at publish time: got '$ACTUAL_VER', expected '$VERSION'"
echo "published=false" >> $GITHUB_OUTPUT
exit 1
fi
PUBLISH_OUTPUT=$(cargo publish --no-verify --allow-dirty --token "${CARGO_REGISTRY_TOKEN}" 2>&1)
PUBLISH_EXIT_CODE=$?
if [ $PUBLISH_EXIT_CODE -eq 0 ]; then
echo "✅ Successfully published ${CRATE_NAME} v${VERSION}"
echo "published=true" >> $GITHUB_OUTPUT
echo "Waiting 30 seconds for crates.io to index..."
sleep 30
else
echo "❌ Failed to publish ${CRATE_NAME} v${VERSION}"
echo "Error output:"
echo "$PUBLISH_OUTPUT"
echo "published=false" >> $GITHUB_OUTPUT
# "already exists" here means our version check was wrong (stale API cache or
# a parallel run beat us) — treat it as a hard failure so the run is visible.
exit $PUBLISH_EXIT_CODE
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Verify published crate is available
if: |
env.CARGO_REGISTRY_TOKEN != '' &&
steps.check-version.outputs.exists == 'false'
run: |
VERSION="${{ steps.version.outputs.version }}"
CRATE_NAME=$(grep -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
echo "Verifying ${CRATE_NAME} v${VERSION} is available on crates.io..."
# Try to fetch the crate info
MAX_RETRIES=5
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if cargo search "$CRATE_NAME" --limit 1 2>/dev/null | grep -q "v${VERSION}"; then
echo "✅ ${CRATE_NAME} v${VERSION} is now available on crates.io"
echo "📦 Crate page: https://crates.io/crates/${CRATE_NAME}/${VERSION}"
break
else
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "⏳ Waiting for crates.io to index (attempt $RETRY_COUNT/$MAX_RETRIES)..."
sleep 10
else
echo "⚠️ ${CRATE_NAME} v${VERSION} not yet visible on crates.io (may take a few minutes)"
fi
fi
done
- name: Download attestation artifacts
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }}
uses: actions/download-artifact@v4
with:
pattern: '*-attestation-data'
merge-multiple: true
path: attestation-data
- name: Collect build metadata for attestation
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }}
id: metadata
run: |
# Get toolchain versions
RUSTC_VERSION=$(rustc --version | cut -d' ' -f2)
CARGO_VERSION=$(cargo --version | cut -d' ' -f2)
# Z3 version (blvm-spec-lock uses Z3)
Z3_VERSION="unknown"
if command -v z3 &> /dev/null; then
Z3_VERSION=$(z3 --version 2>&1 | head -1 || echo "unknown")
fi
if [ -f Cargo.lock ]; then
CARGO_LOCK_HASH=$(sha256sum Cargo.lock | cut -d' ' -f1)
else
CARGO_LOCK_HASH=$(sha256sum Cargo.toml | cut -d' ' -f1)
fi
# Check if debug assertions are enabled (check Cargo.toml or build flags)
DEBUG_ASSERTIONS="false"
if grep -q "debug-assertions = true" Cargo.toml 2>/dev/null; then
DEBUG_ASSERTIONS="true"
fi
# Extract spec-lock data from artifacts
SPEC_LOCK_VERIFIED="false"
SPEC_LOCK_CONSENSUS_FUNCS="0"
SPEC_LOCK_NODE_FUNCS="0"
SPEC_LOCK_TOTAL="0"
SPEC_LOCK_OUTPUT_HASH=""
if [ -f "attestation-data/spec_lock_consensus_output.txt" ] && [ -f "attestation-data/spec_lock_node_output.txt" ]; then
SPEC_LOCK_VERIFIED="true"
SPEC_LOCK_CONSENSUS_FUNCS=$(grep -c "Status: PASSED" attestation-data/spec_lock_consensus_output.txt 2>/dev/null || echo "0")
SPEC_LOCK_NODE_FUNCS=$(grep -c "Status: PASSED" attestation-data/spec_lock_node_output.txt 2>/dev/null || echo "0")
SPEC_LOCK_TOTAL=$((SPEC_LOCK_CONSENSUS_FUNCS + SPEC_LOCK_NODE_FUNCS))
SPEC_LOCK_OUTPUT_HASH=$(sha256sum attestation-data/spec_lock_consensus_output.txt attestation-data/spec_lock_node_output.txt | sha256sum | cut -d' ' -f1)
fi
# Output all metadata (spec-lock verification)
echo "spec_lock_verified=$SPEC_LOCK_VERIFIED" >> $GITHUB_OUTPUT
echo "spec_lock_consensus_functions=$SPEC_LOCK_CONSENSUS_FUNCS" >> $GITHUB_OUTPUT
echo "spec_lock_node_functions=$SPEC_LOCK_NODE_FUNCS" >> $GITHUB_OUTPUT
echo "spec_lock_total_functions=$SPEC_LOCK_TOTAL" >> $GITHUB_OUTPUT
echo "spec_lock_output_hash=$SPEC_LOCK_OUTPUT_HASH" >> $GITHUB_OUTPUT
echo "debug_assertions_enabled=$DEBUG_ASSERTIONS" >> $GITHUB_OUTPUT
echo "rustc_version=$RUSTC_VERSION" >> $GITHUB_OUTPUT
echo "z3_version=$Z3_VERSION" >> $GITHUB_OUTPUT
echo "cargo_version=$CARGO_VERSION" >> $GITHUB_OUTPUT
echo "cargo_lock_hash=$CARGO_LOCK_HASH" >> $GITHUB_OUTPUT
echo "📋 Build Metadata:"
echo " Spec-lock verified: $SPEC_LOCK_VERIFIED"
echo " Spec-lock consensus functions: $SPEC_LOCK_CONSENSUS_FUNCS"
echo " Spec-lock node functions: $SPEC_LOCK_NODE_FUNCS"
echo " Spec-lock total: $SPEC_LOCK_TOTAL"
echo " Spec-lock output hash: $SPEC_LOCK_OUTPUT_HASH"
echo " Debug assertions: $DEBUG_ASSERTIONS"
echo " rustc version: $RUSTC_VERSION"
echo " Z3 version: $Z3_VERSION"
echo " cargo version: $CARGO_VERSION"
echo " Cargo.lock hash: $CARGO_LOCK_HASH"
- name: Create build attestation
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }}
id: attestation
run: |
# Create predicate JSON file
cat > predicate.json << EOF
{
"spec_lock_verified": ${{ steps.metadata.outputs.spec_lock_verified == 'true' && 'true' || 'false' }},
"spec_lock_total_functions": ${{ steps.metadata.outputs.spec_lock_total_functions }},
"spec_lock_output_hash": "${{ steps.metadata.outputs.spec_lock_output_hash }}",
"debug_assertions_enabled": ${{ steps.metadata.outputs.debug_assertions_enabled == 'true' && 'true' || 'false' }},
"rustc_version": "${{ steps.metadata.outputs.rustc_version }}",
"z3_version": "${{ steps.metadata.outputs.z3_version }}",
"cargo_version": "${{ steps.metadata.outputs.cargo_version }}",
"cargo_lock_hash": "${{ steps.metadata.outputs.cargo_lock_hash }}"
}
EOF
cat predicate.json
- name: Upload build metadata
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }}
uses: actions/upload-artifact@v4
with:
name: build-metadata
path: predicate.json
retention-days: 365
- name: Attest build provenance
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }}
uses: actions/attest-build-provenance@v1
with:
subject-name: "blvm-node"
subject-digest: "sha256:${{ steps.metadata.outputs.cargo_lock_hash }}"
push-to-registry: false
show-summary: true
- name: Create git tag
run: |
VERSION_TAG="${{ steps.version.outputs.version_tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if tag already exists
if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then
echo "⚠️ Tag ${VERSION_TAG} already exists, skipping tag creation"
else
git tag -a "$VERSION_TAG" -m "Release ${VERSION_TAG}"
git push origin "$VERSION_TAG"
echo "✅ Created and pushed tag ${VERSION_TAG}"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.version_tag }}
name: Release ${{ steps.version.outputs.version_tag }}
body: |
## ${{ steps.version.outputs.version_tag }}
Automated release of blvm-node.
**Published to crates.io**: ${{ steps.publish.outputs.published == 'true' && '✅ Yes' || (steps.check-version.outputs.exists == 'true' && '⚠️ Already exists' || '❌ Failed or skipped') }}
### Changes
See [commit history](https://github.com/${{ github.repository }}/compare/${{ github.event.before }}...${{ github.sha }})
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
teardown:
name: Teardown (cleanup runner disk)
runs-on: [self-hosted, Linux, X64, builds]
if: always()
needs: [build-dev, test, clippy, verify, fmt, docs, security, fuzz, build]
steps:
- name: Delete current run target dir
run: |
RUN_DIR="${{ github.workspace }}/.build/gh-blvm-node-build/${{ github.run_id }}"
if [ -d "$RUN_DIR" ]; then
echo "Removing $RUN_DIR ($(du -sh "$RUN_DIR" 2>/dev/null | cut -f1))"
rm -rf "$RUN_DIR"
fi
# Belt-and-suspenders: also remove any other leftover run dirs
find ${{ github.workspace }}/.build/gh-blvm-node-build -mindepth 1 -maxdepth 1 -type d \
-exec rm -rf {} + 2>/dev/null || true
# Clean fuzz temp dir
rm -rf ${{ github.workspace }}/.build/gh-fuzz-blvm-node 2>/dev/null || true
echo "=== disk after teardown ==="
df -h /