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: '1'
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 }}
steps:
- name: Disk check
uses: BTCDecoded/rust-ci/runner-disk-guard@main
- 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 consensus
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-consensus
path: _temp-blvm-consensus
- name: Move consensus
run: |
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
mv _temp-blvm-consensus ../blvm-consensus
- name: Setup local dependencies for build
run: |
mkdir -p .cargo
cat > .cargo/config.toml << EOF
[patch.crates-io]
blvm-consensus = { path = "../blvm-consensus" }
EOF
echo "✅ Created .cargo/config.toml for local dependency builds"
- name: Cache
id: setup-cache
run: |
CACHE_ROOT="/tmp/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 ../blvm-consensus/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}"
# 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"
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 "cache-key=$CACHE_KEY" >> $GITHUB_OUTPUT
echo "consensus-cache-key=$CONSENSUS_CACHE_KEY" >> $GITHUB_OUTPUT
mkdir -p "$CARGO_CACHE_DIR"/{registry,git} "$TARGET_CACHE_DIR" "$CONSENSUS_TARGET_DIR"
- name: Install Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
test:
name: Test
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 != '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: Checkout consensus
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-consensus
path: _temp-blvm-consensus
- name: Move consensus
run: |
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
mv _temp-blvm-consensus ../blvm-consensus
- name: Cache
run: |
CACHE_ROOT="/tmp/runner-cache"
CACHE_KEY="${{ needs.setup.outputs.cache-key }}"
CONSENSUS_CACHE_KEY="${{ needs.setup.outputs.consensus-cache-key }}"
echo "CARGO_CACHE_DIR=$CACHE_ROOT/cargo/$CACHE_KEY" >> $GITHUB_ENV
echo "TARGET_CACHE_DIR=$CACHE_ROOT/target/$CACHE_KEY" >> $GITHUB_ENV
echo "CONSENSUS_TARGET_DIR=$CACHE_ROOT/blvm-consensus-target/$CONSENSUS_CACHE_KEY" >> $GITHUB_ENV
- 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 with coverage
run: |
# Install tarpaulin
cargo install cargo-tarpaulin --version 0.27.0
# Run tests with coverage in one step
# Use --lib --bins --tests to ensure all test types run
# Use --skip-clean to reuse cached build artifacts, --timeout to prevent hangs
cargo tarpaulin --lib --bins --tests --out xml --output-dir coverage --skip-clean --timeout 120
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage/cobertura.xml
- 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
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 != '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: Checkout consensus
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-consensus
path: _temp-blvm-consensus
- name: Move consensus
run: |
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
mv _temp-blvm-consensus ../blvm-consensus
- name: Setup local dependencies for build
run: |
mkdir -p .cargo
cat > .cargo/config.toml << EOF
[patch.crates-io]
blvm-consensus = { path = "../blvm-consensus" }
EOF
echo "✅ Created .cargo/config.toml for local dependency builds"
- 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
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
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 != '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
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 != '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: 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: Build docs
run: cargo doc --no-deps
- name: Check docs
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 != '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: Install cargo-audit
run: cargo install cargo-audit --version 0.22.1 --locked --force
- name: Run security audit
run: |
export PATH="${HOME}/.cargo/bin:${PATH}"
command -v cargo-audit
cargo-audit --version
cargo audit
build:
name: Build
needs:
- setup
- test
- clippy
- fmt
- docs
- security
runs-on: [self-hosted, Linux, X64, builds]
if: |
always() &&
needs.setup.result == 'success' &&
(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.clippy.result == 'success' &&
needs.fmt.result == 'success' && needs.docs.result == 'success' &&
needs.security.result == 'success'))
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: Checkout consensus
uses: actions/checkout@v4
with:
repository: BTCDecoded/blvm-consensus
path: _temp-blvm-consensus
- name: Move consensus
run: |
if [ -d "../blvm-consensus" ]; then rm -rf ../blvm-consensus; fi
mv _temp-blvm-consensus ../blvm-consensus
- name: Setup local dependencies for build
run: |
mkdir -p .cargo
cat > .cargo/config.toml << EOF
[patch.crates-io]
blvm-consensus = { path = "../blvm-consensus" }
EOF
echo "✅ Created .cargo/config.toml for local dependency builds"
- name: Cache
run: |
CACHE_ROOT="/tmp/runner-cache"
CACHE_KEY="${{ needs.setup.outputs.cache-key }}"
CONSENSUS_CACHE_KEY="${{ needs.setup.outputs.consensus-cache-key }}"
echo "CARGO_CACHE_DIR=$CACHE_ROOT/cargo/$CACHE_KEY" >> $GITHUB_ENV
echo "TARGET_CACHE_DIR=$CACHE_ROOT/target/$CACHE_KEY" >> $GITHUB_ENV
echo "CONSENSUS_TARGET_DIR=$CACHE_ROOT/blvm-consensus-target/$CONSENSUS_CACHE_KEY" >> $GITHUB_ENV
- 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: cargo build --release
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
release:
name: Release
needs: [build, test, clippy, fmt, docs, security]
runs-on: [self-hosted, Linux, X64, builds]
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_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]') &&
!contains(github.event.head_commit.message, '[skip release]'))) &&
((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true') ||
(needs.test.result == 'success' && needs.clippy.result == 'success' &&
needs.fmt.result == 'success' && needs.docs.result == 'success' &&
needs.security.result == 'success'))
permissions:
contents: write
id-token: write
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 Rust
uses: BTCDecoded/rust-ci/install-rust-toolchain@main
- name: Determine version
id: version
run: |
# Auto-increment patch version from Cargo.toml
# Extract only the package version (first version = after [package])
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"
echo "Debug: Looking for version in [package] section"
grep -A 10 '^\[package\]' Cargo.toml | head -10
exit 1
fi
echo "Current version: ${CURRENT}"
# Validate version format (should be X.Y.Z)
if ! echo "$CURRENT" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "❌ Invalid version format: ${CURRENT} (expected X.Y.Z)"
exit 1
fi
# Increment patch version (X.Y.Z -> X.Y.(Z+1))
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Validate components are numeric
if ! [[ "$MAJOR" =~ ^[0-9]+$ ]] || ! [[ "$MINOR" =~ ^[0-9]+$ ]] || ! [[ "$PATCH" =~ ^[0-9]+$ ]]; then
echo "❌ Version components must be numeric: ${MAJOR}.${MINOR}.${PATCH}"
exit 1
fi
PATCH=$((PATCH + 1))
VERSION="${MAJOR}.${MINOR}.${PATCH}"
# Check if version already exists and increment until we find a free one
CRATE_NAME=$(grep -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
MAX_ATTEMPTS=10
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
echo "Checking if ${CRATE_NAME} v${VERSION} already exists on crates.io..."
# Update cargo index first to ensure we have latest data
cargo update --dry-run 2>/dev/null || true
VERSION_EXISTS="false"
# Method 1: Check using cargo search with updated index (most reliable for cargo's view)
CARGO_SEARCH_OUTPUT=$(cargo search "$CRATE_NAME" --limit 100 2>/dev/null || echo "")
if echo "$CARGO_SEARCH_OUTPUT" | grep -qE "\"${CRATE_NAME}\".*\"${VERSION}\""; then
VERSION_EXISTS="true"
echo "Found via cargo search (with updated index)"
else
# Method 2: Check using crates.io API
API_RESPONSE=$(curl -s --max-time 10 "https://crates.io/api/v1/crates/${CRATE_NAME}/versions" 2>/dev/null || echo "")
if [ -n "$API_RESPONSE" ] && ! echo "$API_RESPONSE" | grep -q '"detail"'; then
# Parse JSON to check if version exists
if command -v jq &> /dev/null; then
EXISTS_CHECK=$(echo "$API_RESPONSE" | jq -r ".versions[] | select(.num == \"${VERSION}\") | .num" | head -1)
if [ "$EXISTS_CHECK" = "$VERSION" ]; then
VERSION_EXISTS="true"
echo "Found via crates.io API"
fi
else
# Fallback: grep for version in JSON
if echo "$API_RESPONSE" | grep -q "\"num\":\"${VERSION}\""; then
VERSION_EXISTS="true"
echo "Found via crates.io API (grep)"
fi
fi
fi
# Method 3: If still not found, try a temporary Cargo.toml update and dry-run check
# This uses cargo's index directly (same as cargo publish)
if [ "$VERSION_EXISTS" = "false" ]; then
# Save original Cargo.toml
cp Cargo.toml Cargo.toml.bak
# Temporarily update [package] version only (same as blvm-consensus: do not sed every
# top-level `version =` line — that corrupts e.g. [dependencies.smallvec] version pins).
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
DRY_RUN_OUTPUT=$(cargo publish --dry-run --allow-dirty 2>&1 || true)
# Restore original Cargo.toml
mv Cargo.toml.bak Cargo.toml
if echo "$DRY_RUN_OUTPUT" | grep -qiE "(already exists|already been uploaded|already exists on crates.io)"; then
VERSION_EXISTS="true"
echo "Found via cargo publish dry-run (cargo index check)"
fi
fi
fi
if [ "$VERSION_EXISTS" = "true" ]; then
echo "⚠️ Version ${VERSION} already exists, incrementing..."
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
PATCH=$((PATCH + 1))
VERSION="${MAJOR}.${MINOR}.${PATCH}"
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
VERSION_TAG="v${VERSION}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT
echo "✅ Releasing version: ${VERSION_TAG}"
- name: Get latest blvm-consensus version from crates.io
id: consensus-version
run: |
CONSENSUS_VER=$(cargo search blvm-consensus --limit 1 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
if [ -z "$CONSENSUS_VER" ]; then
echo "ERROR: blvm-consensus not found on crates.io. It must be published first."
exit 1
fi
echo "version=${CONSENSUS_VER}" >> $GITHUB_OUTPUT
echo "Using blvm-consensus version: ${CONSENSUS_VER}"
- name: Update Cargo.toml to use crates.io dependencies
run: |
VERSION="${{ steps.version.outputs.version }}"
CONSENSUS_VER="${{ steps.consensus-version.outputs.version }}"
# Update [package] version only (see blvm-consensus release job; naive sed breaks optional dep pins).
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
# Update blvm-consensus dependency to use crates.io version
sed -i 's|blvm-consensus = {[^}]*}|blvm-consensus = "'"${CONSENSUS_VER}"'"|g' Cargo.toml
echo "Updated Cargo.toml:"
echo " package version: ${VERSION}"
echo " blvm-consensus: ${CONSENSUS_VER}"
grep -A 12 '^\[package\]' Cargo.toml | grep '^version = ' | head -1
grep '^blvm-consensus = ' Cargo.toml | head -1
- 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 --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 -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
echo "🔍 Checking if ${CRATE_NAME} v${VERSION} already exists on crates.io..."
# Update cargo index first to ensure we have latest data
cargo update --dry-run 2>/dev/null || true
# Use crates.io API to check if version exists (more reliable than cargo search)
# The API returns versions in the "versions" array
API_RESPONSE=$(curl -s "https://crates.io/api/v1/crates/${CRATE_NAME}/versions" 2>/dev/null || echo "")
if [ -z "$API_RESPONSE" ] || echo "$API_RESPONSE" | grep -q '"detail"'; then
echo "⚠️ Could not check crates.io API (crate may not exist yet), using cargo search as fallback"
# Fallback to cargo search - check all versions
SEARCH_OUTPUT=$(cargo search "$CRATE_NAME" --limit 100 2>/dev/null || echo "")
if echo "$SEARCH_OUTPUT" | grep -qE "\"${CRATE_NAME}\".*\"${VERSION}\""; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "⚠️ Version ${VERSION} already published for ${CRATE_NAME} (found via cargo search)"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "✅ Version ${VERSION} not yet published for ${CRATE_NAME} (cargo search)"
fi
else
# Show existing versions for debugging
echo "📋 Existing versions on crates.io:"
if command -v jq &> /dev/null; then
echo "$API_RESPONSE" | jq -r ".versions[].num" | head -5
else
echo "$API_RESPONSE" | grep -o '"num":"[^"]*"' | sed 's/"num":"\(.*\)"/\1/' | head -5
fi
echo ""
# Parse JSON to check if version exists (using jq if available, or grep as fallback)
if command -v jq &> /dev/null; then
VERSION_EXISTS=$(echo "$API_RESPONSE" | jq -r ".versions[] | select(.num == \"${VERSION}\") | .num" | head -1)
if [ "$VERSION_EXISTS" = "$VERSION" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "⚠️ Version ${VERSION} already published for ${CRATE_NAME} (found via API)"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "✅ Version ${VERSION} not yet published for ${CRATE_NAME} (API check)"
fi
else
# Fallback: grep for version in JSON
if echo "$API_RESPONSE" | grep -q "\"num\":\"${VERSION}\""; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "⚠️ Version ${VERSION} already published for ${CRATE_NAME} (found via grep)"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "✅ Version ${VERSION} not yet published for ${CRATE_NAME} (grep check)"
fi
fi
fi
echo ""
echo "📊 Version check result: ${{ steps.check-version.outputs.exists }}"
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/')
echo "🔍 Publish Debug Info:"
echo " Version: ${VERSION}"
echo " Crate: ${CRATE_NAME}"
echo " Version exists check: ${{ steps.check-version.outputs.exists }}"
# Skip if version already exists
if [ "${{ steps.check-version.outputs.exists }}" = "true" ]; then
echo "⚠️ Version ${VERSION} already exists on crates.io, skipping publish"
echo "published=false" >> $GITHUB_OUTPUT
exit 0
fi
echo ""
echo "📦 Publishing ${CRATE_NAME} v${VERSION} to crates.io..."
echo "Running: cargo publish --token [REDACTED] --allow-dirty"
# Update cargo index before publishing to ensure we have latest data
cargo update --dry-run 2>/dev/null || true
PUBLISH_OUTPUT=$(cargo publish --token "${CARGO_REGISTRY_TOKEN}" --allow-dirty 2>&1)
PUBLISH_EXIT_CODE=$?
echo ""
echo "📋 Publish Output:"
echo "$PUBLISH_OUTPUT"
echo ""
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
echo "✅ Publish complete - version should be available on crates.io"
else
# Check if error is due to version already existing
if echo "$PUBLISH_OUTPUT" | grep -qiE "(already exists|already been uploaded|already exists on crates.io)"; then
echo "⚠️ Version ${VERSION} already exists on crates.io (detected during publish)"
echo "published=false" >> $GITHUB_OUTPUT
exit 0
else
echo "❌ Failed to publish ${CRATE_NAME} v${VERSION}"
echo "Exit code: $PUBLISH_EXIT_CODE"
echo "Error output:"
echo "$PUBLISH_OUTPUT"
echo ""
echo "::error::cargo publish failed — job must fail until this is fixed"
echo "published=false" >> $GITHUB_OUTPUT
exit 1
fi
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Verify published crate is available
if: |
env.CARGO_REGISTRY_TOKEN != '' &&
steps.publish.outputs.published == 'true'
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: 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: Generate release notes
id: release_notes
run: |
VERSION="${{ steps.version.outputs.version }}"
VERSION_TAG="${{ steps.version.outputs.version_tag }}"
CRATE_NAME=$(grep -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
DESCRIPTION=$(grep -E '^description = ' Cargo.toml | sed -E 's/^description = "([^"]+)".*/\1/' || echo "Bitcoin protocol abstraction layer")
# Get commit range for this release
if [ -n "${{ github.event.before }}" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
COMMIT_RANGE="${{ github.event.before }}..${{ github.sha }}"
COMMIT_COUNT=$(git rev-list --count "$COMMIT_RANGE" 2>/dev/null || echo "?")
else
COMMIT_RANGE="HEAD~10..HEAD"
COMMIT_COUNT="?"
fi
cat > /tmp/release_notes.md << EOF
## $VERSION_TAG
**$DESCRIPTION**
### 📦 Installation
Add to your \`Cargo.toml\`:
\`\`\`toml
[dependencies]
$CRATE_NAME = "$VERSION"
\`\`\`
Or install via cargo:
\`\`\`bash
cargo add $CRATE_NAME@$VERSION
\`\`\`
### 🔗 Links
- **Crates.io**: [${CRATE_NAME} v${VERSION}](https://crates.io/crates/${CRATE_NAME}/${VERSION})
- **Documentation**: [docs.rs/${CRATE_NAME}/${VERSION}](https://docs.rs/${CRATE_NAME}/${VERSION})
- **Repository**: [GitHub](https://github.com/${{ github.repository }})
### 📝 Changes
This release includes $COMMIT_COUNT commits since the last release.
**Published to crates.io**: ${{ steps.publish.outputs.published == 'true' && '✅ Yes' || '⚠️ Already exists or skipped' }}
See [full commit history](https://github.com/${{ github.repository }}/compare/$COMMIT_RANGE) for details.
EOF
echo "notes<<EOF" >> $GITHUB_OUTPUT
cat /tmp/release_notes.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- 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.release_notes.outputs.notes }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}