feat(scripts): add env isolation, incremental logging, and test timeouts
- Wrap all Claude CLI subprocess calls with `env -u CLAUDECODE` to prevent parent env var interference with child processes (17 sites across 7 files) - Add `flush_log_to_repo()` to epic-execute.sh for incremental log persistence after each story completes or fails (prevents log loss on interruption) - Add portable `run_with_timeout` utility to utils.sh and wrap all test invocations in epic-execute.sh and regression-gate.sh with configurable timeout (default 120s via REGRESSION_TEST_TIMEOUT) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ae7d403ee0
commit
a58fb564d1
|
|
@ -803,7 +803,7 @@ REPORT_GENERATED: $CHAIN_REPORT_FILE"
|
|||
log "Invoking report generator..."
|
||||
|
||||
# Execute report generation
|
||||
report_result=$(claude --dangerously-skip-permissions -p "$report_prompt" 2>&1) || true
|
||||
report_result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$report_prompt" 2>&1) || true
|
||||
|
||||
echo "$report_result" >> "$LOG_FILE"
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ DESIGN COMPLETE: $story_id"
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$design_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$design_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
|
|||
|
|
@ -126,9 +126,9 @@ init_regression_baseline() {
|
|||
|
||||
# Check if there's a test:json script for better parsing
|
||||
if grep -q '"test:json"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
|
||||
test_output=$(cd "$PROJECT_ROOT" && npm run test:json 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm run test:json) || true
|
||||
else
|
||||
test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || true
|
||||
fi
|
||||
|
||||
BASELINE_PASSING_TESTS=$(extract_test_count "$test_output")
|
||||
|
|
@ -152,14 +152,14 @@ init_regression_baseline() {
|
|||
elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
||||
# Rust project
|
||||
log "Capturing baseline test count (Rust)..."
|
||||
test_output=$(cd "$PROJECT_ROOT" && cargo test 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" cargo test) || true
|
||||
BASELINE_PASSING_TESTS=$(extract_test_count "$test_output")
|
||||
log "Baseline passing tests: $BASELINE_PASSING_TESTS"
|
||||
|
||||
elif [ -f "$PROJECT_ROOT/go.mod" ]; then
|
||||
# Go project
|
||||
log "Capturing baseline test count (Go)..."
|
||||
test_output=$(cd "$PROJECT_ROOT" && go test ./... -v 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" go test ./... -v) || true
|
||||
BASELINE_PASSING_TESTS=$(extract_test_count "$test_output")
|
||||
log "Baseline passing tests: $BASELINE_PASSING_TESTS"
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ init_regression_baseline() {
|
|||
# Python project
|
||||
if command -v pytest >/dev/null 2>&1; then
|
||||
log "Capturing baseline test count (Python)..."
|
||||
test_output=$(cd "$PROJECT_ROOT" && pytest -v 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" pytest -v) || true
|
||||
BASELINE_PASSING_TESTS=$(extract_test_count "$test_output")
|
||||
log "Baseline passing tests: $BASELINE_PASSING_TESTS"
|
||||
fi
|
||||
|
|
@ -199,23 +199,23 @@ execute_regression_gate() {
|
|||
if [ -f "$PROJECT_ROOT/package.json" ]; then
|
||||
# Check if there's a test:json script for better parsing
|
||||
if grep -q '"test:json"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
|
||||
test_output=$(cd "$PROJECT_ROOT" && npm run test:json 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm run test:json) || true
|
||||
else
|
||||
test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || true
|
||||
fi
|
||||
current_tests=$(extract_test_count "$test_output")
|
||||
|
||||
elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
||||
test_output=$(cd "$PROJECT_ROOT" && cargo test 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" cargo test) || true
|
||||
current_tests=$(extract_test_count "$test_output")
|
||||
|
||||
elif [ -f "$PROJECT_ROOT/go.mod" ]; then
|
||||
test_output=$(cd "$PROJECT_ROOT" && go test ./... -v 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" go test ./... -v) || true
|
||||
current_tests=$(extract_test_count "$test_output")
|
||||
|
||||
elif [ -f "$PROJECT_ROOT/requirements.txt" ] || [ -f "$PROJECT_ROOT/pyproject.toml" ]; then
|
||||
if command -v pytest >/dev/null 2>&1; then
|
||||
test_output=$(cd "$PROJECT_ROOT" && pytest -v 2>&1) || true
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" pytest -v) || true
|
||||
current_tests=$(extract_test_count "$test_output")
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ After outputting the spec block:
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$spec_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$spec_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ After implementing the tests:
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$impl_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$impl_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,41 @@
|
|||
# Usage: Sourced by epic-execute.sh
|
||||
#
|
||||
|
||||
# =============================================================================
|
||||
# Portable Timeout Wrapper
|
||||
# =============================================================================
|
||||
|
||||
# Run a command with a timeout (portable, no coreutils needed)
|
||||
# Usage: run_with_timeout <seconds> <command...>
|
||||
# Returns: command output via stdout; exit code 0 on success, 1 on timeout
|
||||
run_with_timeout() {
|
||||
local timeout_secs="$1"; shift
|
||||
local output_file
|
||||
output_file=$(mktemp /tmp/bmad-timeout-XXXXXX)
|
||||
|
||||
( "$@" > "$output_file" 2>&1 ) &
|
||||
local cmd_pid=$!
|
||||
|
||||
( sleep "$timeout_secs" && kill -TERM "$cmd_pid" 2>/dev/null && sleep 2 && kill -9 "$cmd_pid" 2>/dev/null ) &
|
||||
local watchdog_pid=$!
|
||||
|
||||
wait "$cmd_pid" 2>/dev/null
|
||||
local exit_code=$?
|
||||
|
||||
# Kill watchdog if command finished in time
|
||||
kill "$watchdog_pid" 2>/dev/null
|
||||
wait "$watchdog_pid" 2>/dev/null
|
||||
|
||||
cat "$output_file"
|
||||
rm -f "$output_file"
|
||||
|
||||
# 143 = SIGTERM, 137 = SIGKILL (timed out)
|
||||
if [ $exit_code -eq 143 ] || [ $exit_code -eq 137 ]; then
|
||||
return 1
|
||||
fi
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# M1: Retry Logic with Exponential Backoff
|
||||
# =============================================================================
|
||||
|
|
@ -102,7 +137,7 @@ execute_claude_with_retry() {
|
|||
|
||||
# Wrapper function for retry
|
||||
_claude_invoke() {
|
||||
timeout "$timeout" claude --dangerously-skip-permissions -p "$1" 2>&1
|
||||
timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$1" 2>&1
|
||||
local code=$?
|
||||
if [ $code -eq 124 ]; then
|
||||
echo "TIMEOUT: Claude invocation timed out after ${timeout}s"
|
||||
|
|
@ -591,7 +626,7 @@ execute_claude_verbose() {
|
|||
|
||||
# Execute with output tee'd to both terminal and log file
|
||||
local result
|
||||
result=$(timeout "$timeout" claude --dangerously-skip-permissions -p "$prompt" 2>&1 | tee -a "$LOG_FILE")
|
||||
result=$(timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" 2>&1 | tee -a "$LOG_FILE")
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
|
|
@ -605,7 +640,7 @@ execute_claude_verbose() {
|
|||
else
|
||||
# Non-verbose mode: capture output silently
|
||||
local result
|
||||
result=$(timeout "$timeout" claude --dangerously-skip-permissions -p "$prompt" 2>&1)
|
||||
result=$(timeout "$timeout" env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" 2>&1)
|
||||
local exit_code=$?
|
||||
|
||||
# Log to file only
|
||||
|
|
|
|||
|
|
@ -249,6 +249,32 @@ save_log_to_repo() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Flush the current log to the repo after each story completes/fails.
|
||||
# Unlike save_log_to_repo (which runs once at exit), this overwrites
|
||||
# the same file incrementally so the repo always has the latest progress.
|
||||
flush_log_to_repo() {
|
||||
# Skip if no log file or it's empty
|
||||
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create logs directory if needed
|
||||
if [ -n "$LOGS_DIR" ]; then
|
||||
mkdir -p "$LOGS_DIR" 2>/dev/null || true
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Use a stable filename so each flush overwrites the previous snapshot
|
||||
if [ -n "$EPIC_ID" ]; then
|
||||
local flush_file="$LOGS_DIR/epic-${EPIC_ID}-latest.log"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
|
||||
cp "$LOG_FILE" "$flush_file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Git Safety Functions
|
||||
# =============================================================================
|
||||
|
|
@ -1394,7 +1420,7 @@ Do NOT use 'git add -A' or 'git add .' - only stage files you created or modifie
|
|||
|
||||
# Execute in isolated context
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$dev_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$dev_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -1534,7 +1560,7 @@ Stage any fixes with explicit file paths: git add <file1> <file2> ..."
|
|||
|
||||
# Execute in isolated context
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$review_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$review_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -1782,7 +1808,7 @@ Address all review findings now. This is attempt $attempt_num of 3."
|
|||
return 0
|
||||
fi
|
||||
|
||||
result=$(claude --dangerously-skip-permissions -f "$temp_prompt_file" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -f "$temp_prompt_file" 2>&1) || true
|
||||
rm -f "$temp_prompt_file"
|
||||
else
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
|
|
@ -1791,7 +1817,7 @@ Address all review findings now. This is attempt $attempt_num of 3."
|
|||
fi
|
||||
|
||||
# Execute in isolated context
|
||||
result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
fi
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
|
@ -1930,7 +1956,7 @@ $build_output
|
|||
if grep -q '"test"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
|
||||
log "Running tests..."
|
||||
local test_output
|
||||
test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || {
|
||||
test_output=$(cd "$PROJECT_ROOT" && run_with_timeout "${REGRESSION_TEST_TIMEOUT:-120}" npm test) || {
|
||||
local exit_code=$?
|
||||
|
||||
# Check if there are NEW failures (not just pre-existing baseline failures)
|
||||
|
|
@ -2248,7 +2274,7 @@ Stage any fixes with: git add <file1> <file2> ..."
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$arch_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$arch_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -2325,7 +2351,7 @@ Stage any fixes with: git add <file1> <file2> ..."
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$quality_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$quality_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -2460,7 +2486,7 @@ Analyze traceability now. Read story files on-demand as needed."
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$trace_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$trace_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -2538,7 +2564,7 @@ Generate missing tests now."
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -2892,7 +2918,7 @@ Generate the UAT document now. Read story files on-demand as needed."
|
|||
fi
|
||||
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$uat_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$uat_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
@ -2988,6 +3014,8 @@ for story_file in "${STORIES[@]}"; do
|
|||
if type save_checkpoint >/dev/null 2>&1; then
|
||||
save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED"
|
||||
fi
|
||||
# Flush log to repo after story failure
|
||||
flush_log_to_repo
|
||||
continue
|
||||
fi
|
||||
else
|
||||
|
|
@ -3003,6 +3031,8 @@ for story_file in "${STORIES[@]}"; do
|
|||
if type save_checkpoint >/dev/null 2>&1; then
|
||||
save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED"
|
||||
fi
|
||||
# Flush log to repo after story failure
|
||||
flush_log_to_repo
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
|
@ -3030,6 +3060,9 @@ for story_file in "${STORIES[@]}"; do
|
|||
if type save_checkpoint >/dev/null 2>&1; then
|
||||
save_checkpoint "$STORY_INDEX" "$story_id" "$COMPLETED" "$FAILED" "$SKIPPED"
|
||||
fi
|
||||
|
||||
# Flush log to repo after each completed story
|
||||
flush_log_to_repo
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -859,7 +859,7 @@ HUMAN_ACTION_NEEDED: {yes/no}"
|
|||
|
||||
# Execute in isolated context
|
||||
local result
|
||||
result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
result=$(env -u CLAUDECODE claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
|
||||
|
||||
echo "$result" >> "$LOG_FILE"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue