Skip to content

Update GitHub Actions workflow to match current single-architecture a… #167

Update GitHub Actions workflow to match current single-architecture a…

Update GitHub Actions workflow to match current single-architecture a… #167

Workflow file for this run

name: Create and publish a Docker image
# Configures this workflow to run only on signed release tags
on:
push:
tags: ['fennel-node-*'] # run only for release tags
workflow_dispatch:
inputs:
rebuild_release:
description: 'Rebuild release artifacts for existing tag'
required: false
type: boolean
default: true
# 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
# This workflow has multiple jobs that run in sequence to avoid race conditions:
# 1. build-binaries: Build node binaries for all architectures
# 2. build-and-push-docker: Create multi-platform Docker image
# 3. generate-chainspecs: Generate chainspecs with bootnode injection
# 4. package-and-release: Package Helm chart and create GitHub release
jobs:
build-binaries:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
include:
- arch: amd64
rust_target: x86_64-unknown-linux-gnu
- arch: arm64
rust_target: aarch64-unknown-linux-gnu
# 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 # Extract metadata
- 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
flavor: |
suffix=-amd64
# Export production keys for release builds
- name: Export production public keys from GitHub Secrets
if: startsWith(github.ref, 'refs/tags/')
env:
PROD_SUDO_SS58: ${{ secrets.PROD_SUDO_SS58 }}
PROD_VAL1_AURA_PUB: ${{ secrets.PROD_VAL1_AURA_PUB }}
PROD_VAL1_GRANDPA_PUB: ${{ secrets.PROD_VAL1_GRANDPA_PUB }}
PROD_VAL1_STASH_SS58: ${{ secrets.PROD_VAL1_STASH_SS58 }}
PROD_VAL2_AURA_PUB: ${{ secrets.PROD_VAL2_AURA_PUB }}
PROD_VAL2_GRANDPA_PUB: ${{ secrets.PROD_VAL2_GRANDPA_PUB }}
PROD_VAL2_STASH_SS58: ${{ secrets.PROD_VAL2_STASH_SS58 }}
run: |
set -euo pipefail
echo "🔐 Setting up production environment variables from GitHub Secrets..."
# For production releases, use real keys from GitHub Secrets
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "🏭 Production release detected - exporting production keys from GitHub Secrets"
# Export production keys from GitHub Secrets (now available as env vars)
export SUDO_SS58="${PROD_SUDO_SS58}"
export VAL1_AURA_PUB="${PROD_VAL1_AURA_PUB}"
export VAL1_GRANDPA_PUB="${PROD_VAL1_GRANDPA_PUB}"
export VAL1_STASH_SS58="${PROD_VAL1_STASH_SS58}"
export VAL2_AURA_PUB="${PROD_VAL2_AURA_PUB}"
export VAL2_GRANDPA_PUB="${PROD_VAL2_GRANDPA_PUB}"
export VAL2_STASH_SS58="${PROD_VAL2_STASH_SS58}"
# 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 GitHub Secrets!"
echo "🔧 Add PROD_* secrets to GitHub repository settings"
echo "💡 Use extract-github-secrets.sh script to get the required values"
exit 1
fi
done
echo "✅ All 7 production environment variables exported from GitHub Secrets and validated"
echo "🔒 Using production public keys from GitHub Secrets (private keys remain secure in Vault)"
# Export for subsequent steps
echo "SUDO_SS58=$SUDO_SS58" >> $GITHUB_ENV
echo "VAL1_AURA_PUB=$VAL1_AURA_PUB" >> $GITHUB_ENV
echo "VAL1_GRANDPA_PUB=$VAL1_GRANDPA_PUB" >> $GITHUB_ENV
echo "VAL1_STASH_SS58=$VAL1_STASH_SS58" >> $GITHUB_ENV
echo "VAL2_AURA_PUB=$VAL2_AURA_PUB" >> $GITHUB_ENV
echo "VAL2_GRANDPA_PUB=$VAL2_GRANDPA_PUB" >> $GITHUB_ENV
echo "VAL2_STASH_SS58=$VAL2_STASH_SS58" >> $GITHUB_ENV
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 runtime with srtool
- name: Build runtime with srtool & extract Wasm hash
id: wasm
env:
PROD_SUDO_SS58: ${{ secrets.PROD_SUDO_SS58 }}
PROD_VAL1_AURA_PUB: ${{ secrets.PROD_VAL1_AURA_PUB }}
PROD_VAL1_GRANDPA_PUB: ${{ secrets.PROD_VAL1_GRANDPA_PUB }}
PROD_VAL1_STASH_SS58: ${{ secrets.PROD_VAL1_STASH_SS58 }}
PROD_VAL2_AURA_PUB: ${{ secrets.PROD_VAL2_AURA_PUB }}
PROD_VAL2_GRANDPA_PUB: ${{ secrets.PROD_VAL2_GRANDPA_PUB }}
PROD_VAL2_STASH_SS58: ${{ secrets.PROD_VAL2_STASH_SS58 }}
run: |
set -euo pipefail
echo "🛠️ Running srtool to build compact runtime…"
# Build command with conditional environment variables
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "🏭 Building production runtime with GitHub Secrets-sourced public keys"
docker run --rm \
-v "${PWD}":/build \
-e RUNTIME_DIR=runtime/fennel \
-e PACKAGE=fennel-node-runtime \
-e SUDO_SS58="${PROD_SUDO_SS58}" \
-e VAL1_AURA_PUB="${PROD_VAL1_AURA_PUB}" \
-e VAL1_GRANDPA_PUB="${PROD_VAL1_GRANDPA_PUB}" \
-e VAL1_STASH_SS58="${PROD_VAL1_STASH_SS58}" \
-e VAL2_AURA_PUB="${PROD_VAL2_AURA_PUB}" \
-e VAL2_GRANDPA_PUB="${PROD_VAL2_GRANDPA_PUB}" \
-e VAL2_STASH_SS58="${PROD_VAL2_STASH_SS58}" \
--workdir /build \
paritytech/srtool:1.84.1
else
echo "🧪 Building development/staging runtime"
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"
# Build fennel node using Parity CI Unified image
- name: Build fennel node using Parity CI Unified image
id: build_node
env:
PROD_SUDO_SS58: ${{ secrets.PROD_SUDO_SS58 }}
PROD_VAL1_AURA_PUB: ${{ secrets.PROD_VAL1_AURA_PUB }}
PROD_VAL1_GRANDPA_PUB: ${{ secrets.PROD_VAL1_GRANDPA_PUB }}
PROD_VAL1_STASH_SS58: ${{ secrets.PROD_VAL1_STASH_SS58 }}
PROD_VAL2_AURA_PUB: ${{ secrets.PROD_VAL2_AURA_PUB }}
PROD_VAL2_GRANDPA_PUB: ${{ secrets.PROD_VAL2_GRANDPA_PUB }}
PROD_VAL2_STASH_SS58: ${{ secrets.PROD_VAL2_STASH_SS58 }}
run: |
set -euo pipefail
echo "🔧 Building fennel node using Parity CI Unified image..."
# Export production environment variables for release builds
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "🏭 Building production node with GitHub Secrets-sourced public keys"
export SUDO_SS58="${PROD_SUDO_SS58}"
export VAL1_AURA_PUB="${PROD_VAL1_AURA_PUB}"
export VAL1_GRANDPA_PUB="${PROD_VAL1_GRANDPA_PUB}"
export VAL1_STASH_SS58="${PROD_VAL1_STASH_SS58}"
export VAL2_AURA_PUB="${PROD_VAL2_AURA_PUB}"
export VAL2_GRANDPA_PUB="${PROD_VAL2_GRANDPA_PUB}"
export VAL2_STASH_SS58="${PROD_VAL2_STASH_SS58}"
else
echo "🧪 Building development/staging node"
fi
# Use Parity's CI Unified image for building
docker run --rm \
-v "${PWD}":/build \
-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/ci-unified:latest \
bash -c "
# Add target for cross-compilation
rustup target add x86_64-unknown-linux-gnu
# Build with Parity's recommended flags
cargo build --locked --release --target x86_64-unknown-linux-gnu -p fennel-node
# Create distribution directory and install binary
mkdir -p dist
install -Dm0755 target/x86_64-unknown-linux-gnu/release/fennel-node dist/fennel-node
# Verify binary works
./dist/fennel-node --version
"
echo "✅ fennel-node built and staged in dist/"
# Package fennel node tarball
- name: Package fennel node tarball
id: pkg_node
run: |
set -euo pipefail
VER="${GITHUB_REF_NAME}"
OUT="fennel-node-${VER}-linux-x86_64.tar.gz"
mkdir -p artifacts-node
tar -czf "artifacts-node/${OUT}" -C dist fennel-node
TARBALL_HASH=$(sha256sum "artifacts-node/${OUT}" | awk '{print $1}')
echo "tarball=artifacts-node/${OUT}" >> "$GITHUB_OUTPUT"
echo "sha256=$TARBALL_HASH" >> "$GITHUB_OUTPUT"
echo "ansible_checksum=sha256:${TARBALL_HASH}" >> "$GITHUB_OUTPUT"
echo "$TARBALL_HASH ${OUT}" > "artifacts-node/${OUT}.sha256"
echo "✅ Node tarball packaged: artifacts-node/${OUT}"
# Prepare raw binary for Parity Ansible
- name: Prepare raw binary for Parity Ansible
id: raw_binary
run: |
set -euo pipefail
cp dist/fennel-node artifacts-node/fennel-node-linux-x86_64
chmod 0755 artifacts-node/fennel-node-linux-x86_64
RAW_HASH=$(sha256sum artifacts-node/fennel-node-linux-x86_64 | awk '{print $1}')
echo "binary=artifacts-node/fennel-node-linux-x86_64" >> "$GITHUB_OUTPUT"
echo "binary_sha256=artifacts-node/fennel-node-linux-x86_64.sha256" >> "$GITHUB_OUTPUT"
echo "sha256=$RAW_HASH" >> "$GITHUB_OUTPUT"
echo "ansible_checksum=sha256:${RAW_HASH}" >> "$GITHUB_OUTPUT"
echo "$RAW_HASH fennel-node-linux-x86_64" > artifacts-node/fennel-node-linux-x86_64.sha256
echo "✅ Raw binary prepared: artifacts-node/fennel-node-linux-x86_64"
# Clean up srtool artifacts
- name: Clean up srtool artifacts
run: |
echo "=== Cleaning up srtool build artifacts ==="
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
docker system prune -af --volumes || true
sudo rm -rf /tmp/* || true
sudo apt-get clean || true
echo "=== Disk usage after cleanup ==="
df -h
# Install chain-spec-builder
- name: Install chain-spec-builder using Parity CI Unified image
run: |
mkdir -p "${HOME}/.cargo/bin"
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/
/cargo-bin/chain-spec-builder --version || echo 'Version check not available'
"
echo "✅ Installed chain-spec-builder"
echo "${HOME}/.cargo/bin" >> $GITHUB_PATH
# Generate chain specs
- name: Create development chain spec
run: |
set -euo pipefail
mkdir -p chainspecs/development
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
echo "Converting to raw format..."
chain-spec-builder \
-c chainspecs/development/development-raw.json \
convert-to-raw \
chainspecs/development/development.json
echo "✅ Generated development chain specifications"
- name: Inject bootnodes into development chain spec
run: |
set -euo pipefail
echo "🔗 Injecting bootnode peer IDs into development chain spec..."
TARGET="development"
SPEC="chainspecs/${TARGET}/development.json"
EXTERNAL_BOOTNODES="$EXTERNAL_BOOTNODES_JSON"
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"
echo "🔄 Regenerating raw development spec with bootnodes..."
chain-spec-builder \
-c "chainspecs/${TARGET}/development-raw.json" \
convert-to-raw \
"chainspecs/${TARGET}/development.json"
echo "✅ Development chainspec updated with bootnodes"
# Similar steps for staging...
- name: Create staging chain specs
run: |
set -euo pipefail
mkdir -p chainspecs/staging
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
chain-spec-builder \
-c chainspecs/staging/staging-raw.json \
convert-to-raw \
chainspecs/staging/staging-chainspec.json
echo "✅ Generated staging chain specifications"
- name: Inject bootnodes into staging chain spec
run: |
set -euo pipefail
TARGET="staging"
SPEC="chainspecs/${TARGET}/staging-chainspec.json"
EXTERNAL_BOOTNODES="$EXTERNAL_BOOTNODES_JSON"
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"
chain-spec-builder \
-c "chainspecs/${TARGET}/staging-raw.json" \
convert-to-raw \
"chainspecs/${TARGET}/staging-chainspec.json"
echo "✅ Staging chainspec updated with bootnodes"
# Production chain specs (release only)
- name: Create production chain specs
if: startsWith(github.ref, 'refs/tags/')
env:
PROD_SUDO_SS58: ${{ secrets.PROD_SUDO_SS58 }}
PROD_VAL1_AURA_PUB: ${{ secrets.PROD_VAL1_AURA_PUB }}
PROD_VAL1_GRANDPA_PUB: ${{ secrets.PROD_VAL1_GRANDPA_PUB }}
PROD_VAL1_STASH_SS58: ${{ secrets.PROD_VAL1_STASH_SS58 }}
PROD_VAL2_AURA_PUB: ${{ secrets.PROD_VAL2_AURA_PUB }}
PROD_VAL2_GRANDPA_PUB: ${{ secrets.PROD_VAL2_GRANDPA_PUB }}
PROD_VAL2_STASH_SS58: ${{ secrets.PROD_VAL2_STASH_SS58 }}
run: |
set -euo pipefail
echo "🏭 Generating production chainspecs for release tag..."
export SUDO_SS58="${PROD_SUDO_SS58}"
export VAL1_AURA_PUB="${PROD_VAL1_AURA_PUB}"
export VAL1_GRANDPA_PUB="${PROD_VAL1_GRANDPA_PUB}"
export VAL1_STASH_SS58="${PROD_VAL1_STASH_SS58}"
export VAL2_AURA_PUB="${PROD_VAL2_AURA_PUB}"
export VAL2_GRANDPA_PUB="${PROD_VAL2_GRANDPA_PUB}"
export VAL2_STASH_SS58="${PROD_VAL2_STASH_SS58}"
mkdir -p chainspecs/production
env SUDO_SS58="${SUDO_SS58}" \
VAL1_AURA_PUB="${VAL1_AURA_PUB}" \
VAL1_GRANDPA_PUB="${VAL1_GRANDPA_PUB}" \
VAL1_STASH_SS58="${VAL1_STASH_SS58}" \
VAL2_AURA_PUB="${VAL2_AURA_PUB}" \
VAL2_GRANDPA_PUB="${VAL2_GRANDPA_PUB}" \
VAL2_STASH_SS58="${VAL2_STASH_SS58}" \
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
chain-spec-builder \
-c chainspecs/production/production-raw.json \
convert-to-raw \
chainspecs/production/production-chainspec.json
echo "✅ Generated production chain specifications"
- name: Inject bootnodes into production chain spec
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
TARGET="production"
SPEC="chainspecs/${TARGET}/production-chainspec.json"
EXTERNAL_BOOTNODES="$EXTERNAL_BOOTNODES_JSON"
jq --argjson arr "$EXTERNAL_BOOTNODES" \
--arg ss58 "42" \
'. as $original
| .bootNodes = $arr
| .chainType = "Live"
| .name = "Fennel Production Network"
| .id = "fennel_production"
| .protocolId = "fenn"
| .properties = {
"ss58Format": ($ss58|tonumber),
"tokenDecimals": 18,
"tokenSymbol": "FNL"
}
| .genesis = $original.genesis
| .codeSubstitutes = $original.codeSubstitutes
| .telemetryEndpoints = $original.telemetryEndpoints' "$SPEC" > tmp.json && mv tmp.json "$SPEC"
chain-spec-builder \
-c "chainspecs/${TARGET}/production-raw.json" \
convert-to-raw \
"chainspecs/${TARGET}/production-chainspec.json"
echo "✅ Production chainspec updated with bootnodes"
# Commit chainspecs to repository
- name: Commit chainspecs to repository
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add chainspecs/
if git diff --staged --quiet; then
echo "No changes to chainspecs"
else
git commit -m "Update chainspecs with bootnode peer IDs [ci skip]
- Development, staging, and production chainspecs
- Bootnode 1: $BOOTNODE1_PEER_ID
- Bootnode 2: $BOOTNODE2_PEER_ID"
git push origin HEAD:main
echo "✅ Chainspecs committed and pushed to repository"
fi
# Compute chainspec SHA-256
- name: Compute chainspec SHA-256
id: specsha
run: |
set -euo pipefail
echo "🔐 Computing chainspec SHA-256 hashes..."
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"
fi
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"
fi
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"
fi
# Build and push AMD64 Docker image
- name: Build and push AMD64 Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.description=Fennel Node - Polkadot-based blockchain node (AMD64)
org.opencontainers.image.vendor=Fennel Network
fennel.wasm.sha256=${{ env.WASM_HASH }}
build-args: |
WASM_HASH=${{ env.WASM_HASH }}
BUILDARCH=amd64
cache-from: type=gha,scope=amd64
cache-to: type=gha,mode=max,scope=amd64
provenance: false
# Upload artifacts
- name: Upload chain specs as artifacts
uses: actions/upload-artifact@v4
with:
name: fennel-chainspecs-amd64
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
- name: Upload node binary artifact
uses: actions/upload-artifact@v4
with:
name: fennel-node-binary-amd64
path: |
${{ steps.pkg_node.outputs.tarball }}
${{ steps.pkg_node.outputs.tarball }}.sha256
${{ steps.raw_binary.outputs.binary }}
${{ steps.raw_binary.outputs.binary_sha256 }}
if-no-files-found: error
# Build ARM64 image (simplified, reuses artifacts from AMD64)
build-arm64:
runs-on: ubuntu-latest
needs: [build-amd64]
permissions:
contents: read
packages: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Free up disk space
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # 1.3.1
with:
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: false
# Set up QEMU for multi-arch builds
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/arm64
# Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Log in to the Container registry
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata
- 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
flavor: |
suffix=-arm64
# Build and push ARM64 Docker image
- name: Build and push ARM64 Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.description=Fennel Node - Polkadot-based blockchain node (ARM64)
org.opencontainers.image.vendor=Fennel Network
fennel.wasm.sha256=${{ needs.build-amd64.outputs.wasm-hash }}
build-args: |
WASM_HASH=${{ needs.build-amd64.outputs.wasm-hash }}
BUILDARCH=arm64
echo "=== Disk usage after cleanup ==="

Check failure on line 678 in .github/workflows/publish.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/publish.yml

Invalid workflow file

You have an error in your yaml syntax on line 678
df -h
# Upload runtime WASM for chainspec generation
- name: Upload runtime WASM artifact
uses: actions/upload-artifact@v4
with:
name: fennel-runtime-wasm-${{ matrix.arch }}
path: runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm
- name: Upload node binary artifact
uses: actions/upload-artifact@v4
with:
name: fennel-node-binary-${{ matrix.arch }}
path: |
${{ steps.pkg_node.outputs.tarball }}
${{ steps.pkg_node.outputs.tarball }}.sha256
${{ steps.raw_binary.outputs.binary }}
${{ steps.raw_binary.outputs.binary_sha256 }}
if-no-files-found: error
# ------------------------------------------------------------
# Build and push multi-platform Docker image
# Runs after matrix builds complete to create proper manifest list
# ------------------------------------------------------------
build-and-push-docker:
runs-on: ubuntu-latest
needs: build-binaries
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
# Download artifacts from matrix builds
- name: Download runtime WASM artifact
uses: actions/download-artifact@v4
with:
name: fennel-runtime-wasm-amd64
path: runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/
# Download all binary artifacts to make them available for Docker build
- name: Download binary artifacts
uses: actions/download-artifact@v4
with:
pattern: fennel-node-binary-*
merge-multiple: true
path: artifacts-node/
# Set up QEMU for multi-platform builds
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Set up Docker Buildx for multi-platform builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Log in to Container Registry
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata for Docker 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
# Compute WASM hash for labels
- name: Set WASM hash for Docker labels
run: |
WASM_FILE="runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm"
if [ -f "$WASM_FILE" ]; then
HASH=$(sha256sum "$WASM_FILE" | awk '{print "0x"$1}')
echo "WASM_HASH=$HASH" >> $GITHUB_ENV
echo "✅ WASM hash: $HASH"
else
echo "❌ WASM file not found: $WASM_FILE"
exit 1
fi
# Build and push multi-platform Docker image
- name: Build and push multi-platform Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.description=Fennel Node - Polkadot-based blockchain node
org.opencontainers.image.vendor=Fennel Network
fennel.wasm.sha256=${{ env.WASM_HASH }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push=true,annotation-index.org.opencontainers.image.description=Fennel Node - Polkadot-based blockchain node
# Output image info for later jobs
- 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
# Extract SBOM from built image for release
- name: Extract SBOM from image
run: |
mkdir -p ./artifacts
# Extract SBOM using buildkit
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} --format '{{ json .SBOM }}' > ./artifacts/fennel-docker-sbom.json || echo "No SBOM found"
# If SBOM extraction failed, create a fallback note
if [ ! -s ./artifacts/fennel-docker-sbom.json ]; then
echo '{"note": "SBOM generation enabled but not available in this Docker build action version"}' > ./artifacts/fennel-docker-sbom.json
fi
- name: Upload Docker image info artifact
uses: actions/upload-artifact@v4
with:
name: fennel-docker-image-info
path: ./artifacts/image-info.txt
- name: Upload Docker SBOM artifact
uses: actions/upload-artifact@v4
with:
name: fennel-docker-sbom
path: ./artifacts/fennel-docker-sbom.json
# ------------------------------------------------------------
# Generate chainspecs in a separate job to prevent race conditions
# This job runs after all matrix builds complete successfully
# ------------------------------------------------------------
generate-chainspecs:
runs-on: ubuntu-latest
needs: build-and-push-docker
permissions:
contents: write # Allow pushing commits back to repository
packages: read # Allow reading published artifacts
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ github.ref }}
# Download artifacts from the matrix build (prefer amd64 for consistency)
- name: Download runtime WASM (amd64)
uses: actions/download-artifact@v4
with:
name: fennel-runtime-wasm-amd64
path: runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/
# Ensure WASM hash is available for consistency
- name: Set WASM hash for chainspec generation
id: wasm_hash
run: |
# Compute WASM hash from downloaded artifact
WASM_FILE="runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm"
if [ -f "$WASM_FILE" ]; then
HASH=$(sha256sum "$WASM_FILE" | awk '{print $1}')
echo "WASM_HASH=$HASH" >> $GITHUB_ENV
echo "wasm_hash=$HASH" >> $GITHUB_OUTPUT
echo "✅ WASM hash: $HASH"
else
echo "❌ WASM file not found: $WASM_FILE"
exit 1
fi
# Set up bootnode configuration from secrets
- name: Set up bootnode configuration
env:
BOOTNODE1_PEER_ID: ${{ secrets.BOOTNODE1_PEER_ID }}
BOOTNODE2_PEER_ID: ${{ secrets.BOOTNODE2_PEER_ID }}
run: |
echo "🔗 Setting up bootnode configuration from GitHub secrets..."
# Validate peer IDs are set (fail early if missing)
if [ -z "$BOOTNODE1_PEER_ID" ] || [ -z "$BOOTNODE2_PEER_ID" ]; then
echo "❌ Missing bootnode peer IDs in GitHub secrets"
echo "Required: BOOTNODE1_PEER_ID, BOOTNODE2_PEER_ID"
exit 1
fi
# Derive external bootnode URLs from peer IDs
# Using standardized port 30333 and domain pattern
BOOTNODE1_ADDR="/dns4/bootnode1.fennel.network/tcp/30333/p2p/$BOOTNODE1_PEER_ID"
BOOTNODE2_ADDR="/dns4/bootnode2.fennel.network/tcp/30333/p2p/$BOOTNODE2_PEER_ID"
# Create JSON array for external bootnodes
EXTERNAL_BOOTNODES_JSON=$(cat <<EOF
[
"$BOOTNODE1_ADDR",
"$BOOTNODE2_ADDR"
]
EOF
)
# Export for use in subsequent steps
echo "EXTERNAL_BOOTNODES_JSON=$EXTERNAL_BOOTNODES_JSON" >> $GITHUB_ENV
echo "BOOTNODE1_PEER_ID=$BOOTNODE1_PEER_ID" >> $GITHUB_ENV
echo "BOOTNODE2_PEER_ID=$BOOTNODE2_PEER_ID" >> $GITHUB_ENV
echo "✅ Bootnode configuration set up:"
echo " Bootnode 1: $BOOTNODE1_ADDR"
echo " Bootnode 2: $BOOTNODE2_ADDR"
# ------------------------------------------------------------
# Chainspec generation starts here
# ------------------------------------------------------------
- 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 using Parity CI Unified image
run: |
# Create directory for chain-spec-builder
mkdir -p "${HOME}/.cargo/bin"
# Use Parity's CI Unified image for installation (canonical Parity practice)
docker run --rm \
-v "${PWD}":/build \
-v "${HOME}/.cargo/bin":/cargo-bin \
--workdir /build \
paritytech/ci-unified:latest \
bash -c "
# Install chain-spec-builder with Parity's recommended approach
cargo install staging-chain-spec-builder --locked --root /tmp/cargo-install
cp /tmp/cargo-install/bin/chain-spec-builder /cargo-bin/
# Verify installation
/cargo-bin/chain-spec-builder --version || echo 'Version check not available'
"
echo "✅ Installed chain-spec-builder using Parity CI Unified image"
# 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 \
named-preset development
# Convert to raw format
echo "Converting to raw format..."
chain-spec-builder \
-c chainspecs/development/development-raw.json \
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 bootnode peer IDs 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
# Use bootnodes JSON directly (peer IDs are public and safe to log)
EXTERNAL_BOOTNODES="$EXTERNAL_BOOTNODES_JSON"
echo "📋 Using external-only bootnode architecture:"
# 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 peer IDs and chain metadata..."
jq --argjson arr "$EXTERNAL_BOOTNODES" \
--arg ss58 "42" \
'. as $original
| .bootNodes = $arr
| .properties = {
"ss58Format": ($ss58|tonumber),
"tokenDecimals": 18,
"tokenSymbol": "FNL"
}
| .genesis = $original.genesis
| .codeSubstitutes = $original.codeSubstitutes
| .telemetryEndpoints = $original.telemetryEndpoints' "$SPEC" > tmp.json && mv tmp.json "$SPEC"
# Regenerate raw spec with bootnodes included
echo "🔄 Regenerating raw ${TARGET} spec with bootnodes..."
chain-spec-builder \
-c "chainspecs/${TARGET}/development-raw.json" \
"chainspecs/${TARGET}/development.json"
echo "✅ Bootnodes updated successfully with 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 bootnode peer IDs [ci skip]
- Peer IDs from GitHub secrets (BOOTNODE1_PEER_ID, BOOTNODE2_PEER_ID)
- 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 (push current HEAD to main branch)
git push origin HEAD:main
echo "✅ Development chainspecs committed and pushed to repository"
fi
# ------------------------------------------------------------
# Package Helm chart and create releases (runs after chainspecs)
# This also avoids race conditions in Helm chart updates
# ------------------------------------------------------------
package-and-release:
runs-on: ubuntu-latest
needs: generate-chainspecs
concurrency:
group: package-and-release-${{ github.repository }}
cancel-in-progress: false
permissions:
contents: write
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ github.ref }}
# Download artifacts from previous jobs
- name: Download chainspecs
uses: actions/download-artifact@v4
with:
name: fennel-chainspecs
path: chainspecs/
- name: Download binary artifacts (both architectures)
uses: actions/download-artifact@v4
with:
pattern: fennel-node-binary-*
merge-multiple: true
path: artifacts-node/
- name: Download Docker image info artifact
uses: actions/download-artifact@v4
with:
name: fennel-docker-image-info
path: artifacts/
- name: Download Docker SBOM artifact
uses: actions/download-artifact@v4
with:
name: fennel-docker-sbom
path: artifacts/
# Get the build outputs from the matrix job artifacts
- name: Restore build context from artifacts
id: restore_context
run: |
# Set basic environment variables
echo "REGISTRY=ghcr.io" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ github.repository }}" >> $GITHUB_ENV
# Extract WASM hash from image info (should be consistent across architectures)
if [ -f "artifacts/image-info.txt" ]; then
WASM_HASH=$(grep "Wasm hash:" artifacts/image-info.txt | head -n1 | awk '{print $3}')
echo "WASM_HASH=$WASM_HASH" >> $GITHUB_ENV
echo "✅ Restored WASM hash: $WASM_HASH"
# Extract image digest for summary
IMAGE_DIGEST=$(grep "Digest:" artifacts/image-info.txt | head -n1 | awk '{print $2}')
echo "image_digest=$IMAGE_DIGEST" >> $GITHUB_OUTPUT
echo "✅ Restored image digest: $IMAGE_DIGEST"
else
echo "⚠️ Could not find image info artifact"
fi
# Extract binary information from downloaded artifacts
echo "📦 Extracting binary information from artifacts..."
# Find tarball files (should be consistent across architectures)
TARBALL_FILE=$(find artifacts-node -name "*.tar.gz" | head -n1)
if [ -n "$TARBALL_FILE" ]; then
TARBALL_BASENAME=$(basename "$TARBALL_FILE")
echo "tarball_file=$TARBALL_FILE" >> $GITHUB_OUTPUT
echo "tarball_basename=$TARBALL_BASENAME" >> $GITHUB_OUTPUT
# Extract tarball SHA256 from .sha256 file
if [ -f "${TARBALL_FILE}.sha256" ]; then
TARBALL_SHA256=$(cat "${TARBALL_FILE}.sha256" | awk '{print $1}')
echo "tarball_sha256=$TARBALL_SHA256" >> $GITHUB_OUTPUT
echo "tarball_ansible_checksum=sha256:$TARBALL_SHA256" >> $GITHUB_OUTPUT
fi
echo "✅ Found tarball: $TARBALL_BASENAME"
else
echo "❌ No tarball found in artifacts"
fi
# Find raw binary files (use pattern to match any architecture)
RAW_BINARY_FILE=$(find artifacts-node -name "fennel-node-linux-*" | grep -v ".sha256" | head -n1)
if [ -n "$RAW_BINARY_FILE" ]; then
RAW_BINARY_BASENAME=$(basename "$RAW_BINARY_FILE")
echo "raw_binary_file=$RAW_BINARY_FILE" >> $GITHUB_OUTPUT
echo "raw_binary_basename=$RAW_BINARY_BASENAME" >> $GITHUB_OUTPUT
# Extract raw binary SHA256 from .sha256 file
if [ -f "${RAW_BINARY_FILE}.sha256" ]; then
RAW_BINARY_SHA256=$(cat "${RAW_BINARY_FILE}.sha256" | awk '{print $1}')
echo "raw_binary_sha256=$RAW_BINARY_SHA256" >> $GITHUB_OUTPUT
echo "raw_binary_ansible_checksum=sha256:$RAW_BINARY_SHA256" >> $GITHUB_OUTPUT
fi
echo "✅ Found raw binary: $RAW_BINARY_BASENAME"
else
echo "❌ No raw binary found in artifacts"
fi
# Continue with Helm chart packaging and release creation steps here
# (Implementation continues...)
# ------------------------------------------------------------
# PRODUCTION CHAINSPEC GENERATION
# Only generate production chainspecs for release tags
# ------------------------------------------------------------
- name: Create production chain specs
if: startsWith(github.ref, 'refs/tags/')
env:
PROD_SUDO_SS58: ${{ secrets.PROD_SUDO_SS58 }}
PROD_VAL1_AURA_PUB: ${{ secrets.PROD_VAL1_AURA_PUB }}
PROD_VAL1_GRANDPA_PUB: ${{ secrets.PROD_VAL1_GRANDPA_PUB }}
PROD_VAL1_STASH_SS58: ${{ secrets.PROD_VAL1_STASH_SS58 }}
PROD_VAL2_AURA_PUB: ${{ secrets.PROD_VAL2_AURA_PUB }}
PROD_VAL2_GRANDPA_PUB: ${{ secrets.PROD_VAL2_GRANDPA_PUB }}
PROD_VAL2_STASH_SS58: ${{ secrets.PROD_VAL2_STASH_SS58 }}
run: |
set -euo pipefail
echo "🏭 Generating production chainspecs for release tag..."
# Export environment variables for chain-spec-builder
export SUDO_SS58="${PROD_SUDO_SS58}"
export VAL1_AURA_PUB="${PROD_VAL1_AURA_PUB}"
export VAL1_GRANDPA_PUB="${PROD_VAL1_GRANDPA_PUB}"
export VAL1_STASH_SS58="${PROD_VAL1_STASH_SS58}"
export VAL2_AURA_PUB="${PROD_VAL2_AURA_PUB}"
export VAL2_GRANDPA_PUB="${PROD_VAL2_GRANDPA_PUB}"
export VAL2_STASH_SS58="${PROD_VAL2_STASH_SS58}"
# Verify environment variables are set
echo "🔍 Verifying production environment variables:"
echo "SUDO_SS58: ${SUDO_SS58}"
echo "VAL1_AURA_PUB: ${VAL1_AURA_PUB}"
echo "VAL1_GRANDPA_PUB: ${VAL1_GRANDPA_PUB}"
echo "VAL1_STASH_SS58: ${VAL1_STASH_SS58}"
echo "VAL2_AURA_PUB: ${VAL2_AURA_PUB}"
echo "VAL2_GRANDPA_PUB: ${VAL2_GRANDPA_PUB}"
echo "VAL2_STASH_SS58: ${VAL2_STASH_SS58}"
# Create production directory in chainspecs if it doesn't exist
mkdir -p chainspecs/production
# Debug: Check available presets in the runtime
echo "🔍 Checking available presets in production runtime..."
env SUDO_SS58="${SUDO_SS58}" \
VAL1_AURA_PUB="${VAL1_AURA_PUB}" \
VAL1_GRANDPA_PUB="${VAL1_GRANDPA_PUB}" \
VAL1_STASH_SS58="${VAL1_STASH_SS58}" \
VAL2_AURA_PUB="${VAL2_AURA_PUB}" \
VAL2_GRANDPA_PUB="${VAL2_GRANDPA_PUB}" \
VAL2_STASH_SS58="${VAL2_STASH_SS58}" \
chain-spec-builder list-presets \
--runtime runtime/fennel/target/srtool/release/wbuild/fennel-node-runtime/fennel_node_runtime.compact.wasm || echo "Failed to list presets"
# Generate production chain spec
echo "Generating production chain spec..."
env SUDO_SS58="${SUDO_SS58}" \
VAL1_AURA_PUB="${VAL1_AURA_PUB}" \
VAL1_GRANDPA_PUB="${VAL1_GRANDPA_PUB}" \
VAL1_STASH_SS58="${VAL1_STASH_SS58}" \
VAL2_AURA_PUB="${VAL2_AURA_PUB}" \
VAL2_GRANDPA_PUB="${VAL2_GRANDPA_PUB}" \
VAL2_STASH_SS58="${VAL2_STASH_SS58}" \
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 bootnode peer IDs 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
# Use bootnodes JSON directly (peer IDs are public and safe to log)
EXTERNAL_BOOTNODES="$EXTERNAL_BOOTNODES_JSON"
echo "📋 Using external-only bootnode architecture:"
# Update chain spec with production-specific settings (preserve genesis)
echo "📝 Updating production chain spec with peer IDs and production settings..."
jq --argjson arr "$EXTERNAL_BOOTNODES" \
--arg ss58 "42" \
'. as $original
| .bootNodes = $arr
| .chainType = "Live"
| .name = "Fennel Production Network"
| .id = "fennel_production"
| .protocolId = "fenn"
| .properties = {
"ss58Format": ($ss58|tonumber),
"tokenDecimals": 18,
"tokenSymbol": "FNL"
}
| .genesis = $original.genesis
| .codeSubstitutes = $original.codeSubstitutes
| .telemetryEndpoints = $original.telemetryEndpoints' "$SPEC" > tmp.json && mv tmp.json "$SPEC"
# Regenerate raw spec with bootnodes included
echo "🔄 Regenerating raw production spec with 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 from GitHub secrets (BOOTNODE1_PEER_ID, BOOTNODE2_PEER_ID)
- Using external-only bootnode architecture (Parity best practice)
- Chain Type: Live (Production)
- Bootnode 1: $BOOTNODE1_PEER_ID
- Bootnode 2: $BOOTNODE2_PEER_ID"
# Push the changes back to the repository (push current HEAD to main branch)
git push origin HEAD:main
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
- name: Download chainspecs
uses: actions/download-artifact@v4
with:
name: fennel-chainspecs
path: chainspecs/
- name: Download binary artifacts (both architectures)
uses: actions/download-artifact@v4
with:
pattern: fennel-node-binary-*
merge-multiple: true
path: artifacts-node/
- name: Download Docker image info artifact
uses: actions/download-artifact@v4
with:
name: fennel-docker-image-info
path: artifacts/
# Get the build outputs from the matrix job artifacts
- name: Restore build context from artifacts
run: |
# Set basic environment variables
echo "REGISTRY=ghcr.io" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ github.repository }}" >> $GITHUB_ENV
# Extract WASM hash from image info (should be consistent across architectures)
if [ -f "artifacts/image-info.txt" ]; then
WASM_HASH=$(grep "Wasm hash:" artifacts/image-info.txt | head -n1 | awk '{print $3}')
echo "WASM_HASH=$WASM_HASH" >> $GITHUB_ENV
echo "✅ Restored WASM hash: $WASM_HASH"
else
echo "⚠️ Could not find image info artifact"
fi
# Extract image digest from image info (for Helm chart updates)
if [ -f "artifacts/image-info.txt" ]; then
IMAGE_DIGEST=$(grep "Digest:" artifacts/image-info.txt | head -n1 | awk '{print $2}')
echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_ENV
echo "✅ Restored image digest: $IMAGE_DIGEST"
else
echo "⚠️ Could not find image info artifact"
fi
# Install yq for YAML processing in Helm chart updates
- name: Install yq
uses: mikefarah/yq@v4
# Update Helm chart values with image digest and tag
- name: Update Helm chart values
id: extract_tag
run: |
set -euo pipefail
# 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, use a simple sha tag format
SHA_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
echo "tag=${SHA_TAG}" >> $GITHUB_OUTPUT
echo "✅ Using SHA tag: ${SHA_TAG}"
fi
# Update Chart.yaml version to match release tag (Helm best practice: chart version == app version)
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
# 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
fi
# Update image tag and digest in values.yaml
for file in "Charts/fennel-node/values.yaml" "Charts/fennel-node/values-staging.yaml"; do
if [ -f "$file" ]; then
echo "🔧 Updating $file with image tag and digest..."
# Update image.tag
yq -i '.image.tag = env(tag)' "$file"
# Update image.digest
yq -i '.image.digest = env(IMAGE_DIGEST)' "$file"
else
echo "⚠️ $file not found, skipping..."
fi
done
- name: Lint Helm chart
run: |
echo "🔍 Linting base chart..."
helm lint --strict Charts/fennel-node
echo "🔍 Linting with staging values..."
helm lint --strict 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: Verify release files exist
if: startsWith(github.ref, 'refs/tags/')
run: |
echo "🔍 Verifying all expected release files exist..."
# Check Helm chart
echo "📦 Helm chart files:"
ls -la release/ || echo "❌ No release directory or files found"
# Check artifacts
echo "📋 Artifact files:"
ls -la ./artifacts/ || echo "❌ No artifacts directory found"
# Check chainspecs
echo "🔗 Development chainspecs:"
ls -la chainspecs/development/ || echo "❌ No development chainspecs directory found"
echo "🔗 Staging chainspecs:"
ls -la chainspecs/staging/ || echo "❌ No staging chainspecs directory found"
echo "🔗 Production chainspecs:"
ls -la chainspecs/production/ || echo "❌ No production chainspecs directory found"
# Check node binary artifacts
echo "🧮 Node binary artifacts:"
ls -la artifacts-node/ || echo "❌ No node binary artifacts directory found"
# Verify specific files
echo "✅ Checking specific files:"
for file in \
"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" \
"artifacts-node/*.tar.gz" \
"artifacts-node/*.sha256" \
"artifacts-node/fennel-node-linux-amd64" \
"artifacts-node/fennel-node-linux-amd64.sha256" \
"artifacts-node/fennel-node-linux-arm64" \
"artifacts-node/fennel-node-linux-arm64.sha256"; do
if ls $file 1> /dev/null 2>&1; then
echo " ✅ $file exists"
else
echo " ❌ $file MISSING"
fi
done
- name: Derive artifact basenames for release body
id: relvars
run: |
echo "tarball_name=${{ steps.restore_context.outputs.tarball_basename }}" >> "$GITHUB_OUTPUT"
echo "raw_name=${{ steps.restore_context.outputs.raw_binary_basename }}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
release/*.tgz
./artifacts/image-info.txt
./artifacts/fennel-docker-sbom.json
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
${{ steps.restore_context.outputs.tarball_file }}
${{ steps.restore_context.outputs.tarball_file }}.sha256
${{ steps.restore_context.outputs.raw_binary_file }}
${{ steps.restore_context.outputs.raw_binary_file }}.sha256
generate_release_notes: true
fail_on_unmatched_files: true
overwrite_files: true
body: |
## 🚀 Fennel Node ${{ github.ref_name }}
This release ships the Fennel node binary, Docker image, Helm chart and full chainspec set.
### 📦 Quick start for Ansible
```yaml
# Copy these into your Ansible vars (the values are literal, not templated at runtime)
fennel_node_version: "${{ github.ref_name }}"
fennel_node_binary_url: "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ steps.relvars.outputs.raw_name }}"
fennel_node_binary_checksum: "${{ steps.restore_context.outputs.raw_binary_ansible_checksum }}"
fennel_node_tarball_url: "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ steps.relvars.outputs.tarball_name }}"
fennel_node_tarball_checksum: "${{ steps.restore_context.outputs.tarball_ansible_checksum }}"
```
```yaml
- name: Install Fennel node
ansible.builtin.get_url:
url: "{{ fennel_node_binary_url }}"
dest: /usr/local/bin/fennel-node
mode: "0755" # Required: GitHub releases don't preserve executable permissions
checksum: "{{ fennel_node_binary_checksum }}"
```
### 🐳 Docker
```bash
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
```
### � Security & Supply Chain
This release includes:
- **Provenance attestations** for Docker images (buildx `--provenance=true`)
- **Software Bill of Materials (SBOM)** attached as `fennel-docker-sbom.json`
- **Deterministic builds** using srtool for reproducible WASM runtime
### �📋 Chainspecs
Dev · Staging · Production JSON + raw files are attached.
**Runtime WASM SHA-256:** `${{ env.WASM_HASH }}`
---
*Built deterministically with srtool* 🛠️
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 "### 🧮 Node Binary" >> $GITHUB_STEP_SUMMARY
echo "- **Tarball**: \`${{ steps.restore_context.outputs.tarball_basename }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Tarball SHA256**: \`${{ steps.restore_context.outputs.tarball_sha256 }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Tarball Ansible Checksum**: \`${{ steps.restore_context.outputs.tarball_ansible_checksum }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Raw Binary**: \`${{ steps.restore_context.outputs.raw_binary_basename }}\` (for Parity Ansible)" >> $GITHUB_STEP_SUMMARY
echo "- **Raw Binary SHA256**: \`${{ steps.restore_context.outputs.raw_binary_sha256 }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Raw Binary Ansible Checksum**: \`${{ steps.restore_context.outputs.raw_binary_ansible_checksum }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Architecture**: Multi-arch (amd64, arm64)" >> $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.restore_context.outputs.image_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 (multi-arch)" >> $GITHUB_STEP_SUMMARY
echo "- ⚓ **fennel-helm-chart**: Packaged Helm chart (\`.tgz\`)" >> $GITHUB_STEP_SUMMARY
echo "- 🧮 **fennel-node-binary**: Node binary tarballs and SHA256 checksums (amd64, arm64)" >> $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 "- **Node Binary**: \`${{ steps.restore_context.outputs.tarball_basename }}\` (Multi-arch)" >> $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
echo "🤖 **For Parity Ansible Deployment**:" >> $GITHUB_STEP_SUMMARY
BINARY_FILENAME="${{ steps.restore_context.outputs.tarball_basename }}"
RAW_BINARY_FILENAME="${{ steps.restore_context.outputs.raw_binary_basename }}"
echo "- **Tarball URL**: \`https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${BINARY_FILENAME}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Raw Binary URL**: \`https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${RAW_BINARY_FILENAME}\` (recommended)" >> $GITHUB_STEP_SUMMARY
echo "- **Raw Binary Ansible Checksum**: \`${{ steps.restore_context.outputs.raw_binary_ansible_checksum }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Architecture**: Multi-arch (amd64, arm64)" >> $GITHUB_STEP_SUMMARY
echo "- **Usage**: Direct download for Parity Ansible roles (avoids unarchive step)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📋 **Ansible Inventory Example**:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`yaml" >> $GITHUB_STEP_SUMMARY
echo "node_binary: \"https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${RAW_BINARY_FILENAME}\"" >> $GITHUB_STEP_SUMMARY
echo "node_binary_checksum: \"${{ steps.restore_context.outputs.raw_binary_ansible_checksum }}\"" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **Important: Always Set Executable Permissions**" >> $GITHUB_STEP_SUMMARY
echo "GitHub Releases don't preserve POSIX permissions reliably. Always set mode explicitly:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`yaml" >> $GITHUB_STEP_SUMMARY
echo "- name: Download and install Fennel node binary" >> $GITHUB_STEP_SUMMARY
echo " ansible.builtin.get_url:" >> $GITHUB_STEP_SUMMARY
echo " url: \"https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${RAW_BINARY_FILENAME}\"" >> $GITHUB_STEP_SUMMARY
echo " dest: /usr/local/bin/fennel-node" >> $GITHUB_STEP_SUMMARY
echo " mode: '0755'" >> $GITHUB_STEP_SUMMARY
echo " checksum: \"${{ steps.restore_context.outputs.raw_binary_ansible_checksum }}\"" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "This prevents 'Permission denied' errors and follows Substrate community best practices." >> $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