From 21fda664b4a20b43097b4e187939436555cd8349 Mon Sep 17 00:00:00 2001 From: KakkoiDev Date: Fri, 31 Jul 2026 22:28:01 +0900 Subject: [PATCH] Enforce documented CI compatibility --- .github/workflows/test.yml | 28 ++++++++++++++--- scripts/filter.sh | 1 + scripts/helpers.sh | 5 ++- scripts/worktree_manager.sh | 31 +++++++++---------- .../integration/test_worktree_lifecycle.bats | 6 ++-- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6a9390..b2d9b75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - tmux-version: ['3.2', '3.3', '3.4'] + # README.md supports tmux 3.0+, so keep the compatibility floor in CI. + tmux-version: ['3.0', '3.1', '3.2', '3.3', '3.4'] fail-fast: false steps: @@ -51,9 +52,7 @@ jobs: run: sudo apt-get install -y shellcheck - name: Lint scripts - run: | - shellcheck scripts/*.sh || true - echo "Shellcheck completed (warnings allowed)" + run: shellcheck scripts/*.sh # lib/ is vendored from KakkoiDev/tmux-toolkit via git subtree. Editing it # in place here is how five plugins accumulated 26 duplicated helpers in @@ -73,6 +72,27 @@ jobs: fi echo "lib/ matches tmux-toolkit $(cat lib/VERSION)" + bash-3-2: + # macOS still ships /bin/bash 3.2, the minimum documented in README.md. + runs-on: macos-15 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: brew install bats-core expect tmux + + - name: Verify compatibility shell + run: | + /bin/bash --version + /bin/bash -c '[[ ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]} == 3.2 ]]' + + - name: Run unit tests with Bash 3.2 + run: make test-unit SHELL=/bin/bash + + - name: Run integration tests with Bash 3.2 + run: make test-integration SHELL=/bin/bash + smoke: runs-on: ubuntu-latest steps: diff --git a/scripts/filter.sh b/scripts/filter.sh index ef1dfc1..54c9c2a 100755 --- a/scripts/filter.sh +++ b/scripts/filter.sh @@ -35,6 +35,7 @@ matches_filter() { # Use bash extended pattern matching # * becomes * (works in case) # ? becomes ? (works in case) + # shellcheck disable=SC2254 # The user-supplied glob is intentional here. case "$lower_string" in $lower_pattern) return 0 ;; *) return 1 ;; diff --git a/scripts/helpers.sh b/scripts/helpers.sh index 0c46cdb..7a0c2f1 100755 --- a/scripts/helpers.sh +++ b/scripts/helpers.sh @@ -8,6 +8,7 @@ # VERSION # ============================================================================== +# shellcheck disable=SC2034 # Public variable consumed by worktree_manager.sh. TMUX_WORKTREE_VERSION="0.1.0" # Determine plugin directory (works when sourced or executed) @@ -19,6 +20,7 @@ else PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" fi +# shellcheck disable=SC2034 # Public variable used by plugin loaders and tests. SCRIPTS_DIR="$PLUGIN_DIR/scripts" # ============================================================================== @@ -34,7 +36,7 @@ SCRIPTS_DIR="$PLUGIN_DIR/scripts" # `if [ -n "$TMUX_SOCKET" ]` branches into one. tk_tmux prepends -L when it is # set, so every call site becomes a plain `tk_tmux ...` with no fork. -# shellcheck source=../lib/toolkit.sh +# shellcheck disable=SC1091 # Runtime path is derived from the plugin checkout. source "$PLUGIN_DIR/lib/toolkit.sh" tk_require_version 0.2.0 tk_init worktree "${WORKTREE_BASE:-$HOME/.tmux-worktree}" @@ -268,6 +270,7 @@ load_config() { if _is_cache_valid "$cache_file"; then # Fast path: read from cache file + # shellcheck disable=SC1090 # The generated cache path is intentionally dynamic. source "$cache_file" debug_log "load_config: from cache=$cache_file" else diff --git a/scripts/worktree_manager.sh b/scripts/worktree_manager.sh index e4f8c12..94b7a9b 100755 --- a/scripts/worktree_manager.sh +++ b/scripts/worktree_manager.sh @@ -23,9 +23,11 @@ else fi # Source helpers and load config (only if not already loaded in test context) +# shellcheck disable=SC1091 # Runtime path is derived from this script's location. source "$SCRIPT_DIR/helpers.sh" +# shellcheck disable=SC1091 # Runtime path is derived from this script's location. source "$SCRIPT_DIR/filter.sh" -# shellcheck source=../lib/menu.sh +# shellcheck disable=SC1091 # Runtime path is derived from the plugin checkout. source "${PLUGIN_DIR:-$SCRIPT_DIR/..}/lib/menu.sh" # Skip load_config if WORKTREE_BASE is already set to a temp path (test mode) if [[ ! "$WORKTREE_BASE" == /tmp/* ]]; then @@ -81,13 +83,13 @@ fetch_remote_branches() { debug_log "Starting git fetch in $(pwd) with timeout ${timeout_seconds}s" # Build fetch command (--prune only when explicitly enabled) - local fetch_args="--all" + local fetch_args=(--all) if [ "${FETCH_PRUNE:-off}" = "on" ]; then - fetch_args="--all --prune" + fetch_args+=(--prune) fi # Run git fetch with timeout, capture stderr - if run_with_timeout "$timeout_seconds" git fetch $fetch_args 2>"$error_file"; then + if run_with_timeout "$timeout_seconds" git fetch "${fetch_args[@]}" 2>"$error_file"; then debug_log "Fetch completed successfully" tmux display-message "Remote branches fetched successfully" rm -f "$error_file" @@ -96,7 +98,7 @@ fetch_remote_branches() { local exit_code=$? local error_msg error_msg=$(head -1 "$error_file" 2>/dev/null | cut -c1-80) - error_log "fetch_remote_branches: cwd=$(pwd) args=$fetch_args exit=$exit_code err=$(cat "$error_file" 2>/dev/null)" + error_log "fetch_remote_branches: cwd=$(pwd) args=${fetch_args[*]} exit=$exit_code err=$(cat "$error_file" 2>/dev/null)" # Show first line of error (truncated) or generic message if [ -n "$error_msg" ]; then @@ -523,11 +525,6 @@ _add_nav_items() { local extra2=${6:-} local script_path="$SCRIPT_DIR/worktree_manager.sh" - local filter_args="" - if [ -n "$filter" ]; then - filter_args="'$filter'" - fi - # Build the sub-args list for tk_menu_cmd. # We must pass each positional arg separately so quoting works. local _nav_args=() @@ -538,7 +535,8 @@ _add_nav_items() { [ -n "$filter" ] && _nav_args+=("$filter") [ -n "$extra1" ] && _nav_args+=("$extra1") [ -n "$extra2" ] && _nav_args+=("$extra2") - local prev_cmd="display-message 'Loading...' ; $(tk_menu_cmd "${_nav_args[@]}")" + local prev_cmd + prev_cmd="display-message 'Loading...' ; $(tk_menu_cmd "${_nav_args[@]}")" tk_menu_item "◀ Previous" "$KEY_PREV" "$prev_cmd" fi @@ -548,7 +546,8 @@ _add_nav_items() { [ -n "$filter" ] && _nav_args+=("$filter") [ -n "$extra1" ] && _nav_args+=("$extra1") [ -n "$extra2" ] && _nav_args+=("$extra2") - local next_cmd="display-message 'Loading...' ; $(tk_menu_cmd "${_nav_args[@]}")" + local next_cmd + next_cmd="display-message 'Loading...' ; $(tk_menu_cmd "${_nav_args[@]}")" tk_menu_item "Next ▶" "$KEY_NEXT" "$next_cmd" fi @@ -627,7 +626,7 @@ show_worktree_menu() { # Clear filter option (only when filter active, preserves sort_recent) if [ -n "$filter" ]; then - tk_menu_item "Clear filter" "$KEY_CLEAR_FILTER" "$(tk_menu_cmd "$script_path" show_worktree_menu 1 '' $sort_recent)" + tk_menu_item "Clear filter" "$KEY_CLEAR_FILTER" "$(tk_menu_cmd "$script_path" show_worktree_menu 1 '' "$sort_recent")" fi # Parse TSV worktree items @@ -847,7 +846,7 @@ adopt_current_session() { # Adoption must derive the project from this session's own directory, never # from a TMUX_WORKTREE_PROJECT inherited by the hook/sweep child process # (it leaks across unrelated sessions and mis-stamps every adopted name). - project=$(TMUX_WORKTREE_PROJECT= get_project_name) + project=$(TMUX_WORKTREE_PROJECT='' get_project_name) [ -z "$project" ] && return 0 # Sanitized "-" prefix (matches whatever get_session_name will emit, @@ -1086,7 +1085,7 @@ show_add_worktree_menu() { # Clear filter option (only when filter active) if [ -n "$filter" ]; then - tk_menu_item "Clear filter" "$KEY_CLEAR_FILTER" "$(tk_menu_cmd "$script_path" show_add_worktree_menu 1 '' $include_remotes)" + tk_menu_item "Clear filter" "$KEY_CLEAR_FILTER" "$(tk_menu_cmd "$script_path" show_add_worktree_menu 1 '' "$include_remotes")" fi # Parse TSV branch items @@ -1389,7 +1388,7 @@ show_options_menu() { # Trim whitespace from each entry. local _i for _i in "${!_age_arr[@]}"; do - _age_arr[$_i]="${_age_arr[$_i]// /}" + _age_arr[_i]="${_age_arr[_i]// /}" done if [ "${#_age_arr[@]}" -eq 0 ]; then _age_arr=("7" "30" "90") diff --git a/tests/integration/test_worktree_lifecycle.bats b/tests/integration/test_worktree_lifecycle.bats index e0d6623..30fec14 100644 --- a/tests/integration/test_worktree_lifecycle.bats +++ b/tests/integration/test_worktree_lifecycle.bats @@ -92,15 +92,15 @@ teardown() { # WORKTREE DATA TESTS # ============================================================================== -@test "get_worktree_data returns menu format" { +@test "get_worktree_data returns TSV data" { local wt_dir="$WORKTREE_BASE/$(get_project_name)/feature-one" mkdir -p "$(dirname "$wt_dir")" git worktree add -q "$wt_dir" feature-one run get_worktree_data 1 "" assert_success - assert_contains "$output" 'feature-one' - assert_contains "$output" 'run-shell' + assert_contains "$output" $'feature-one\tfeature-one\t' + [[ "$output" != *run-shell* ]] git worktree remove --force "$wt_dir" }