Skip to content

release: 아무도 밟지 않으면 영원히 멈추던 두 흐름 회수 (도메인 검증 · 배포 결과) - #163

Merged
dldnsgkr merged 4 commits into
mainfrom
develop
Aug 19, 2026
Merged

release: 아무도 밟지 않으면 영원히 멈추던 두 흐름 회수 (도메인 검증 · 배포 결과)#163
dldnsgkr merged 4 commits into
mainfrom
develop

Conversation

@dldnsgkr

Copy link
Copy Markdown
Collaborator

develop → main. #160 · #161 두 건이고 마이그레이션 변경은 없다.

두 건 다 같은 부류다 — 아무도 밟지 않으면 영원히 그 상태에 머무는 흐름에 밟아 줄 주체를 붙였다.

#160 도메인 VERIFYING 자동 검증

VERIFYING 에서 CONNECTED 로 가는 길은 checkVerification 호출 하나뿐인데 그것을 밟는 주체가 없었다. domainSummary.urlCONNECTED 일 때만 채워지므로, DNS 도 호스팅 설정도 끝나 주소를 직접 치면 열리는 도메인의 링크를 사용자가 영영 못 봤다.

DomainVerificationWorker 가 1분마다 검증한다.

#161 멈춘 배포 회수

워커가 PENDING 만 집으므로 결과 웹훅을 한 번 놓치면 그 이력은 영구히 IN_PROGRESS 에 멈춘다. 화면에는 "진행 중"으로도 "실패"로도 보이지 않는다.

StuckDeploymentRecoveryWorker 가 리스 없는 IN_PROGRESS 를 1분마다 GitHub 에 직접 물어 확정한다.

시간만 보고 닫지 않는다. 원래 사고가 "GitHub 은 성공했는데 우리 이력만 멈춘 것"이었으므로, 시간 기준으로 닫으면 멀쩡히 배포된 것을 실패라고 알리게 된다. 판정을 못 얻은 채 2시간을 넘긴 것만 닫고, 그때 사유도 "실패"가 아니라 "배포 결과를 확인할 수 없습니다"로 적는다.

결과 확정 로직은 DeploymentOutcomeService 로 모아 웹훅과 워커가 공유한다.

dev 실사용 검증 (2026-08-19)

FE 로컬 → BE dev 로 실제 흐름을 끝까지 돌렸다.

프로젝트 생성 → 에이전트 코드 생성 → 저장소 생성·연결 → 배포 3회
배포 8   LIVE  49초
배포 9   LIVE  29초
배포 10  LIVE  27초
사이트   https://dldnsgkr.github.io/worker-test-0819/  HTTP 200

부수적으로 FE 쪽 결함 두 개도 이 과정에서 갈라져 FE 가 수정했다(배포 완료 안내 폴링 교착, 재인증 진입점 부재). 백엔드는 메시지를 붙이고 있었고 화면이 읽지 않던 것이었다.

검증

  • 마이그레이션 없음
  • 단위 테스트 15개 추가(도메인 워커 6 · 서비스 3 · 배포 회수 6), ./gradlew test 전체 통과
  • CI 통과, dev 배포 성공(bbff40e, a3857a1)

배포 후 주의

두 워커가 운영에서 1분 주기로 돌기 시작한다. VERIFYING 도메인과 리스 없는 IN_PROGRESS 배포가 있으면 그만큼 Cloudflare·GitHub API 를 쓴다. 배치 상한은 각각 20이고 전부 환경변수로 조절할 수 있다.

관련: #162 (동시 요청 토큰 갱신 경합 — 워커가 늘어 창이 넓어졌다)

🤖 Generated with Claude Code

dldnsgkr and others added 4 commits August 19, 2026 11:23
VERIFYING 에서 CONNECTED 로 가는 길은 checkVerification 호출 하나뿐인데 그것을
밟는 주체가 없었다. 그래서 DNS 도 호스팅 설정도 끝난 도메인이 영원히 VERIFYING 에
머물고, domainSummary.url 은 status 가 CONNECTED 일 때만 채워지므로 사용자는
주소를 직접 치면 열리는 도메인의 링크를 화면에서 영영 못 봤다.

