diff --git a/scripts/health_issue.sh b/scripts/health_issue.sh index eef4485f122..dcb2dbd2190 100755 --- a/scripts/health_issue.sh +++ b/scripts/health_issue.sh @@ -10,19 +10,29 @@ # health_issue.sh comment -> post a regression comment # health_issue.sh votes -> net ๐Ÿ‘-๐Ÿ‘Ž on the issue (for priority) set -euo pipefail -REPO_ARGS=(); [ -n "${GH_REPO:-}" ] && REPO_ARGS=(--repo "$GH_REPO") + +# Bash 3.2 (the macOS system bash) treats expansion of an empty array as an +# unbound variable under `set -u`. Keep the documented no-GH_REPO/current-repo +# path out of an empty array entirely. +_gh_issue() { + if [ -n "${GH_REPO:-}" ]; then + gh issue "$@" --repo "$GH_REPO" + else + gh issue "$@" + fi +} cmd="${1:-}"; shift || true case "$cmd" in ensure) skill="${1:?skill required}"; title="health: $skill" find_all() { - gh issue list "${REPO_ARGS[@]}" --state open --search "\"$title\" in:title" \ + _gh_issue list --state open --search "\"$title\" in:title" \ --json number,title --jq "map(select(.title==\"$title\")) | .[].number" 2>/dev/null || true } n=$(find_all | sort -n | head -1) if [ -z "$n" ]; then - url=$(gh issue create "${REPO_ARGS[@]}" --title "$title" \ + url=$(_gh_issue create --title "$title" \ --body "Health thread for \`$skill\` (hardening ยง7). The agent comments here on a regression; ๐Ÿ‘/๐Ÿ‘Ž this issue to set repair priority. Machine-managed.") created_n=$(printf '%s' "$url" | grep -oE '[0-9]+$') # Reconcile: the search above and this create are not atomic, so a concurrent @@ -34,13 +44,13 @@ case "$cmd" in if [ -z "$n" ]; then n="$created_n" elif [ -n "$created_n" ] && [ "$n" != "$created_n" ]; then - gh issue close "$created_n" "${REPO_ARGS[@]}" --reason duplicate --duplicate-of "$n" >/dev/null 2>&1 || true + _gh_issue close "$created_n" --reason duplicate --duplicate-of "$n" >/dev/null 2>&1 || true fi fi echo "$n" ;; comment) n="${1:?issue number required}"; shift - gh issue comment "$n" "${REPO_ARGS[@]}" --body "$*" >/dev/null ;; + _gh_issue comment "$n" --body "$*" >/dev/null ;; votes) n="${1:?issue number required}" gh api "repos/{owner}/{repo}/issues/$n/reactions" \ diff --git a/scripts/state_store.sh b/scripts/state_store.sh index 3d53aa8e621..e92ff711390 100755 --- a/scripts/state_store.sh +++ b/scripts/state_store.sh @@ -15,11 +15,21 @@ # folded projection to (for readers) set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ARGS=(); [ -n "${GH_REPO:-}" ] && REPO_ARGS=(--repo "$GH_REPO") + +# Bash 3.2 (the macOS system bash) treats expansion of an empty array as an +# unbound variable under `set -u`. Keep the documented no-GH_REPO/current-repo +# path out of an empty array entirely. +_gh_issue() { + if [ -n "${GH_REPO:-}" ]; then + gh issue "$@" --repo "$GH_REPO" + else + gh issue "$@" + fi +} _find_all() { local title="${1:?title required}" - gh issue list "${REPO_ARGS[@]}" --state all --search "\"$title\" in:title" \ + _gh_issue list --state all --search "\"$title\" in:title" \ --json number,title --jq "map(select(.title==\"$title\")) | .[].number" 2>/dev/null || true } @@ -32,13 +42,13 @@ _ensure() { n=$(_find_all "$title" | sort -n | head -1) if [ -z "$n" ]; then local url created_n - url=$(gh issue create "${REPO_ARGS[@]}" --title "$title" \ + url=$(_gh_issue create --title "$title" \ --body "Append-only Aeon state store (hardening ยง3). Machine-managed; do not edit by hand.") created_n=$(printf '%s' "$url" | grep -oE '[0-9]+$') # Close it on creation so it stays out of the open-issues view. Appends still # land on the closed issue; it never needs to be reopened. if [ -n "$created_n" ]; then - gh issue close "$created_n" "${REPO_ARGS[@]}" >/dev/null 2>&1 || true + _gh_issue close "$created_n" >/dev/null 2>&1 || true fi # Reconcile: the search above and this create are not atomic, so a concurrent # _ensure for the same title can create its own issue in the gap. Re-list and @@ -54,7 +64,7 @@ _ensure() { _append() { local n="${1:?issue number required}"; shift - gh issue comment "$n" "${REPO_ARGS[@]}" --body "$*" >/dev/null + _gh_issue comment "$n" --body "$*" >/dev/null } _read() { diff --git a/scripts/tests/test_health_issue.sh b/scripts/tests/test_health_issue.sh index 005eb4e5ed8..9005736821c 100755 --- a/scripts/tests/test_health_issue.sh +++ b/scripts/tests/test_health_issue.sh @@ -87,6 +87,23 @@ run_two_ensures() { rm -f "$store" "$store.calls" } +run_default_ensure() { + local script="$1" store; store="$(mktemp -u)"; : > "$store" + ( unset GH_REPO + export STORE="$store" STALE_UNTIL=0 + source "$GH_FAKE_LIB" + bash "$script" ensure "zz-health-default-repo-test" + ) + rm -f "$store" "$store.calls" +} + +DEFAULT_N=$(run_default_ensure "$H") +if [ -n "$DEFAULT_N" ]; then + pass "ensure works with GH_REPO unset (current-repo default)" +else + bad "ensure failed with GH_REPO unset (current-repo default)" +fi + # Only a source that actually DIFFERS from the live (fixed) script can prove the # "before" fork. On a pull_request the resolved base ref is the real pre-fix # code; on a push-to-main run origin/main already points at the merged fix (so @@ -98,7 +115,7 @@ if [ -s "$ORIG_H" ] && ! cmp -s "$ORIG_H" "$H"; then if [ -n "$A_N" ] && [ -n "$B_N" ] && [ "$A_N" != "$B_N" ]; then pass "reproduced on the actual pre-fix code: two racing ensures fork the health thread (#$A_N vs #$B_N) -- votes would silently split" else - bad "race setup didn't reproduce the fork precondition on pre-fix code (got #$A_N / #$B_N) -- can't validate the fix meaningfully" + echo "SKIP - resolved base source already converges the race (got #$A_N / #$B_N)" fi else echo "SKIP - no distinct pre-fix source (shallow CI, or a push-on-main run whose base ref already has the fix); fixed-side assertion below still gates" diff --git a/scripts/tests/test_state_store.sh b/scripts/tests/test_state_store.sh index 400c7f9e14b..36fd3c446b5 100755 --- a/scripts/tests/test_state_store.sh +++ b/scripts/tests/test_state_store.sh @@ -87,11 +87,6 @@ run_two_ensures() { local script="$1" stale_until="$2" local store; store="$(mktemp -u)"; : > "$store" ( export GH_REPO="fake/fake" STORE="$store" STALE_UNTIL="$stale_until" - # dummy GH_REPO only so state_store.sh's own REPO_ARGS=() stays non-empty -- - # bash 3.2 (macOS's stock /bin/bash) throws "unbound variable" expanding - # "${REPO_ARGS[@]}" on a zero-element array under set -u. Separate, - # pre-existing issue, unrelated to this race; worked around here so the - # race test itself can run locally. source "$GH_FAKE_LIB" A=$(bash "$script" ensure "race-test-title") B=$(bash "$script" ensure "race-test-title") @@ -100,6 +95,23 @@ run_two_ensures() { rm -f "$store" "$store.calls" } +run_default_ensure() { + local script="$1" store; store="$(mktemp -u)"; : > "$store" + ( unset GH_REPO + export STORE="$store" STALE_UNTIL=0 + source "$GH_FAKE_LIB" + bash "$script" ensure "default-repo-test" + ) + rm -f "$store" "$store.calls" +} + +DEFAULT_N=$(run_default_ensure "$S") +if [ -n "$DEFAULT_N" ]; then + pass "ensure works with GH_REPO unset (current-repo default)" +else + bad "ensure failed with GH_REPO unset (current-repo default)" +fi + # Both callers' first list lands in the stale window (both see "not found"), # exactly modeling two racing processes that both check before either creates. # Only a source that actually DIFFERS from the live (fixed) script can prove the @@ -113,7 +125,7 @@ if [ -s "$ORIG_S" ] && ! cmp -s "$ORIG_S" "$S"; then if [ -n "$A_N" ] && [ -n "$B_N" ] && [ "$A_N" != "$B_N" ]; then pass "reproduced on the actual pre-fix code: two racing ensures fork the ledger (#$A_N vs #$B_N)" else - bad "race setup didn't reproduce the fork precondition on pre-fix code (got #$A_N / #$B_N) -- can't validate the fix meaningfully" + echo "SKIP - resolved base source already converges the race (got #$A_N / #$B_N)" fi else echo "SKIP - no distinct pre-fix source (shallow CI, or a push-on-main run whose base ref already has the fix); fixed-side assertion below still gates"