TSHARPS-CI/ci-runner.sh
Claude BM 9b7abec506 feat: TSHARPS-CI external runner — branch-independent CI pipeline
Complete CI system that lives outside TSHARPS branches:
- ci-webhook.py: HTTP server on port 9500, receives Gitea push webhooks
- ci-runner.sh: runs feature manifests, pytest, package checks (read-only)
- ci-notify.sh: sends results to Telegram CICD Pipeline topic (4706)
- ci-config.json: branch→worktree mapping, tokens, timeouts
- README.md: branch model, promotion workflow, switch-back plan

Same tests for ALL branches. No drift. Runner self-monitors for crashes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 03:25:31 +00:00

136 lines
4.0 KiB
Bash
Executable File

#!/bin/bash
# TSHARPS CI — External Test Runner
# Runs tests, feature manifests, and package checks against TSHARPS worktrees.
# READ-ONLY — never writes, commits, or pushes to TSHARPS.
# Same checks for ALL branches — no branch-specific logic.
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG="$SCRIPT_DIR/ci-config.json"
BRANCH="$1"
COMMIT="$2"
ACTOR="${3:-unknown}"
START_TIME=$(date +%s)
# Load config
TIMEOUT=$(python3 -c "import json; print(json.load(open('$CONFIG')).get('test_timeout', 120))")
WORKTREE=$(python3 -c "import json; b=json.load(open('$CONFIG'))['branches']; print(b.get('$BRANCH',{}).get('worktree',''))")
if [ -z "$WORKTREE" ] || [ ! -d "$WORKTREE" ]; then
echo "ERROR: Unknown branch '$BRANCH' or worktree not found"
bash "$SCRIPT_DIR/ci-notify.sh" "$BRANCH" "$COMMIT" "error" "Unknown branch or missing worktree" "" "0s" "$ACTOR"
exit 1
fi
# Prevent concurrent runs on the same branch
LOCKFILE="/tmp/tsharps-ci-${BRANCH}.lock"
exec 200>"$LOCKFILE"
if ! flock -n 200; then
echo "SKIP: CI already running for branch $BRANCH"
exit 0
fi
echo "=== TSHARPS CI Runner ==="
echo "Branch: $BRANCH"
echo "Commit: $COMMIT"
echo "Worktree: $WORKTREE"
echo "Timeout: ${TIMEOUT}s"
echo "========================="
PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
ERROR_COUNT=0
FEATURES_RESULT=""
OVERALL="pass"
# ─── Step 1: Feature Manifest Check ───
echo ""
echo "--- Feature Manifest Check ---"
if [ -f "$WORKTREE/ops/verify-features.py" ] && [ -d "$WORKTREE/.features" ]; then
FEATURE_OUTPUT=$(cd "$WORKTREE" && python3 ops/verify-features.py --verbose 2>&1)
FEATURE_EXIT=$?
FEATURES_RESULT=$(echo "$FEATURE_OUTPUT" | grep "RESULT:" | tail -1)
if [ -z "$FEATURES_RESULT" ]; then
FEATURES_RESULT=$(echo "$FEATURE_OUTPUT" | tail -1)
fi
echo "$FEATURE_OUTPUT"
if [ $FEATURE_EXIT -ne 0 ]; then
OVERALL="fail"
echo "FEATURE CHECK FAILED"
fi
else
FEATURES_RESULT="No manifests found"
echo "No feature manifests — skipping"
fi
# ─── Step 2: Run Test Suite ───
echo ""
echo "--- Test Suite ---"
PYTHON="$WORKTREE/.venv/bin/python3"
if [ ! -f "$PYTHON" ]; then
PYTHON="python3"
fi
TEST_OUTPUT=$($PYTHON -m pytest "$WORKTREE/backend/tests/" --tb=line -q --timeout="$TIMEOUT" 2>&1)
TEST_EXIT=$?
PASS_COUNT=$(echo "$TEST_OUTPUT" | grep -oP '\d+ passed' | grep -oP '\d+' || echo 0)
FAIL_COUNT=$(echo "$TEST_OUTPUT" | grep -oP '\d+ failed' | grep -oP '\d+' || echo 0)
SKIP_COUNT=$(echo "$TEST_OUTPUT" | grep -oP '\d+ skipped' | grep -oP '\d+' || echo 0)
ERROR_COUNT=$(echo "$TEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' || echo 0)
echo "Tests: $PASS_COUNT passed, $FAIL_COUNT failed, $SKIP_COUNT skipped, $ERROR_COUNT errors"
if [ "$FAIL_COUNT" != "0" ] && [ "$FAIL_COUNT" != "" ]; then
OVERALL="fail"
echo "TESTS FAILED"
fi
if [ "$ERROR_COUNT" != "0" ] && [ "$ERROR_COUNT" != "" ]; then
OVERALL="fail"
echo "TEST COLLECTION ERRORS"
fi
# ─── Step 3: Package Check ───
echo ""
echo "--- Package Check ---"
PKG_OUTPUT=$($PYTHON -c "
import sys
required = ['fastapi','uvicorn','sqlalchemy','psycopg2','bcrypt','jwt','pandas','openpyxl','pydantic','ortools','astral','requests','dotenv','httpx']
missing = []
for mod in required:
try: __import__(mod)
except ImportError: missing.append(mod)
if missing:
print(f'FAIL: {len(missing)} missing: {missing}')
sys.exit(1)
print(f'OK: All {len(required)} packages verified')
" 2>&1)
PKG_EXIT=$?
echo "$PKG_OUTPUT"
if [ $PKG_EXIT -ne 0 ]; then
OVERALL="fail"
fi
# ─── Results ───
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
SUMMARY="${PASS_COUNT} passed, ${FAIL_COUNT} failed, ${SKIP_COUNT} skipped"
echo ""
echo "========================="
echo "Result: $OVERALL"
echo "Summary: $SUMMARY"
echo "Features: $FEATURES_RESULT"
echo "Duration: ${DURATION}s"
echo "========================="
# ─── Send Notification ───
bash "$SCRIPT_DIR/ci-notify.sh" \
"$BRANCH" "$COMMIT" "$OVERALL" "$SUMMARY" "$FEATURES_RESULT" "${DURATION}s" "$ACTOR"
# Release lock
flock -u 200
exit 0