Enhance release tagging and CI/CD robustness #74
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 | |
| # 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 | |
| # Show tag information for debugging | |
| echo "📋 Tag information:" | |
| git show --no-patch --format=" Type: %s%n Author: %an <%ae>%n Date: %ai" "$TAG" || echo "Could not show tag info" | |
| # Verify the tag signature (will fail if tag is unsigned or signature is bad) | |
| if git verify-tag "$TAG"; then | |
| echo "✅ Tag signature verified successfully" | |
| else | |
| echo "❌ Tag signature verification failed" | |
| 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 | |
| # ------------------------------------------------------------ | |
| # 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 runtime inside srtool container (outputs verbose log to console) | |
| docker run --rm \ | |
| -v "${PWD}":/build \ | |
| -e RUNTIME_DIR=runtime/fennel \ | |
| -e PACKAGE=fennel-node-runtime \ | |
| --workdir /build \ | |
| paritytech/srtool:1.84.1 | |
| # 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..." | |
| jq --argjson arr "$EXTERNAL_BOOTNODES" '.bootNodes = $arr' "$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..." | |
| jq --argjson arr "$EXTERNAL_BOOTNODES" '.bootNodes = $arr' "$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 | |
| # 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 | |
| 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 | |
| for spec in "chainspecs/development/development.json" "chainspecs/staging/staging-chainspec.json"; 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 | |
| for rawspec in "chainspecs/development/development-raw.json" "chainspecs/staging/staging-raw.json"; 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 | |
| # 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 | |
| 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 | |
| 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 | |
| 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 | |
| echo "- 🔗 **fennel-chainspecs**: Development and staging chainspecs (JSON + raw)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| 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 |