server: mesh_peer_boxes uses aws s3 ls, not the broken s3api subcommand (#840)
Found live on box A after applying #822's Terraform grant: aws s3api list-objects-v2 (and the older list-objects) crash on this fleet's aws-cli/Python combination with ValueError "badly formed help string" — a Python 3.13+ argparse regression in botocore's generated help text, thrown while building the argument parser, before any request is even sent. mesh_peer_boxes silently swallowed this (any aws failure degrades to "no peers found"), so the feature was inert rather than broken-loud, but it never actually worked. aws s3 ls hits the same ListObjectsV2 API underneath and is unaffected — it's what the rest of this script already relies on for uploads. Its plain-text output (date, time, size, key) has everything mesh_peer_boxes needs; switched the staleness-cutoff comparison to match its space-separated timestamp shape instead of ISO8601's T-separated one. Verified live against box A's real bucket/prefix and credentials (mesh_peer_boxes sourced in isolation, not the whole script, to avoid triggering a real publish): returns [] cleanly now instead of crashing — the stats prefix is genuinely empty today since no box has ever published there yet. Dry-run fixtures regenerate identical to committed (this code path never runs in dry-run mode).
Co-authored-by: Claude <noreply@anthropic.com>
1 file changed,
+27
−16
deploy/status-publish.sh+27 −16
diff --git a/deploy/status-publish.sh b/deploy/status-publish.sh| index a0afb74..1d7d78e 100755 |
| --- a/deploy/status-publish.sh |
| +++ b/deploy/status-publish.sh |
| @@ -365,31 +365,42 @@ mesh_peer_boxes() { |
| region=$(env_str "$ROUTER_ENV" LCHAT_HIBERNATE_REGION) |
| region=${region:-eu-west-1} |
| - local listf="$TMP/mesh-stats-objects.json" |
| - if ! aws s3api list-objects-v2 --bucket "$bucket" --prefix "${prefix}-stats/" \ |
| - --region "$region" --output json > "$listf" 2>/dev/null; then |
| - echo '[]'; return |
| - fi |
| - |
| - # ISO8601 timestamps sort lexicographically, so a plain string compare |
| - # against the cutoff is exact — no date-library parsing needed, and a box |
| + # `aws s3 ls` (the high-level command), never `aws s3api list-objects*` — |
| + # the latter crashes on this fleet's aws-cli/Python combination (a Python |
| + # 3.13+ argparse regression in botocore's generated help text: ValueError |
| + # "badly formed help string", thrown while building the argument parser, |
| + # before any request is even sent — found live on box A, 2026-07-13, |
| + # confirmed against both list-objects-v2 and the older list-objects; `aws |
| + # s3 ls`/`aws s3 cp` are unaffected, and are what the rest of this script |
| + # already relies on). `s3 ls` hits the same ListObjectsV2 API underneath; |
| + # its plain-text output already has everything needed (date, time, key). |
| + local listing |
| + listing=$(aws s3 ls "s3://$bucket/${prefix}-stats/" --region "$region" 2>/dev/null) || true |
| + [ -n "$listing" ] || { echo '[]'; return; } |
| + |
| + # A plain string compare against the cutoff is exact as long as both sides |
| + # use the same "YYYY-MM-DD HH:MM:SS" shape (space-, not T-separated — |
| + # `aws s3 ls`'s own format) — no date-library parsing needed, and a box |
| # whose clock is off only ever misjudges its OWN staleness, same tradeoff |
| # mesh.go's own reader-vs-S3-clock note accepts for membership. |
| - local cutoff peers="[]" key lastmod tmp body |
| - cutoff=$(date -u -d "-${MESH_STATS_STALE_SECONDS} seconds" +%Y-%m-%dT%H:%M:%S) |
| - |
| - while IFS=$'\t' read -r key lastmod; do |
| + local cutoff peers="[]" line date_part time_part key tmp body |
| + cutoff=$(date -u -d "-${MESH_STATS_STALE_SECONDS} seconds" '+%Y-%m-%d %H:%M:%S') |
| + |
| + while IFS= read -r line; do |
| + [ -n "$line" ] || continue |
| + date_part=$(printf '%s' "$line" | awk '{print $1}') |
| + time_part=$(printf '%s' "$line" | awk '{print $2}') |
| + key=$(printf '%s' "$line" | awk '{print $4}') |
| [ -n "$key" ] || continue |
| - [ "$lastmod" '>' "$cutoff" ] || continue # stale — treat like a dead mesh member |
| + [ "$date_part $time_part" '>' "$cutoff" ] || continue # stale — treat like a dead mesh member |
| tmp="$TMP/mesh-stat-$(printf '%s' "$key" | tr '/' '_').json" |
| - aws s3 cp "s3://$bucket/$key" "$tmp" --region "$region" >/dev/null 2>&1 || continue |
| + aws s3 cp "s3://$bucket/${prefix}-stats/$key" "$tmp" --region "$region" >/dev/null 2>&1 || continue |
| body=$(cat "$tmp" 2>/dev/null) || continue |
| [ -n "$body" ] || continue |
| peers=$(sqj "SELECT json_insert( |
| json('$(printf '%s' "$peers" | sed "s/'/''/g")'), '\$[#]', |
| json('$(printf '%s' "$body" | sed "s/'/''/g")'))") |
| - done < <(sqj "SELECT json_extract(value, '\$.Key') || char(9) || json_extract(value, '\$.LastModified') |
| - FROM json_each(json_extract(readfile('$listf'), '\$.Contents'))" 2>/dev/null) |
| + done <<< "$listing" |
| printf '%s' "$peers" |
| } |