feat: 운영 승격 게이트 — project confirm(dev 검증 사인오프) 없으면 prod 차단 (v0.27.0)

- project confirm [vX.Y.Z]: 개발계 검증 사인오프를 .yakcloud.confirm 에 기록(로컬·git 미포함)
- promote --to prod: 확정 버전 일치 시에만 통과(val 자유). 불일치/없음 차단
- 스캐폴드: CLAUDE.md 운영 게이트 규칙 + 에이전트 임의 confirm 금지, .gitignore 에 .yakcloud.confirm
- SKILL.md: confirm 실행 전 확인 + 운영 게이트 경고

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-08-31 11:42:09 +09:00
parent 6d3118dd47
commit 9b66d32695
2 changed files with 70 additions and 5 deletions

View File

@ -22,10 +22,14 @@ Gitea Actions 가 이미지 빌드/푸시 + 콘솔 API 로 데이터소스 리
CLI 가 스스로 전제조건을 검사하고 오류를 안내하므로, 실패하면 그 출력을 그대로 전달하고 필요한 만큼만 후속 조치.
**안전장치 — 아래 "되돌리기 어렵거나 외부로 나가는" 서브커맨드만 실행 전 1줄로 확인**하고 승인 시 실행:
`project deploy` · `project promote` · `domain` · `cluster create` · `dev down` · `source add` · `datasource migrate apply`
`project deploy` · `project confirm` · `project promote` · `domain` · `cluster create` · `dev down` · `source add` · `datasource migrate apply`
(그 외 `project init/info/check` · `dev up/deploy/status` · `datasource ls`/`capture` · `cluster ls` · `config`/`login`
읽기·로컬·멱등 명령은 확인 없이 즉시 실행.)
**⚠ 운영 승격 게이트 — `project confirm` 은 사람이 dev 검증을 마쳤다는 사인오프다. 에이전트가 임의로 실행하지 마라.**
`project promote --to prod <ver>` 는 그 버전의 `project confirm` 이 선행돼야 통과한다(val 은 자유). 사람의 명시적 검증 확인이 없으면
confirm/prod-promote 를 실행하지 말고, "dev 에서 확인 후 confirm 하라"고 안내만 한다.
## CLI (권장)
설치: `curl -fsSL https://gitea.yakenator.io/yakenator/yakcloud-starter/raw/branch/main/install.sh | sh`
(전역 `/yakcloud` 슬래시 명령도 설치됨. `yakcloud upgrade` 로 최신화.) 자격 저장 = `yakcloud login`.

View File

