Deploy Afresto Next
스택: backend Go (push→GHCR→Watchtower ~5분) · web React (Vite→Cloudflare Worker, 수동 wrangler deploy) · mobile RN/Expo, 별도 저장소 <repo-mobile> (EAS OTA). 프로덕션 배포는 항상 사용자 허가를 먼저 받아야 합니다 (매번 사용자가 승인).
🔴 규칙 #1 — 이 저장소는 여러 AGENT TAB이 동시에 사용함
git status에 다른 세션의 수정된 파일이 자주 표시됩니다. 따라서:
git add -a/git commit -a/git add .하지 마세요 — 다른 세션의 작업 중인 파일까지 커밋에 포함됩니다.- 선택적으로(stage selektif) 자신의 파일만 추가한 후, 확인:
git diff --cached --name-only— 다른 세션의 파일이 없는지 확인 (특히web/src/pages/ChatPage.tsx,backend/internal/db/dbgen/models.go가 당신이 재생성한 것이 아니라면 자주 섞입니다). - 두 세션이 공유하는 파일(예: ExamPage)에서 당신의 hunk만 원한다면:
stash/reset하지 마세요 (다른 에이전트의 작업을 빼앗게 됩니다). 대신 Git 인덱스를 blob으로 편집하세요:git show :path > tmp→tmp에서 그들의 hunk를 제거 →SHA=$(git hash-object -w tmp)→git update-index --cacheinfo 100644,$SHA,path. 디스크의 파일은 변경되지 않습니다. - 🚨 Bash의
cwd도구가 이전 호출의cd .../web(또는 worktree)에서 유지될 수 있습니다. 그러면git add backend/...가pathspec did not match오류를 내거나 잘못된 트리를 스테이징합니다. 선택적 스테이징 전에 **cd <repo-root> &&**를 먼저 실행하세요 (또는 절대 경로 사용).git diff --cached --name-only가 올바른 루트 상대 경로(../backend,src/...가 아닌)를 표시하는지 확인하세요. - 배포 후:
git status를 확인하여 다른 세션의 작업 파일이 그대로 있는지 확인하세요.
Go 품질 게이트 (backend 커밋 전)
Laravel의 Pint + PHPStan에 해당하는 Go 버전입니다. backend 파일을 스테이징하기 전에 로컬에서 실행하세요:
- 포맷:
cd backend && gofmt -l .→ 비어 있어야 함. 내용이 있으면 →gofmt -w .(자동 수정), 결과를 스테이징하세요. (pint --test에 해당) - 빌드 + vet:
go build ./... && go vet ./...가 통과해야 함. - 정적 분석 (목표):
golangci-lint run ./...(gofmt/govet/staticcheck/errcheck/ineffassign/unused/misspell 포함).phpstan analyse에 해당. - ⚠️ 상태: CI 게이트 아직 활성화되지 않음. 아직
backend/.golangci.yml또는.github/workflows/ci.yml이 없으며,staticcheck도 구버전 → 따라서 포맷+빌드+vet은 수동으로 진행. CI에서 강제 적용 및 전체 설정 계획:docs/plan/00-tooling-quality-gate.md. 생성된 코드internal/db/dbgen은 lint에서 제외. - 변경 사항에 테스트 포함 → afresto-testing 스킬 (
go test ./...;TEST_DATABASE_URL없으면 integration은 건너뜀).
🔴 규칙 #00 — 의미 있는 작업은 먼저 GitHub Issue로 등록 (2026년 9월 5일부터)
규칙 #0 (브랜치 + PR) 이전에 적용: 코드를 전혀 건드리기 전에.
목적은 형식적이지 않습니다. 대화로 들어온 요청은 두 당사자만 알게 됩니다 — 팀은 읽을 수 없고, 소유자는 할당할 곳이 없으며, 모듈은 누가 무엇을 하고 있는지 흔적을 남기지 않습니다.
Issue가 필수인 경우
새 기능 · 학교에서 볼 수 있는 변경 · DB 스키마 변경 · 제품 결정이 필요한 모든 것 · 두 개 이상의 PR이 필요한 작업.
Issue가 필요 없는 경우
명확하고 좁은 버그 · 텍스트/레이블 수정 · 문서 · PR 리뷰 코멘트의 직접적인 후속 조치.
의심되면 → issue를 만드세요. 저렴하고, 닫을 수 있습니다. 이 기준이 규칙을 살아있게 합니다: 모든 사소한 요청이 issue가 되면, 사람들은 조용히 이를 무시하기 시작하고, 한 번 무시해도 결과가 없으면 규칙은 죽습니다.
에이전트가 할 일
- 코드를 충분히 탐색하여 "초기 발견"을 채우고 blocker를 찾으세요 — 추측하지 마세요.
.github/ISSUE_TEMPLATE/pekerjaan.md템플릿을 사용해 issue를 만들고,modul:*레이블을 붙이세요 (모듈별 기록을 형성합니다). 결정이 필요한 사항이 있으면butuh-keputusan도 추가하세요.- 멈추세요. 코딩하지 말고, 브랜치를 만들지 말고, 자신을 할당하지 마세요. 할당은 소유자의 행동입니다. 팀이 피드백을 제공하거나 아이디어를 수정할 공간도 필요합니다.
- 소유자에게 issue 번호와 함께 결정이 필요한 사항(있는 경우)을 보고하세요.
🔑 소유자는 "지금 작업"이라고 말함으로써 이것을 건너뛸 수 있습니다 — issue는 계속 기록으로 만들고, 작업은 진행됩니다. 이 규칙은 기본값을 규정할 뿐, 가두려는 것이 아닙니다.
작업 중간에 새로 발견된 Blocker
계속하기 전에 알리세요: issue에 코멘트 (팀도 볼 수 있도록) 그리고 소유자에게 전달하세요.
결정이 작업 중단으로 이어지면 → butuh-keputusan 레이블을 추가하세요.
자신의 해석을 조용히 선택하고 계속 진행하지 마세요 — 그렇게 하면 "완료"되었지만 잘못된 작업이 탄생합니다.
고리 닫기
PR 본문에 Closes #<번호> 를 반드시 포함하세요. 이것이 issue → PR → 사람을 연결하여,
gh issue list --label modul:ujian --state all이 그 모듈에서 누가 무엇을 하고 있는지 기록하게 됩니다.
이 줄이 없으면 흔적이 끊깁니다.
🔴 규칙 #0 — main에 직접 commit/push하지 마세요 (4인 팀, 2026년 7월 25일부터)
모든 작업은 브랜치 + Pull Request를 통해; 리뷰어만 main에 병합합니다 (병합 = 배포 트리거).
- 시작 전:
git checkout main && git pull— 새 마이그레이션이 있으면 먼저 실행하세요. - 코드를 건드리기 전에 주제별 브랜치를 만들거나 체크아웃하세요:
- 기능 →
feat/<이름>· 버그 →bugfix/<이름>· issue가 있는 버그 → 새 브랜치 + issue 학습.
- 기능 →
- 선택적 커밋 →
git push -u origin <브랜치>(main이 아닌). main으로 PR 열기:gh로그인 시gh pr create; 아니면 사용자에게 URL 제공https://github.com/Afresto-Next/next/compare/main...<브랜치>?expand=1.- 리뷰어 승인 & 병합 → 배포. 직접 PR 병합 금지.
- 아래 "Push backend / Deploy WEB" 부분은 PR이
main에 병합된 후 실행되는 메커니즘입니다 (보통 배포 담당자가 수행),main에 직접 push 허용이 아닙니다. - 스킬 변경도 PR을 통해 (CODEOWNERS).
실행 순서
1. 커밋 (Bash 도구 = Git Bash / sh, PowerShell 아님)
- 여러 줄 메시지: heredoc
git commit -q -F - <<'EOF' … EOF.@'…'@를 사용하지 마세요 (PowerShell here-string으로, 제목이@ ...로 오염됨). - 메시지 끝:
Co-Authored-By: Claude Opus 4.8 <[email protected]>. - "CRLF will be replaced by LF" 경고는 정상, 무시하세요.
- 당신의 작업 브랜치 (
feat/*·bugfix/*)에 커밋,main이 아님 (규칙 #0 참조). 사용자가 요청할 때 커밋.
🔴 git add -A / git add <dir>/ 하지 마세요 — 이미 3번 사용자 파일을 휩쓸었습니다
이 저장소에는 추적되지 않은 많은 사용자 작업 파일이 있습니다: docs/bskap/ (이미지+PDF),
docs/analisis butir soal/, elibrary/, cloudflare/**/.wrangler/ (계정 캐시). 한 번 -A로 모든 것이 포함됩니다.
실제 사례: git add docs/가 BSKAP PDF/xls/이미지를 포함 (7월 25일 & 8월 2일); git add -A가
446개 파일을 포함, wrangler-account.json도 포함 (8월 9일).
git add path/ke/berkas1 path/ke/berkas2 # 항상 하나씩
git commit -q -F - <<'EOF'
...
EOF
git show --name-only --format="" HEAD | wc -l # 의도한 것과 일치하는지 확인?
숫자가 맞지 않으면: git reset --soft HEAD~1 && git reset, 올바르게 다시 스테이징.
이미 자신의 브랜치에 푸시했다면, git push --force-with-lease는 안전합니다.
2. DB 마이그레이션 — TIP 커밋에 필수
.github/workflows/migrate.yml이 git diff --name-only HEAD~1 HEAD (마지막 커밋만)로 마이그레이션을 감지하며,
"Build & Push Docker Images" 성공 후 (workflow_run) 실행됩니다.
- 푸시에 마이그레이션이 포함된 경우 → 마이그레이션 반드시 TIP 커밋에 있어야 함 (마이그레이션+코드가 하나의 커밋에 있어도 안전).
- 마이그레이션은 빌드 후 self-hosted runner VM에서 자동으로 실행됩니다.
run --rm migrate동기화 → 오류 시 job이 심하게 실패 (조용하지 않음). - 마이그레이션 확인: 해당 커밋의 Actions → "Migrate DB" 녹색 탭 확인 (
ghCLI 사용 가능:gh run list --workflow="Migrate DB (self-hosted)" --limit 1— 에이전트가 직접 확인 가능, 사용자에게 요청할 필요 없음). Watchtower가 다음 폴링(~5분)에서api이미지를 교체하며, 거의 항상 마이그레이션 완료 후에 발생. - 마이그레이션 푸시 전, 로컬 DB(v102 = 프로덕션)에서 psql
BEGIN; … ROLLBACK;으로 SQL을 확인하세요.afresto-db-change스킬 / 엔지니어링 함정 §4 참조.
🔴 마이그레이션 번호: 브랜치 생성 시점이 아닌, 병합 직전에 다시 확인
팀원이 당신의 브랜치가 열려 있는 동안 다른 마이그레이션 시리즈를 병합할 수 있습니다.
중복 번호는 goose가 sortAndConnectMigrations에서 패닉을 일으킵니다 — SQL 오류가 아니므로 쉽게 잘못 읽힙니다.
✅ 데이터 손상 없음: 패닉은 마이그레이션 목록을 수집할 때 발생하며, 하나도 실행되기 전입니다.
실제 사례 (2026년 8월 9일): 브랜치 생성 시 00138 선택; 병합 시 팀이 이미 00138–00140을 사용 중.
첫 번째 수정은 00140으로 이름을 바꾸었지만 여전히 충돌 — 다시 추측했기 때문.
추측하지 말고 계산하세요:
git pull # 필수, 팀의 마이그레이션을 확인하기 위해
MAX=$(ls backend/migrations/ | sed 's/_.*//' | sort -n | tail -1)
NEXT=$(printf "%05d" $((10#$MAX + 1)))
ls backend/migrations/ | sed 's/_.*//' | sort | uniq -d # 반드시 비어 있어야 함
이미 잘못되었다면: git mv로 새 번호로 변경, 작은 PR, 병합, 그런 다음 Actions 탭에서
"Migrate DB (self-hosted)" 워크플로를 다시 실행하세요.
3. Backend 푸시 (PR이 main에 병합된 후 — 규칙 #0 참조)
PR 병합 → main 업데이트 → GHCR → Watchtower ~5분 후 이미지 교체. backend 변경 없음 = Watchtower 기다릴 필요 없음. (개발 중: git push -u origin <브랜치> → PR — main에 푸시하지 마세요.)
⚠️ backend 병합 후: 502 확인 (이미 6번 재발)
api/worker 컨테이너 재생성은 자주 전체 중단을 유발합니다 — 모든 컨테이터가 Up이지만
모든 경로가 502. 실질적으로 모든 backend 병합은 주사위 던지기입니다.
curl -s -o /dev/null -w "%{http_code}\n" --max-time 15 https://next.afresto.co/api/v1/auth/me
# 401 = 정상 (인증 필요) · 502 = 중단
502인 경우 → Actions → "Ops — pulihkan API" → restart-docker (SSH 없이, 1분 미만 복구).
새로 배포된 기능을 분석하지 마세요: 502 + 컨테이너 Up = Docker 데몬 재시작.
세부 사항: docs/ops-auto-pulih.md.
4. WEB 배포 — 깨끗한 WORKTREE에서 (여러 agent tab 때문에)
npm run build는 전체 working tree를 읽습니다 → 다른 세션의 작업 중인 파일도 함께 게시됩니다.
따라서 당신의 커밋에서 worktree로 빌드 및 배포:
rm -rf /c/wtd 2>/dev/null; git worktree add --detach /c/wtd <SHA-커밋>
cd /c/wtd && git status --short # 반드시 비어 있어야 함 (깨끗)
cd /c/wtd/web && npm ci && npm run build
cd /c/wtd/cloudflare/web-worker && npx wrangler deploy
cd <repo-projectTwo> && git worktree remove --force /c/wtd && git worktree prune
- 🔥 Worktree 경로는 반드시 짧아야 (
/c/wtd) — 긴 PDF 이름의 DKI 학사 일정 파일과 결합되어 Windows에서 "Filename too long" 오류 발생. - 필요한 경우 번들이 올바른지 확인:
grep -rl "<기능 표식>" /c/wtd/web/dist/assets/*.js— 하지만 거짓 양성 주의 (일반 표식은 다른 기능에서도 나올 수 있음). - Cloudflare는 자산을 ~20초 캐시 → Ctrl+F5로 테스트. (200이지만 크기가 ~397바이트면 fallback, 실제 자산 아님.)
5. Mobile OTA — 별도 저장소, --environment 필수
cd "<repo-mobile>"
npx eas update --branch preview --environment preview --message "…" --non-interactive
- 🔥
--environment필수 (preview/production) — 없으면 잘못된 환경 업데이트/실패. - 모바일 저장소 GitHub 리모트 있음 (
Afresto-Next/afresto-next-mobile) → 규칙 #0 적용: 브랜치 → PR → 병합,main에 직접 푸시 금지. - OTA는 JS/자산만 변경. 네이티브 모듈 변경 → OTA로 CRASH → 다시 빌드 필수.
🔥 eas update는 WORKING TREE를 보내며, 커밋이 아님
4단계의 npm run build와 정확히 동일 — 전체 작업 디렉터리가 번들에 포함되며,
다른 세션의 작업 중인 파일도 포함됩니다. 게시 결과의 Commit <hash>* 줄에서 * = 더티 트리.
게시 직전에 반드시 출력하고, 결과의 Commit 줄과 일치시켜 확인:
echo "다음에서 게시: $(git branch --show-current) @ $(git log --oneline -1 --format=%h)"
git log --oneline HEAD..origin/main # 비어 있음 = 다른 사람의 작업이 남아 있지 않음
main에서 게시하세요, 기능 브랜치가 아닌. 브랜치에서 게시하면, 이미 병합된 다른 사람의 작업이 채널에서 사라집니다 — OTA는 전체 번들을 교체하며, 패치하지 않습니다.- 프로덕션: 먼저 다른 세션의 커밋되지 않은 변경 사항을 치워두세요
(
git stash push -- <파일>…git stash pop). 프로덕션은 검토되지 않은 코드를 포함해서는 안 됩니다.
🚨 작업 중간에 브랜치가 변경될 수 있음 (여러 agent tab, 하나의 클론)
2026년 8월 20일 사건: preview 게시는 분명히 main @ d0938fa에서; 다음 명령어는
HEAD가 이미 다른 브랜치 @ 8126a50에 있음을 발견 — 같은 클론에서 다른 세션이 브랜치를 전환한 것.
프로덕션에서 3개 PR의 코드를 더 오래 실행했으며, 단 하나의 오류도 없이.
➡️ 브랜치가 명령어 사이에 동일하다고 가정하지 마세요. 매번 다시 출력하세요 (위 명령어). ➡️ 진정한 예방: OTA를 WORKTREE에서 수행, 4단계의 웹 배포와 동일하게 — 다른 세션이 브랜치를 변경할 수 없습니다:
rm -rf /c/wtm 2>/dev/null; git worktree add --detach /c/wtm <SHA-커밋>
cd /c/wtm && git status --short # 반드시 비어 있어야 함
npm ci && npx eas update --branch <ch> --environment <env> --message "…" --non-interactive
cd <repo-mobile> && git worktree remove --force /c/wtm && git worktree prune
🪤 Worktree 경로 짧게 (/c/wtm) — 4단계와 같은 이유.
🪤 worktree에서 npm ci는 시간이 걸림; 작은 OTA의 경우 메인 클론에서 그대로 진행해도 되지만
위의 브랜치+커밋 확인을 실행하고 결과가 일치하는지 확인하세요.
🏢 GHCR 및 GitHub 조직 — 저장소가 이제 Afresto-Next 조직에 있음
저장소 (next, afresto-next-mobile, afresto-claude-skills)가 ristology/* → Afresto-Next 조직으로 이전됨 (2026년 7월 20일). 전체 전송 런북: docs/runbook-transfer-next-org.md.
- 🚨 저장소 조직 이전 = GHCR 경로가 소유자를 따라감 → Watchtower가 조용히 멈춤.
CI는 이미지에
ghcr.io/${GITHUB_REPOSITORY_OWNER}/afresto-next-{api,web,migrate}태그를 붙임 — 소유자는 저장소 소유자를 따릅니다. 설정이 변경되지 않으면 Watchtower는 이전 경로를 계속 폴링하다가 새 이미지를 찾지 못합니다. - 수정:
docker-compose.yml에서image:값을 업데이트하고,docker compose pull을 다시 실행하세요. 그런 다음 Watchtower를 재시작: (docker restart watchtower또는SIGHUP시그널). - 🚨
GITHUB_REPOSITORY_OWNER는 조직 이전 후에도 CI 내에서 자동으로 업데이트됩니다. 수동으로 수정할 필요 없음. 단, 이전 경로를 수동으로 덮어쓰지 않았는지 확인하세요. - 개인 저장소가 있는 경우 (
ristology/*) 동기화되지 않으며 작업이 게시되지 않습니다.
배포 확인 목록
- backend 배포 후 502 체크 수행
- 웹: worktree가 깨끗하고 빌드가 올바른 커밋에서 생성되었는지 확인
- mobile: 올바른 브랜치/커밋에서 게시,
--environment포함 - 마이그레이션: 번호가 고유하고 TIP 커밋에 포함되어 있는지 확인
- Git 작업: 선택적 스테이징, 변경 사항 확인, 다른 세션의 파일 손상 없음
- 모든 규칙 준수 (#00, #0, #1)