VERIFYING 도메인을 1분마다 검증하는 DomainVerificationWorker 를 둔다.

- 워커에는 요청한 사용자가 없다. checkVerificationAsSystem 이 도메인의 프로젝트에서
  소유자를 찾아 검증에 쓸 GitHub 토큰의 주인을 얻는다
- 인증서는 종료 조건이 아니다. 관리형 서브도메인은 Cloudflare 프록시 뒤에 있어
  GitHub Pages 가 인증서를 발급하지 못하고 certificateStatus 가 영원히 ACTIVE 가
  되지 않는다(#154). 연결 판정은 dnsConnected && domainConfigured 만 본다
- 포기 조건은 타입별로 다르다. 관리형은 우리가 Cloudflare 레코드를 직접 만들므로
  30분, 커스텀은 사용자가 자기 registrar 에서 CNAME 을 걸어야 하므로 24시간.
  넘기면 FAILED 로 닫는다 — 잘못된 CNAME 은 영원히 성공하지 않는다
- 한 도메인의 실패가 나머지를 막지 않는다. 배치 상한(기본 20)으로 주기당 외부 API
  호출 수를 묶는다
- 구매형은 검증 자체가 미지원이라 건너뛴다. 태우면 매 주기 예외만 남는다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
워커는 PENDING 만 집으므로 웹훅을 한 번 놓치면 그 이력은 영구히 IN_PROGRESS 에
멈춘다. 화면에는 그것이 "진행 중"으로도 "실패"로도 보이지 않는다 — 대화에는
"접수했습니다"가 마지막 말로 남고, FE 폴링은 붙을 메시지가 없어 조용히 끝난다.
사용자에게는 아무 일도 일어나지 않은 것처럼 보인다.

그렇다고 시간이 지났다고 FAILED 로 닫아서는 안 된다. 실제로 겪은 사고가 정확히
그 반대였다 — GitHub 쪽은 전부 성공하고 사이트도 200 으로 떴는데 우리 이력만
멈춰 있었다. 그것을 실패로 적으면 멀쩡히 배포된 것을 실패했다고 알리게 된다.

그래서 StuckDeploymentRecoveryWorker 는 상태를 추측하지 않고 GitHub 의 실행
결과를 읽어 그대로 반영한다.

- 대상은 리스 없이 IN_PROGRESS 인 이력이다. 워커가 붙들고 준비 중인 것은 리스를
  갖고 있고 그 만료는 recoverExpiredLeases 가 따로 회수한다. markDispatched 가
  리스를 비우므로, 리스 없는 IN_PROGRESS 는 GitHub 의 답만 기다리는 상태다
- 완료면 conclusion 그대로 확정한다. 아직 도는 중이면 건드리지 않는다
- 판정을 못 얻은 채 포기 시각(기본 2시간)을 넘긴 것만 닫고, 사유를 "실패"가 아니라
  "결과를 확인할 수 없습니다"로 적는다
- runId 가 비어 있는 이력은 correlationId 로 다시 찾는다. finishDispatch 는
  실행을 못 찾아도 IN_PROGRESS 로 넘기기 때문이다

결과 확정 로직은 DeploymentOutcomeService 로 옮겨 웹훅과 워커가 공유한다. 각자
자기 버전을 갖고 있으면 한쪽만 고쳐져 배포는 성공했는데 대화에는 아무 말도 안
남는 식으로 갈라진다. 웹훅 테스트는 이 서비스의 실제 인스턴스를 끼워 넣어, 수신부터
대화 알림까지 한 경로를 그대로 검증하게 뒀다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(domain): VERIFYING 도메인을 아무도 검증하지 않아 영원히 멈추던 문제
fix(deploy): 결과 웹훅을 놓쳐 IN_PROGRESS 에 멈춘 배포를 회수하도록
@dldnsgkr
dldnsgkr merged commit d2af993 into main Aug 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant