disinto/.woodpecker/agent-smoke.sh

240 lines
9 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# .woodpecker/agent-smoke.sh — CI smoke test: syntax check + function resolution
#
# Checks:
# 1. bash -n syntax check on all .sh files in agent directories
# 2. Every custom function called by agent scripts is defined in lib/ or the script itself
#
# Fast (<10s): no network, no tmux, no Claude needed.
set -euo pipefail
cd "$(dirname "$0")/.."
# CI-side filesystem snapshot: show lib/ state at smoke time (#600)
echo "=== smoke environment snapshot ==="
ls -la lib/ 2>&1 | head -50
echo "=== "
FAILED=0
# ── helpers ─────────────────────────────────────────────────────────────────
# Extract function names defined in a bash script (top-level or indented).
# Uses awk instead of grep -Eo for busybox/Alpine compatibility (#296).
get_fns() {
local f="$1"
# Pure-awk implementation: avoids grep/sed cross-platform differences
# (BusyBox grep BRE quirks, sed ; separator issues on Alpine).
awk '
/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_][a-zA-Z0-9_]*[[:space:]]*[(][)]/ {
line = $0
gsub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]*[(].*/, "", line)
print line
}
' "$f" 2>/dev/null | sort -u || true
}
# Extract call-position identifiers that look like custom function calls:
# - strip comment lines
# - split into statements by ; and $(
# - strip leading shell keywords (if/while/! etc.) from each statement
# - take the first word; skip if it looks like an assignment (var= or var =)
# - keep only lowercase identifiers containing underscore
# - skip if the identifier is followed by ) or : (case labels, Python patterns)
get_candidates() {
local script="$1"
awk '
/^[[:space:]]*#/ { next }
{
n = split($0, parts, /;|\$\(/)
for (i = 1; i <= n; i++) {
p = parts[i]
gsub(/^[[:space:]]+/, "", p)
# Skip variable assignments (var= or var =value, including Python-style "var = value")
if (p ~ /^[a-zA-Z_][a-zA-Z0-9_]* *=/) continue
# Strip leading shell keywords and negation operator
do {
changed = 0
if (p ~ /^(if|while|until|for|case|do|done|then|else|elif|fi|esac|!) /) {
sub(/^[^ ]+ /, "", p)
changed = 1
}
} while (changed)
# Skip for-loop iteration variable ("varname in list")
if (p ~ /^[a-zA-Z_][a-zA-Z0-9_]* in /) continue
# Extract first word if it looks like a custom function (lowercase + underscore)
if (match(p, /^[a-z][a-zA-Z0-9_]*_[a-zA-Z0-9_]+/)) {
word = substr(p, RSTART, RLENGTH)
rest = substr(p, RSTART + RLENGTH, 1)
# Skip: function definitions (word(), case labels (word) or word|),
# Python/jq patterns (word:), object method calls (word.method),
# assignments (word=)
if (rest == "(" || rest == ")" || rest == "|" || rest == ":" || rest == "." || rest == "=") continue
print word
}
}
}
' "$script" | sort -u || true
}
# ── 1. bash -n syntax check ──────────────────────────────────────────────────
echo "=== 1/2 bash -n syntax check ==="
while IFS= read -r -d '' f; do
if ! bash -n "$f" 2>&1; then
printf 'FAIL [syntax] %s\n' "$f"
FAILED=1
fi
2026-04-01 09:55:44 +00:00
done < <(find dev gardener review planner supervisor architect lib vault -name "*.sh" -print0 2>/dev/null)
echo "syntax check done"
# ── 2. Function-resolution check ─────────────────────────────────────────────
echo "=== 2/2 Function resolution ==="
# Enumerate ALL lib/*.sh files in stable lexicographic order (#742).
# Previous approach used a hand-maintained REQUIRED_LIBS list, which silently
# became incomplete as new libs were added, producing partial LIB_FUNS that
# caused non-deterministic "undef" failures.
#
# Excluded from LIB_FUNS (not sourced inline by agents):
# lib/ci-debug.sh — standalone CLI tool, run directly (not sourced)
# lib/parse-deps.sh — executed via `bash lib/parse-deps.sh` (not sourced)
# lib/hooks/*.sh — Claude Code hook scripts, executed by the harness (not sourced)
EXCLUDED_LIBS="lib/ci-debug.sh lib/parse-deps.sh"
# Build the list of lib files in deterministic order (LC_ALL=C sort).
# Fail loudly if no lib files are found — checkout is broken.
mapfile -t ALL_LIBS < <(LC_ALL=C find lib -maxdepth 1 -name '*.sh' -print | LC_ALL=C sort)
if [ "${#ALL_LIBS[@]}" -eq 0 ]; then
echo 'FAIL [no-libs] no lib/*.sh files found at smoke time' >&2
printf ' pwd=%s\n' "$(pwd)" >&2
echo '=== SMOKE TEST FAILED (precondition) ===' >&2
exit 2
fi
# Build LIB_FUNS from all non-excluded lib files.
# Use set -e inside the subshell so a failed get_fns aborts loudly
# instead of silently shrinking the function list.
LIB_FUNS=$(
set -e
for f in "${ALL_LIBS[@]}"; do
# shellcheck disable=SC2086
skip=0; for ex in $EXCLUDED_LIBS; do [ "$f" = "$ex" ] && skip=1; done
[ "$skip" -eq 1 ] && continue
get_fns "$f"
done | sort -u
)
# Known external commands and shell builtins — never flag these
# (shell keywords are quoted to satisfy shellcheck SC1010)
KNOWN_CMDS=(
awk bash break builtin cat cd chmod chown claude command continue
cp curl cut date declare 'do' 'done' elif else eval exit export
false 'fi' find flock for getopts git grep gzip gunzip head hash
'if' jq kill local ln ls mapfile mkdir mktemp mv nc pgrep printf
python3 python read readarray return rm sed set sh shift sleep
sort source stat tail tar tea test 'then' tmux touch tr trap true type
unset until wait wc while which xargs
)
is_known_cmd() {
local fn="$1"
for k in "${KNOWN_CMDS[@]}"; do
[ "$fn" = "$k" ] && return 0
done
return 1
}
# check_script SCRIPT [EXTRA_DEFINITION_SOURCES...]
# Checks that every custom function called by SCRIPT is defined in:
# - SCRIPT itself
# - Any EXTRA_DEFINITION_SOURCES (for cross-sourced scripts)
# - The shared lib files (LIB_FUNS)
check_script() {
local script="$1"
shift
[ -f "$script" ] || { printf 'SKIP (not found): %s\n' "$script"; return; }
# Collect all function definitions available to this script
local all_fns
all_fns=$(
{
printf '%s\n' "$LIB_FUNS"
get_fns "$script"
for extra in "$@"; do
if [ -f "$extra" ]; then get_fns "$extra"; fi
done
} | sort -u
)
local candidates
candidates=$(get_candidates "$script")
while IFS= read -r fn; do
[ -z "$fn" ] && continue
is_known_cmd "$fn" && continue
# Use here-string (<<<) instead of pipe to avoid SIGPIPE race (#742):
# with pipefail, `printf | grep -q` can fail when grep closes the pipe
# early after finding a match, causing printf to get SIGPIPE (exit 141).
# This produced non-deterministic false "undef" failures.
if ! grep -qxF "$fn" <<< "$all_fns"; then
printf 'FAIL [undef] %s: %s\n' "$script" "$fn"
printf ' all_fns count: %d\n' "$(grep -c . <<< "$all_fns")"
printf ' LIB_FUNS contains "%s": %s\n' "$fn" "$(grep -cxF "$fn" <<< "$LIB_FUNS")"
printf ' defining lib (if any): %s\n' "$(grep -l "^[[:space:]]*${fn}[[:space:]]*()" lib/*.sh 2>/dev/null | tr '\n' ' ')"
FAILED=1
fi
done <<< "$candidates"
}
# Inline-sourced lib files — check that their own function calls resolve.
# These are already in LIB_FUNS (their definitions are available to agents),
# but this verifies calls *within* each lib file are also resolvable.
check_script lib/env.sh lib/mirrors.sh
check_script lib/agent-sdk.sh
check_script lib/ci-helpers.sh
check_script lib/secret-scan.sh
check_script lib/tea-helpers.sh lib/secret-scan.sh
check_script lib/formula-session.sh lib/ops-setup.sh
check_script lib/load-project.sh
check_script lib/mirrors.sh lib/env.sh
check_script lib/guard.sh
check_script lib/pr-lifecycle.sh
check_script lib/issue-lifecycle.sh lib/secret-scan.sh
# Standalone lib scripts (not sourced by agents; run directly or as services).
# Still checked for function resolution against LIB_FUNS + own definitions.
check_script lib/ci-debug.sh
check_script lib/parse-deps.sh
fix: bug: architect pitch prompt guardrail is prose-only — model bypasses "NEVER call Forgejo API" via Bash tool; fix via permission scoping + PR-driven sub-issue filing (#764) Shift the guardrail from prose prompt constraints into Forgejo's permission layer. architect-bot loses all write access on the project repo (now read-only for context gathering). Sub-issues are produced by a new filer-bot identity that runs only after a human merges a sprint PR on the ops repo. Changes: - architect-run.sh: remove all project-repo writes (add_inprogress_label, close_vision_issue, check_and_close_completed_visions); add ## Sub-issues block to pitch format with filer:begin/end markers - formulas/run-architect.toml: add Sub-issues schema to pitch format; strip issue-creation API refs; document read-only constraint on project repo - lib/formula-session.sh: remove Create issue curl template from build_prompt_footer (architect cannot create issues) - lib/sprint-filer.sh (new): parser + idempotent filer using FORGE_FILER_TOKEN; parses filer:begin/end blocks, creates issues with decomposed-from markers, adds in-progress label, handles vision lifecycle closure - .woodpecker/ops-filer.yml (new): CI pipeline on ops repo main-branch push that invokes sprint-filer.sh after sprint PR merge - lib/env.sh, .env.example, docker-compose.yml: add FORGE_FILER_TOKEN for filer-bot identity; add filer-bot to FORGE_BOT_USERNAMES - AGENTS.md: add Filer agent entry; update in-progress label docs - .woodpecker/agent-smoke.sh: register sprint-filer.sh for smoke test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:41:16 +00:00
check_script lib/sprint-filer.sh
# Agent scripts — list cross-sourced files where function scope flows across files.
check_script dev/dev-agent.sh
check_script dev/dev-poll.sh
check_script dev/phase-test.sh
check_script gardener/gardener-run.sh lib/formula-session.sh
check_script review/review-pr.sh lib/agent-sdk.sh
check_script review/review-poll.sh
check_script planner/planner-run.sh lib/formula-session.sh
check_script supervisor/supervisor-poll.sh
check_script supervisor/update-prompt.sh
check_script supervisor/supervisor-run.sh lib/formula-session.sh
check_script supervisor/preflight.sh
check_script predictor/predictor-run.sh
2026-04-01 09:55:44 +00:00
check_script architect/architect-run.sh
echo "function resolution check done"
if [ "$FAILED" -ne 0 ]; then
echo "=== SMOKE TEST FAILED ==="
exit 1
fi
echo "=== SMOKE TEST PASSED ==="