diff --git a/.github/workflows/rpc_healthcheck.yml b/.github/workflows/rpc_healthcheck.yml new file mode 100644 index 00000000..cd08d189 --- /dev/null +++ b/.github/workflows/rpc_healthcheck.yml @@ -0,0 +1,248 @@ +name: RPC Healthcheck + +permissions: + contents: read + pull-requests: write + +on: + schedule: + - cron: '41 2 * * *' + - cron: '41 10 * * *' + - cron: '41 18 * * *' + workflow_dispatch: + + +jobs: + check-networkinfo: + name: Check ${{ matrix.network }} networkInfo + if: ${{ github.repository == 'QuarkChain/goquarkchain' }} + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + include: + - network: mainnet + rpc_url: https://rpc.indexer.mainnet.quarkchain.io:38391 + expected_network_id: "0x1" + - network: devnet + rpc_url: https://rpc.indexer.devnet.quarkchain.io:38392 + expected_network_id: "0xff" + - network: mainnetHost + rpc_url: http://65.108.121.223:38391 + expected_network_id: "0x1" + - network: devnetHost + rpc_url: http://50.112.62.65:38391 + expected_network_id: "0xff" + + steps: + - name: Check endpoint JSON field + shell: bash + env: + RPC_URL: ${{ matrix.rpc_url }} + EXPECTED_NETWORK_ID: ${{ matrix.expected_network_id }} + NETWORK_NAME: ${{ matrix.network }} + run: | + set -euo pipefail + + payload='{"jsonrpc":"2.0","id":"id","method":"networkInfo","params":[]}' + resp_file="${RUNNER_TEMP}/networkinfo_${NETWORK_NAME}.json" + report_file="${RUNNER_TEMP}/networkinfo_report_${NETWORK_NAME}.json" + : > "$resp_file" + + code="$( + curl --silent --show-error --location \ + --retry 3 --retry-delay 5 --retry-all-errors \ + --connect-timeout 10 --max-time 30 \ + -X POST \ + -H 'Content-Type: application/json' \ + --data "$payload" \ + --output "$resp_file" --write-out '%{http_code}' \ + "$RPC_URL" \ + || echo '000' + )" + + result="success" + error_msg="" + + if [[ "$code" != "200" ]]; then + result="failure" + error_msg="HTTP status not 200: $code" + elif ! jq -e --arg expected "$EXPECTED_NETWORK_ID" '.result.networkId == $expected' "$resp_file" > /dev/null; then + result="failure" + error_msg="JSON check failed: .result.networkId == $EXPECTED_NETWORK_ID" + fi + + jq -n \ + --arg network "$NETWORK_NAME" \ + --arg rpc_url "$RPC_URL" \ + --arg result "$result" \ + --arg error "$error_msg" \ + '{network: $network, rpc_url: $rpc_url, result: $result, error: $error}' > "$report_file" + + if [[ "$result" != "success" ]]; then + echo "[$NETWORK_NAME] $error_msg" + cat "$resp_file" + exit 1 + fi + + echo "[$NETWORK_NAME] JSON check passed: .result.networkId == $EXPECTED_NETWORK_ID" + + - name: Upload endpoint report + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: rpc-healthcheck-${{ matrix.network }} + path: ${{ runner.temp }}/networkinfo_report_${{ matrix.network }}.json + if-no-files-found: error + + notify: + name: Notify (email) + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: + - check-networkinfo + if: ${{ always() && github.repository == 'QuarkChain/goquarkchain' }} + + steps: + - name: Download endpoint reports + uses: actions/download-artifact@v4 + with: + pattern: rpc-healthcheck-* + merge-multiple: true + path: ${{ runner.temp }}/rpc-healthcheck + + - name: Compose email + id: compose + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + EVENT_SCHEDULE: ${{ github.event.schedule }} + CHECK_RESULT: ${{ needs.check-networkinfo.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + send=false + overall="OK" + report_dir="${RUNNER_TEMP}/rpc-healthcheck" + + table_rows_with_error='' + table_rows_no_error='' + html_rows_with_error='' + html_rows_no_error='' + + shopt -s nullglob + report_files=("$report_dir"/*.json) + if [[ ${#report_files[@]} -eq 0 ]]; then + overall="FAILED" + send=true + table_rows_with_error='| unknown | unknown | failure | missing endpoint report artifact |' + html_rows_with_error='unknownunknownfailuremissing endpoint report artifact' + else + for f in "${report_files[@]}"; do + network="$(jq -r '.network' "$f")" + rpc_url="$(jq -r '.rpc_url' "$f")" + result="$(jq -r '.result' "$f")" + error="$(jq -r '.error' "$f")" + + if [[ "$result" != "success" ]]; then + overall="FAILED" + send=true + if [[ -z "$error" ]]; then + error="unknown error" + fi + else + if [[ -z "$error" ]]; then + error="-" + fi + fi + + # Escape markdown table special chars + rpc_url="${rpc_url//|/\\|}" + error="${error//|/\\|}" + table_rows_with_error+="| ${network} | ${rpc_url} | ${result} | ${error} |\n" + table_rows_no_error+="| ${network} | ${rpc_url} | ${result} |\n" + + network_html="${network//&/&}" + network_html="${network_html///>}" + rpc_url_html="${rpc_url//&/&}" + rpc_url_html="${rpc_url_html///>}" + result_html="${result//&/&}" + result_html="${result_html///>}" + error_html="${error//&/&}" + error_html="${error_html///>}" + html_rows_with_error+="${network_html}${rpc_url_html}${result_html}${error_html}" + html_rows_no_error+="${network_html}${rpc_url_html}${result_html}" + done + fi + + if [[ "$CHECK_RESULT" != "success" ]]; then + overall="FAILED" + send=true + else + # Success: send only on manual trigger, or once per day for scheduled runs. + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + send=true + elif [[ "$EVENT_NAME" == "schedule" ]]; then + if [[ "${EVENT_SCHEDULE:-}" == "41 18 * * *" ]]; then + send=true + fi + fi + fi + if [[ "$overall" == "OK" ]]; then + subject="✅ QuarkChain RPC Healthcheck OK" + else + subject="❌ QuarkChain RPC Healthcheck FAILED" + fi + + echo "send=$send" >> "$GITHUB_OUTPUT" + echo "subject=$subject" >> "$GITHUB_OUTPUT" + echo "body<> "$GITHUB_OUTPUT" + echo "QuarkChain RPC Healthcheck: $overall" >> "$GITHUB_OUTPUT" + echo >> "$GITHUB_OUTPUT" + echo "Event: $EVENT_NAME" >> "$GITHUB_OUTPUT" + echo "Schedule: ${EVENT_SCHEDULE:-N/A}" >> "$GITHUB_OUTPUT" + echo "Run: $RUN_URL" >> "$GITHUB_OUTPUT" + echo >> "$GITHUB_OUTPUT" + if [[ "$overall" == "OK" ]]; then + echo "| network | rpc_url | result |" >> "$GITHUB_OUTPUT" + echo "|---|---|---|" >> "$GITHUB_OUTPUT" + printf "%b" "$table_rows_no_error" >> "$GITHUB_OUTPUT" + else + echo "| network | rpc_url | result | error |" >> "$GITHUB_OUTPUT" + echo "|---|---|---|---|" >> "$GITHUB_OUTPUT" + printf "%b" "$table_rows_with_error" >> "$GITHUB_OUTPUT" + fi + echo "EOF" >> "$GITHUB_OUTPUT" + + echo "html_body<> "$GITHUB_OUTPUT" + echo "

