feat: Implement production-grade genesis config and Vault authenticat… #82
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Create and publish a Docker image | |
| # Configures this workflow to run on main branch pushes and signed release tags | |
| on: | |
| push: | |
| branches: ['main'] # keep quick checks for PRs | |
| tags: ['fennel-node-*'] # run only for release tags | |
| # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| # NOTE: Bootnode configurations are now dynamically generated from GitHub secrets | |
| # to prevent peer ID collisions and follow external-only bootnode architecture | |
| # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. | |
| jobs: | |
| build-and-push-image: | |
| runs-on: ubuntu-latest | |
| # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. | |
| permissions: | |
| contents: write # Changed from read to write to allow pushing commits | |
| packages: write | |
| # | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for tag verification | |
| fetch-tags: true # Explicitly fetch all tags | |
| ref: ${{ github.ref }} # Ensure we check out the exact ref that triggered the workflow | |
| # Ensure tag objects are properly fetched (GitHub Actions sometimes fetches only refs) | |
| - name: Refetch tag objects | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| TAG=${GITHUB_REF#refs/tags/} | |
| echo "🔄 Ensuring tag object is available for: $TAG" | |
| # Force refetch the tag with its object | |
| git fetch origin "refs/tags/$TAG:refs/tags/$TAG" --force | |
| # Verify we now have the tag object | |
| echo "📋 Post-fetch verification:" | |
| echo " Tag type: $(git cat-file -t "refs/tags/$TAG" 2>/dev/null || echo 'unknown')" | |
| echo " Tag hash: $(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo 'unknown')" | |
| echo " Commit hash: $(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo 'unknown')" | |
| # They should be different for annotated tags | |
| TAG_HASH=$(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo '') | |
| COMMIT_HASH=$(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo '') | |
| if [ "$TAG_HASH" != "$COMMIT_HASH" ] && [ -n "$TAG_HASH" ] && [ -n "$COMMIT_HASH" ]; then | |
| echo "✅ Annotated tag object successfully fetched" | |
| else | |
| echo "⚠️ Tag object may not be properly fetched, attempting alternative fetch..." | |
| # Try fetching all tag objects | |
| git fetch origin '+refs/tags/*:refs/tags/*' --force | |
| # Re-verify | |
| NEW_TAG_HASH=$(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo '') | |
| NEW_COMMIT_HASH=$(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo '') | |
| echo " After alternative fetch - Tag: $NEW_TAG_HASH, Commit: $NEW_COMMIT_HASH" | |
| fi | |
| # Verify tag signature for release builds | |
| - name: Verify tag signature | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| TAG=${GITHUB_REF#refs/tags/} | |
| echo "🔐 Verifying signature for tag: $TAG" | |
| # Import the trusted public key | |
| gpg --import .github/release-signing.pub.asc | |
| # Basic validation that this is a proper release tag | |
| if [[ ! "$TAG" =~ ^fennel-node-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "❌ Invalid tag format. Expected: fennel-node-X.Y.Z" | |
| exit 1 | |
| fi | |
| # Ensure the tag exists and is available locally | |
| echo "🔍 Checking if tag exists locally..." | |
| if ! git tag -l | grep -q "^$TAG$"; then | |
| echo "⚠️ Tag not found locally, fetching..." | |
| git fetch origin "refs/tags/$TAG:refs/tags/$TAG" | |
| fi | |
| # Verify tag object type and show debugging info | |
| echo "🔍 Debugging tag object..." | |
| # Check different ways to reference the tag | |
| echo " Tag type (direct): $(git cat-file -t "$TAG" 2>/dev/null || echo 'unknown')" | |
| echo " Tag type (refs/tags): $(git cat-file -t "refs/tags/$TAG" 2>/dev/null || echo 'unknown')" | |
| echo " Tag exists in local refs: $(git tag -l | grep "^$TAG$" || echo 'NOT FOUND')" | |
| # Determine the correct tag reference to use | |
| TAG_REF="$TAG" | |
| if git cat-file -t "refs/tags/$TAG" 2>/dev/null | grep -q "tag"; then | |
| TAG_REF="refs/tags/$TAG" | |
| echo " Using full ref: $TAG_REF" | |
| elif git cat-file -t "$TAG" 2>/dev/null | grep -q "tag"; then | |
| echo " Using short ref: $TAG_REF" | |
| else | |
| echo "⚠️ Warning: Tag reference resolves to commit, trying explicit refs..." | |
| # List all possible tag references | |
| echo " Available tag refs:" | |
| git for-each-ref refs/tags/ --format="%(refname) -> %(objecttype)" | grep "$TAG" || echo " No matching tag refs found" | |
| # Try to find the actual tag object | |
| if git show-ref --tags | grep -q "refs/tags/$TAG$"; then | |
| TAG_REF="refs/tags/$TAG" | |
| echo " Found explicit tag ref: $TAG_REF" | |
| fi | |
| fi | |
| # Show tag information for debugging | |
| echo "📋 Tag object information (using $TAG_REF):" | |
| if git cat-file -t "$TAG_REF" 2>/dev/null | grep -q "tag"; then | |
| echo "✅ Found tag object, showing tag info:" | |
| git cat-file tag "$TAG_REF" | head -10 | |
| else | |
| echo "⚠️ Not a tag object, showing commit info:" | |
| git show --no-patch --format=" Commit: %H%n Author: %an <%ae>%n Date: %ai%n Subject: %s" "$TAG_REF" | |
| fi | |
| # Verify the tag signature using the correct reference | |
| echo "🔐 Attempting tag signature verification (using $TAG_REF)..." | |
| if git verify-tag "$TAG_REF" 2>&1; then | |
| echo "✅ Tag signature verified successfully" | |
| else | |
| echo "❌ Tag signature verification failed" | |
| echo "🔍 Debugging tag verification issue..." | |
| # Show detailed git information | |
| echo " Git ref information:" | |
| git show-ref | grep "$TAG" || echo " No refs found for $TAG" | |
| echo " Tag object details:" | |
| git for-each-ref refs/tags/ --format="%(refname) %(objecttype) %(objectname)" | grep "$TAG" || echo " No tag object found" | |
| # Try alternative verification approaches | |
| echo " Trying alternative verification methods..." | |
| # Method 1: Direct tag object verification | |
| if git show-ref --tags | grep -q "refs/tags/$TAG$"; then | |
| echo " Method 1: Using explicit refs/tags/$TAG" | |
| git verify-tag "refs/tags/$TAG" --verbose 2>&1 || echo " Failed" | |
| fi | |
| # Method 2: Check if it's an annotated tag | |
| COMMIT_HASH=$(git rev-parse "$TAG^{commit}" 2>/dev/null || echo "") | |
| TAG_HASH=$(git rev-parse "$TAG" 2>/dev/null || echo "") | |
| echo " Commit hash: $COMMIT_HASH" | |
| echo " Tag hash: $TAG_HASH" | |
| if [ "$COMMIT_HASH" != "$TAG_HASH" ]; then | |
| echo " Tag is annotated (hashes differ), should be verifiable" | |
| else | |
| echo " Tag appears to be lightweight (hashes same), cannot verify signature" | |
| fi | |
| exit 1 | |
| fi | |
| # Free up additional disk space on GitHub runner (~20-25 GB) | |
| # Enhanced cleanup for Polkadot SDK builds without cache | |
| - name: Free Disk Space (Ubuntu) | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # 1.3.1 | |
| with: | |
| android: true # 12 GB save | |
| dotnet: true # 3 GB save | |
| haskell: true # 2 GB save | |
| large-packages: true # 4 GB save | |
| swap-storage: false # Keep swap for large builds | |
| # Set up Docker Buildx for efficient layer caching | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| # ------------------------------------------------------------ | |
| # SECURITY: Derive peer IDs from GitHub secrets | |
| # Private keys are decoded temporarily in memory, never logged | |
| # ------------------------------------------------------------ | |
| - name: Derive bootnode peer IDs from secrets | |
| run: | | |
| set -euo pipefail | |
| echo "🔐 Deriving bootnode peer IDs from GitHub secrets..." | |
| # Install polkadot binary from official releases (most reliable) | |
| echo "📥 Installing polkadot binary..." | |
| POLKADOT_VERSION="v1.9.0" | |
| wget -q "https://github.com/paritytech/polkadot-sdk/releases/download/polkadot-${POLKADOT_VERSION}/polkadot" -O polkadot | |
| chmod +x polkadot | |
| # Verify installation | |
| ./polkadot --version | |
| # Helper: derive peer-id from a key file using polkadot | |
| # Parse only last line (PeerID) to avoid logging sensitive data | |
| derive_peer () { | |
| ./polkadot key inspect-node-key --file "$1" 2>/dev/null | tail -1 | |
| } | |
| # Decode secrets to tmpfs (runner's /tmp is already tmpfs), derive IDs | |
| BOOT1=$(mktemp) | |
| BOOT2=$(mktemp) | |
| trap 'rm -f "$BOOT1" "$BOOT2"' EXIT # always wipe | |
| echo "${{ secrets.BOOTNODE1_KEY_B64 }}" | base64 -d > "$BOOT1" | |
| echo "${{ secrets.BOOTNODE2_KEY_B64 }}" | base64 -d > "$BOOT2" | |
| PEER1="$(derive_peer "$BOOT1")" | |
| PEER2="$(derive_peer "$BOOT2")" | |
| # Mask *right now* so nothing else can leak it | |
| echo "::add-mask::$PEER1" | |
| echo "::add-mask::$PEER2" | |
| # Build JSON array (still invisible in logs because masked) | |
| BOOTNODES_JSON=$(jq -cn --arg id1 "$PEER1" --arg id2 "$PEER2" ' | |
| [ | |
| "/dns4/bootnode1.fennel.network/tcp/30333/p2p/\($id1)", | |
| "/dns4/bootnode2.fennel.network/tcp/30333/p2p/\($id2)" | |
| ]') | |
| # Set environment variables for subsequent steps (use base64 for safety) | |
| echo "EXTERNAL_BOOTNODES_B64=$(printf %s "$BOOTNODES_JSON" | base64 -w0)" >> $GITHUB_ENV | |
| echo "BOOTNODE1_PEER_ID=$PEER1" >> $GITHUB_ENV | |
| echo "BOOTNODE2_PEER_ID=$PEER2" >> $GITHUB_ENV | |
| # Log success (peer IDs are masked) | |
| echo "✅ Successfully derived peer IDs from secrets using official Parity container" | |
| echo "🌐 Using external-only bootnode architecture (following Parity best practices)" | |
| echo "📋 Bootnode configuration: [MASKED - using derived peer IDs]" | |
| # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. | |
| - name: Extract metadata (tags, labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') }} | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha,format=long | |
| # ------------------------------------------------------------ | |
| # PRODUCTION KEY EXPORT (following Vault/CI/CD methodology) | |
| # Export public keys for production builds - MANDATORY for releases | |
| # Following the field-tested methodology from MOSTUPTODATEMETHODOLOGYEURKEA.md | |
| # ------------------------------------------------------------ | |
| - name: Export production public keys from Vault/Secrets | |
| run: | | |
| set -euo pipefail | |
| echo "🔐 Setting up production environment variables following Vault/CI/CD methodology..." | |
| # For production releases, use real keys from GitHub Secrets (Vault integration) | |
| if [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "🏭 Production release detected - exporting production keys from Vault" | |
| # Install Vault CLI for public key access | |
| echo "📦 Installing Vault CLI..." | |
| wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg | |
| echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list | |
| sudo apt update && sudo apt install vault | |
| # Configure Vault connection | |
| # TODO: Replace with proper production Vault endpoint and GitHub OIDC auth | |
| export VAULT_ADDR="${{ secrets.VAULT_ADDR }}" # e.g., https://vault.your-domain.com | |
| export VAULT_TOKEN="${{ secrets.VAULT_CI_TOKEN }}" # CI-specific read-only token | |
| # Verify Vault connection | |
| if ! vault status; then | |
| echo "❌ ERROR: Cannot connect to Vault at $VAULT_ADDR" | |
| echo "🔧 Check VAULT_ADDR and VAULT_CI_TOKEN secrets" | |
| exit 1 | |
| fi | |
| # MANDATORY production keys - build will fail if any are missing | |
| # Following the methodology: GitHub Actions pulls public values from Vault, | |
| # exports them as environment variables, and Rust compiler substitutes them | |
| # into the preset at build time via env!() macros | |
| echo "🔑 Fetching production public keys from Vault..." | |
| # Fetch keys from Vault KV store | |
| export SUDO_SS58=$(vault kv get -field=sudo_ss58 kv/fennel-production/ci-cd/sudo 2>/dev/null || echo "") | |
| export VAL1_AURA_PUB=$(vault kv get -field=aura_public kv/fennel-production/ci-cd/validator-1 2>/dev/null || echo "") | |
| export VAL1_GRANDPA_PUB=$(vault kv get -field=grandpa_public kv/fennel-production/ci-cd/validator-1 2>/dev/null || echo "") | |
| export VAL1_STASH_SS58=$(vault kv get -field=stash_ss58 kv/fennel-production/ci-cd/validator-1 2>/dev/null || echo "") | |
| export VAL2_AURA_PUB=$(vault kv get -field=aura_public kv/fennel-production/ci-cd/validator-2 2>/dev/null || echo "") | |
| export VAL2_GRANDPA_PUB=$(vault kv get -field=grandpa_public kv/fennel-production/ci-cd/validator-2 2>/dev/null || echo "") | |
| export VAL2_STASH_SS58=$(vault kv get -field=stash_ss58 kv/fennel-production/ci-cd/validator-2 2>/dev/null || echo "") | |
| # Verify all production variables are set (prevent empty values) | |
| for var in SUDO_SS58 VAL1_AURA_PUB VAL1_GRANDPA_PUB VAL1_STASH_SS58 VAL2_AURA_PUB VAL2_GRANDPA_PUB VAL2_STASH_SS58; do | |
| if [ -z "${!var:-}" ]; then | |
| echo "❌ ERROR: Production variable $var is empty or missing from Vault!" | |
| echo "🔧 Check Vault KV store at kv/fennel-production/ci-cd/" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ All 7 production environment variables fetched from Vault and validated" | |
| echo "🔒 Using production public keys from Vault (private keys remain secure in Vault)" | |
| else | |
| echo "🧪 Development/staging build - production variables not required" | |
| echo "📋 Development/staging presets use Alice/Bob hardcoded keys from sp_keyring" | |
| echo "⚠️ Production environment variables will NOT be set for non-release builds" | |
| echo "🛡️ This is intentional - development uses different preset logic" | |
| fi | |
| # ------------------------------------------------------------ | |
| # Build the runtime deterministically with srtool and capture | |
| # its SHA-256 so we can inject it as an OCI label. | |
| # ------------------------------------------------------------ | |
| - name: Build runtime with srtool & extract Wasm hash | |
| id: wasm | |
| run: | | |
| set -euo pipefail | |
| echo "🛠️ Running srtool to build compact runtime…" | |
| # Build command with conditional environment variables | |
| # For production releases: pass mandatory environment variables | |
| # For development/staging: build without production env vars (uses Alice/Bob presets) | |
| if [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "🏭 Building production runtime with Vault-sourced public keys" | |
| # Production build with mandatory environment variables | |
| # These MUST be set or build.rs will fail with clear error message | |
| docker run --rm \ | |
| -v "${PWD}":/build \ | |
| -e RUNTIME_DIR=runtime/fennel \ | |
| -e PACKAGE=fennel-node-runtime \ | |
| -e SUDO_SS58="${SUDO_SS58}" \ | |
| -e VAL1_AURA_PUB="${VAL1_AURA_PUB}" \ | |
| -e VAL1_GRANDPA_PUB="${VAL1_GRANDPA_PUB}" \ | |
| -e VAL1_STASH_SS58="${VAL1_STASH_SS58}" \ | |
| -e VAL2_AURA_PUB="${VAL2_AURA_PUB}" \ | |
| -e VAL2_GRANDPA_PUB="${VAL2_GRANDPA_PUB}" \ | |
| -e VAL2_STASH_SS58="${VAL2_STASH_SS58}" \ | |
| --workdir /build \ | |
| paritytech/srtool:1.84.1 | |
| else | |
| echo "🧪 Building development/staging runtime (no production env vars needed)" | |
| # Development/staging build without production environment variables | |
| # build.rs will NOT enforce mandatory variables for non-production builds | |
| docker run --rm \ | |
| -v "${PWD}":/build \ | |
| -e RUNTIME_DIR=runtime/fennel \ | |
| -e PACKAGE=fennel-node-runtime \ | |
| --workdir /build \ | |
| paritytech/srtool:1.84.1 | |
| fi | |
| # After the container exits the compiled Wasm lives in the mounted volume | |
| HASH=$(sha256sum runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm | awk '{print "0x"$1}') | |
| echo "WASM_HASH=$HASH" >> $GITHUB_ENV | |
| echo "hash=$HASH" >> $GITHUB_OUTPUT | |
| echo "✅ Deterministic Wasm hash: $HASH" | |
| # Clean up srtool build artifacts to free disk space | |
| - name: Clean up srtool artifacts | |
| run: | | |
| echo "=== Cleaning up srtool build artifacts ===" | |
| # Remove intermediate build files but keep the final WASM | |
| find runtime/fennel/target/srtool -type f -name "*.rlib" -delete || true | |
| find runtime/fennel/target/srtool -type f -name "*.rmeta" -delete || true | |
| find runtime/fennel/target/srtool -type d -name "deps" -exec rm -rf {} + || true | |
| find runtime/fennel/target/srtool -type d -name "build" -exec rm -rf {} + || true | |
| # Additional aggressive cleanup | |
| echo "=== Additional cleanup ===" | |
| # Remove Docker build cache and unused images | |
| docker system prune -af --volumes || true | |
| # Remove temporary files | |
| sudo rm -rf /tmp/* || true | |
| # Remove apt cache | |
| sudo apt-get clean || true | |
| echo "=== Disk usage after cleanup ===" | |
| df -h | |
| # ------------------------------------------------------------ | |
| # Generate chain specifications using chain-spec-builder | |
| # ------------------------------------------------------------ | |
| - name: Install jq for JSON processing | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y jq | |
| echo "✅ Installed jq" | |
| - name: Install chain-spec-builder | |
| run: | | |
| # Create directory for chain-spec-builder | |
| mkdir -p "${HOME}/.cargo/bin" | |
| # Use Parity's prebuilt container that has Rust, protoc, and all dependencies | |
| docker run --rm \ | |
| -v "${PWD}":/build \ | |
| -v "${HOME}/.cargo/bin":/cargo-bin \ | |
| --workdir /build \ | |
| paritytech/ci-unified:latest \ | |
| bash -c " | |
| cargo install staging-chain-spec-builder --locked --root /tmp/cargo-install && \ | |
| cp /tmp/cargo-install/bin/chain-spec-builder /cargo-bin/ | |
| " | |
| echo "✅ Installed chain-spec-builder" | |
| # Add cargo bin to PATH for subsequent steps | |
| echo "${HOME}/.cargo/bin" >> $GITHUB_PATH | |
| - name: Create development chain spec | |
| run: | | |
| set -euo pipefail | |
| # Create development directory in chainspecs if it doesn't exist | |
| mkdir -p chainspecs/development | |
| # Debug: Check chain-spec-builder version and help | |
| echo "Chain-spec-builder version:" | |
| chain-spec-builder --version || echo "No version flag available" | |
| echo -e "\nChain-spec-builder help:" | |
| chain-spec-builder --help || echo "No help available" | |
| echo -e "\nChain-spec-builder create help:" | |
| chain-spec-builder create --help || echo "No create help available" | |
| # Check if the runtime has a development preset | |
| echo "Checking available presets..." | |
| chain-spec-builder list-presets \ | |
| --runtime runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm || true | |
| # Check if runtime exists | |
| echo "Checking if runtime WASM exists..." | |
| ls -la runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm || echo "Runtime WASM not found!" | |
| # Generate development chain spec | |
| echo "Creating development chain spec..." | |
| chain-spec-builder \ | |
| -c chainspecs/development/development.json \ | |
| create \ | |
| -r runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm \ | |
| named-preset development | |
| # Convert to raw format | |
| echo "Converting to raw format..." | |
| chain-spec-builder \ | |
| -c chainspecs/development/development-raw.json \ | |
| convert-to-raw \ | |
| chainspecs/development/development.json | |
| # Verify the specs were created | |
| echo "✅ Generated chain specifications:" | |
| ls -l chainspecs/development/ | |
| # Quick verification of content | |
| echo "Verifying development.json content:" | |
| jq '.name' chainspecs/development/development.json || echo "Failed to parse JSON" | |
| # Comprehensive verification | |
| echo -e "\n📋 Verification of generated files:" | |
| ls -l chainspecs/development/development.json chainspecs/development/development-raw.json | |
| echo -e "\n📄 Chain spec structure (first 5 lines):" | |
| jq . chainspecs/development/development.json 2>/dev/null | head -n 5 || true | |
| echo -e "\n🔍 Checking file sizes:" | |
| stat -c "development.json: %s bytes" chainspecs/development/development.json | |
| stat -c "development-raw.json: %s bytes" chainspecs/development/development-raw.json | |
| # Verify raw spec is SCALE-encoded (not JSON) | |
| echo -e "\n🔬 Verifying raw spec format (first 200 bytes):" | |
| head -c 200 chainspecs/development/development-raw.json | xxd | head -n 5 || echo "Could not read raw spec" | |
| # Ensure files are not empty | |
| if [ ! -s chainspecs/development/development.json ]; then | |
| echo "❌ Error: development.json is empty!" | |
| exit 1 | |
| fi | |
| if [ ! -s chainspecs/development/development-raw.json ]; then | |
| echo "❌ Error: development-raw.json is empty!" | |
| exit 1 | |
| fi | |
| echo "✅ All development chain spec files are populated" | |
| - name: Inject bootnodes into development chain spec | |
| run: | | |
| set -euo pipefail | |
| echo "🔗 Injecting dynamically derived bootnodes into development chain spec..." | |
| TARGET="development" | |
| SPEC="chainspecs/${TARGET}/development.json" | |
| # Use external-only bootnodes (Parity's recommended approach) | |
| # Both internal and external nodes use the same public endpoints | |
| # Decode bootnodes from base64 (safe transport through GitHub Actions env) | |
| EXTERNAL_BOOTNODES=$(echo "$EXTERNAL_BOOTNODES_B64" | base64 -d) | |
| echo "📋 Using external-only bootnode architecture: [MASKED - derived peer IDs]" | |
| # Only update if there are changes (idempotent) | |
| CURRENT_BOOTNODES=$(jq -c '.bootNodes // []' "$SPEC") | |
| if [ "$CURRENT_BOOTNODES" != "$EXTERNAL_BOOTNODES" ]; then | |
| echo "📝 Updating bootNodes in ${TARGET} chain spec with derived peer IDs and chain metadata..." | |
| jq --argjson arr "$EXTERNAL_BOOTNODES" \ | |
| --arg ss58 "42" \ | |
| '.bootNodes = $arr | |
| | .protocolId = "fenn" | |
| | .properties = { | |
| "ss58Format": ($ss58|tonumber), | |
| "tokenDecimals": 18, | |
| "tokenSymbol": "FNL" | |
| }' "$SPEC" > tmp.json && mv tmp.json "$SPEC" | |
| # Regenerate raw spec with bootnodes included | |
| echo "🔄 Regenerating raw ${TARGET} spec with derived bootnodes..." | |
| chain-spec-builder \ | |
| -c "chainspecs/${TARGET}/development-raw.json" \ | |
| convert-to-raw \ | |
| "chainspecs/${TARGET}/development.json" | |
| echo "✅ Bootnodes updated successfully with derived peer IDs" | |
| else | |
| echo "✅ No changes needed - bootnodes already up to date" | |
| fi | |
| # Verify bootnodes configuration | |
| echo "📊 Current bootNodes configuration:" | |
| jq '.bootNodes' "$SPEC" || echo "No bootNodes found" | |
| - name: Commit development chainspecs to repository | |
| run: | | |
| # Configure git | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Add the development chainspecs | |
| git add chainspecs/development/development.json chainspecs/development/development-raw.json | |
| # Check if there are changes to commit | |
| if git diff --staged --quiet; then | |
| echo "No changes to development chainspecs" | |
| else | |
| # Commit the changes with [ci skip] to prevent retriggering | |
| git commit -m "Update development chainspecs with derived bootnode peer IDs [ci skip] | |
| - Peer IDs derived from GitHub secrets (BOOTNODE1_KEY_B64, BOOTNODE2_KEY_B64) | |
| - Using external-only bootnode architecture (Parity best practice) | |
| - Bootnode 1: $BOOTNODE1_PEER_ID | |
| - Bootnode 2: $BOOTNODE2_PEER_ID" | |
| # Push the changes back to the repository | |
| git push origin ${GITHUB_REF#refs/heads/} | |
| echo "✅ Development chainspecs committed and pushed to repository on branch ${GITHUB_REF#refs/heads/}" | |
| fi | |
| - name: Create staging chain specs | |
| run: | | |
| set -euo pipefail | |
| # Create staging directory in chainspecs if it doesn't exist | |
| mkdir -p chainspecs/staging | |
| # Generate staging chain spec | |
| echo "Generating staging chain spec..." | |
| chain-spec-builder \ | |
| -c chainspecs/staging/staging-chainspec.json \ | |
| create \ | |
| -r runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm \ | |
| named-preset staging | |
| # Generate raw staging chain spec | |
| echo "Generating raw staging chain spec..." | |
| chain-spec-builder \ | |
| -c chainspecs/staging/staging-raw.json \ | |
| convert-to-raw \ | |
| chainspecs/staging/staging-chainspec.json | |
| # Verify the staging specs were created | |
| echo "✅ Generated staging chain specifications:" | |
| ls -la chainspecs/staging/ | |
| # Quick verification of content | |
| echo "Verifying staging-chainspec.json content:" | |
| jq '.name' chainspecs/staging/staging-chainspec.json || echo "Failed to parse JSON" | |
| # Comprehensive verification | |
| echo -e "\n📋 Verification of generated files:" | |
| ls -l chainspecs/staging/staging-chainspec.json chainspecs/staging/staging-raw.json | |
| echo -e "\n📄 Chain spec structure (first 5 lines):" | |
| jq . chainspecs/staging/staging-chainspec.json 2>/dev/null | head -n 5 || true | |
| echo -e "\n🔍 Checking file sizes:" | |
| stat -c "staging-chainspec.json: %s bytes" chainspecs/staging/staging-chainspec.json | |
| stat -c "staging-raw.json: %s bytes" chainspecs/staging/staging-raw.json | |
| # Verify raw spec is SCALE-encoded (not JSON) | |
| echo -e "\n🔬 Verifying raw spec format (first 200 bytes):" | |
| head -c 200 chainspecs/staging/staging-raw.json | xxd | head -n 5 || echo "Could not read raw spec" | |
| # Ensure files are not empty | |
| if [ ! -s chainspecs/staging/staging-chainspec.json ]; then | |
| echo "❌ Error: staging-chainspec.json is empty!" | |
| exit 1 | |
| fi | |
| if [ ! -s chainspecs/staging/staging-raw.json ]; then | |
| echo "❌ Error: staging-raw.json is empty!" | |
| exit 1 | |
| fi | |
| echo "✅ All staging chain spec files are populated" | |
| - name: Inject bootnodes into staging chain spec | |
| run: | | |
| set -euo pipefail | |
| echo "🔗 Injecting dynamically derived bootnodes into staging chain spec..." | |
| TARGET="staging" | |
| SPEC="chainspecs/${TARGET}/staging-chainspec.json" | |
| # Use external-only bootnodes (Parity's recommended approach) | |
| # Both internal and external nodes use the same public endpoints | |
| # Decode bootnodes from base64 (safe transport through GitHub Actions env) | |
| EXTERNAL_BOOTNODES=$(echo "$EXTERNAL_BOOTNODES_B64" | base64 -d) | |
| echo "📋 Using external-only bootnode architecture: [MASKED - derived peer IDs]" | |
| # Only update if there are changes (idempotent) | |
| CURRENT_BOOTNODES=$(jq -c '.bootNodes // []' "$SPEC") | |
| if [ "$CURRENT_BOOTNODES" != "$EXTERNAL_BOOTNODES" ]; then | |
| echo "📝 Updating bootNodes in ${TARGET} chain spec with derived peer IDs and chain metadata..." | |
| jq --argjson arr "$EXTERNAL_BOOTNODES" \ | |
| --arg ss58 "42" \ | |
| '.bootNodes = $arr | |
| | .protocolId = "fenn" | |
| | .properties = { | |
| "ss58Format": ($ss58|tonumber), | |
| "tokenDecimals": 18, | |
| "tokenSymbol": "FNL" | |
| }' "$SPEC" > tmp.json && mv tmp.json "$SPEC" | |
| # Regenerate raw spec with bootnodes included | |
| echo "🔄 Regenerating raw ${TARGET} spec with derived bootnodes..." | |
| chain-spec-builder \ | |
| -c "chainspecs/${TARGET}/staging-raw.json" \ | |
| convert-to-raw \ | |
| "chainspecs/${TARGET}/staging-chainspec.json" | |
| echo "✅ Bootnodes updated successfully with derived peer IDs" | |
| else | |
| echo "✅ No changes needed - bootnodes already up to date" | |
| fi | |
| # Verify bootnodes configuration | |
| echo "📊 Current bootNodes configuration:" | |
| jq '.bootNodes' "$SPEC" || echo "No bootNodes found" | |
| - name: Commit staging chainspecs to repository | |
| run: | | |
| # Configure git | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Add the staging chainspecs | |
| git add chainspecs/staging/staging-chainspec.json chainspecs/staging/staging-raw.json | |
| # Check if there are changes to commit | |
| if git diff --staged --quiet; then | |
| echo "No changes to staging chainspecs" | |
| else | |
| # Commit the changes with [ci skip] to prevent retriggering | |
| git commit -m "Update staging chainspecs with derived bootnode peer IDs [ci skip] | |
| - Peer IDs derived from GitHub secrets (BOOTNODE1_KEY_B64, BOOTNODE2_KEY_B64) | |
| - Using external-only bootnode architecture (Parity best practice) | |
| - Bootnode 1: $BOOTNODE1_PEER_ID | |
| - Bootnode 2: $BOOTNODE2_PEER_ID" | |
| # Push the changes back to the repository | |
| git push origin ${GITHUB_REF#refs/heads/} | |
| echo "✅ Staging chainspecs committed and pushed to repository on branch ${GITHUB_REF#refs/heads/}" | |
| fi | |
| # ------------------------------------------------------------ | |
| # PRODUCTION CHAINSPEC GENERATION | |
| # Only generate production chainspecs for release tags | |
| # ------------------------------------------------------------ | |
| - name: Create production chain specs | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| set -euo pipefail | |
| echo "🏭 Generating production chainspecs for release tag..." | |
| # Create production directory in chainspecs if it doesn't exist | |
| mkdir -p chainspecs/production | |
| # Generate production chain spec | |
| echo "Generating production chain spec..." | |
| chain-spec-builder \ | |
| -c chainspecs/production/production-chainspec.json \ | |
| create \ | |
| -r runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm \ | |
| named-preset production | |
| # Generate raw production chain spec | |
| echo "Generating raw production chain spec..." | |
| chain-spec-builder \ | |
| -c chainspecs/production/production-raw.json \ | |
| convert-to-raw \ | |
| chainspecs/production/production-chainspec.json | |
| # Verify the production specs were created | |
| echo "✅ Generated production chain specifications:" | |
| ls -la chainspecs/production/ | |
| # Quick verification of content | |
| echo "Verifying production-chainspec.json content:" | |
| jq '.name' chainspecs/production/production-chainspec.json || echo "Failed to parse JSON" | |
| # Comprehensive verification | |
| echo -e "\n📋 Verification of generated files:" | |
| ls -l chainspecs/production/production-chainspec.json chainspecs/production/production-raw.json | |
| echo -e "\n📄 Chain spec structure (first 5 lines):" | |
| jq . chainspecs/production/production-chainspec.json 2>/dev/null | head -n 5 || true | |
| echo -e "\n🔍 Checking file sizes:" | |
| stat -c "production-chainspec.json: %s bytes" chainspecs/production/production-chainspec.json | |
| stat -c "production-raw.json: %s bytes" chainspecs/production/production-raw.json | |
| # Ensure files are not empty | |
| if [ ! -s chainspecs/production/production-chainspec.json ]; then | |
| echo "❌ Error: production-chainspec.json is empty!" | |
| exit 1 | |
| fi | |
| if [ ! -s chainspecs/production/production-raw.json ]; then | |
| echo "❌ Error: production-raw.json is empty!" | |
| exit 1 | |
| fi | |
| echo "✅ All production chain spec files are populated" | |
| - name: Inject bootnodes into production chain spec | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| set -euo pipefail | |
| echo "🔗 Injecting dynamically derived bootnodes into production chain spec..." | |
| TARGET="production" | |
| SPEC="chainspecs/${TARGET}/production-chainspec.json" | |
| # Use external-only bootnodes (Parity's recommended approach) | |
| # Both internal and external nodes use the same public endpoints | |
| # Decode bootnodes from base64 (safe transport through GitHub Actions env) | |
| EXTERNAL_BOOTNODES=$(echo "$EXTERNAL_BOOTNODES_B64" | base64 -d) | |
| echo "📋 Using external-only bootnode architecture: [MASKED - derived peer IDs]" | |
| # Update chain spec with production-specific settings | |
| echo "📝 Updating production chain spec with derived peer IDs and production settings..." | |
| jq --argjson arr "$EXTERNAL_BOOTNODES" \ | |
| --arg ss58 "42" \ | |
| '.bootNodes = $arr | |
| | .chainType = "Live" | |
| | .name = "Fennel Production Network" | |
| | .id = "fennel_production" | |
| | .protocolId = "fenn" | |
| | .properties = { | |
| "ss58Format": ($ss58|tonumber), | |
| "tokenDecimals": 18, | |
| "tokenSymbol": "FNL" | |
| }' "$SPEC" > tmp.json && mv tmp.json "$SPEC" | |
| # Regenerate raw spec with bootnodes included | |
| echo "🔄 Regenerating raw production spec with derived bootnodes..." | |
| chain-spec-builder \ | |
| -c "chainspecs/${TARGET}/production-raw.json" \ | |
| convert-to-raw \ | |
| "chainspecs/${TARGET}/production-chainspec.json" | |
| echo "✅ Production chainspec updated successfully with derived peer IDs" | |
| # Verify bootnodes configuration | |
| echo "📊 Current production bootNodes configuration:" | |
| jq '.bootNodes' "$SPEC" || echo "No bootNodes found" | |
| # Show production chain details | |
| echo "📋 Production chain configuration:" | |
| jq '{name: .name, id: .id, chainType: .chainType}' "$SPEC" | |
| - name: Commit production chainspecs to repository | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| # Configure git | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Add the production chainspecs | |
| git add chainspecs/production/production-chainspec.json chainspecs/production/production-raw.json | |
| # Check if there are changes to commit | |
| if git diff --staged --quiet; then | |
| echo "No changes to production chainspecs" | |
| else | |
| # Commit the changes with [ci skip] to prevent retriggering | |
| git commit -m "Add production chainspecs for release ${GITHUB_REF#refs/tags/} [ci skip] | |
| 🏭 Production Release: ${GITHUB_REF#refs/tags/} | |
| - Peer IDs derived from GitHub secrets (BOOTNODE1_KEY_B64, BOOTNODE2_KEY_B64) | |
| - Using external-only bootnode architecture (Parity best practice) | |
| - Chain Type: Live (Production) | |
| - Bootnode 1: $BOOTNODE1_PEER_ID | |
| - Bootnode 2: $BOOTNODE2_PEER_ID | |
| 🔐 Security Notes: | |
| - Chainspec contains placeholder validator keys | |
| - Real validator and bootnode keys managed via HashiCorp Vault | |
| - Production deployment uses offline-generated keys" | |
| # Push the changes back to the repository | |
| git push origin ${GITHUB_REF#refs/heads/} | |
| echo "✅ Production chainspecs committed and pushed to repository" | |
| fi | |
| # Compute chainspec SHA-256 for security verification | |
| - name: Compute chainspec SHA-256 | |
| id: specsha | |
| run: | | |
| set -euo pipefail | |
| echo "🔐 Computing chainspec SHA-256 hashes for security verification..." | |
| # Compute SHA-256 for staging chainspec | |
| if [ -f "chainspecs/staging/staging-raw.json" ]; then | |
| STAGING_SHA=$(sha256sum chainspecs/staging/staging-raw.json | awk '{print $1}') | |
| echo "staging_sha=$STAGING_SHA" >> $GITHUB_OUTPUT | |
| echo "✅ Staging chainspec SHA-256: $STAGING_SHA" | |
| else | |
| echo "⚠️ Staging chainspec not found" | |
| fi | |
| # Compute SHA-256 for development chainspec | |
| if [ -f "chainspecs/development/development-raw.json" ]; then | |
| DEV_SHA=$(sha256sum chainspecs/development/development-raw.json | awk '{print $1}') | |
| echo "dev_sha=$DEV_SHA" >> $GITHUB_OUTPUT | |
| echo "✅ Development chainspec SHA-256: $DEV_SHA" | |
| else | |
| echo "⚠️ Development chainspec not found" | |
| fi | |
| # Compute SHA-256 for production chainspec (only for release tags) | |
| if [ -f "chainspecs/production/production-raw.json" ]; then | |
| PRODUCTION_SHA=$(sha256sum chainspecs/production/production-raw.json | awk '{print $1}') | |
| echo "production_sha=$PRODUCTION_SHA" >> $GITHUB_OUTPUT | |
| echo "✅ Production chainspec SHA-256: $PRODUCTION_SHA" | |
| else | |
| echo "⚠️ Production chainspec not found (expected for non-release builds)" | |
| fi | |
| echo "🔒 Chainspec hashes computed for runtime verification" | |
| - name: Verify chainspec integrity | |
| run: | | |
| set -euo pipefail | |
| echo "🔍 Verifying chainspec integrity..." | |
| # Check that chainspecs are not empty and contain expected structure | |
| EXPECTED_SPECS=("chainspecs/development/development.json" "chainspecs/staging/staging-chainspec.json") | |
| # Add production chainspec verification for release tags | |
| if [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| EXPECTED_SPECS+=("chainspecs/production/production-chainspec.json") | |
| fi | |
| for spec in "${EXPECTED_SPECS[@]}"; do | |
| if [ -f "$spec" ]; then | |
| # Verify it's valid JSON with expected fields | |
| if jq -e '.name' "$spec" >/dev/null 2>&1; then | |
| echo "✅ $spec: Valid JSON structure" | |
| else | |
| echo "❌ $spec: Invalid JSON structure" | |
| exit 1 | |
| fi | |
| else | |
| echo "❌ Missing chainspec: $spec" | |
| exit 1 | |
| fi | |
| done | |
| # Verify raw chainspecs are not empty | |
| EXPECTED_RAW=("chainspecs/development/development-raw.json" "chainspecs/staging/staging-raw.json") | |
| # Add production raw chainspec verification for release tags | |
| if [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| EXPECTED_RAW+=("chainspecs/production/production-raw.json") | |
| fi | |
| for rawspec in "${EXPECTED_RAW[@]}"; do | |
| if [ -f "$rawspec" ] && [ -s "$rawspec" ]; then | |
| echo "✅ $rawspec: Non-empty raw chainspec" | |
| else | |
| echo "❌ $rawspec: Missing or empty raw chainspec" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ All chainspecs verified successfully" | |
| - name: Upload chain specs as artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fennel-chainspecs | |
| path: | | |
| chainspecs/development/development.json | |
| chainspecs/development/development-raw.json | |
| chainspecs/staging/staging-chainspec.json | |
| chainspecs/staging/staging-raw.json | |
| chainspecs/production/production-chainspec.json | |
| chainspecs/production/production-raw.json | |
| if-no-files-found: ignore | |
| # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. | |
| # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. | |
| # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| WASM_HASH=${{ env.WASM_HASH }} | |
| # Remove problematic cache settings that are causing the blob error | |
| # cache-from: type=gha | |
| # cache-to: type=gha,mode=max | |
| provenance: false | |
| # Create and upload artifact containing image info | |
| - name: Output image info to artifact | |
| run: | | |
| mkdir -p ./artifacts | |
| echo "Image name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" > ./artifacts/image-info.txt | |
| echo "Tags: ${{ steps.meta.outputs.tags }}" >> ./artifacts/image-info.txt | |
| echo "Wasm hash: ${{ env.WASM_HASH }}" >> ./artifacts/image-info.txt | |
| echo "Digest: ${{ steps.build.outputs.digest }}" >> ./artifacts/image-info.txt | |
| echo "Created: $(date -u +\"%Y-%m-%dT%H:%M:%SZ\")" >> ./artifacts/image-info.txt | |
| - name: Upload Docker image info artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fennel-node-image-info | |
| path: ./artifacts/image-info.txt | |
| # ------------------------------------------------------------ | |
| # Package and publish Helm chart with updated values | |
| # ------------------------------------------------------------ | |
| - name: Install Helm | |
| uses: azure/setup-helm@v3 | |
| with: | |
| version: 'v3.13.0' | |
| - name: Add Parity Helm repo | |
| run: | | |
| helm repo add parity https://paritytech.github.io/helm-charts | |
| helm repo update | |
| - name: Update Helm dependencies | |
| run: | | |
| cd Charts/fennel-node | |
| helm dependency update | |
| - name: Extract image tag from metadata | |
| id: extract-tag | |
| run: | | |
| # For tagged releases, use the tag name directly | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| TAG="${{ github.ref_name }}" | |
| echo "tag=${TAG}" >> $GITHUB_OUTPUT | |
| echo "✅ Using release tag: ${TAG}" | |
| else | |
| # For branch builds, extract the sha-prefixed tag from the metadata | |
| SHA_TAG=$(echo "${{ steps.meta.outputs.tags }}" | grep -oE 'sha-[a-f0-9]+' | head -1) | |
| echo "tag=${SHA_TAG}" >> $GITHUB_OUTPUT | |
| echo "✅ Extracted image tag: ${SHA_TAG}" | |
| fi | |
| # Update Chart.yaml version to match release tag (Helm best practice: chart version == app version) | |
| - name: Update Chart.yaml version for releases | |
| if: startsWith(github.ref, 'refs/tags/') | |
| run: | | |
| # Extract version from tag (fennel-node-X.Y.Z -> X.Y.Z) | |
| CHART_VERSION="${{ github.ref_name }}" | |
| CHART_VERSION="${CHART_VERSION#fennel-node-}" # Remove fennel-node- prefix | |
| echo "🏷️ Updating Chart.yaml version to: $CHART_VERSION" | |
| # Update both version and appVersion in Chart.yaml | |
| sed -i "s/^version: .*/version: $CHART_VERSION/" Charts/fennel-node/Chart.yaml | |
| sed -i "s/^appVersion: .*/appVersion: \"$CHART_VERSION\"/" Charts/fennel-node/Chart.yaml | |
| echo "✅ Chart.yaml updated:" | |
| grep -E "^(version|appVersion):" Charts/fennel-node/Chart.yaml | |
| - name: Update base values.yaml | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values.yaml' | |
| propertyPath: 'image.tag' | |
| value: ${{ steps.extract-tag.outputs.tag }} | |
| commitChange: false | |
| - name: Update base values.yaml with digest | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values.yaml' | |
| propertyPath: 'image.digest' | |
| value: ${{ steps.build.outputs.digest }} | |
| commitChange: false | |
| - name: Update staging values.yaml | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values-staging.yaml' | |
| propertyPath: 'image.tag' | |
| value: ${{ steps.extract-tag.outputs.tag }} | |
| commitChange: false | |
| - name: Update staging values.yaml with digest | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values-staging.yaml' | |
| propertyPath: 'image.digest' | |
| value: ${{ steps.build.outputs.digest }} | |
| commitChange: false | |
| - name: Update staging values with chainspec SHA-256 | |
| if: steps.specsha.outputs.staging_sha | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values-staging.yaml' | |
| propertyPath: 'node.customChainspecSha256' | |
| value: ${{ steps.specsha.outputs.staging_sha }} | |
| commitChange: false | |
| - name: Update staging values with release tag (for tagged releases) | |
| if: startsWith(github.ref, 'refs/tags/') | |
| uses: fjogeleit/yaml-update-action@v0.13.2 | |
| with: | |
| valueFile: 'Charts/fennel-node/values-staging.yaml' | |
| propertyPath: 'releaseTag' | |
| value: ${{ github.ref_name }} | |
| commitChange: false | |
| - name: Lint Helm chart | |
| run: | | |
| echo "🔍 Linting base chart..." | |
| helm lint Charts/fennel-node | |
| echo "🔍 Linting with staging values..." | |
| helm lint Charts/fennel-node -f Charts/fennel-node/values-staging.yaml | |
| - name: Package Helm chart | |
| run: | | |
| mkdir -p release | |
| helm package Charts/fennel-node --destination release | |
| echo "📦 Packaged chart:" | |
| ls -la release/ | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| if: startsWith(github.ref, 'refs/tags/') | |
| with: | |
| files: | | |
| release/*.tgz | |
| ./artifacts/image-info.txt | |
| chainspecs/development/development.json | |
| chainspecs/development/development-raw.json | |
| chainspecs/staging/staging-chainspec.json | |
| chainspecs/staging/staging-raw.json | |
| chainspecs/production/production-chainspec.json | |
| chainspecs/production/production-raw.json | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| overwrite_files: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run chart-releaser | |
| uses: helm/chart-releaser-action@v1.6.0 | |
| with: | |
| charts_dir: Charts | |
| skip_existing: true | |
| env: | |
| CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" | |
| - name: Upload Helm chart artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fennel-helm-chart | |
| path: release/*.tgz | |
| retention-days: 30 | |
| - name: Output workflow summary | |
| run: | | |
| echo "## 📊 Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 🐳 Docker Image" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image**: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Tag**: \`${{ steps.extract-tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Wasm Hash**: \`${{ env.WASM_HASH }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📋 Chain Specs" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Development chain spec generated" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Staging chain spec generated" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| echo "- 🏭 **Production chain spec generated** (Release only)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- ⏭️ Production chain spec (Release builds only)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📦 Helm Chart" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Chart packaged and published" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: \`$(helm show chart release/*.tgz | grep '^version:' | awk '{print $2}')\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Digest**: \`${{ steps.build.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 🔐 Security Checks" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Chainspec SHA-256 computed" >> $GITHUB_STEP_SUMMARY | |
| echo "- Staging: \`${{ steps.specsha.outputs.staging_sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Development: \`${{ steps.specsha.outputs.dev_sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| echo "- Production: \`${{ steps.specsha.outputs.production_sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📦 Artifacts" >> $GITHUB_STEP_SUMMARY | |
| echo "- 🐳 **fennel-node-image-info**: Docker image metadata and build info" >> $GITHUB_STEP_SUMMARY | |
| echo "- ⚓ **fennel-helm-chart**: Packaged Helm chart (\`.tgz\`)" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| echo "- 🔗 **fennel-chainspecs**: Development, staging, and **production** chainspecs (JSON + raw)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- 🔗 **fennel-chainspecs**: Development and staging chainspecs (JSON + raw)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| echo "### 🏭 Production Release Information" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Release Tag**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Production Chainspec**: \`chainspecs/production/production-raw.json\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Chain Type**: Live (Production)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Bootnode Architecture**: External-only (Parity best practice)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Key Management**: Placeholder keys (Replace with Vault-managed keys for deployment)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "⚠️ **Important for Production Deployment**:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Production chainspec contains placeholder validator keys" >> $GITHUB_STEP_SUMMARY | |
| echo "- Real validator and bootnode keys are managed via HashiCorp Vault" >> $GITHUB_STEP_SUMMARY | |
| echo "- Use offline-generated keys for production security" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "🔍 **View artifacts**: Go to the [Actions tab](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for this workflow run." >> $GITHUB_STEP_SUMMARY |