diff --git a/.github/workflows/deploy-tests.yml b/.github/workflows/deploy-tests.yml new file mode 100644 index 0000000..7feb127 --- /dev/null +++ b/.github/workflows/deploy-tests.yml @@ -0,0 +1,17 @@ +name: Deploy scripts +on: + pull_request: + paths: ['deploy/**', '.github/workflows/deploy-tests.yml'] + push: + branches: [main, 'release/**'] + paths: ['deploy/**', '.github/workflows/deploy-tests.yml'] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - run: bash deploy/chain/tests/run.sh diff --git a/deploy/chain/README.md b/deploy/chain/README.md index 039002e..1503bb2 100644 --- a/deploy/chain/README.md +++ b/deploy/chain/README.md @@ -96,7 +96,8 @@ cyberproxy as below. agent-a: see the variant after step 8. `bin/cybernode node --help` lists `--chain --radio --peers`. 2. Scripts: copy `cybernode-check` to `/srv/forge/claude/chain/bin/` and `agent-a/*.sh` next to it (`SOFT3_CHECK` overrides where run-node.sh looks - for the check; it falls back to `PATH`, then to `../cybernode-check`). + for the check; order: `SOFT3_CHECK`, `PATH`, `cybernode-check` next to the script, + then `../cybernode-check`). 3. Parameters: `agent-a.env.example` → `etc/.env`, `agent-a.peers.toml.example` → `etc/.peers.toml` with cyberproxy's id filled in. 4. `SOFT3_ETC=/srv/forge/claude/chain/etc SOFT3_STATE=/srv/forge/claude/chain/state cybernode-check --pre `: diff --git a/deploy/chain/agent-a/run-node.sh b/deploy/chain/agent-a/run-node.sh index f765c01..4d7ceb7 100755 --- a/deploy/chain/agent-a/run-node.sh +++ b/deploy/chain/agent-a/run-node.sh @@ -25,7 +25,9 @@ ulimit -n 65536 2>/dev/null || echo "run-node: could not raise nofile to 65536 ( here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" check="${SOFT3_CHECK:-$(command -v cybernode-check || true)}" -[[ -n "$check" ]] || check="$here/../cybernode-check" +if [[ -z "$check" ]]; then + if [[ -f "$here/cybernode-check" ]]; then check="$here/cybernode-check"; else check="$here/../cybernode-check"; fi +fi bin="$root/bin/cybernode" [[ -x "$bin" ]] || { echo "run-node: $bin is not executable" >&2; exit 2; } diff --git a/deploy/chain/cybernode-check b/deploy/chain/cybernode-check index 93da2f3..909927c 100755 --- a/deploy/chain/cybernode-check +++ b/deploy/chain/cybernode-check @@ -383,7 +383,7 @@ fi udp_bound() { # port -> 0 when a UDP socket listens on it if command -v ss >/dev/null 2>&1; then - ss -uln 2>/dev/null | awk '{print $5}' | grep -qE "[:.]$1$" + ss -uln 2>/dev/null | awk -v p="$1" '{for(i=1;i<=NF;i++) if($i ~ "[:.]" p "$") {f=1}} END{exit !f}' else local hex hex=$(printf '%04X' "$1") diff --git a/deploy/chain/tests/run.sh b/deploy/chain/tests/run.sh new file mode 100755 index 0000000..511c60c --- /dev/null +++ b/deploy/chain/tests/run.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Run every deploy/chain test; non-zero exit if any fails. +dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +rc=0 +for t in "$dir"/test_*.sh; do + if out="$(bash "$t" 2>&1)"; then echo "PASS $(basename "$t")" + else echo "FAIL $(basename "$t")"; [[ -n "$out" ]] && sed 's/^/ /' <<<"$out"; rc=1; fi +done +exit "$rc" diff --git a/deploy/chain/tests/test_run_node_check.sh b/deploy/chain/tests/test_run_node_check.sh new file mode 100755 index 0000000..3447167 --- /dev/null +++ b/deploy/chain/tests/test_run_node_check.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# run-node.sh must find cybernode-check installed next to it (README: bin/ holds both). +set -euo pipefail +src="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/agent-a/run-node.sh" +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/bin" "$tmp/etc" +cat > "$tmp/etc/testchain.env" <<'E' +SOFT3_CHAIN=testchain +SOFT3_BIND=127.0.0.1:7792 +SOFT3_RADIO=0.0.0.0:7793 +SOFT3_MONIKER=test +E +printf '#!/bin/sh\nexit 0\n' > "$tmp/bin/cybernode"; chmod +x "$tmp/bin/cybernode" +cp "$src" "$tmp/bin/run-node.sh" +printf '#!/usr/bin/env bash\ntouch "%s/called"\nexit 1\n' "$tmp" > "$tmp/bin/cybernode-check" +env -u SOFT3_CHECK CHAIN_ROOT="$tmp" PATH="/usr/bin:/bin" \ + timeout 20 bash "$tmp/bin/run-node.sh" testchain >/dev/null 2>&1 || true +[[ -e "$tmp/called" ]] || { echo "cybernode-check next to run-node.sh was not used"; exit 1; } diff --git a/deploy/chain/tests/test_udp_bound.sh b/deploy/chain/tests/test_udp_bound.sh new file mode 100755 index 0000000..c45cbc5 --- /dev/null +++ b/deploy/chain/tests/test_udp_bound.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# udp_bound must work with ss output with and without a Netid column. +set -euo pipefail +script="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/cybernode-check" +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/bin" +awk '/^udp_bound\(\)/{p=1} p{print} p&&/^}/{exit}' "$script" > "$tmp/fn.sh" +[[ -s "$tmp/fn.sh" ]] || { echo "udp_bound not found in script"; exit 1; } +# shellcheck disable=SC1091 +source "$tmp/fn.sh" +export PATH="$tmp/bin:$PATH" +fail=0 +check() { # label + if udp_bound 7793; then :; else echo "$1: 7793 should be bound"; fail=1; fi + if udp_bound 7794; then echo "$1: 7794 should not be bound"; fail=1; fi +} +cat > "$tmp/bin/ss" <<'E' +#!/bin/sh +cat <<'O' +State Recv-Q Send-Q Local Address:Port Peer Address:Port Process +UNCONN 0 0 0.0.0.0:7793 0.0.0.0:* +UNCONN 0 0 [::]:7793 [::]:* +O +E +chmod +x "$tmp/bin/ss" +check "no-netid" +cat > "$tmp/bin/ss" <<'E' +#!/bin/sh +cat <<'O' +Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process +udp UNCONN 0 0 0.0.0.0:7793 0.0.0.0:* +udp UNCONN 0 0 [::]:7793 [::]:* +O +E +check "netid" +exit "$fail" diff --git a/deploy/chain/tests/test_ufw_present.sh b/deploy/chain/tests/test_ufw_present.sh new file mode 100755 index 0000000..b4d2e3b --- /dev/null +++ b/deploy/chain/tests/test_ufw_present.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# ufw-chain.sh present() must match plain `ufw status` (ALLOW, no IN) and still match verbose (ALLOW IN). +set -euo pipefail +script="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/ufw-chain.sh" +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/bin" +grep -E '^present\(\)' "$script" > "$tmp/fn.sh" +[[ -s "$tmp/fn.sh" ]] || { echo "present() not found in script"; exit 1; } +# shellcheck disable=SC1091 +source "$tmp/fn.sh" +export PATH="$tmp/bin:$PATH" +fail=0 +check() { # label + port=7793; if present; then :; else echo "$1: 7793/udp should be present"; fail=1; fi + port=7794; if present; then echo "$1: 7794/udp should not be present"; fail=1; fi +} +cat > "$tmp/bin/ufw" <<'E' +#!/bin/sh +cat <<'O' +Status: active + +To Action From +-- ------ ---- +7793/udp ALLOW Anywhere # cybernode-pussy-radio +7793/udp (v6) ALLOW Anywhere (v6) # cybernode-pussy-radio +O +E +chmod +x "$tmp/bin/ufw" +check "plain" +cat > "$tmp/bin/ufw" <<'E' +#!/bin/sh +cat <<'O' +Status: active + +To Action From +-- ------ ---- +7793/udp ALLOW IN Anywhere # cybernode-pussy-radio +O +E +check "verbose" +exit "$fail" diff --git a/deploy/chain/ufw-chain.sh b/deploy/chain/ufw-chain.sh index a573a81..9d3361a 100755 --- a/deploy/chain/ufw-chain.sh +++ b/deploy/chain/ufw-chain.sh @@ -21,7 +21,7 @@ comment="cybernode-$chain-radio" log="/var/log/cybernode/ufw.log" mkdir -p "$(dirname "$log")" -present() { ufw status | grep -qE "^$port/udp\s+ALLOW IN\s+Anywhere" ; } +present() { ufw status | grep -qE "^$port/udp\s+ALLOW(\s+IN)?\s+Anywhere" ; } case "$action" in show)