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.
# Would have caught: kill_tmux_session (renamed), create_agent_session (missing),
# read_phase (missing from dev-agent.sh scope)
set -euo pipefail
cd " $( dirname " $0 " ) /.. "
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-03-20 12:02:40 +00:00
awk ' /^[ \t ] *[ a-zA-Z_] [ a-zA-Z0-9_] +[ \t ] *\( \) / {
sub( /^[ \t ] +/, "" )
sub( /[ \t ] *\( \) .*/, "" )
2026-03-19 19:43:42 +00:00
print
} ' " $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-19 13:47:10 +01:00
# Skip: case labels (word) or word|), Python/jq patterns (word:),
2026-03-18 17:11:02 +00:00
# object method calls (word.method), assignments (word=)
2026-03-19 13:47:10 +01:00
if ( 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-03-19 07:25:25 +00:00
done < <( find dev gardener review planner supervisor lib vault action -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-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-21 06:41:48 +00:00
# lib/agent-session.sh — sourced by orchestrators (create_agent_session, monitor_phase_loop, etc.)
# 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-03-21 06:49:34 +00:00
# lib/file-action-issue.sh — sourced by gardener-run.sh (file_action_issue)
2026-03-21 09:55:58 +00:00
# lib/secret-scan.sh — sourced by file-action-issue.sh, phase-handler.sh (scan_for_secrets, redact_secrets)
2026-03-21 06:41:48 +00:00
# lib/formula-session.sh — sourced by formula-driven agents (acquire_cron_lock, run_formula_and_monitor, etc.)
2026-03-23 19:20:59 +00:00
# lib/mirrors.sh — sourced by merge sites (mirror_push)
2026-03-23 21:46:59 +00:00
# lib/guard.sh — sourced by all cron entry points (check_active)
2026-03-21 06:41:48 +00:00
#
# Excluded — not sourced inline by agents:
# lib/ci-debug.sh — standalone CLI tool, run directly (not sourced)
# lib/matrix_listener.sh — standalone systemd daemon (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-03-23 21:46:59 +00:00
for f in lib/agent-session.sh lib/env.sh lib/ci-helpers.sh lib/load-project.sh lib/secret-scan.sh lib/file-action-issue.sh lib/formula-session.sh lib/mirrors.sh lib/guard.sh; do
2026-03-18 17:11:02 +00:00
if [ -f " $f " ] ; then get_fns " $f " ; fi
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 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
if ! printf '%s\n' " $all_fns " | grep -qxF " $fn " ; then
printf 'FAIL [undef] %s: %s\n' " $script " " $fn "
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-21 06:49:34 +00:00
check_script lib/env.sh
2026-03-21 06:41:48 +00:00
check_script lib/agent-session.sh
check_script lib/ci-helpers.sh
2026-03-21 09:55:58 +00:00
check_script lib/secret-scan.sh
check_script lib/file-action-issue.sh lib/secret-scan.sh
2026-03-21 06:41:48 +00:00
check_script lib/formula-session.sh lib/agent-session.sh
check_script lib/load-project.sh
2026-03-23 19:20:59 +00:00
check_script lib/mirrors.sh
2026-03-23 21:46:59 +00:00
check_script lib/guard.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/matrix_listener.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.
# dev-agent.sh sources phase-handler.sh; phase-handler.sh calls helpers defined in dev-agent.sh.
check_script dev/dev-agent.sh dev/phase-handler.sh
2026-03-21 09:55:58 +00:00
check_script dev/phase-handler.sh dev/dev-agent.sh lib/secret-scan.sh
2026-03-18 17:11:02 +00:00
check_script dev/dev-poll.sh
check_script dev/phase-test.sh
2026-03-20 12:49:08 +00:00
check_script gardener/gardener-run.sh
2026-03-18 17:11:02 +00:00
check_script review/review-pr.sh
check_script review/review-poll.sh
2026-03-20 13:53:33 +00:00
check_script planner/planner-run.sh
2026-03-18 17:11:02 +00:00
check_script supervisor/supervisor-poll.sh
check_script supervisor/update-prompt.sh
check_script vault/vault-agent.sh
check_script vault/vault-fire.sh
check_script vault/vault-poll.sh
check_script vault/vault-reject.sh
2026-03-19 07:25:25 +00:00
check_script action/action-poll.sh
2026-03-20 17:39:44 +01:00
check_script action/action-agent.sh dev/phase-handler.sh
2026-03-21 00:30:22 +00:00
check_script supervisor/supervisor-run.sh
check_script supervisor/preflight.sh
check_script predictor/predictor-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 ==="