2026-03-18 17:11:02 +00:00
|
|
|
#!/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")/.."
|
|
|
|
|
|
2026-04-10 17:59:36 +00:00
|
|
|
# CI-side filesystem snapshot: show lib/ state at smoke time (#600)
|
|
|
|
|
echo "=== smoke environment snapshot ==="
|
|
|
|
|
ls -la lib/ 2>&1 | head -50
|
|
|
|
|
echo "=== "
|
|
|
|
|
|
2026-03-18 17:11:02 +00:00
|
|
|
FAILED=0
|
|
|
|
|
|
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
# Extract function names defined in a bash script (top-level or indented).
|
2026-03-19 19:43:42 +00:00
|
|
|
# Uses awk instead of grep -Eo for busybox/Alpine compatibility (#296).
|
2026-03-18 17:11:02 +00:00
|
|
|
get_fns() {
|
|
|
|
|
local f="$1"
|
2026-04-06 06:57:28 +00:00
|
|
|
# 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
|
2026-03-18 17:11:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 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)
|
2026-03-26 20:28:18 +00:00
|
|
|
# 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
|
2026-03-18 17:11:02 +00:00
|
|
|
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)
|
2026-03-18 17:11:02 +00:00
|
|
|
echo "syntax check done"
|
|
|
|
|
|
|
|
|
|
# ── 2. Function-resolution check ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
echo "=== 2/2 Function resolution ==="
|
|
|
|
|
|
2026-04-10 17:59:36 +00:00
|
|
|
# Required lib files for LIB_FUNS construction. Missing any of these means the
|
|
|
|
|
# checkout is incomplete or the test is misconfigured — fail loudly, do NOT
|
|
|
|
|
# silently produce a partial LIB_FUNS list (that masquerades as "undef" errors
|
|
|
|
|
# in unrelated scripts; see #600).
|
|
|
|
|
REQUIRED_LIBS=(
|
|
|
|
|
lib/agent-sdk.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh
|
|
|
|
|
lib/secret-scan.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh
|
|
|
|
|
lib/pr-lifecycle.sh lib/issue-lifecycle.sh lib/worktree.sh
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for f in "${REQUIRED_LIBS[@]}"; do
|
|
|
|
|
if [ ! -f "$f" ]; then
|
|
|
|
|
printf 'FAIL [missing-lib] expected %s but it is not present at smoke time\n' "$f" >&2
|
|
|
|
|
printf ' pwd=%s\n' "$(pwd)" >&2
|
|
|
|
|
printf ' ls lib/=%s\n' "$(ls lib/ 2>&1 | tr '\n' ' ')" >&2
|
|
|
|
|
echo '=== SMOKE TEST FAILED (precondition) ===' >&2
|
|
|
|
|
exit 2
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2026-03-21 06:41:48 +00:00
|
|
|
# Functions provided by shared lib files (available to all agent scripts via source).
|
|
|
|
|
#
|
|
|
|
|
# Included — these are inline-sourced by agent scripts:
|
fix: Replace Codeberg dependency with local Forgejo instance (#611)
- Add setup_forge() to bin/disinto: provisions Forgejo via Docker,
creates admin + bot users (dev-bot, review-bot), generates API
tokens, creates repo, and pushes code — all automated
- Rename env vars: CODEBERG_TOKEN→FORGE_TOKEN, REVIEW_BOT_TOKEN→
FORGE_REVIEW_TOKEN, CODEBERG_REPO→FORGE_REPO, CODEBERG_API→
FORGE_API, CODEBERG_WEB→FORGE_WEB, CODEBERG_BOT_USERNAMES→
FORGE_BOT_USERNAMES (with backwards-compat fallbacks)
- Rename API helpers: codeberg_api()→forge_api(), codeberg_api_all()
→forge_api_all() (with compat aliases)
- Add forge_url field to project TOML; load-project.sh derives
FORGE_API/FORGE_WEB from forge_url + repo
- Update parse_repo_slug() to accept any host URL, not just codeberg
- Forgejo data stored under ~/.disinto/forgejo/ (not in factory repo)
- Update all 58 files: agent scripts, formulas, docs, site HTML
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:57:12 +00:00
|
|
|
# lib/env.sh — sourced by every agent (log, forge_api, etc.)
|
2026-03-28 06:36:32 +00:00
|
|
|
# lib/agent-sdk.sh — sourced by SDK agents (agent_run, agent_recover_session)
|
2026-03-21 06:41:48 +00:00
|
|
|
# lib/ci-helpers.sh — sourced by pollers and review (ci_passed, classify_pipeline_failure, etc.)
|
|
|
|
|
# lib/load-project.sh — sourced by env.sh when PROJECT_TOML is set
|
2026-04-10 17:59:36 +00:00
|
|
|
# lib/secret-scan.sh — standalone CLI tool, run directly (not sourced)
|
2026-04-10 08:54:11 +00:00
|
|
|
# lib/formula-session.sh — sourced by formula-driven agents (acquire_run_lock, check_memory, etc.)
|
2026-03-23 19:20:59 +00:00
|
|
|
# lib/mirrors.sh — sourced by merge sites (mirror_push)
|
2026-04-10 08:54:11 +00:00
|
|
|
# lib/guard.sh — sourced by all polling-loop entry points (check_active)
|
2026-03-27 18:28:17 +00:00
|
|
|
# lib/issue-lifecycle.sh — sourced by agents for issue claim/release/block/deps
|
2026-03-27 19:06:31 +00:00
|
|
|
# lib/worktree.sh — sourced by agents for worktree create/recover/cleanup/preserve
|
2026-03-21 06:41:48 +00:00
|
|
|
#
|
|
|
|
|
# Excluded — not sourced inline by agents:
|
2026-03-25 13:22:56 +00:00
|
|
|
# lib/tea-helpers.sh — sourced conditionally by env.sh (tea_file_issue, etc.); checked standalone below
|
2026-03-21 06:41:48 +00:00
|
|
|
# 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)
|
|
|
|
|
#
|
|
|
|
|
# If a new lib file is added and sourced by agents, add it to LIB_FUNS below
|
|
|
|
|
# and add a check_script call for it in the lib files section further down.
|
2026-03-18 17:11:02 +00:00
|
|
|
LIB_FUNS=$(
|
2026-04-10 17:59:36 +00:00
|
|
|
for f in "${REQUIRED_LIBS[@]}"; do get_fns "$f"; done | sort -u
|
2026-03-18 17:11:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 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
|
2026-03-25 12:20:15 +00:00
|
|
|
sort source stat tail tar tea test 'then' tmux touch tr trap true type
|
2026-03-18 17:11:02 +00:00
|
|
|
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
|
|
|
|
|
if ! printf '%s\n' "$all_fns" | grep -qxF "$fn"; then
|
|
|
|
|
printf 'FAIL [undef] %s: %s\n' "$script" "$fn"
|
2026-04-10 17:59:36 +00:00
|
|
|
# Diagnostic dump (#600): if the function is expected to be in a known lib,
|
|
|
|
|
# print what the actual all_fns set looks like so we can tell whether the
|
|
|
|
|
# function is genuinely missing or whether the resolution loop is broken.
|
|
|
|
|
printf ' all_fns count: %d\n' "$(printf '%s\n' "$all_fns" | wc -l)"
|
|
|
|
|
printf ' LIB_FUNS contains "%s": %s\n' "$fn" "$(printf '%s\n' "$LIB_FUNS" | grep -cxF "$fn")"
|
|
|
|
|
printf ' defining lib (if any): %s\n' "$(grep -l "^[[:space:]]*${fn}[[:space:]]*()" lib/*.sh 2>/dev/null | tr '\n' ' ')"
|
2026-03-18 17:11:02 +00:00
|
|
|
FAILED=1
|
|
|
|
|
fi
|
|
|
|
|
done <<< "$candidates"
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 06:41:48 +00:00
|
|
|
# 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.
|
2026-03-26 21:46:21 +01:00
|
|
|
check_script lib/env.sh lib/mirrors.sh
|
2026-03-28 06:36:32 +00:00
|
|
|
check_script lib/agent-sdk.sh
|
2026-03-21 06:41:48 +00:00
|
|
|
check_script lib/ci-helpers.sh
|
2026-03-21 09:55:58 +00:00
|
|
|
check_script lib/secret-scan.sh
|
2026-03-25 12:20:15 +00:00
|
|
|
check_script lib/tea-helpers.sh lib/secret-scan.sh
|
fix: chore: remove dead tmux-based session code (agent-session.sh, phase-handler.sh) (#262)
- Delete lib/agent-session.sh (entirely dead file with no active callers)
- Delete dev/phase-handler.sh (entirely dead file with no active callers)
- Update lib/formula-session.sh to remove tmux-based functions:
- Removed: start_formula_session, run_formula_and_monitor, formula_phase_callback,
write_compact_context, remove_formula_worktree, cleanup_stale_crashed_worktrees
- Kept utility functions: acquire_cron_lock, check_memory, load_formula,
profile_write_journal, formula_prepare_profile_context, build_graph_section, etc.
- Update dev/phase-test.sh to inline read_phase() function (no longer sources agent-session.sh)
- Update documentation: AGENTS.md, lib/AGENTS.md, dev/AGENTS.md, .woodpecker/agent-smoke.sh,
docs/PHASE-PROTOCOL.md, lib/pr-lifecycle.sh
- All 38 phase tests pass
2026-04-05 22:25:53 +00:00
|
|
|
check_script lib/formula-session.sh
|
2026-03-21 06:41:48 +00:00
|
|
|
check_script lib/load-project.sh
|
2026-03-26 21:46:21 +01:00
|
|
|
check_script lib/mirrors.sh lib/env.sh
|
2026-03-23 21:46:59 +00:00
|
|
|
check_script lib/guard.sh
|
2026-03-27 18:01:06 +00:00
|
|
|
check_script lib/pr-lifecycle.sh
|
2026-03-27 18:28:17 +00:00
|
|
|
check_script lib/issue-lifecycle.sh lib/secret-scan.sh
|
2026-03-21 06:41:48 +00:00
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
2026-03-18 17:11:02 +00:00
|
|
|
# Agent scripts — list cross-sourced files where function scope flows across files.
|
2026-03-27 21:20:55 +00:00
|
|
|
check_script dev/dev-agent.sh
|
2026-03-18 17:11:02 +00:00
|
|
|
check_script dev/dev-poll.sh
|
|
|
|
|
check_script dev/phase-test.sh
|
2026-04-10 14:52:11 +00:00
|
|
|
check_script gardener/gardener-run.sh lib/formula-session.sh
|
2026-03-28 06:36:32 +00:00
|
|
|
check_script review/review-pr.sh lib/agent-sdk.sh
|
2026-03-18 17:11:02 +00:00
|
|
|
check_script review/review-poll.sh
|
fix: chore: remove dead tmux-based session code (agent-session.sh, phase-handler.sh) (#262)
- Delete lib/agent-session.sh (entirely dead file with no active callers)
- Delete dev/phase-handler.sh (entirely dead file with no active callers)
- Update lib/formula-session.sh to remove tmux-based functions:
- Removed: start_formula_session, run_formula_and_monitor, formula_phase_callback,
write_compact_context, remove_formula_worktree, cleanup_stale_crashed_worktrees
- Kept utility functions: acquire_cron_lock, check_memory, load_formula,
profile_write_journal, formula_prepare_profile_context, build_graph_section, etc.
- Update dev/phase-test.sh to inline read_phase() function (no longer sources agent-session.sh)
- Update documentation: AGENTS.md, lib/AGENTS.md, dev/AGENTS.md, .woodpecker/agent-smoke.sh,
docs/PHASE-PROTOCOL.md, lib/pr-lifecycle.sh
- All 38 phase tests pass
2026-04-05 22:25:53 +00:00
|
|
|
check_script planner/planner-run.sh lib/formula-session.sh
|
2026-03-18 17:11:02 +00:00
|
|
|
check_script supervisor/supervisor-poll.sh
|
|
|
|
|
check_script supervisor/update-prompt.sh
|
2026-04-09 19:43:47 +00:00
|
|
|
check_script supervisor/supervisor-run.sh lib/formula-session.sh
|
2026-03-21 00:30:22 +00:00
|
|
|
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
|
2026-03-18 17:11:02 +00:00
|
|
|
|
|
|
|
|
echo "function resolution check done"
|
|
|
|
|
|
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
|
|
|
echo "=== SMOKE TEST FAILED ==="
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "=== SMOKE TEST PASSED ==="
|