diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cd12222 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + SCRIPTS: kill-dev-servers.sh install.sh tests/test-cli.sh + +jobs: + test: + # The suite spawns real PIDs and reads /proc, so it needs a Linux runner. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Syntax check + # bash -n only reads its first file argument, so check one at a time. + run: for script in $SCRIPTS; do bash -n "$script"; done + + - name: Shellcheck + run: shellcheck $SCRIPTS + + - name: CLI tests + run: bash tests/test-cli.sh diff --git a/kill-dev-servers.sh b/kill-dev-servers.sh index 6b2935b..5d193a7 100755 --- a/kill-dev-servers.sh +++ b/kill-dev-servers.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -#!/usr/bin/env bash set -euo pipefail usage() { @@ -276,7 +275,7 @@ discover_listeners_with_ss() { rest="$line" while [[ "$rest" =~ pid=([0-9]+) ]]; do printf '%s\t%s\n' "${BASH_REMATCH[1]}" "$endpoint" - rest="${rest#*pid=${BASH_REMATCH[1]}}" + rest="${rest#*pid="${BASH_REMATCH[1]}"}" done done || true } @@ -362,9 +361,9 @@ collect_candidates() { [[ -z "${seen_bindings["$target_key|$binding"]:-}" ]] || continue seen_bindings["$target_key|$binding"]=1 if [[ -n "${candidate_bindings[$candidate_index]}" ]]; then - candidate_bindings[$candidate_index]+=$'\n' + candidate_bindings[candidate_index]+=$'\n' fi - candidate_bindings[$candidate_index]+="$binding" + candidate_bindings[candidate_index]+="$binding" done <<<"${listener_bindings_by_pid[$pid]:-}" done } @@ -385,7 +384,7 @@ while IFS=$'\t' read -r pid binding; do listener_bindings_by_pid[$pid]+=$'\n' listener_bindings_by_pid[$pid]+="$binding" fi -done < <(discover_listeners | sort -k1,1n -k2,2 -u) +done < <(discover_listeners | LC_ALL=C sort -k1,1n -k2,2 -u) declare -a candidate_pids=() declare -a candidate_pgids=() @@ -427,7 +426,7 @@ if [[ -n "$target_pid" ]]; then printf 'Refusing --pid %s: it is not a current KDS candidate. Run kds --dry-run again.\n' "$target_pid" >&2 exit 1 fi - target_indices=("$selected_index") + selected_indices=("$selected_index") else if (( ${#listener_pids[@]} == 0 )); then echo "No TCP listeners found." @@ -437,13 +436,13 @@ else echo "No matching host dev servers found." exit 0 fi - target_indices=("${!candidate_pids[@]}") + selected_indices=("${!candidate_pids[@]}") fi declare -A terminated_pgids=() declare -A terminated_pids=() -for index in "${target_indices[@]}"; do +for index in "${selected_indices[@]}"; do pid="${candidate_pids[$index]}" pgid="${candidate_pgids[$index]}" kill_mode="${candidate_modes[$index]}" diff --git a/tests/test-cli.sh b/tests/test-cli.sh index 2a343b3..7af0f7a 100755 --- a/tests/test-cli.sh +++ b/tests/test-cli.sh @@ -99,8 +99,8 @@ fail() { exit 1 } -preview="$($repo_dir/kill-dev-servers.sh --dry-run)" -short_preview="$($repo_dir/kill-dev-servers.sh -n)" +preview="$("$repo_dir"/kill-dev-servers.sh --dry-run)" +short_preview="$("$repo_dir"/kill-dev-servers.sh -n)" [[ "$preview" == "$short_preview" ]] || fail '-n differs from --dry-run' grep -Eq "candidate [0-9]+: pid=$pid_one pgid=81001 cmd=node /work/one/node_modules/.bin/vite \| stop with: kds --pid $pid_one" <<<"$preview" || fail 'first server is not actionable' grep -Eq "candidate [0-9]+: pid=$pid_two pgid=81002 cmd=next dev --turbopack \| stop with: kds --pid $pid_two" <<<"$preview" || fail 'second server is not actionable' @@ -110,17 +110,17 @@ if grep -Fq "pid=$pid_four" <<<"$preview"; then fi [[ ! -e "$KILL_LOG" ]] || fail 'dry-run signaled a process' -pretty_preview="$($repo_dir/kill-dev-servers.sh --dry-run --pretty)" +pretty_preview="$("$repo_dir"/kill-dev-servers.sh --dry-run --pretty)" grep -Eq '^TYPE +ELAPSED +RSS +CPU +BIND +CWD +STOP$' <<<"$pretty_preview" || fail 'pretty preview has no header' grep -Fq "node 01:23 64 MiB 1.5% 127.0.0.1:3000, [::1]:3001" <<<"$pretty_preview" || fail 'first pretty row lacks process metadata or bindings' grep -Fq "/work/one" <<<"$pretty_preview" || fail 'first pretty row lacks cwd' grep -Fq "kds --pid $pid_one" <<<"$pretty_preview" || fail 'first pretty row is not actionable' -grep -Fq "bun 2-03:04:05 512 KiB 0.0% 127.0.0.1:3002, *:3002" <<<"$pretty_preview" || fail 'second pretty row lacks process metadata or bindings' +grep -Fq "bun 2-03:04:05 512 KiB 0.0% *:3002, 127.0.0.1:3002" <<<"$pretty_preview" || fail 'second pretty row lacks process metadata or bindings' grep -Fq "/work/two with spaces" <<<"$pretty_preview" || fail 'second pretty row lacks cwd' [[ "$(grep -Fo '127.0.0.1:3000' <<<"$pretty_preview" | wc -l)" -eq 1 ]] || fail 'duplicate bindings were not removed' [[ ! -e "$KILL_LOG" ]] || fail 'pretty dry-run signaled a process' -missing_preview="$(MISSING_PRETTY_METADATA=1 $repo_dir/kill-dev-servers.sh --dry-run --pretty)" +missing_preview="$(MISSING_PRETTY_METADATA=1 "$repo_dir"/kill-dev-servers.sh --dry-run --pretty)" grep -Eq '^- +- +- +- +' <<<"$missing_preview" || fail 'missing pretty metadata has no fallbacks' grep -Fq "kds --pid $pid_one" <<<"$missing_preview" || fail 'missing metadata removed actionable stop command'