chore(harness): Claude Code 하네스 구성 — 안전 가드·규칙 검사·배포 스킬
- .claude/settings.json (공유) 신설, hooks 를 settings.local.json 에서 이동 - bash_guard (PreToolUse): 로컬 스택 기동·운영 DB alembic·_test 아닌 pytest·main 강제 push·위험 rm 차단, main push/deploy.sh 는 장중 경고와 함께 확인 요청, backend 커밋 전 유닛 스위트 게이트 - rules_check (PostToolUse): py_compile + CLAUDE.md 규칙(json.dumps, except 후 logger.debug, except-pass, 실시간 계층 DB I/O) — diff 추가 라인만 - session_context (SessionStart): 브랜치·미커밋·장 상태·alembic head 주입 - stop_check (Stop): frontend 변경 시 tsc - auto-orchestrate: 서브에이전트 보고·시스템 알림·질문형 프롬프트 오발동 제거 - /deploy 스킬 + deploy.sh / prod_smoke.sh: 테스트→dev push→main 병합→재기동 감시→공개 API 스모크 - 팀 에이전트·orchestrate 스킬 추적 시작, settings.local.json 추적 해제(.gitignore) - CLAUDE.md §9 하네스 - 훅 테스트 69 passed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
136
.claude/scripts/deploy.sh
Executable file
136
.claude/scripts/deploy.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# auto-stock 배포: 작업트리 확인 → 깨끗한 worktree 유닛 스위트 → push dev → 임시 worktree 에서 main 병합·푸시
|
||||
# → 재기동 감시(비-200 → 200) → 공개 API 스모크.
|
||||
# 사용:
|
||||
# deploy.sh [-m "병합 메시지"] [--no-test] [--dry-run]
|
||||
# deploy.sh --watch-only # 이미 푸시된 배포의 재기동 감시 + 스모크만
|
||||
# 사용자의 작업 브랜치(dev)는 바꾸지 않는다. 운영 서버/KIS 직접 호출 없음(공개 GET 만).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT"
|
||||
SCRIPT_DIR="$ROOT/.claude/scripts"
|
||||
BASE="${PROD_BASE_URL:-https://stock.shium.net}"
|
||||
WATCH_URL="$BASE/api/v1/analysis/performance"
|
||||
WATCH_MAX_SEC="${WATCH_MAX_SEC:-900}"
|
||||
WATCH_INTERVAL="${WATCH_INTERVAL:-15}"
|
||||
DEFAULT_MSG="Merge dev into main (deploy)"
|
||||
TRAILER="Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
|
||||
MSG="$DEFAULT_MSG"; NO_TEST=0; WATCH_ONLY=0; DRY=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-m|--message) MSG="${2:?-m 에 메시지 필요}"; shift 2 ;;
|
||||
--no-test) NO_TEST=1; shift ;;
|
||||
--watch-only) WATCH_ONLY=1; shift ;;
|
||||
--dry-run) DRY=1; shift ;;
|
||||
-h|--help) sed -n '2,9p' "$0"; exit 0 ;;
|
||||
*) echo "알 수 없는 옵션: $1" >&2; exit 64 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TMPDIRS=()
|
||||
cleanup() { for d in "${TMPDIRS[@]:-}"; do [ -n "$d" ] && git -C "$ROOT" worktree remove --force "$d" >/dev/null 2>&1 || true; done; git -C "$ROOT" worktree prune >/dev/null 2>&1 || true; }
|
||||
trap cleanup EXIT
|
||||
|
||||
kst() { TZ=Asia/Seoul date "$@"; }
|
||||
market_warning() {
|
||||
local dow hm
|
||||
dow="$(kst +%u)"; hm="$(kst +%H%M)"; hm=$((10#$hm))
|
||||
if [ "$dow" -le 5 ] && [ "$hm" -ge 830 ] && [ "$hm" -le 1540 ]; then
|
||||
echo "!! 장중 배포 경고 — KST $(kst '+%H:%M') (평일 08:30~15:40). 재기동이 매매에 영향(DT 포지션·스케줄러 상태·KIS 토큰 재발급)."
|
||||
else
|
||||
echo "배포 시각 KST $(kst '+%Y-%m-%d %H:%M') — 장외."
|
||||
fi
|
||||
}
|
||||
|
||||
http_code() { local c; c="$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "$1" 2>/dev/null || true)"; echo "${c:-000}"; }
|
||||
|
||||
watch_restart() {
|
||||
echo "== (e) 배포 감시: $WATCH_URL (비-200 → 200 재기동 감지, 최대 ${WATCH_MAX_SEC}s, ${WATCH_INTERVAL}s 간격) =="
|
||||
local start now code seen_down=0 first=1 elapsed
|
||||
start="$(date +%s)"
|
||||
while :; do
|
||||
now="$(date +%s)"; elapsed=$((now - start))
|
||||
code="$(http_code "$WATCH_URL")"
|
||||
printf '[%4ss] http=%s\n' "$elapsed" "$code"
|
||||
if [ "$code" != "200" ]; then
|
||||
[ "$seen_down" = 0 ] && echo " -> 다운 감지(재기동 중)"
|
||||
seen_down=1
|
||||
elif [ "$seen_down" = 1 ]; then
|
||||
echo " -> 재기동 완료: 200 복귀 (${elapsed}s)"; return 0
|
||||
elif [ "$first" = 1 ]; then
|
||||
echo " (현재 200 — 아직 옛 컨테이너일 수 있음. 빌드/재기동 대기)"
|
||||
fi
|
||||
first=0
|
||||
if [ "$elapsed" -ge "$WATCH_MAX_SEC" ]; then
|
||||
if [ "$seen_down" = 1 ]; then echo "!! ${WATCH_MAX_SEC}s 경과 — 여전히 비-200. Jenkins 로그/컨테이너 확인 필요."; return 2; fi
|
||||
echo "!! ${WATCH_MAX_SEC}s 동안 재기동 미감지(200 유지) — 빌드 지연·무중단 교체·변경 없음 가능. Jenkins 로그 확인."; return 1
|
||||
fi
|
||||
sleep "$WATCH_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$WATCH_ONLY" = 1 ]; then
|
||||
market_warning
|
||||
rc=0; watch_restart || rc=$?
|
||||
echo "== (f) 스모크 =="; "$SCRIPT_DIR/prod_smoke.sh" || rc=$?
|
||||
exit "$rc"
|
||||
fi
|
||||
|
||||
market_warning
|
||||
|
||||
# (a) 작업트리 확인
|
||||
BR="$(git rev-parse --abbrev-ref HEAD)"
|
||||
[ "$BR" = "dev" ] || { echo "중단: 현재 브랜치가 dev 가 아님($BR). 작업은 dev 에서만(branch-workflow-dev-only)." >&2; exit 1; }
|
||||
DIRTY="$(git status --porcelain | grep -v -E '^.. \.claude/settings\.local\.json$' || true)"
|
||||
if [ -n "$DIRTY" ]; then echo "중단: 미커밋 변경이 있다(.claude/settings.local.json 제외):" >&2; echo "$DIRTY" >&2; exit 1; fi
|
||||
HEAD_SHA="$(git rev-parse HEAD)"
|
||||
echo "== (a) 작업트리 깨끗 — dev @ ${HEAD_SHA:0:9} =="
|
||||
|
||||
git fetch origin main --quiet || { echo "중단: origin fetch 실패" >&2; exit 1; }
|
||||
if git diff --name-only origin/main..HEAD -- backend/alembic/versions | grep -q .; then
|
||||
echo "!! alembic 마이그레이션 포함: $(git diff --name-only origin/main..HEAD -- backend/alembic/versions | tr '\n' ' ')"
|
||||
echo " 배포 후 Jenkins 로그에서 'alembic upgrade head' 성공 여부를 반드시 확인(앱 기동보다 먼저 실행돼야 함)."
|
||||
fi
|
||||
|
||||
# (b) 깨끗한 worktree 에서 유닛 스위트
|
||||
if [ "$NO_TEST" = 1 ]; then
|
||||
echo "!! 경고: --no-test — 유닛 스위트 검증 없이 진행한다."
|
||||
else
|
||||
PY="$ROOT/.venv/bin/python"
|
||||
[ -x "$PY" ] || { echo "중단: $PY 없음(테스트 불가). 정말 건너뛰려면 --no-test." >&2; exit 1; }
|
||||
WT1="$(mktemp -d "${TMPDIR:-/tmp}/autostock-deploy-test.XXXXXX")"; rmdir "$WT1"
|
||||
git worktree add --detach "$WT1" "$HEAD_SHA" >/dev/null; TMPDIRS+=("$WT1")
|
||||
echo "== (b) HEAD 기준 유닛 스위트 (깨끗한 worktree) =="
|
||||
( cd "$WT1/backend" && DATABASE_URL="postgresql+asyncpg://x:x@localhost/autostock_test" \
|
||||
"$PY" -m pytest tests/ --ignore=tests/integration -q -x -p no:cacheprovider ) \
|
||||
|| { echo "중단: 유닛 스위트 실패 — 배포하지 않음." >&2; exit 1; }
|
||||
fi
|
||||
|
||||
if [ "$DRY" = 1 ]; then echo "== --dry-run: 푸시/병합 생략. 여기까지 통과 =="; exit 0; fi
|
||||
|
||||
# (c) dev 푸시
|
||||
echo "== (c) git push origin dev =="
|
||||
git push origin dev
|
||||
|
||||
# (d) 임시 worktree 에서 origin/main 에 --no-ff 병합 후 푸시 (작업 브랜치 불변)
|
||||
git fetch origin main --quiet
|
||||
WT2="$(mktemp -d "${TMPDIR:-/tmp}/autostock-deploy-merge.XXXXXX")"; rmdir "$WT2"
|
||||
git worktree add --detach "$WT2" origin/main >/dev/null; TMPDIRS+=("$WT2")
|
||||
echo "== (d) origin/main 에 dev(${HEAD_SHA:0:9}) --no-ff 병합 =="
|
||||
if git -C "$WT2" merge-base --is-ancestor "$HEAD_SHA" origin/main; then
|
||||
echo "main 이 이미 dev 를 포함 — 병합/푸시할 것 없음. 스모크만 수행."
|
||||
"$SCRIPT_DIR/prod_smoke.sh"; exit $?
|
||||
fi
|
||||
if ! git -C "$WT2" merge --no-ff "$HEAD_SHA" -m "$MSG" -m "$TRAILER"; then
|
||||
git -C "$WT2" merge --abort >/dev/null 2>&1 || true
|
||||
echo "중단: 병합 충돌. main 은 건드리지 않았다. dev 에 origin/main 을 먼저 병합해 해결하라." >&2; exit 1
|
||||
fi
|
||||
git -C "$WT2" push origin HEAD:main
|
||||
echo " main -> $(git -C "$WT2" rev-parse --short HEAD) 푸시 완료 (Jenkins 배포 트리거)"
|
||||
|
||||
# (e)(f)
|
||||
rc=0; watch_restart || rc=$?
|
||||
echo "== (f) 스모크 =="; "$SCRIPT_DIR/prod_smoke.sh" || rc=$?
|
||||
exit "$rc"
|
||||
28
.claude/scripts/prod_smoke.sh
Executable file
28
.claude/scripts/prod_smoke.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# 운영 공개 읽기 API 스모크 — GET 전용, API 키 불필요 엔드포인트만. KIS/주문 경로 호출 없음.
|
||||
# 사용: .claude/scripts/prod_smoke.sh (환경변수 PROD_BASE_URL 로 대상 변경 가능)
|
||||
set -u
|
||||
BASE="${PROD_BASE_URL:-https://stock.shium.net}"
|
||||
ENDPOINTS=(
|
||||
"/api/v1/market/indices"
|
||||
"/api/v1/journals/stats"
|
||||
"/api/v1/analysis/performance"
|
||||
)
|
||||
fail=0
|
||||
echo "== 운영 스모크 ($BASE) $(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S KST') =="
|
||||
for ep in "${ENDPOINTS[@]}"; do
|
||||
tmp="$(mktemp)"
|
||||
out="$(curl -s -o "$tmp" -w '%{http_code} %{time_total}' --max-time 15 "$BASE$ep" 2>/dev/null)" || out="000 0"
|
||||
code="${out%% *}"; secs="${out##* }"
|
||||
bytes="$(wc -c < "$tmp" | tr -d ' ')"
|
||||
valid="-"
|
||||
if command -v python3 >/dev/null 2>&1 && [ "$code" = "200" ]; then
|
||||
python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$tmp" 2>/dev/null && valid="json-ok" || valid="json-INVALID"
|
||||
fi
|
||||
if [ "$code" = "200" ] && [ "$valid" != "json-INVALID" ]; then mark="OK "; else mark="FAIL"; fail=1; fi
|
||||
printf '%s %s http=%s %ss %sB %s\n' "$mark" "$ep" "$code" "$secs" "$bytes" "$valid"
|
||||
[ "$code" != "200" ] && head -c 200 "$tmp" | sed 's/^/ /' && echo
|
||||
rm -f "$tmp"
|
||||
done
|
||||
[ "$fail" = 0 ] && echo "== 스모크 통과 ==" || echo "== 스모크 실패 — Jenkins 로그/컨테이너 상태 확인 =="
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user