|
| 1 | +name: Check Bootnodes |
| 2 | + |
| 3 | +on: |
| 4 | + schedule: |
| 5 | + - cron: "0 5 * * 0" # Runs every Sunday at 5:00 AM UTC |
| 6 | + workflow_dispatch: |
| 7 | + |
| 8 | +jobs: |
| 9 | + check-bootnodes: |
| 10 | + runs-on: ubuntu-latest |
| 11 | + permissions: |
| 12 | + contents: read |
| 13 | + strategy: |
| 14 | + fail-fast: false |
| 15 | + matrix: |
| 16 | + network: |
| 17 | + - name: moonbeam |
| 18 | + chain: moonbeam |
| 19 | + spec_file: specs/moonbeam/parachain-embedded-specs.json |
| 20 | + type: parachain |
| 21 | + - name: moonriver |
| 22 | + chain: moonriver |
| 23 | + spec_file: specs/moonriver/parachain-embedded-specs.json |
| 24 | + type: parachain |
| 25 | + - name: moonbase-alpha |
| 26 | + chain: moonbase-alpha |
| 27 | + spec_file: specs/alphanet/parachain-embedded-specs-v8.json |
| 28 | + type: parachain |
| 29 | + - name: moonbase-alpha-relay |
| 30 | + chain: moonbase-alpha |
| 31 | + spec_file: specs/alphanet/westend-embedded-specs-v8.json |
| 32 | + type: relay |
| 33 | + steps: |
| 34 | + - name: Checkout |
| 35 | + uses: actions/checkout@v4 |
| 36 | + |
| 37 | + - name: Get latest moonbeam release |
| 38 | + if: matrix.network.type == 'parachain' |
| 39 | + id: get-moonbeam-release |
| 40 | + run: | |
| 41 | + LATEST_CLIENT=$(curl -s https://api.github.com/repos/moonbeam-foundation/moonbeam/releases | jq -r '.[] | select(.name | test("v";"i")) | .tag_name' | sort -rs | head -n 1 | tr -d '[:blank:]') |
| 42 | + echo "latest_client=$LATEST_CLIENT" >> $GITHUB_OUTPUT |
| 43 | + echo "Latest Moonbeam release: $LATEST_CLIENT" |
| 44 | +
|
| 45 | + - name: Download moonbeam binary |
| 46 | + if: matrix.network.type == 'parachain' |
| 47 | + run: | |
| 48 | + mkdir -p target/release |
| 49 | + wget -q https://github.com/moonbeam-foundation/moonbeam/releases/download/${{ steps.get-moonbeam-release.outputs.latest_client }}/moonbeam -O target/release/moonbeam |
| 50 | + chmod +x target/release/moonbeam |
| 51 | +
|
| 52 | + - name: Get latest polkadot release |
| 53 | + if: matrix.network.type == 'relay' |
| 54 | + id: get-polkadot-release |
| 55 | + run: | |
| 56 | + LATEST_POLKADOT=$(curl -s https://api.github.com/repos/paritytech/polkadot-sdk/releases/latest | jq -r '.tag_name') |
| 57 | + echo "latest_polkadot=$LATEST_POLKADOT" >> $GITHUB_OUTPUT |
| 58 | + echo "Latest Polkadot SDK release: $LATEST_POLKADOT" |
| 59 | +
|
| 60 | + - name: Download polkadot binary |
| 61 | + if: matrix.network.type == 'relay' |
| 62 | + run: | |
| 63 | + mkdir -p target/release |
| 64 | + wget -q https://github.com/paritytech/polkadot-sdk/releases/download/${{ steps.get-polkadot-release.outputs.latest_polkadot }}/polkadot -O target/release/polkadot |
| 65 | + chmod +x target/release/polkadot |
| 66 | +
|
| 67 | + - name: Validate bootnodes |
| 68 | + run: | |
| 69 | + BOOTNODES=$(jq -r '.bootNodes[]' ${{ matrix.network.spec_file }}) |
| 70 | + TOTAL=$(echo "$BOOTNODES" | wc -l | tr -d ' ') |
| 71 | + SPEC_TYPE="${{ matrix.network.type }}" |
| 72 | +
|
| 73 | + echo "Validating $TOTAL $SPEC_TYPE bootnodes for ${{ matrix.network.name }}..." |
| 74 | + mkdir -p test_results reports |
| 75 | +
|
| 76 | + test_bootnode() { |
| 77 | + local bootnode="$1" idx="$2" |
| 78 | + local base_port=$((30000 + idx * 100)) |
| 79 | + local peer_id=$(echo "$bootnode" | grep -oE "12D3KooW[a-zA-Z0-9]+") |
| 80 | + local log_file="test_results/node_${idx}.log" |
| 81 | + local result="TIMEOUT" |
| 82 | +
|
| 83 | + if [ "$SPEC_TYPE" = "parachain" ]; then |
| 84 | + timeout 65s ./target/release/moonbeam \ |
| 85 | + --chain=${{ matrix.network.chain }} --reserved-only --reserved-nodes "$bootnode" \ |
| 86 | + --tmp --port "$base_port" --rpc-port 0 --no-prometheus --no-hardware-benchmarks \ |
| 87 | + --network-backend libp2p \ |
| 88 | + -- --port "$((base_port + 1))" --rpc-port 0 --no-prometheus --network-backend libp2p \ |
| 89 | + > "$log_file" 2>&1 & |
| 90 | + else |
| 91 | + timeout 65s ./target/release/polkadot \ |
| 92 | + --chain=${{ matrix.network.spec_file }} --reserved-only --reserved-nodes "$bootnode" \ |
| 93 | + --tmp --port "$base_port" --rpc-port 0 --no-prometheus --no-hardware-benchmarks \ |
| 94 | + --network-backend libp2p \ |
| 95 | + > "$log_file" 2>&1 & |
| 96 | + fi |
| 97 | + local pid=$! |
| 98 | +
|
| 99 | + local new_peer_id="" |
| 100 | + for _ in {1..60}; do |
| 101 | + sleep 1 |
| 102 | + if [ -n "$peer_id" ] && grep -q "provided a different peer ID.*$peer_id" "$log_file" 2>/dev/null; then |
| 103 | + new_peer_id=$(grep "provided a different peer ID.*$peer_id" "$log_file" | grep -oE "provided a different peer ID \`[^\`]+\`" | grep -oE "12D3KooW[a-zA-Z0-9]+" | head -1) |
| 104 | + result="PEER_ID_MISMATCH"; break |
| 105 | + fi |
| 106 | + if [ "$SPEC_TYPE" = "parachain" ]; then |
| 107 | + grep -qE "\[🌗\].*\([1-9][0-9]* peers?\)" "$log_file" 2>/dev/null && { result="SUCCESS"; break; } |
| 108 | + else |
| 109 | + grep -qE "\([1-9][0-9]* peers?\)" "$log_file" 2>/dev/null && { result="SUCCESS"; break; } |
| 110 | + fi |
| 111 | + done |
| 112 | +
|
| 113 | + kill $pid 2>/dev/null; wait $pid 2>/dev/null |
| 114 | + if [ "$result" = "PEER_ID_MISMATCH" ] && [ -n "$new_peer_id" ]; then |
| 115 | + echo "${result}:${new_peer_id}:${bootnode}" > "test_results/result_${idx}.txt" |
| 116 | + else |
| 117 | + echo "${result}::${bootnode}" > "test_results/result_${idx}.txt" |
| 118 | + fi |
| 119 | + } |
| 120 | +
|
| 121 | + idx=0 |
| 122 | + while IFS= read -r bootnode; do |
| 123 | + [ -z "$bootnode" ] && continue |
| 124 | + test_bootnode "$bootnode" "$idx" & |
| 125 | + idx=$((idx + 1)) |
| 126 | + sleep 0.5 |
| 127 | + done <<< "$BOOTNODES" |
| 128 | + wait |
| 129 | +
|
| 130 | + # Collect results |
| 131 | + SUCCESS=0 FAILED=0 TIMEOUTS="" MISMATCHES="" |
| 132 | + for f in test_results/result_*.txt; do |
| 133 | + [ -f "$f" ] || continue |
| 134 | + content=$(cat "$f") |
| 135 | + status="${content%%:*}" |
| 136 | + rest="${content#*:}" |
| 137 | + new_peer="${rest%%:*}" |
| 138 | + node="${rest#*:}" |
| 139 | + case "$status" in |
| 140 | + SUCCESS) echo "SUCCESS: $node"; SUCCESS=$((SUCCESS + 1)) ;; |
| 141 | + PEER_ID_MISMATCH) |
| 142 | + echo "PEER_ID_MISMATCH: $node (actual: $new_peer)" |
| 143 | + FAILED=$((FAILED + 1)) |
| 144 | + MISMATCHES+="- \`$node\` → actual peer ID: \`$new_peer\`"$'\n' |
| 145 | + ;; |
| 146 | + *) echo "TIMEOUT: $node"; FAILED=$((FAILED + 1)); TIMEOUTS+="- \`$node\`"$'\n' ;; |
| 147 | + esac |
| 148 | + done |
| 149 | +
|
| 150 | + echo -e "\nResults: $SUCCESS/$TOTAL successful" |
| 151 | +
|
| 152 | + # Save report |
| 153 | + { |
| 154 | + echo "network=${{ matrix.network.name }}" |
| 155 | + echo "total=$TOTAL" |
| 156 | + echo "success=$SUCCESS" |
| 157 | + echo "failed=$FAILED" |
| 158 | + echo "---TIMEOUT---" |
| 159 | + echo -n "$TIMEOUTS" |
| 160 | + echo "---PEER_ID_MISMATCH---" |
| 161 | + echo -n "$MISMATCHES" |
| 162 | + } > reports/${{ matrix.network.name }}.txt |
| 163 | +
|
| 164 | + - name: Upload results |
| 165 | + uses: actions/upload-artifact@v4 |
| 166 | + with: |
| 167 | + name: bootnode-results-${{ matrix.network.name }} |
| 168 | + path: reports/ |
| 169 | + retention-days: 1 |
| 170 | + |
| 171 | + report: |
| 172 | + needs: check-bootnodes |
| 173 | + runs-on: ubuntu-latest |
| 174 | + if: always() |
| 175 | + permissions: |
| 176 | + issues: write |
| 177 | + steps: |
| 178 | + - name: Download all results |
| 179 | + uses: actions/download-artifact@v4 |
| 180 | + with: |
| 181 | + path: results |
| 182 | + pattern: bootnode-results-* |
| 183 | + merge-multiple: true |
| 184 | + |
| 185 | + - name: Create issue if failures detected |
| 186 | + env: |
| 187 | + GH_TOKEN: ${{ github.token }} |
| 188 | + run: | |
| 189 | + TOTAL_FAILURES=0 |
| 190 | +
|
| 191 | + # Build summary table |
| 192 | + TABLE="| Network | Status | Working |\n|---------|--------|---------|" |
| 193 | + DETAILS="" |
| 194 | +
|
| 195 | + for report in results/*.txt; do |
| 196 | + [ -f "$report" ] || continue |
| 197 | + network=$(grep "^network=" "$report" | cut -d= -f2) |
| 198 | + total=$(grep "^total=" "$report" | cut -d= -f2) |
| 199 | + success=$(grep "^success=" "$report" | cut -d= -f2) |
| 200 | + failed=$(grep "^failed=" "$report" | cut -d= -f2) |
| 201 | + TOTAL_FAILURES=$((TOTAL_FAILURES + failed)) |
| 202 | +
|
| 203 | + if [ "$failed" -gt 0 ]; then |
| 204 | + TABLE+="\n| **$network** | :x: | $success/$total |" |
| 205 | + timeouts=$(sed -n '/---TIMEOUT---/,/---PEER_ID_MISMATCH---/p' "$report" | grep -v "^---" | grep -v "^$" || true) |
| 206 | + mismatches=$(sed -n '/---PEER_ID_MISMATCH---/,$ p' "$report" | grep -v "^---" | grep -v "^$" || true) |
| 207 | +
|
| 208 | + DETAILS+="\n### $network\n" |
| 209 | + [ -n "$mismatches" ] && DETAILS+="\n<details>\n<summary>:warning: Peer ID Mismatch</summary>\n\n$mismatches\n</details>\n" |
| 210 | + [ -n "$timeouts" ] && DETAILS+="\n<details>\n<summary>:hourglass: Connection Timeout</summary>\n\n$timeouts\n</details>\n" |
| 211 | + else |
| 212 | + TABLE+="\n| **$network** | :white_check_mark: | $success/$total |" |
| 213 | + fi |
| 214 | + done |
| 215 | +
|
| 216 | + [ "$TOTAL_FAILURES" -eq 0 ] && { echo "All bootnodes healthy!"; exit 0; } |
| 217 | +
|
| 218 | + BODY="The weekly bootnode validation check has detected connectivity issues.\n\n$(echo -e "$TABLE")\n$DETAILS\n---\n**Workflow Run:** [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" |
| 219 | +
|
| 220 | + ISSUE_TITLE="Bootnode Validation Failures" |
| 221 | + EXISTING=$(gh issue list --repo ${{ github.repository }} --state open --search "in:title \"$ISSUE_TITLE\"" --json number --jq '.[0].number' 2>/dev/null || true) |
| 222 | +
|
| 223 | + if [ -n "$EXISTING" ]; then |
| 224 | + echo "Updating issue #$EXISTING" |
| 225 | + echo -e "$BODY" | gh issue edit "$EXISTING" --repo ${{ github.repository }} --body-file - |
| 226 | + else |
| 227 | + echo "Creating new issue" |
| 228 | + echo -e "$BODY" | gh issue create --repo ${{ github.repository }} --title "$ISSUE_TITLE" --body-file - --label "bootnodes" |
| 229 | + fi |
0 commit comments