하네스 규칙 검사 전역 위반 117→0, 회차별 검증(code-verifier opus 4도메인)으로 Critical/High 해소. 주문·리스크 - MDD 강제축소 반복 매도(종목별 완료 집합·재시도 상한·날짜 롤오버·잔고 오류 보류·epoch·장외 미주문) - KIS 거부 FAILED 기록, 결과 불명(전송 후) 재전송 금지·PENDING, 미전송 실패만 재시도(msg_cd 분류) - 국내 전량·손실 매도 시장가, 부분 익절 직전 현재가, 지정가만 미체결 취소(cancel_order_v2: TR 0013U·조직번호) + 0체결 롤백 - 누적 4%·현금·레버리지(신규분 포함)·사이클 내 매수 누적, 수동매수 공용 가드(buy_guards), 수동매도 DT 차감·락 - 가드 우회 엔드포인트 차단(/recommendations/auto-trade 위임, US /execute 매수 410·매도 가드) - 매수 슬롯=성공 매수, 평가 상한·회전, 업무거부/계좌 쿨다운, 프로세스 내 in-flight 주문 중복 차단 데이트레이딩 - 진입·청산 shield, 청산 진행 중 표식(소유 토큰), 2단계 청산 체크, 결과 불명 → 보유 감소 판정·비DT 차감 상한 - 전일 포지션 재시도(일시 실패 미산정), 분할매도 영속, ORB 구간 손절, /cycle 락, 사이클 내 현금 차감, 체결 수량 보정 스윙 - 손절 공백(판정 예외 시 하드 바닥), 단일 손절 소스·범위 검증, 매도 직전 KIS 잔고 상한(min(주문가능,보유)−DT) - 결과 불명 체결 대조·장부 반영, 대체가(나이·진입 이후·ATR 캐시), 분할 단계 기록, 국면 실패 fail-closed - 듀얼 플레이북·서킷브레이커 게이트, 평균회귀 우선·듀얼 독립, 시간손절 거래일 - 거래 확대(사용자 결정, 백테스트 미검증): RSI 46/35 정본 경로, 모멘텀 v2 ON, SIDEWAYS ADX AND, 트레일링 1차 익절 후, 20일 시간손절 ON 인프라·보안 - 계좌·손익 GET 인증, 세션 is_active·관리자 경로, 서명키 파생, realip, WS 토큰 마스킹 - KIS 토큰 즉시 실패(sleep 제거)·무효 시 캐시 삭제, 주문 5xx+JSON 본문 반환 - db_worker 종료 drain(멱등 재실행·커밋 불명 격리·예산·밴 커밋), stop_grace 40s, Redis 타임아웃·volatile-lru, /health DLQ·캐시 - KRX 휴장일, py3.11 비호환 f-string 수정 + Jenkins 이미지 내 compileall·헬스 실패 시 빌드 실패 하네스: rules_check 에 PEP 701 f-string 검사, 마커(allow-*), deploy 스모크 401 기준 CLAUDE.md: 실제 동작·플래그·백로그(§8.5-27~38) 갱신 backend 1451 passed / hooks 112 passed / tsc ok / py3.9 compile 0 errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
6.6 KiB
Bash
Executable File
140 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# auto-stock 배포: 작업트리 확인 → 깨끗한 worktree 유닛 스위트 → push dev → 임시 worktree 에서 main 병합·푸시
|
|
# → 재기동 감시(다운 → 기동) → 공개/인증 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}"
|
|
# DB 만 읽는 가벼운 엔드포인트. 2026-10-02(H-1)부터 키 없이 401 — 200(구버전)·401(신버전) 모두 "기동됨".
|
|
WATCH_URL="$BASE/api/v1/journals/stats"
|
|
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}"; }
|
|
# 앱이 요청을 처리하고 있는가: 200(공개/구버전) 또는 401(인증 필수 — 정상 거부)
|
|
is_up() { [ "$1" = "200" ] || [ "$1" = "401" ]; }
|
|
|
|
watch_restart() {
|
|
echo "== (e) 배포 감시: $WATCH_URL (다운 → 200/401 재기동 감지, 최대 ${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 ! is_up "$code"; then
|
|
[ "$seen_down" = 0 ] && echo " -> 다운 감지(재기동 중)"
|
|
seen_down=1
|
|
elif [ "$seen_down" = 1 ]; then
|
|
echo " -> 재기동 완료: http=$code 복귀 (${elapsed}s)"; return 0
|
|
elif [ "$first" = 1 ]; then
|
|
echo " (현재 http=$code — 아직 옛 컨테이너일 수 있음. 빌드/재기동 대기)"
|
|
fi
|
|
first=0
|
|
if [ "$elapsed" -ge "$WATCH_MAX_SEC" ]; then
|
|
if [ "$seen_down" = 1 ]; then echo "!! ${WATCH_MAX_SEC}s 경과 — 여전히 다운(200/401 아님). Jenkins 로그/컨테이너 확인 필요."; return 2; fi
|
|
echo "!! ${WATCH_MAX_SEC}s 동안 재기동 미감지(계속 기동 상태) — 빌드 지연·무중단 교체·변경 없음 가능. 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"
|