QuarkChain RPC Healthcheck: ${overall}

" >> "$GITHUB_OUTPUT" + echo "

Event: ${EVENT_NAME}
Schedule: ${EVENT_SCHEDULE:-N/A}
Run: ${RUN_URL}

" >> "$GITHUB_OUTPUT" + echo "" >> "$GITHUB_OUTPUT" + if [[ "$overall" == "OK" ]]; then + echo "" >> "$GITHUB_OUTPUT" + echo "${html_rows_no_error}" >> "$GITHUB_OUTPUT" + else + echo "" >> "$GITHUB_OUTPUT" + echo "${html_rows_with_error}" >> "$GITHUB_OUTPUT" + fi + echo "
networkrpc_urlresult
networkrpc_urlresulterror
" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Send email + if: ${{ steps.compose.outputs.send == 'true' }} + uses: dawidd6/action-send-mail@v6 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.RPC_HEALTHCHECK_SMTP_USERNAME }} + password: ${{ secrets.RPC_HEALTHCHECK_SMTP_PASSWORD }} + from: QuarkChainRPC + to: ${{ secrets.RPC_HEALTHCHECK_EMAIL_TO }} + subject: ${{ steps.compose.outputs.subject }} + body: ${{ steps.compose.outputs.body }} + html_body: ${{ steps.compose.outputs.html_body }}