From ebcbf492ea21e6f25809ead9c4c90b9046e43ec1 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sat, 21 Feb 2026 17:19:58 -0500 Subject: [PATCH 1/4] security: harden shell JSON parsing with shared helper --- bin/baudbot | 35 ++++++----- bin/deploy.sh | 10 +++- bin/lib/json-common.sh | 107 ++++++++++++++++++++++++++++++++++ bin/lib/json-common.test.sh | 112 ++++++++++++++++++++++++++++++++++++ bin/rollback-release.sh | 6 +- bin/security-audit.sh | 11 +++- bin/update-release.sh | 4 +- test/shell-scripts.test.mjs | 4 ++ 8 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 bin/lib/json-common.sh create mode 100644 bin/lib/json-common.test.sh diff --git a/bin/baudbot b/bin/baudbot index a70512d..9e326bc 100755 --- a/bin/baudbot +++ b/bin/baudbot @@ -21,6 +21,9 @@ if [ -z "${BAUDBOT_ROOT:-}" ]; then BAUDBOT_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)" fi +# shellcheck source=bin/lib/json-common.sh +source "$BAUDBOT_ROOT/bin/lib/json-common.sh" + # Colors (disabled if not a terminal) if [ -t 1 ]; then BOLD='\033[1m' @@ -34,11 +37,15 @@ else fi version() { - if [ -f "$BAUDBOT_ROOT/package.json" ]; then - grep '"version"' "$BAUDBOT_ROOT/package.json" | head -1 | sed 's/.*: *"\(.*\)".*/\1/' - else - echo "unknown" + local package_json="$BAUDBOT_ROOT/package.json" + local pkg_version="" + + if [ -f "$package_json" ]; then + pkg_version="$(json_get_string_or_empty "$package_json" "version")" + [ -n "$pkg_version" ] && echo "$pkg_version" && return 0 fi + + echo "unknown" } version_sha() { @@ -73,7 +80,7 @@ version_sha() { # Runtime metadata fallback version_file="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}/.pi/agent/baudbot-version.json" if [ -r "$version_file" ]; then - sha="$(grep -E '"sha"[[:space:]]*:' "$version_file" | head -1 | sed 's/.*"sha"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" + sha="$(json_get_string_or_empty "$version_file" "sha")" [ -n "$sha" ] && echo "${sha:0:7}" && return 0 fi @@ -282,7 +289,6 @@ has_systemd() { print_deployed_version() { local agent_user="${BAUDBOT_AGENT_USER:-baudbot_agent}" local version_file="/home/$agent_user/.pi/agent/baudbot-version.json" - local version_json="" local short="" local sha="" local branch="" @@ -290,12 +296,18 @@ print_deployed_version() { local line="" if [ -r "$version_file" ]; then - version_json="$(cat "$version_file" 2>/dev/null || true)" + short="$(json_get_string_or_empty "$version_file" "short")" + sha="$(json_get_string_or_empty "$version_file" "sha")" + branch="$(json_get_string_or_empty "$version_file" "branch")" + deployed_at="$(json_get_string_or_empty "$version_file" "deployed_at")" elif [ "$(id -u)" -eq 0 ] && id "$agent_user" >/dev/null 2>&1; then - version_json="$(sudo -u "$agent_user" cat "$version_file" 2>/dev/null || true)" + short="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "short" 2>/dev/null || true)" + sha="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)" + branch="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "branch" 2>/dev/null || true)" + deployed_at="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "deployed_at" 2>/dev/null || true)" fi - if [ -z "$version_json" ]; then + if [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then local release_target="" local release_sha="" @@ -309,11 +321,6 @@ print_deployed_version() { return 0 fi - short="$(printf '%s\n' "$version_json" | grep -E '"short"[[:space:]]*:' | head -1 | sed 's/.*"short"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" - sha="$(printf '%s\n' "$version_json" | grep -E '"sha"[[:space:]]*:' | head -1 | sed 's/.*"sha"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" - branch="$(printf '%s\n' "$version_json" | grep -E '"branch"[[:space:]]*:' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" - deployed_at="$(printf '%s\n' "$version_json" | grep -E '"deployed_at"[[:space:]]*:' | head -1 | sed 's/.*"deployed_at"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" - if [ -z "$short" ] && [ -n "$sha" ]; then short="${sha:0:7}" fi diff --git a/bin/deploy.sh b/bin/deploy.sh index 209066b..e2c7dca 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -18,6 +18,10 @@ BAUDBOT_SRC="${BAUDBOT_SRC:-$(cd "$(dirname "$0")/.." && pwd)}" BAUDBOT_HOME="${BAUDBOT_HOME:-/home/baudbot_agent}" AGENT_USER="baudbot_agent" DRY_RUN=0 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# shellcheck source=bin/lib/json-common.sh +source "$SCRIPT_DIR/lib/json-common.sh" # Helper: run a command as baudbot_agent as_agent() { @@ -385,9 +389,9 @@ if [ "$DRY_RUN" -eq 0 ]; then GIT_SHA_SHORT=$(cd "$BAUDBOT_SRC" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") GIT_BRANCH=$(cd "$BAUDBOT_SRC" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") elif [ -f "$RELEASE_META_FILE" ]; then - GIT_SHA=$(grep '"sha"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true) - GIT_SHA_SHORT=$(grep '"short"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true) - GIT_BRANCH=$(grep '"branch"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true) + GIT_SHA="$(json_get_string_or_empty "$RELEASE_META_FILE" "sha")" + GIT_SHA_SHORT="$(json_get_string_or_empty "$RELEASE_META_FILE" "short")" + GIT_BRANCH="$(json_get_string_or_empty "$RELEASE_META_FILE" "branch")" fi [ -n "$GIT_SHA" ] || GIT_SHA="unknown" diff --git a/bin/lib/json-common.sh b/bin/lib/json-common.sh new file mode 100644 index 0000000..85edf48 --- /dev/null +++ b/bin/lib/json-common.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Shared JSON parsing helpers for shell scripts. +# +# Return codes: +# 0 => key found and value printed +# 1 => JSON parsed, but key missing/non-string +# 2 => JSON/file/tool error + +_json_filter='if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' + +json_get_string() { + local file="$1" + local key="$2" + + [ -n "$file" ] || return 2 + [ -n "$key" ] || return 2 + [ -r "$file" ] || return 2 + + if command -v jq >/dev/null 2>&1; then + jq -er --arg k "$key" "$_json_filter" "$file" 2>/dev/null + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + *) return 2 ;; + esac + fi + + if ! command -v python3 >/dev/null 2>&1; then + return 2 + fi + + python3 - "$file" "$key" <<'PY' +import json +import sys + +if len(sys.argv) != 3: + sys.exit(2) + +path = sys.argv[1] +key = sys.argv[2] + +try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) +except Exception: + sys.exit(2) + +if not isinstance(data, dict): + sys.exit(1) + +value = data.get(key) +if not isinstance(value, str): + sys.exit(1) + +sys.stdout.write(value) +PY +} + +json_get_string_stdin() { + local key="$1" + + [ -n "$key" ] || return 2 + + if command -v jq >/dev/null 2>&1; then + jq -er --arg k "$key" "$_json_filter" 2>/dev/null + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + *) return 2 ;; + esac + fi + + if ! command -v python3 >/dev/null 2>&1; then + return 2 + fi + + python3 - "$key" <<'PY' +import json +import sys + +if len(sys.argv) != 2: + sys.exit(2) + +key = sys.argv[1] + +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(2) + +if not isinstance(data, dict): + sys.exit(1) + +value = data.get(key) +if not isinstance(value, str): + sys.exit(1) + +sys.stdout.write(value) +PY +} + +json_get_string_or_empty() { + local file="$1" + local key="$2" + + json_get_string "$file" "$key" 2>/dev/null || true +} diff --git a/bin/lib/json-common.test.sh b/bin/lib/json-common.test.sh new file mode 100644 index 0000000..829ae00 --- /dev/null +++ b/bin/lib/json-common.test.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# Tests for bin/lib/json-common.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=bin/lib/json-common.sh +source "$SCRIPT_DIR/json-common.sh" + +TOTAL=0 +PASSED=0 +FAILED=0 + +run_test() { + local name="$1" + shift + local out + + TOTAL=$((TOTAL + 1)) + printf " %-45s " "$name" + + out="$(mktemp /tmp/baudbot-json-common-test-output.XXXXXX)" + if "$@" >"$out" 2>&1; then + echo "✓" + PASSED=$((PASSED + 1)) + else + echo "✗ FAILED" + tail -40 "$out" | sed 's/^/ /' + FAILED=$((FAILED + 1)) + fi + rm -f "$out" +} + +test_parses_string_key_with_whitespace_variations() { + ( + set -euo pipefail + local tmp file value + tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)" + trap 'rm -rf "$tmp"' EXIT + + file="$tmp/meta.json" + cat > "$file" <<'EOF' +{ + "sha" : "abc123", + "short": "abc123", + "nested": { "x": 1 } +} +EOF + + value="$(json_get_string "$file" "sha")" + [ "$value" = "abc123" ] + ) +} + +test_missing_key_returns_nonzero() { + ( + set -euo pipefail + local tmp file + tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)" + trap 'rm -rf "$tmp"' EXIT + + file="$tmp/meta.json" + printf '{"sha":"abc123"}\n' > "$file" + + if json_get_string "$file" "branch" >/dev/null 2>&1; then + return 1 + fi + + [ -z "$(json_get_string_or_empty "$file" "branch")" ] + ) +} + +test_malformed_json_returns_nonzero() { + ( + set -euo pipefail + local tmp file + tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)" + trap 'rm -rf "$tmp"' EXIT + + file="$tmp/bad.json" + printf '{"sha": "abc123"\n' > "$file" + + if json_get_string "$file" "sha" >/dev/null 2>&1; then + return 1 + fi + ) +} + +test_stdin_parser_handles_whitespace() { + ( + set -euo pipefail + local value + + value="$(printf '{\n "deployed_at" : "2026-02-21T20:00:00Z"\n}\n' | json_get_string_stdin "deployed_at")" + [ "$value" = "2026-02-21T20:00:00Z" ] + ) +} + +echo "=== json-common tests ===" +echo "" + +run_test "parses string key with whitespace" test_parses_string_key_with_whitespace_variations +run_test "missing key returns nonzero" test_missing_key_returns_nonzero +run_test "malformed json returns nonzero" test_malformed_json_returns_nonzero +run_test "stdin parser handles whitespace" test_stdin_parser_handles_whitespace + +echo "" +echo "=== $PASSED/$TOTAL passed, $FAILED failed ===" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi diff --git a/bin/rollback-release.sh b/bin/rollback-release.sh index 2f8bad6..8938044 100755 --- a/bin/rollback-release.sh +++ b/bin/rollback-release.sh @@ -41,6 +41,8 @@ EOF # shellcheck source=bin/lib/release-common.sh source "$SCRIPT_DIR/lib/release-common.sh" +# shellcheck source=bin/lib/json-common.sh +source "$SCRIPT_DIR/lib/json-common.sh" TARGET_SPEC="${1:-previous}" if [ "$#" -gt 0 ]; then @@ -159,10 +161,10 @@ run_restart_and_health() { local expected_sha local deployed_sha - expected_sha=$(grep '"sha"' "$TARGET_RELEASE/baudbot-release.json" 2>/dev/null | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true) + expected_sha="$(json_get_string_or_empty "$TARGET_RELEASE/baudbot-release.json" "sha")" [ -n "$expected_sha" ] || expected_sha="$(basename "$TARGET_RELEASE")" - deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "grep '\"sha\"' '$version_file' 2>/dev/null | head -1 | sed 's/.*\"sha\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'")" + deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)" if [ -z "$deployed_sha" ]; then die "deployed version file missing or unreadable: $version_file" diff --git a/bin/security-audit.sh b/bin/security-audit.sh index 3da4169..6ffdb3a 100755 --- a/bin/security-audit.sh +++ b/bin/security-audit.sh @@ -14,6 +14,11 @@ set -euo pipefail BAUDBOT_HOME="${BAUDBOT_HOME:-/home/baudbot_agent}" # Source repo — auto-detect from this script's location, or use env override BAUDBOT_SRC="${BAUDBOT_SRC:-$(cd "$(dirname "$0")/.." && pwd)}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# shellcheck source=bin/lib/json-common.sh +source "$SCRIPT_DIR/lib/json-common.sh" + DEEP=0 FIX=0 for arg in "$@"; do @@ -224,8 +229,10 @@ VERSION_FILE="$BAUDBOT_HOME/.pi/agent/baudbot-version.json" MANIFEST_FILE="$BAUDBOT_HOME/.pi/agent/baudbot-manifest.json" if [ -f "$VERSION_FILE" ]; then - deploy_sha=$(grep '"short"' "$VERSION_FILE" 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "?") - deploy_ts=$(grep '"deployed_at"' "$VERSION_FILE" 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "?") + deploy_sha="$(json_get_string_or_empty "$VERSION_FILE" "short")" + deploy_ts="$(json_get_string_or_empty "$VERSION_FILE" "deployed_at")" + [ -n "$deploy_sha" ] || deploy_sha="?" + [ -n "$deploy_ts" ] || deploy_ts="?" ok "Deployed version: $deploy_sha ($deploy_ts)" else finding "WARN" "No version stamp found — run deploy.sh" "" diff --git a/bin/update-release.sh b/bin/update-release.sh index cbc6eab..360a21a 100755 --- a/bin/update-release.sh +++ b/bin/update-release.sh @@ -58,6 +58,8 @@ die() { # shellcheck source=bin/lib/release-common.sh source "$SCRIPT_DIR/lib/release-common.sh" +# shellcheck source=bin/lib/json-common.sh +source "$SCRIPT_DIR/lib/json-common.sh" cleanup() { if [ -n "$CHECKOUT_DIR" ] && [ -d "$CHECKOUT_DIR" ]; then @@ -305,7 +307,7 @@ run_restart_and_health() { local version_file="$BAUDBOT_AGENT_HOME/.pi/agent/baudbot-version.json" local deployed_sha - deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "grep '\"sha\"' '$version_file' 2>/dev/null | head -1 | sed 's/.*\"sha\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'")" + deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)" if [ -z "$deployed_sha" ]; then die "deployed version file missing or unreadable: $version_file" diff --git a/test/shell-scripts.test.mjs b/test/shell-scripts.test.mjs index be571da..b5f04b3 100644 --- a/test/shell-scripts.test.mjs +++ b/test/shell-scripts.test.mjs @@ -31,4 +31,8 @@ describe("shell script test suites", () => { expect(() => runScript("bin/env.test.sh")).not.toThrow(); }); + it("json helper", () => { + expect(() => runScript("bin/lib/json-common.test.sh")).not.toThrow(); + }); + }); From 498fd60008590687fa0c3fa11c567fd877879d0a Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sat, 21 Feb 2026 17:24:40 -0500 Subject: [PATCH 2/4] deploy: require jq for shell JSON parsing --- README.md | 2 + bin/doctor.sh | 6 +++ bin/lib/json-common.sh | 100 ++++++++++------------------------------- install.sh | 6 +-- 4 files changed, 34 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 2afb514..a79a07a 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Baudbot is designed as shared engineering infrastructure, not a single-user desk | **CPU** | 2 vCPU | 4 vCPU | | **Disk** | 20 GB | 40 GB+ (repos, dependencies, Docker images) | +System package dependencies (installed by `baudbot install`): `git`, `curl`, `tmux`, `iptables`, `docker`, `gh`, `jq`, `sudo`. + ## Quick Start ```bash diff --git a/bin/doctor.sh b/bin/doctor.sh index 89d4b0b..6356e7b 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -71,6 +71,12 @@ else fail "varlock not found" fi +if command -v jq &>/dev/null; then + pass "jq is installed" +else + fail "jq not found (required for shell JSON parsing)" +fi + if command -v docker &>/dev/null; then pass "docker is available" else diff --git a/bin/lib/json-common.sh b/bin/lib/json-common.sh index 85edf48..c174195 100644 --- a/bin/lib/json-common.sh +++ b/bin/lib/json-common.sh @@ -1,13 +1,19 @@ #!/bin/bash # Shared JSON parsing helpers for shell scripts. # +# jq is a required runtime dependency. +# # Return codes: # 0 => key found and value printed # 1 => JSON parsed, but key missing/non-string -# 2 => JSON/file/tool error +# 2 => JSON/file/tool error (including missing jq) _json_filter='if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' +json_require_jq() { + command -v jq >/dev/null 2>&1 +} + json_get_string() { local file="$1" local key="$2" @@ -15,88 +21,28 @@ json_get_string() { [ -n "$file" ] || return 2 [ -n "$key" ] || return 2 [ -r "$file" ] || return 2 - - if command -v jq >/dev/null 2>&1; then - jq -er --arg k "$key" "$_json_filter" "$file" 2>/dev/null - case "$?" in - 0) return 0 ;; - 1) return 1 ;; - *) return 2 ;; - esac - fi - - if ! command -v python3 >/dev/null 2>&1; then - return 2 - fi - - python3 - "$file" "$key" <<'PY' -import json -import sys - -if len(sys.argv) != 3: - sys.exit(2) - -path = sys.argv[1] -key = sys.argv[2] - -try: - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) -except Exception: - sys.exit(2) - -if not isinstance(data, dict): - sys.exit(1) - -value = data.get(key) -if not isinstance(value, str): - sys.exit(1) - -sys.stdout.write(value) -PY + json_require_jq || return 2 + + jq -er --arg k "$key" "$_json_filter" "$file" 2>/dev/null + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + *) return 2 ;; + esac } json_get_string_stdin() { local key="$1" [ -n "$key" ] || return 2 - - if command -v jq >/dev/null 2>&1; then - jq -er --arg k "$key" "$_json_filter" 2>/dev/null - case "$?" in - 0) return 0 ;; - 1) return 1 ;; - *) return 2 ;; - esac - fi - - if ! command -v python3 >/dev/null 2>&1; then - return 2 - fi - - python3 - "$key" <<'PY' -import json -import sys - -if len(sys.argv) != 2: - sys.exit(2) - -key = sys.argv[1] - -try: - data = json.load(sys.stdin) -except Exception: - sys.exit(2) - -if not isinstance(data, dict): - sys.exit(1) - -value = data.get(key) -if not isinstance(value, str): - sys.exit(1) - -sys.stdout.write(value) -PY + json_require_jq || return 2 + + jq -er --arg k "$key" "$_json_filter" 2>/dev/null + case "$?" in + 0) return 0 ;; + 1) return 1 ;; + *) return 2 ;; + esac } json_get_string_or_empty() { diff --git a/install.sh b/install.sh index 1ac0eae..91a4260 100755 --- a/install.sh +++ b/install.sh @@ -163,7 +163,7 @@ install_prereqs_ubuntu() { for attempt in $(seq 1 5); do if DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 update -qq \ - && DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 install -y -qq git curl tmux iptables docker.io gh sudo 2>&1 | tail -3; then + && DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 install -y -qq git curl tmux iptables docker.io gh jq sudo 2>&1 | tail -3; then return 0 fi @@ -179,10 +179,10 @@ install_prereqs_ubuntu() { } install_prereqs_arch() { - pacman -Syu --noconfirm --needed git curl tmux iptables docker github-cli sudo 2>&1 | tail -5 + pacman -Syu --noconfirm --needed git curl tmux iptables docker github-cli jq sudo 2>&1 | tail -5 } -info "Installing: git, curl, tmux, iptables, docker, gh, sudo" +info "Installing: git, curl, tmux, iptables, docker, gh, jq, sudo" "install_prereqs_$DISTRO" info "Prerequisites installed" From abf24090bae3ac8d843d96436f8d23d27f74220a Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sat, 21 Feb 2026 17:29:03 -0500 Subject: [PATCH 3/4] ci: install jq on arch bootstrap and keep bootstrap cli self-contained --- bin/baudbot | 33 ++++++++++++++++++++++++++------- bin/ci/setup-arch.sh | 4 ++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/bin/baudbot b/bin/baudbot index 9e326bc..9a8b973 100755 --- a/bin/baudbot +++ b/bin/baudbot @@ -18,11 +18,30 @@ if [ -z "${BAUDBOT_ROOT:-}" ]; then # Handle relative symlinks [[ "$SCRIPT" != /* ]] && SCRIPT="$DIR/$SCRIPT" done - BAUDBOT_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)" + + CANDIDATE_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)" + if [ -f "$CANDIDATE_ROOT/bin/baudbot" ] && [ -f "$CANDIDATE_ROOT/package.json" ]; then + BAUDBOT_ROOT="$CANDIDATE_ROOT" + elif [ -d /opt/baudbot/current/bin ]; then + BAUDBOT_ROOT="$(readlink -f /opt/baudbot/current 2>/dev/null || echo /opt/baudbot/current)" + else + BAUDBOT_ROOT="$CANDIDATE_ROOT" + fi fi -# shellcheck source=bin/lib/json-common.sh -source "$BAUDBOT_ROOT/bin/lib/json-common.sh" +json_get_string_or_empty() { + local file="$1" + local key="$2" + [ -r "$file" ] || return 0 + command -v jq >/dev/null 2>&1 || return 0 + jq -er --arg k "$key" 'if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' "$file" 2>/dev/null || true +} + +json_get_string_stdin_or_empty() { + local key="$1" + command -v jq >/dev/null 2>&1 || return 0 + jq -er --arg k "$key" 'if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' 2>/dev/null || true +} # Colors (disabled if not a terminal) if [ -t 1 ]; then @@ -301,10 +320,10 @@ print_deployed_version() { branch="$(json_get_string_or_empty "$version_file" "branch")" deployed_at="$(json_get_string_or_empty "$version_file" "deployed_at")" elif [ "$(id -u)" -eq 0 ] && id "$agent_user" >/dev/null 2>&1; then - short="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "short" 2>/dev/null || true)" - sha="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)" - branch="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "branch" 2>/dev/null || true)" - deployed_at="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "deployed_at" 2>/dev/null || true)" + short="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "short" 2>/dev/null || true)" + sha="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "sha" 2>/dev/null || true)" + branch="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "branch" 2>/dev/null || true)" + deployed_at="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "deployed_at" 2>/dev/null || true)" fi if [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then diff --git a/bin/ci/setup-arch.sh b/bin/ci/setup-arch.sh index 641519d..204ec67 100755 --- a/bin/ci/setup-arch.sh +++ b/bin/ci/setup-arch.sh @@ -7,8 +7,8 @@ set -euo pipefail -echo "=== [Arch] Installing git (needed to init test repo) ===" -pacman -Sy --noconfirm --needed git sudo 2>&1 | tail -3 +echo "=== [Arch] Installing base CI deps ===" +pacman -Sy --noconfirm --needed git jq sudo 2>&1 | tail -3 echo "=== Preparing source ===" useradd -m -s /bin/bash baudbot_admin From c0acf797334bb632b588e1d229f0c916998df780 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sat, 21 Feb 2026 17:34:07 -0500 Subject: [PATCH 4/4] cli: read deployed version json once for sudo path --- bin/baudbot | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/baudbot b/bin/baudbot index 9a8b973..c87061a 100755 --- a/bin/baudbot +++ b/bin/baudbot @@ -320,10 +320,14 @@ print_deployed_version() { branch="$(json_get_string_or_empty "$version_file" "branch")" deployed_at="$(json_get_string_or_empty "$version_file" "deployed_at")" elif [ "$(id -u)" -eq 0 ] && id "$agent_user" >/dev/null 2>&1; then - short="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "short" 2>/dev/null || true)" - sha="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "sha" 2>/dev/null || true)" - branch="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "branch" 2>/dev/null || true)" - deployed_at="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin_or_empty "deployed_at" 2>/dev/null || true)" + local version_json="" + version_json="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" || true)" + if [ -n "$version_json" ]; then + short="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "short" 2>/dev/null || true)" + sha="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "sha" 2>/dev/null || true)" + branch="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "branch" 2>/dev/null || true)" + deployed_at="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "deployed_at" 2>/dev/null || true)" + fi fi if [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then