Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .agents/scripts/tool-version-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#
# Categories: npm, brew, pip, all (default)

# shellcheck disable=SC1091
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit
source "${SCRIPT_DIR}/shared-constants.sh"

Expand Down Expand Up @@ -197,6 +198,17 @@
# Timeout for external package manager queries (seconds)
readonly PKG_QUERY_TIMEOUT=30

get_public_release_tag() {
local repo="$1"
local tag=""
tag=$(timeout_sec "$PKG_QUERY_TIMEOUT" curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null |
grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' |
head -1 |
sed -E 's/.*"v?([^"]+)"/\1/' || true)
echo "$tag"
return 0
}

# Get latest npm version
get_npm_latest() {
local pkg="$1"
Expand All @@ -208,8 +220,12 @@
# Get latest brew version
get_brew_latest() {
local pkg="$1"
if command -v brew &>/dev/null; then
timeout_sec "$PKG_QUERY_TIMEOUT" brew info "$pkg" 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown"
local brew_bin=""
brew_bin=$(command -v brew 2>/dev/null || true)
if [[ -n "$brew_bin" && -x "$brew_bin" ]]; then
timeout_sec "$PKG_QUERY_TIMEOUT" "$brew_bin" info "$pkg" 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown"
elif [[ "$pkg" == "gh" ]]; then
get_public_release_tag "cli/cli"
else
echo "unknown"
fi
Expand Down Expand Up @@ -278,7 +294,7 @@
color="$RED"
((++TIMEOUT_COUNT))
elif [[ "$installed" == "unknown" || "$latest" == "unknown" ]]; then
status="unknown"

Check warning on line 297 in .agents/scripts/tool-version-check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using the literal 'unknown' 10 times.

See more on https://sonarcloud.io/project/issues?id=marcusquinn_aidevops&issues=AZzdsBlxDVdjFamjO2oA&open=AZzdsBlxDVdjFamjO2oA&pullRequest=4143
icon="?"
color="$YELLOW"
((++UNKNOWN_COUNT))
Expand All @@ -299,7 +315,7 @@
json_name="${json_name//\"/\\\"}"
local json_update="${update_cmd//\\/\\\\}"
json_update="${json_update//\"/\\\"}"
JSON_RESULTS+=("{\"name\":\"$json_name\",\"category\":\"$category\",\"installed\":\"$installed\",\"latest\":\"$latest\",\"status\":\"$status\",\"update_cmd\":\"$json_update\"}")
JSON_RESULTS+=("{\"name\": \"$json_name\", \"category\": \"$category\", \"installed\": \"$installed\", \"latest\": \"$latest\", \"status\": \"$status\", \"update_cmd\": \"$json_update\"}")
else
# Console output
if [[ "$QUIET" == "true" && "$status" != "outdated" && "$status" != "timeout" ]]; then
Expand Down
24 changes: 20 additions & 4 deletions aidevops.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ get_remote_version() {
curl -fsSL "https://raw.githubusercontent.com/marcusquinn/aidevops/main/VERSION" 2>/dev/null || echo "unknown"
}

get_public_release_tag() {
local repo="$1"
local tag=""

tag=$(_timeout_cmd 15 curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null |

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This get_public_release_tag function is duplicated in .agents/scripts/tool-version-check.sh. While duplication of simple, self-contained helper functions is acceptable for standalone scripts to avoid source dependencies and maintain independence, it is crucial to ensure consistency across these duplicated functions. Specifically, ensure consistency in timeout values. The cmd_update function in this file uses 30 seconds for package queries, while this function uses 15 seconds. It would be more consistent to use 30 seconds here as well, or define a shared constant.

Suggested change
tag=$(_timeout_cmd 15 curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null |
tag=$(_timeout_cmd 30 curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null |
References
  1. In shell scripts, extract repeated logic into an internal helper function to improve maintainability. This applies even for standalone scripts where external source dependencies are avoided.
  2. For standalone shell scripts, it is acceptable to duplicate simple, self-contained helper functions (e.g., a cross-platform sed wrapper) instead of introducing source dependencies. This maintains script independence and avoids risks like path resolution issues, which is particularly important in focused bugfix pull requests.

grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' |
head -1 |
sed -E 's/.*"v?([^"]+)"/\1/' || true)

printf '%s\n' "$tag"
return 0
}

# Check if a command exists
check_cmd() {
command -v "$1" >/dev/null 2>&1
Expand Down Expand Up @@ -924,11 +937,14 @@ cmd_update() {
# Get latest version (npm, brew, or GitHub API) — timeout prevents hangs on slow registries
if [[ "$pkg_ref" == brew:* ]]; then
local brew_pkg="${pkg_ref#brew:}"
if command -v brew &>/dev/null; then
latest=$(_timeout_cmd 30 brew info --json=v2 "$brew_pkg" | jq -r '.formulae[0].versions.stable // empty' || true)
local brew_bin=""
brew_bin=$(command -v brew 2>/dev/null || true)
if [[ -n "$brew_bin" && -x "$brew_bin" ]]; then
latest=$(_timeout_cmd 30 "$brew_bin" info --json=v2 "$brew_pkg" | jq -r '.formulae[0].versions.stable // empty' || true)
elif [[ "$brew_pkg" == "gh" ]] && command -v gh &>/dev/null; then
# Fallback: get latest gh version from GitHub API (works without brew)
latest=$(_timeout_cmd 15 gh api repos/cli/cli/releases/latest --jq '.tag_name' 2>/dev/null | sed 's/^v//' || true)
# Fallback: use the public GitHub API so update still works when brew
# is unavailable or gh auth refresh is unhealthy.
latest=$(get_public_release_tag "cli/cli")
fi
else
latest=$(_timeout_cmd 30 npm view "$pkg_ref" version || true)
Expand Down
133 changes: 133 additions & 0 deletions tests/test-update-fallbacks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash

set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLI_SCRIPT="$REPO_DIR/aidevops.sh"
TOOL_CHECK_SCRIPT="$REPO_DIR/.agents/scripts/tool-version-check.sh"

PASS_COUNT=0
FAIL_COUNT=0
TOTAL_COUNT=0

pass() {
PASS_COUNT=$((PASS_COUNT + 1))
TOTAL_COUNT=$((TOTAL_COUNT + 1))
printf "\033[0;32mPASS\033[0m %s\n" "$1"
return 0
}

fail() {
FAIL_COUNT=$((FAIL_COUNT + 1))
TOTAL_COUNT=$((TOTAL_COUNT + 1))
printf "\033[0;31mFAIL\033[0m %s\n" "$1"
if [[ -n "${2:-}" ]]; then
printf " %s\n" "$2"
fi
return 0
}

assert_grep() {
local pattern="$1"
local file="$2"
local name="$3"
if grep -qE "$pattern" "$file"; then
pass "$name"
else
fail "$name" "Pattern not found: $pattern"
fi
return 0
}

assert_not_grep() {
local pattern="$1"
local file="$2"
local name="$3"
if grep -qE "$pattern" "$file"; then
fail "$name" "Unexpected pattern found: $pattern"
else
pass "$name"
fi
return 0
}

assert_contains() {
local haystack="$1"
local needle="$2"
local name="$3"
if [[ "$haystack" == *"$needle"* ]]; then
pass "$name"
else
fail "$name" "Missing substring: $needle"
fi
return 0
}

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

FAKE_BIN="$TMP_DIR/bin"
mkdir -p "$FAKE_BIN"

cat >"$FAKE_BIN/gh" <<'EOF'
#!/usr/bin/env bash
if [[ "$1" == "--version" ]]; then
printf 'gh version 2.0.0\n'
exit 0
fi
printf 'Error: Token refresh failed: 500\n' >&2
exit 1
EOF
chmod +x "$FAKE_BIN/gh"

cat >"$FAKE_BIN/curl" <<'EOF'
#!/usr/bin/env bash
printf '{"tag_name":"v2.1.0"}\n'
EOF
chmod +x "$FAKE_BIN/curl"

cat >"$FAKE_BIN/timeout" <<'EOF'
#!/usr/bin/env bash
shift
exec "$@"
EOF
chmod +x "$FAKE_BIN/timeout"

PATH="$FAKE_BIN:/usr/bin:/bin" TOOL_OUTPUT="$(bash "$TOOL_CHECK_SCRIPT" --category brew --json 2>&1 || true)"

# shellcheck disable=SC2016
BREW_BIN_PATTERN='\[\[ -n "\$brew_bin" && -x "\$brew_bin" \]\]'
GH_LATEST_PATTERN='"latest": "2.1.0"'
GH_INSTALLED_PATTERN='"installed": "2.0.0"'

assert_grep 'brew_bin=.*command -v brew' "$CLI_SCRIPT" 'CLI resolves brew path before timeout use'
assert_grep "$BREW_BIN_PATTERN" "$CLI_SCRIPT" 'CLI requires brew to be executable before invoking timeout'
assert_grep 'get_public_release_tag "cli/cli"' "$CLI_SCRIPT" 'CLI uses public GitHub API fallback for gh latest version'
assert_not_grep 'gh api repos/cli/cli/releases/latest' "$CLI_SCRIPT" 'CLI no longer depends on gh auth for gh latest version checks'

assert_grep 'brew_bin=.*command -v brew' "$TOOL_CHECK_SCRIPT" 'Tool checker resolves brew path before timeout use'
assert_grep "$BREW_BIN_PATTERN" "$TOOL_CHECK_SCRIPT" 'Tool checker requires brew to be executable before invoking timeout'
assert_grep 'get_public_release_tag "cli/cli"' "$TOOL_CHECK_SCRIPT" 'Tool checker uses public GitHub API fallback for gh latest version'

assert_contains "$TOOL_OUTPUT" "$GH_LATEST_PATTERN" 'Tool checker reports gh latest version from public API fallback'
assert_contains "$TOOL_OUTPUT" "$GH_INSTALLED_PATTERN" 'Tool checker keeps the installed gh version when brew is unavailable'

if [[ "$TOOL_OUTPUT" == *'timeout: failed to run command'* ]]; then
fail 'Tool checker avoids timeout/brew ENOENT when brew is unavailable' "$TOOL_OUTPUT"
else
pass 'Tool checker avoids timeout/brew ENOENT when brew is unavailable'
fi

if [[ "$TOOL_OUTPUT" == *'Token refresh failed: 500'* ]]; then
fail 'Tool checker avoids gh auth refresh failures for public latest checks' "$TOOL_OUTPUT"
else
pass 'Tool checker avoids gh auth refresh failures for public latest checks'
fi

printf "\nRan %d tests, %d failed.\n" "$TOTAL_COUNT" "$FAIL_COUNT"

if [[ "$FAIL_COUNT" -ne 0 ]]; then
exit 1
fi

exit 0
Loading