From 98e16726aef421c4453dcf53aafb8e96d7c69e05 Mon Sep 17 00:00:00 2001 From: zanellig Date: Thu, 3 Sep 2026 17:00:51 -0300 Subject: [PATCH 1/2] ci: run tests, shellcheck, and syntax checks on push and PR Adds a GitHub Actions workflow on ubuntu-latest (the suite spawns real PIDs and reads /proc, so it needs a Linux runner). Fixes the shellcheck findings the new gate surfaced: unquoted expansion inside a pattern, redundant $ on arithmetic array indices, a name collision between the associative target_indices local and the top-level index array, an unquoted $repo_dir in the tests, and a duplicated shebang line. Closes #2 Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ kill-dev-servers.sh | 13 ++++++------- tests/test-cli.sh | 8 ++++---- 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d8a153f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + 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..b3756b5 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 } @@ -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..13d6bf8 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,7 +110,7 @@ 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' @@ -120,7 +120,7 @@ grep -Fq "/work/two with spaces" <<<"$pretty_preview" || fail 'second pretty row [[ "$(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' From 3dbdbcebd067ed6e4643ac84b8e3efc88518b2e7 Mon Sep 17 00:00:00 2001 From: zanellig Date: Thu, 3 Sep 2026 17:03:04 -0300 Subject: [PATCH 2/2] fix: order listener bindings by C collation The first CI run failed on the pretty-table assertion: `sort` collates by locale, so a listener bound to both `*:3002` and `127.0.0.1:3002` came out in one order under en_US.UTF-8 and the other under the runner's C locale. BIND ordering should not depend on the environment's locale, so pin the sort to LC_ALL=C and assert the C order in the suite. Also limit the push trigger to main so PR branches get one run, not two. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 1 + kill-dev-servers.sh | 2 +- tests/test-cli.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8a153f..cd12222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: permissions: diff --git a/kill-dev-servers.sh b/kill-dev-servers.sh index b3756b5..5d193a7 100755 --- a/kill-dev-servers.sh +++ b/kill-dev-servers.sh @@ -384,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=() diff --git a/tests/test-cli.sh b/tests/test-cli.sh index 13d6bf8..7af0f7a 100755 --- a/tests/test-cli.sh +++ b/tests/test-cli.sh @@ -115,7 +115,7 @@ grep -Eq '^TYPE +ELAPSED +RSS +CPU +BIND +CWD +STOP$' <<<"$pretty_preview" || fa 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'