name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [published]
repository_dispatch:
types: [build-chain, upstream-changed]
workflow_dispatch:
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 }}
steps:
- name: Disk check
uses: BTCDecoded/rust-ci/runner-disk-guard@main
with:
show-df: false
- 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: Regenerate Orange Paper constants
run: |
echo "🔄 Regenerating constants from Orange Paper..."
# Orange Paper constants moved to blvm-primitives; no regeneration here.
- name: Cache
id: setup-cache
run: |
CACHE_ROOT="/tmp/runner-cache"
# Generate cache key from Cargo.toml (library crates do not commit Cargo.lock)
DEPS_KEY=$(sha256sum 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}"
# Set up cache directories
CARGO_CACHE_DIR="$CACHE_ROOT/cargo/$CACHE_KEY"
TARGET_CACHE_DIR="$CACHE_ROOT/target/$CACHE_KEY"
echo "CARGO_CACHE_DIR=$CARGO_CACHE_DIR" >> $GITHUB_ENV
echo "TARGET_CACHE_DIR=$TARGET_CACHE_DIR" >> $GITHUB_ENV
echo "cache-key=$CACHE_KEY" >> $GITHUB_OUTPUT
mkdir -p "$CARGO_CACHE_DIR"/{registry,git} "$TARGET_CACHE_DIR"
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 != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]'))
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 }}
- 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
run: |
# Run tests only (no coverage) for fast PR feedback
# Coverage runs separately in coverage.yml workflow
# Note: --locked omitted; library crate does not commit Cargo.lock
cargo test --all-features
- name: Capture test output hash
if: github.ref == 'refs/heads/main'
run: |
# Capture test output and hash it
TEST_OUTPUT_FILE="test_output.txt"
# Re-run tests with output capture (or use existing test output if available)
cargo test --lib --bins --tests -- --nocapture 2>&1 | tee "$TEST_OUTPUT_FILE" || true
UNIT_TESTS_HASH=$(sha256sum "$TEST_OUTPUT_FILE" | cut -d' ' -f1)
echo "unit_tests_hash=$UNIT_TESTS_HASH" >> $GITHUB_ENV
echo "Test output hash: $UNIT_TESTS_HASH"
- name: Upload test data for attestation
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: test-attestation-data
path: |
test_output.txt
coverage/cobertura.xml
retention-days: 90
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
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 != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]'))
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
with:
components: clippy
- 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 != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]'))
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: Format
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 != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_docs]'))
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 }}
- 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: Clean target to avoid dep info corruption
run: |
# Clean only the problematic dep info files, not the entire target
# This helps avoid race conditions with concurrent builds
if [ -d "./target" ]; then
find ./target -name "*.d" -type f -delete 2>/dev/null || true
fi
- name: 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 != 'push' || github.event.head_commit == null ||
!contains(github.event.head_commit.message, '[skip_tests]'))
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 audit
run: cargo install cargo-audit --version 0.22.1 --locked
- name: Audit
run: cargo audit
verify:
name: Verify
needs: setup
runs-on: [self-hosted, Linux, X64, builds]
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]'))
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: Setup blvm-spec (PROTOCOL.md + ARCHITECTURE.md for verification)
uses: BTCDecoded/rust-ci/setup-blvm-spec@main
- 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 blvm-spec-lock (with Z3 for full contract verification)
run: cargo install blvm-spec-lock --locked --features z3
- name: Run blvm-spec-lock verify
run: |
echo "🔒 Running blvm-spec-lock verification..."
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 (fallback)"
else
echo "❌ No spec found at ../blvm-spec (PROTOCOL.md, ARCHITECTURE.md, or THE_ORANGE_PAPER.md). setup-spec.sh should have cloned blvm-spec."
exit 1
fi
# Strict mode: SPEC_LOCK_STRICT=1 works with cargo-spec-lock builds that lack `--strict` on the CLI.
export SPEC_LOCK_STRICT=1
cargo-spec-lock verify $SPEC_ARGS 2>&1 | tee spec_lock_output.txt
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "❌ blvm-spec-lock verification failed"
exit $EXIT_CODE
fi
echo "✅ blvm-spec-lock verification passed"
- name: Create spec-lock attestation data
run: |
CONSENSUS_FUNCS=$(grep -c "Status: PASSED" spec_lock_output.txt 2>/dev/null || echo "0")
echo "spec_lock_consensus_functions=$CONSENSUS_FUNCS" >> $GITHUB_ENV
echo "spec_lock_verified=true" >> $GITHUB_ENV
HASH=$(sha256sum spec_lock_output.txt | cut -d' ' -f1)
echo "spec_lock_output_hash=$HASH" >> $GITHUB_ENV
echo "📋 Spec-lock: consensus=$CONSENSUS_FUNCS functions verified"
- 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_output.txt
retention-days: 90
- name: Run property tests with all features
run: |
echo "🧪 Running property-based tests with all features (35+ tests)..."
# Property tests can run in parallel (they're designed to be independent)
cargo test --all-features --test consensus_property_tests || {
echo "❌ Property tests failed"
echo "Mathematical invariants must hold - review failures before release"
exit 1
}
echo "✅ All property tests passed"
- name: MIRI undefined behavior checks
continue-on-error: true
run: |
echo "🔍 Running MIRI checks for undefined behavior..."
rustup toolchain install nightly --component miri || {
echo "⚠️ Could not install MIRI, skipping (non-blocking)"
exit 0
}
cargo +nightly miri setup || {
echo "⚠️ MIRI setup failed, skipping (non-blocking)"
exit 0
}
# Run a subset of critical tests under MIRI (full suite can be slow)
echo "Running critical property tests under MIRI..."
cargo +nightly miri test --all-features --test consensus_property_tests --lib -- --test-threads=1 || {
echo "⚠️ MIRI checks found potential undefined behavior (non-blocking)"
echo "Review MIRI output for details"
exit 0
}
echo "✅ MIRI checks passed"
fuzz-build:
name: Fuzz (build once)
needs: [setup]
runs-on: [self-hosted, Linux, X64, builds]
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_fuzz]'))
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 }}
- 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
rustup component add rustfmt --toolchain nightly 2>/dev/null || true
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: Build all fuzz binaries (once per workflow)
working-directory: fuzz
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo +nightly fuzz build
- name: Save Cargo cache
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
- name: Stash fuzz target for shard jobs
run: |
STASH="/tmp/gh-fuzz-blvm-consensus/${GITHUB_RUN_ID}"
rm -rf "$STASH"
mkdir -p "$STASH"
cp -a fuzz/target "$STASH/"
fuzz:
name: Fuzz (shard ${{ matrix.shard }}/8)
needs: [setup, fuzz-build]
runs-on: [self-hosted, Linux, X64, builds]
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_fuzz]'))
strategy:
fail-fast: false
max-parallel: 6
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7]
steps:
- uses: actions/checkout@v4
- name: Initialize corpus dirs
working-directory: fuzz
run: |
chmod +x init_corpus.sh
./init_corpus.sh
- name: Restore fuzz target from shared stash
run: |
STASH="/tmp/gh-fuzz-blvm-consensus/${GITHUB_RUN_ID}"
if [ ! -d "$STASH/target" ]; then
echo "Expected $STASH/target from fuzz-build (same GITHUB_RUN_ID, shared /tmp on runner pool)"
exit 1
fi
rm -rf fuzz/target
cp -a "$STASH/target" fuzz/target
- name: Run libFuzzer binaries (TARGETS.md shard)
working-directory: fuzz
env:
ASAN_OPTIONS: detect_odr_violation=0
run: |
mapfile -t bins < <(grep -v '^#' TARGETS.md | grep -v '^$' || true)
shard=${{ matrix.shard }}
shards=8
idx=0
for t in "${bins[@]}"; do
if (( idx % shards != shard )); then
((idx++)) || true
continue
fi
((idx++)) || true
echo "=== shard $shard/$shards: $t ==="
bin="$PWD/target/x86_64-unknown-linux-gnu/release/$t"
if [ ! -x "$bin" ]; then
echo "missing executable $bin"
exit 1
fi
timeout 210 "$bin" "corpus/$t" -- \
-max_total_time=120 \
-timeout=10 \
-max_len=100000
done
build:
name: Build
needs: [setup, test, clippy, fmt, docs, security, verify, fuzz-build, 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' &&
(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]'))) &&
((needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.verify.result == 'success' || needs.verify.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.fuzz-build.result == 'success' && needs.fuzz.result == 'success') ||
(needs.fuzz-build.result == 'skipped' && 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: 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
- name: Save Cargo cache (target only)
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: save
save-target-only: "true"
- name: Prune old runner caches
if: always()
uses: BTCDecoded/rust-ci/runner-cargo-cache@main
with:
operation: prune
release:
name: Release
needs: [build, test, verify, fuzz-build, fuzz]
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]'))) &&
((needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.verify.result == 'success' || needs.verify.result == 'skipped') &&
((needs.fuzz-build.result == 'success' && needs.fuzz.result == 'success') ||
(needs.fuzz-build.result == 'skipped' && needs.fuzz.result == 'skipped')))
permissions:
contents: write
id-token: write
attestations: write
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
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: Check for breaking changes
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
echo "🔍 Checking for breaking changes in commit messages..."
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LAST_TAG" ]; then
COMMIT_RANGE="${LAST_TAG}..HEAD"
else
COMMIT_RANGE="HEAD~20..HEAD"
fi
# Check for breaking change indicators
if git log "$COMMIT_RANGE" --pretty=format:"%s" | grep -qiE "(breaking|!|major|incompatible)"; then
echo "⚠️ WARNING: Potential breaking changes detected in commit messages"
echo " Consider bumping major or minor version instead of patch"
git log "$COMMIT_RANGE" --pretty=format:" - %s" | grep -iE "(breaking|!|major|incompatible)" || true
else
echo "✅ No obvious breaking changes detected"
fi
- name: Check CHANGELOG
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
echo "📝 Checking CHANGELOG.md..."
if [ ! -f "CHANGELOG.md" ]; then
echo "⚠️ CHANGELOG.md not found (optional but recommended)"
exit 0
fi
# Check if there's an [Unreleased] section with content
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
UNRELEASED_LINES=$(sed -n '/^## \[Unreleased\]/,/^## /p' CHANGELOG.md | grep -c "^-" || echo "0")
if [ "$UNRELEASED_LINES" -gt 0 ]; then
echo "✅ CHANGELOG.md has [Unreleased] entries ($UNRELEASED_LINES items)"
else
echo "⚠️ CHANGELOG.md has [Unreleased] section but no entries"
fi
else
echo "⚠️ CHANGELOG.md doesn't have [Unreleased] section"
fi
- 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)
PATCH=$((PATCH + 1))
CRATE_NAME=$(grep -A 5 '^\[package\]' Cargo.toml | grep '^name = ' | head -1 | sed -E 's/^name = "([^"]+)".*/\1/')
echo "Fetching published versions for ${CRATE_NAME} from crates.io..."
API_RESPONSE=$(curl -sf \
-H "User-Agent: blvm-ci/1.0 (github.com/BTCDecoded)" \
"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 — assuming version is free."
fi
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 ${VERSION} already exists, incrementing..."
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
# Release tags are always vX.Y.Z — CI gates require tests + spec-lock verify on every release
VERSION_TAG="v${VERSION}"
echo "✅ Release tag: ${VERSION_TAG}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT
- name: Update version in Cargo.toml
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Updating package version to ${VERSION}"
# Use awk to update only the version in [package] section
export VERSION
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
echo "✅ Updated Cargo.toml to version ${VERSION}"
echo "Package version line:"
grep "^version = " Cargo.toml | head -1
- name: Pre-release smoke test
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
# Full compile/tests already ran in CI; validate manifest after version bump only.
cargo metadata --format-version 1 --no-deps > /dev/null
- name: Commit Cargo.toml changes
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Only commit Cargo.toml (Cargo.lock is ignored for libraries)
git add Cargo.toml
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit in Cargo.toml"
else
git commit -m "Bump version to ${{ steps.version.outputs.version }} for release [skip ci]" || true
# Push if REPO_ACCESS_TOKEN is available
if [ -n "${{ secrets.REPO_ACCESS_TOKEN }}" ]; then
git remote set-url origin "https://${{ secrets.REPO_ACCESS_TOKEN }}@github.com/${{ github.repository }}.git"
git push origin HEAD:${{ github.ref }} || true
else
echo "⚠️ REPO_ACCESS_TOKEN not set, skipping push"
fi
fi
- 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}"
- name: Verify package version before packaging
if: env.CARGO_REGISTRY_TOKEN != ''
run: |
EXPECTED_VERSION="${{ steps.version.outputs.version }}"
ACTUAL_VERSION=$(grep -A 10 '^\[package\]' Cargo.toml | grep '^version = ' | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
echo "Expected version: ${EXPECTED_VERSION}"
echo "Actual version in Cargo.toml: ${ACTUAL_VERSION}"
if [ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]; then
echo "❌ Version mismatch! Expected ${EXPECTED_VERSION} but found ${ACTUAL_VERSION}"
exit 1
fi
echo "✅ Version matches: ${ACTUAL_VERSION}"
- 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
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..."
# Use crates.io API to check if version exists (more reliable than cargo search)
# The API returns versions in the "versions" array
# Retry up to 3 times with exponential backoff
API_RESPONSE=""
for i in 1 2 3; do
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
break
fi
if [ $i -lt 3 ]; then
echo "⚠️ API check attempt $i failed, retrying in $((i*2)) seconds..."
sleep $((i*2))
fi
done
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 }}"
# Verify Cargo.toml version matches expected version
ACTUAL_VERSION=$(grep -A 10 '^\[package\]' Cargo.toml | grep '^version = ' | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
echo " Cargo.toml version: ${ACTUAL_VERSION}"
if [ "$ACTUAL_VERSION" != "$VERSION" ]; then
echo "❌ CRITICAL: Cargo.toml version (${ACTUAL_VERSION}) does not match expected version (${VERSION})"
echo "❌ Cannot publish - version mismatch. Check if 'Update version in Cargo.toml' step ran successfully."
echo "published=false" >> $GITHUB_OUTPUT
exit 1
fi
# 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]"
PUBLISH_OUTPUT=$(cargo publish --token "${CARGO_REGISTRY_TOKEN}" 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 -qi "already exists"; then
echo "⚠️ Version ${VERSION} already exists on crates.io (detected during publish)"
echo "published=false" >> $GITHUB_OUTPUT
# Update exists status since we confirmed it exists
echo "exists=true" >> $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: Final verification - check if version exists on crates.io
if: env.CARGO_REGISTRY_TOKEN != ''
id: final-verify
run: |
VERSION="${{ steps.version.outputs.version }}"
CRATE_NAME=$(grep -E '^name = ' Cargo.toml | sed -E 's/^name = "([^"]+)".*/\1/')
echo "🔍 Final verification: Checking if ${CRATE_NAME} v${VERSION} exists on crates.io..."
# Wait a bit for crates.io to index if it was just published
sleep 5
# Use crates.io API to verify version exists
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
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 "✅ Verified: Version ${VERSION} exists on crates.io"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Version ${VERSION} not found on crates.io (may need more time to index)"
echo "exists=false" >> $GITHUB_OUTPUT
fi
else
if echo "$API_RESPONSE" | grep -q "\"num\":\"${VERSION}\""; then
echo "✅ Verified: Version ${VERSION} exists on crates.io"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Version ${VERSION} not found on crates.io (may need more time to index)"
echo "exists=false" >> $GITHUB_OUTPUT
fi
fi
else
echo "⚠️ Could not verify via API, assuming version check from earlier step is correct"
echo "exists=${{ steps.check-version.outputs.exists }}" >> $GITHUB_OUTPUT
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- 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 consensus implementation")
# 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
EOF
# Add links conditionally based on publish status
PUBLISH_STATUS="${{ steps.publish.outputs.published }}"
PUBLISH_EXISTS="${{ steps.publish.outputs.exists }}"
# Use final verification if available (more reliable), otherwise use initial check
FINAL_EXISTS="${{ steps.final-verify.outputs.exists }}"
INITIAL_EXISTS="${{ steps.check-version.outputs.exists }}"
# Priority: publish step exists > final verification > initial check
# The publish step is most reliable since it directly interacts with crates.io
if [ "$PUBLISH_EXISTS" = "true" ]; then
VERSION_EXISTS="true"
elif [ "$FINAL_EXISTS" = "true" ] || [ "$FINAL_EXISTS" = "false" ]; then
VERSION_EXISTS="$FINAL_EXISTS"
else
VERSION_EXISTS="$INITIAL_EXISTS"
fi
# Check if version exists on crates.io (either just published or already existed)
if [ "$PUBLISH_STATUS" = "true" ] || [ "$VERSION_EXISTS" = "true" ]; then
echo "- **Crates.io**: [${CRATE_NAME} v${VERSION}](https://crates.io/crates/${CRATE_NAME}/${VERSION})" >> /tmp/release_notes.md
echo "- **Documentation**: [docs.rs/${CRATE_NAME}/${VERSION}](https://docs.rs/${CRATE_NAME}/${VERSION})" >> /tmp/release_notes.md
else
echo "- **Crates.io**: ⚠️ Not published (publish failed or was skipped)" >> /tmp/release_notes.md
echo "- **Documentation**: Not available (crate not published)" >> /tmp/release_notes.md
fi
echo "- **Repository**: [GitHub](https://github.com/${{ github.repository }})" >> /tmp/release_notes.md
# Determine publish status message
PUBLISH_STATUS_MSG="❌ Failed or skipped"
if [ "$PUBLISH_STATUS" = "true" ]; then
PUBLISH_STATUS_MSG="✅ Yes"
elif [ "$VERSION_EXISTS" = "true" ]; then
PUBLISH_STATUS_MSG="⚠️ Already exists"
fi
# Get repository name for commit history link
REPO_NAME="${{ github.repository }}"
cat >> /tmp/release_notes.md << EOF
### 📝 Changes
This release includes $COMMIT_COUNT commits since the last release.
**Published to crates.io**: $PUBLISH_STATUS_MSG
See [full commit history](https://github.com/${REPO_NAME}/compare/${COMMIT_RANGE}) for details.
EOF
# Add warning only if publish failed AND version doesn't exist
if [ "$PUBLISH_STATUS" != "true" ] && [ "$VERSION_EXISTS" != "true" ]; then
echo "" >> /tmp/release_notes.md
echo "⚠️ **Note**: This version was not published to crates.io. Check the workflow logs for details." >> /tmp/release_notes.md
fi
echo "notes<<EOF" >> $GITHUB_OUTPUT
cat /tmp/release_notes.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Download attestation artifacts
uses: actions/download-artifact@v4
with:
pattern: '*-attestation-data'
merge-multiple: true
path: attestation-data
- name: Collect build metadata
id: metadata
run: |
# Get toolchain versions
RUSTC_VERSION=$(rustc --version | cut -d' ' -f2)
CARGO_VERSION=$(cargo --version | cut -d' ' -f2)
# Hash resolved lock if present (generated during build), else manifest
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
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_OUTPUT_HASH=""
if [ -f "attestation-data/spec_lock_output.txt" ]; then
SPEC_LOCK_VERIFIED="true"
SPEC_LOCK_CONSENSUS_FUNCS=$(grep -c "Status: PASSED" attestation-data/spec_lock_output.txt 2>/dev/null || echo "0")
SPEC_LOCK_OUTPUT_HASH=$(sha256sum attestation-data/spec_lock_output.txt | cut -d' ' -f1)
fi
# Extract test coverage from XML
TEST_COVERAGE_PCT="0.00"
if [ -f "attestation-data/coverage/cobertura.xml" ]; then
TEST_COVERAGE_PCT=$(grep -oP 'line-rate="\K[0-9.]+' attestation-data/coverage/cobertura.xml | head -1 | awk '{printf "%.2f", $1 * 100}')
fi
# Extract unit tests hash
UNIT_TESTS_HASH=""
if [ -f "attestation-data/test_output.txt" ]; then
UNIT_TESTS_HASH=$(sha256sum attestation-data/test_output.txt | cut -d' ' -f1)
fi
# Output all metadata
echo "spec_lock_verified=$SPEC_LOCK_VERIFIED" >> $GITHUB_OUTPUT
echo "spec_lock_consensus_functions=$SPEC_LOCK_CONSENSUS_FUNCS" >> $GITHUB_OUTPUT
echo "spec_lock_output_hash=$SPEC_LOCK_OUTPUT_HASH" >> $GITHUB_OUTPUT
echo "test_coverage_percent=$TEST_COVERAGE_PCT" >> $GITHUB_OUTPUT
echo "unit_tests_hash=$UNIT_TESTS_HASH" >> $GITHUB_OUTPUT
echo "debug_assertions_enabled=$DEBUG_ASSERTIONS" >> $GITHUB_OUTPUT
echo "rustc_version=$RUSTC_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 functions: $SPEC_LOCK_CONSENSUS_FUNCS"
echo " Spec-lock output hash: $SPEC_LOCK_OUTPUT_HASH"
echo " Test coverage: ${TEST_COVERAGE_PCT}%"
echo " Unit tests hash: $UNIT_TESTS_HASH"
echo " Debug assertions: $DEBUG_ASSERTIONS"
echo " rustc version: $RUSTC_VERSION"
echo " cargo version: $CARGO_VERSION"
echo " Cargo.lock hash: $CARGO_LOCK_HASH"
- name: Create build metadata JSON
run: |
# Create metadata JSON file for artifact storage
cat > build-metadata.json << EOF
{
"spec_lock_verified": ${{ steps.metadata.outputs.spec_lock_verified == 'true' && 'true' || 'false' }},
"spec_lock_consensus_functions": ${{ steps.metadata.outputs.spec_lock_consensus_functions }},
"spec_lock_output_hash": "${{ steps.metadata.outputs.spec_lock_output_hash }}",
"test_coverage_percent": ${{ steps.metadata.outputs.test_coverage_percent }},
"unit_tests_hash": "${{ steps.metadata.outputs.unit_tests_hash }}",
"debug_assertions_enabled": ${{ steps.metadata.outputs.debug_assertions_enabled == 'true' && 'true' || 'false' }},
"rustc_version": "${{ steps.metadata.outputs.rustc_version }}",
"cargo_version": "${{ steps.metadata.outputs.cargo_version }}",
"cargo_lock_hash": "${{ steps.metadata.outputs.cargo_lock_hash }}"
}
EOF
cat build-metadata.json
- name: Upload build metadata
uses: actions/upload-artifact@v4
with:
name: build-metadata
path: build-metadata.json
retention-days: 365
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
with:
subject-name: "blvm-consensus"
subject-digest: "sha256:${{ steps.metadata.outputs.cargo_lock_hash }}"
push-to-registry: false
show-summary: true
- 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 }}