@ -16,7 +16,7 @@ set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
VERSION="0.26.0"
VERSION="0.27.0"
# 로컬 개발 클러스터(kind) — 개발 후 운영까지 '직선 배포'의 dev 구간.
DEV_CTX="${YAK_DEV_CONTEXT:-kind-yak-dev}"
DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-dev}"
@ -167,13 +167,16 @@ _scaffold_yakcloud_guardrails() {
- **`docker compose` / `docker-compose` / `docker run` 으로 이 앱을 로컬 실행·검증하지 마라.** (`.claude/settings.json` 이 차단)
- DB·캐시·큐 등 **데이터소스 컨테이너를 손으로 띄우지 마라** — `yakcloud.yaml` 의 `requires` 로 선언한다.
- `Dockerfile` 은 **yakcloud 의 이미지 빌드 전용**. 로컬 구동 도구가 아니다.
- **사람의 명시적 dev 검증 확인 없이 `yakcloud project confirm` / `promote --to prod` 를 실행하지 마라.** 운영 승격은 사람이 개발계에서 검증한 뒤에만.
## 일상 개발 루프 (ALWAYS)
1. 코드를 고친다.
2. **`yakcloud project deploy`** — 로컬 kind 클러스터에 배포(없으면 자동 생성, 오프라인·무료).
관리형과 **동일한 바인딩 env·데이터소스** 로 뜬다.
3. 로컬 도메인 `*.localhost` 로 화면·API·WSS 확인. 반복 개발 = 2번 반복.
4. 확인되면 **`yakcloud project promote --to val|prod`** (같은 이미지, 재빌드 없이 관리형 승격).
4. (검증계 선택) `yakcloud project promote --to val`.
5. **운영 승격에는 게이트가 있다** — 사람이 dev 에서 검증한 뒤 `yakcloud project confirm <ver>` 로 확정해야
`yakcloud project promote --to prod <ver>` 가 통과한다(확정은 그 버전에 묶임). 에이전트가 임의로 confirm 하지 말 것.
## 데이터소스
- `yakcloud.yaml` 의 `requires` 에 `{ name, type }` 선언 → 워크로드 `binds` 로 연결 → `<ALIAS>_URL` 등 자동 주입.
@ -202,6 +205,9 @@ YCLAUDE
}
YSETTINGS
fi
# dev 검증 확정 기록은 로컬·머신단위(git 미포함) — .gitignore 에 보장.
touch .gitignore
grep -qxF ".yakcloud.confirm" .gitignore 2>/dev/null || printf '\n# yakcloud dev 검증 확정(로컬 전용, 운영 승격 게이트)\n.yakcloud.confirm\n' >> .gitignore
}
cmd_project_init() {
@ -384,6 +390,57 @@ cmd_project_check() {
# 승격 — 개발(로컬)에서 검증한 것을 **관리형** 검증/운영 클러스터로. 로컬 이미지는 레지스트리에 없으므로
# 태그 이미지를 build+push(이미 있으면 스킵=운영 재빌드 방지) 후 리컨실 + 도메인 부착.
# 운영 승격 게이트(Phase 1, CLI) — dev 검증 확정(.yakcloud.confirm)이 승격 버전과 일치해야 prod 통과.
# 확정은 정확히 그 버전(태그)에 묶인다 — 새 태그를 올리면 재검증·재확정 필요. (Phase 2: 백엔드 콘솔 휴먼 승인 추가)
_require_dev_confirm() {
local want="$1" have=""
[ -f .yakcloud.confirm ] && have="$(python3 -c 'import json;print((json.load(open(".yakcloud.confirm")) or {}).get("version",""))' 2>/dev/null || true)"
[ "$have" = "$want" ] && return 0
die "운영(prod) 승격 차단 — '$want' 의 개발계 검증 확정이 없습니다(현재 확정: ${have:-<없음>}).
1) 개발계 검증: yakcloud project deploy → 로컬(*.localhost)에서 확인
2) 검증 확정: yakcloud project confirm $want
그다음에 다시: yakcloud project promote --to prod $want
(확정은 그 버전에 묶입니다. 코드가 바뀌어 새 태그를 올리면 재검증·재확정해야 합니다.)"
}
# 개발계 검증 사인오프 — .yakcloud.confirm 에 '이 버전을 dev 에서 검증함'을 기록(로컬·머신단위, git 미포함).
# 운영 승격(promote --to prod)이 이 기록을 요구한다. 사람이 검증한 뒤 실행할 것(에이전트 임의 실행 금지 — CLAUDE.md).
cmd_project_confirm() {
[ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저"
ensure_pyyaml
local tag="" yes=0 note=""
while [ "$#" -gt 0 ]; do case "$1" in
-y|--yes) yes=1; shift ;;
--note) note="${2:-}"; shift 2 || shift ;;
-*) shift ;;
*) [ -z "$tag" ] && tag="$1"; shift ;;
esac; done
if [ -z "$tag" ]; then
git fetch -q --tags origin 2>/dev/null || true
tag="$(git tag -l 'v*' --sort=-v:refname 2>/dev/null | head -1)"
[ -n "$tag" ] || die "확정할 버전을 지정하세요 — 'yakcloud project confirm vX.Y.Z' (project deploy 로 개발계에 올린 그 버전)"
fi
case "$tag" in v*) : ;; *) tag="v$tag" ;; esac
# 최소 증빙: 로컬 dev 클러스터가 있어야(= 개발계 배포·검증을 실제로 거쳤음). 없으면 확정 불가.
local devname; devname="$(_manifest_env dev cluster)"; [ -n "$devname" ] || devname="dev"
_dev_cluster_exists "$devname" || die "개발계('$devname') 로컬 클러스터가 없습니다 — 먼저 'yakcloud project deploy' 로 배포·검증 후 confirm."
if [ "$yes" != 1 ]; then
echo "개발계('$devname')에서 이 앱을 실제로 검증했습니까? 이 확정은 '$tag' 의 운영(prod) 승격을 허용합니다."
echo " (로컬 확인 호스트는 'yakcloud project info' 참고 — *.localhost)"
printf "검증 완료를 확정합니까? [y/N]: "; read -r a
case "$a" in y|Y|yes|YES) : ;; *) die "확정 취소 — 운영 승격은 계속 차단됩니다." ;; esac
fi
YC_TAG="$tag" YC_DEV="$devname" YC_NOTE="$note" python3 - > .yakcloud.confirm <<'PY'
import json,os,datetime,yaml
m=yaml.safe_load(open("yakcloud.yaml")) or {}
rec={"project":m.get("project",""),"version":os.environ["YC_TAG"],"cluster":os.environ["YC_DEV"],
"confirmedAt":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"host":os.uname().nodename,"note":os.environ.get("YC_NOTE","")}
print(json.dumps(rec,ensure_ascii=False,indent=2))
PY
echo "✓ 개발계 검증 확정: $tag. 이제 'yakcloud project promote --to prod $tag' 가능(그 버전 한정)."
}
cmd_project_promote() {
[ -f yakcloud.yaml ] || die "'yakcloud project init' 먼저"
: "${YAKCLOUD_URL:?YAKCLOUD_URL 필요 — 'yakcloud login'}"; : "${YAKCLOUD_TOKEN:?YAKCLOUD_TOKEN 필요 — 'yakcloud login'}"
@ -403,6 +460,8 @@ cmd_project_promote() {
[ -n "$tag" ] || tag="v0.1.0"
fi
case "$tag" in v*) : ;; *) tag="v$tag" ;; esac
# 운영 승격 게이트 — prod 는 개발계 검증 확정(그 버전)이 선행돼야 한다(val 은 자유).
[ "$env" = prod ] && _require_dev_confirm "$tag"
info "'$env' 승격: $tag → 관리형 '$env' 클러스터(이미지 build+push 후 리컨실; 이미 있으면 재빌드 없이 그 이미지)"
_build_and_push "$tag"
YAKCLOUD_ENV="$env" TAG="$tag" python3 "$(_engine deploy)" yakcloud.yaml
@ -832,7 +891,8 @@ yakcloud $VERSION — YakCloud 프로젝트 CLI (개발·검증·운영 모두
dev down [--name N] 로컬 kind 클러스터 삭제(여럿이면 --name 필수). 대상은 콘솔 등록명이 아니라 'dev ls' 의 --name
project init [name] 빈 폴더에 스타터 스캐폴딩(대화형 마법사) + 자격 프롬프트
project deploy [tag] **개발 배포 = 로컬 kind**(environments.dev). 로컬 클러스터 없으면 자동 생성 후 배포(오프라인·무료·CI 불필요)
project promote [vX.Y.Z] --to val|prod **관리형** 검증/운영으로 승격 — 이미지 build+push(이미 있으면 재빌드 없이) 후 리컨실. 기본 --to prod
project confirm [vX.Y.Z] 개발계 검증 확정(사인오프) — **운영 승격의 선행 게이트**. 그 버전 한정, 사람이 검증 후 실행
project promote [vX.Y.Z] --to val|prod **관리형** 검증/운영으로 승격 — 이미지 build+push(이미 있으면 재빌드 없이) 후 리컨실. 기본 --to prod. **prod 는 project confirm 필요**
project info 프로젝트 개괄(매니페스트 + 라이브 상태)
project check [--env dev|val|prod] dry-run(직접 API 계획만)
project update 이 프로젝트의 엔진·CI·스킬만 최신화(앱·매니페스트 보존)
@ -861,11 +921,12 @@ case "${1:-help}" in
case "${1:-}" in
init) shift; cmd_project_init "$@" ;;
deploy) shift; cmd_project_deploy "$@" ;;
confirm) shift; cmd_project_confirm "$@" ;;
promote) shift; cmd_project_promote "$@" ;;
info) shift; ctl info "$@" ;;
check|dry-run) shift; cmd_project_check "$@" ;;
update) shift; cmd_project_update "$@" ;;
*) echo "yakcloud project <init|deploy|promote|info|check|update>"; exit 1 ;;
*) echo "yakcloud project <init|deploy|confirm|promote|info|check|update>"; exit 1 ;;
esac ;;
cluster) shift; cmd_cluster "$@" ;;
dev) shift; cmd_dev "$@" ;;