Skip to content

fix(ci): ensure tag objects are properly fetched in GitHub Actions #79

fix(ci): ensure tag objects are properly fetched in GitHub Actions

fix(ci): ensure tag objects are properly fetched in GitHub Actions #79

Workflow file for this run

name: Create and publish a Docker image
# Configures this workflow to run on main branch pushes and signed release tags
on:
push:
branches: ['main'] # keep quick checks for PRs
tags: ['fennel-node-*'] # run only for release tags
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# NOTE: Bootnode configurations are now dynamically generated from GitHub secrets
# to prevent peer ID collisions and follow external-only bootnode architecture
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: write # Changed from read to write to allow pushing commits
packages: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for tag verification
fetch-tags: true # Explicitly fetch all tags
ref: ${{ github.ref }} # Ensure we check out the exact ref that triggered the workflow
# Ensure tag objects are properly fetched (GitHub Actions sometimes fetches only refs)
- name: Refetch tag objects
if: startsWith(github.ref, 'refs/tags/')
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "🔄 Ensuring tag object is available for: $TAG"
# Force refetch the tag with its object
git fetch origin "refs/tags/$TAG:refs/tags/$TAG" --force
# Verify we now have the tag object
echo "📋 Post-fetch verification:"
echo " Tag type: $(git cat-file -t "refs/tags/$TAG" 2>/dev/null || echo 'unknown')"
echo " Tag hash: $(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo 'unknown')"
echo " Commit hash: $(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo 'unknown')"
# They should be different for annotated tags
TAG_HASH=$(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo '')
COMMIT_HASH=$(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo '')
if [ "$TAG_HASH" != "$COMMIT_HASH" ] && [ -n "$TAG_HASH" ] && [ -n "$COMMIT_HASH" ]; then
echo "✅ Annotated tag object successfully fetched"
else
echo "⚠️ Tag object may not be properly fetched, attempting alternative fetch..."
# Try fetching all tag objects
git fetch origin '+refs/tags/*:refs/tags/*' --force
# Re-verify
NEW_TAG_HASH=$(git rev-parse "refs/tags/$TAG" 2>/dev/null || echo '')
NEW_COMMIT_HASH=$(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || echo '')
echo " After alternative fetch - Tag: $NEW_TAG_HASH, Commit: $NEW_COMMIT_HASH"
fi
# Verify tag signature for release builds
- name: Verify tag signature
if: startsWith(github.ref, 'refs/tags/')
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "🔐 Verifying signature for tag: $TAG"
# Import the trusted public key
gpg --import .github/release-signing.pub.asc
# Basic validation that this is a proper release tag
if [[ ! "$TAG" =~ ^fennel-node-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid tag format. Expected: fennel-node-X.Y.Z"
exit 1
fi
# Ensure the tag exists and is available locally
echo "🔍 Checking if tag exists locally..."
if ! git tag -l | grep -q "^$TAG$"; then
echo "⚠️ Tag not found locally, fetching..."
git fetch origin "refs/tags/$TAG:refs/tags/$TAG"
fi
# Verify tag object type and show debugging info
echo "🔍 Debugging tag object..."
# Check different ways to reference the tag
echo " Tag type (direct): $(git cat-file -t "$TAG" 2>/dev/null || echo 'unknown')"
echo " Tag type (refs/tags): $(git cat-file -t "refs/tags/$TAG" 2>/dev/null || echo 'unknown')"
echo " Tag exists in local refs: $(git tag -l | grep "^$TAG$" || echo 'NOT FOUND')"
# Determine the correct tag reference to use
TAG_REF="$TAG"
if git cat-file -t "refs/tags/$TAG" 2>/dev/null | grep -q "tag"; then
TAG_REF="refs/tags/$TAG"
echo " Using full ref: $TAG_REF"
elif git cat-file -t "$TAG" 2>/dev/null | grep -q "tag"; then
echo " Using short ref: $TAG_REF"
else
echo "⚠️ Warning: Tag reference resolves to commit, trying explicit refs..."
# List all possible tag references
echo " Available tag refs:"
git for-each-ref refs/tags/ --format="%(refname) -> %(objecttype)" | grep "$TAG" || echo " No matching tag refs found"
# Try to find the actual tag object
if git show-ref --tags | grep -q "refs/tags/$TAG$"; then
TAG_REF="refs/tags/$TAG"
echo " Found explicit tag ref: $TAG_REF"
fi
fi
# Show tag information for debugging
echo "📋 Tag object information (using $TAG_REF):"
if git cat-file -t "$TAG_REF" 2>/dev/null | grep -q "tag"; then
echo "✅ Found tag object, showing tag info:"
git cat-file tag "$TAG_REF" | head -10
else
echo "⚠️ Not a tag object, showing commit info:"
git show --no-patch --format=" Commit: %H%n Author: %an <%ae>%n Date: %ai%n Subject: %s" "$TAG_REF"
fi
# Verify the tag signature using the correct reference
echo "🔐 Attempting tag signature verification (using $TAG_REF)..."
if git verify-tag "$TAG_REF" 2>&1; then
echo "✅ Tag signature verified successfully"
else
echo "❌ Tag signature verification failed"
echo "🔍 Debugging tag verification issue..."
# Show detailed git information
echo " Git ref information:"
git show-ref | grep "$TAG" || echo " No refs found for $TAG"
echo " Tag object details:"
git for-each-ref refs/tags/ --format="%(refname) %(objecttype) %(objectname)" | grep "$TAG" || echo " No tag object found"
# Try alternative verification approaches
echo " Trying alternative verification methods..."
# Method 1: Direct tag object verification
if git show-ref --tags | grep -q "refs/tags/$TAG$"; then
echo " Method 1: Using explicit refs/tags/$TAG"
git verify-tag "refs/tags/$TAG" --verbose 2>&1 || echo " Failed"
fi
# Method 2: Check if it's an annotated tag
COMMIT_HASH=$(git rev-parse "$TAG^{commit}" 2>/dev/null || echo "")
TAG_HASH=$(git rev-parse "$TAG" 2>/dev/null || echo "")
echo " Commit hash: $COMMIT_HASH"
echo " Tag hash: $TAG_HASH"
if [ "$COMMIT_HASH" != "$TAG_HASH" ]; then
echo " Tag is annotated (hashes differ), should be verifiable"
else
echo " Tag appears to be lightweight (hashes same), cannot verify signature"
fi
exit 1
fi
# Free up additional disk space on GitHub runner (~20-25 GB)
# Enhanced cleanup for Polkadot SDK builds without cache
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # 1.3.1
with:
android: true # 12 GB save
dotnet: true # 3 GB save
haskell: true # 2 GB save
large-packages: true # 4 GB save
swap-storage: false # Keep swap for large builds
# Set up Docker Buildx for efficient layer caching
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# ------------------------------------------------------------
# SECURITY: Derive peer IDs from GitHub secrets
# Private keys are decoded temporarily in memory, never logged
# ------------------------------------------------------------
- name: Derive bootnode peer IDs from secrets
run: |
set -euo pipefail
echo "🔐 Deriving bootnode peer IDs from GitHub secrets..."
# Install polkadot binary from official releases (most reliable)
echo "📥 Installing polkadot binary..."
POLKADOT_VERSION="v1.9.0"
wget -q "https://github.com/paritytech/polkadot-sdk/releases/download/polkadot-${POLKADOT_VERSION}/polkadot" -O polkadot
chmod +x polkadot
# Verify installation
./polkadot --version
# Helper: derive peer-id from a key file using polkadot
# Parse only last line (PeerID) to avoid logging sensitive data
derive_peer () {
./polkadot key inspect-node-key --file "$1" 2>/dev/null | tail -1
}
# Decode secrets to tmpfs (runner's /tmp is already tmpfs), derive IDs
BOOT1=$(mktemp)
BOOT2=$(mktemp)
trap 'rm -f "$BOOT1" "$BOOT2"' EXIT # always wipe
echo "${{ secrets.BOOTNODE1_KEY_B64 }}" | base64 -d > "$BOOT1"
echo "${{ secrets.BOOTNODE2_KEY_B64 }}" | base64 -d > "$BOOT2"
PEER1="$(derive_peer "$BOOT1")"
PEER2="$(derive_peer "$BOOT2")"
# Mask *right now* so nothing else can leak it
echo "::add-mask::$PEER1"
echo "::add-mask::$PEER2"
# Build JSON array (still invisible in logs because masked)
BOOTNODES_JSON=$(jq -cn --arg id1 "$PEER1" --arg id2 "$PEER2" '
[
"/dns4/bootnode1.fennel.network/tcp/30333/p2p/\($id1)",
"/dns4/bootnode2.fennel.network/tcp/30333/p2p/\($id2)"
]')
# Set environment variables for subsequent steps (use base64 for safety)
echo "EXTERNAL_BOOTNODES_B64=$(printf %s "$BOOTNODES_JSON" | base64 -w0)" >> $GITHUB_ENV
echo "BOOTNODE1_PEER_ID=$PEER1" >> $GITHUB_ENV
echo "BOOTNODE2_PEER_ID=$PEER2" >> $GITHUB_ENV
# Log success (peer IDs are masked)
echo "✅ Successfully derived peer IDs from secrets using official Parity container"
echo "🌐 Using external-only bootnode architecture (following Parity best practices)"
echo "📋 Bootnode configuration: [MASKED - using derived peer IDs]"
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=long
# ------------------------------------------------------------
# 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