v0.17.9 승격 — CI 실패 수리와 운영 반영 기록 (2026-09-10)
- 기간: 2026-09-10, Codex 1세션. 오너 요청: “17.9 릴리스 브랜치 staging->main 으로 병합 절차 밟아줄래 이슈 새로 만들어서”. 이후 선행 #1509 인계·실패 해결 승인.
- 추적: 앱 #1511, release→staging #1509.
- 반영: 수리 #1512·1514·1517·1519·1524·1526·1527은 release
da6fccd2까지 병합. 최종 staging #15286b678da4의 full·배포·smoke와 main 최종 full 성공 후 main #1513을f7195e8c로 병합했다. 이번 릴리스는 신규 SQL migration·Edge 소스 변경 없음. - 설계서: 없음(승격 및 수리 건). Phase 계획은 #1511 댓글.
- 정본: 릴리스 절차, 앱 calendarReadModelStore.ts와 policy-contract.yml.
- 도구: workspace work/의 실패 artifact·로그와 gitignored 집중 실행 스크립트. 기존 ci-local runner에 해당 CASE 필터를 붙여 전용 로컬 DB·준비·정리를 사용했다. 앱 실행 스크립트로 커밋하지 않았다.
- 게이트: npm run check, clean commit의 ci:precheck-local, release Merge Check, 최종 후보 hosted full, staging 및 Production 배포 증거.
- 버그리포트: BUG-096, BUG-097, BUG-098, BUG-099, BUG-100, BUG-101, BUG-102, BUG-103, BUG-104.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 이슈 생성·선행 상태 확인 | 완료 |
| Phase 2 | 확인된 CI 실패 수리·회귀·사전검증 | 완료, 수리 49d54ad4·release a7436af1 |
| Phase 3-1 | main full에서 확인된 리포트 조회 취소 수리 | 완료, 수리 f6b201b8·release 2b6e1725 |
| Phase 3-2 | CASE-040 재진입 경계 수리 | 완료, 수리81ccab0a·release ca411faf |
| Phase 3-3 | JWT 거절 이력·CASE-045 저장 응답 경계 | 완료, 수리6188ad2e·release31aa1fa9 |
| Phase 3-4 | CASE-052 피드 재조회·재진입 경계 | 완료, 수리df114232·release6dceeacc |
| Phase 3-5 | CASE-039 실제 통계 게시 후 옛 번들 재진입 | 완료, 수리c38e6bf3·release1b911c5b |
| Phase 3-6 | CASE-009 부팅 요청 완료 후 문서 교체 | 완료, 수리092d4661·release da6fccd2 |
| Phase 3 | staging→main 승격·환경별 배포·기록 | 완료. 병합·Production 배포·smoke·태그 성공; 별도 브라우저 사후검사 1 pass/14 fail 기록 |
1. 배경
v0.17.9는 release에 준비됐지만 #1509의 전체 CI가 실패하여 staging에 반영되지 않았다. 당시 staging은 main보다 한 커밋 뒤였고, main에 추가할 staging 커밋이 없었다. 인계 승인 후 같은 릴리스의 최소 수리부터 진행했다.
2. 문제 제기
최초 full 실행에서 CASE-047의 달력 조회 취소와 집계기의 typescript 누락이 각각 관측됐다. 요청 취소는 두 번째 저장 뒤 첫 문서에서 발생했으며 늦은 상세 응답 시험·새로고침 이전이었다. 운영 사용자 장애로 확대 단정하지 않았다.
3. 해결 방안
같은 날짜의 강제 갱신은 기존 읽기를 실제 종료까지 추적하고 요청 세대로 오래된 반환·발행을 막는다. 다른 날짜 선택·오너 변경·종료에서는 겹친 모든 읽기를 취소한다. 집계 잡은 기존 공통 npm 설치 action으로 의존성을 준비한다. 오류 무시·CASE 제외·timeout 증가·무조건 재시도는 적용하지 않았다.
4. 적용한 내용
Phase 2 — 선택 조회 생명주기와 집계 실행 의존성
앱 store의 요청 관리와 단위 회귀 2건을 추가했고, 기존 코드 표현을 고정하던 검사 2곳을 새로운 다중 요청 관리에 맞췄다. 집계 workflow에는 설치 단계 하나를 추가했다. CASE-047·필수 목록·오류 감시·checkpoint·정리 단언은 유지했다.
작업 중 드러난 것
수정 전 CASE-047 로컬 단독 실행은 통과했다. 겹침을 통제한 단위 재현은 수정 전 취소 신호 때문에 실패했고 수정 후 통과했다. 첫 전체 check에서 이전 selectedAbort 표현을 요구한 검사 2건이 실패하여 이를 맞췄다. 최초 실패를 뒤의 성공으로 덮어쓰지 않는다.
Phase 3-1 — main full에서 확인된 리포트 요청 취소
CASE-029의 재진입 문서에서 홈 갱신 응답 직후 리포트 요청이 취소됐다. 같은 계정의 applyRemoteData가 진행 중 리포트 읽기를 무조건 취소한 것이 원인이다. 이전 요청은 종료까지 추적하고 데이터 세대로 오래된 응답·오류를 버리도록 수리했다. 인증 변경 때는 종료되지 않은 이전 요청도 모두 취소한다. 실제 React controller 브라우저 회귀 2건은 수리 전 실패·수리 후 통과했으며 관련 6건 모두 통과했다. CASE 시나리오·오류 감시·재시도 수는 유지했다.
사전 검증 누락: 첫 검증에 홈 갱신과 리포트 읽기의 겹침을 고정한 회귀가 없었다. 첫 CASE-029 집중 실행은 동작 1건이 통과했지만 실행 도중 커밋한 작업 실수로 진단 SHA와 최종 report SHA가 달라 증거 검사가 실패했다. f6b201b8을 고정한 재검증은 첫 시도 1 pass·0 fail/skip/flaky, 증거·정리 성공(준비 포함4분17초)했다. 첫 실행을 유효한 통과 증거로 사용하지 않는다.
Phase 3-2 — CASE-040의 문서 재진입 경계
추가 수리 후보의 staging full에서 CASE-040만 실패했다. 옛 버전 탭 A의 첫 문서에서 달력 조회가 시작된 직후 새로고침이 겹쳤고 3ms 뒤 취소됐다. 두 RPC만 기다리던 테스트 준비를 기존 문서 전체 요청 관측 helper로 보완했다. 세 번의 새로고침과 정상 탭 닫기 경계, 한 파일6줄 변경이며 앱·DB·Edge 코드는 변경하지 않는다. 사용자 수정·후속 행 보존·서버 개정번호3와90kg 확인은 실패 실행에서도 통과했다. 사전 검증 누락: 특정 RPC의 순간적 완료를 연쇄 조회 전체의 완료로 취급했다.
Phase 3-3 — 추가 토큰 갱신과 필수 오류 수집 응답
다음 staging full34446524759는 CASE-013의 사유 변경 누락과 CASE-045의 미인정 저장 관측으로 실패했다. CASE-029·040·047은 통과했다. 같은 계정 TOKEN_REFRESHED가 연속401 횟수를 지우는 분기는 제어한 회귀로 재현했고, 실제 복구·계정 변경까지 횟수를 유지하도록 고쳤다. hosted SDK 이벤트의 정확한 순서와 이 분기의 연결은 추정으로 구분한다.
CASE-045는 오류 수집 HTTP200 종료 약6ms 전에 새로고침이 시작됐다. 필수 P0002 저장 응답 본문을 읽은 뒤 새로고침하도록 CASE를 수정했다. 초기 로컬 후보0658c64a의 수집 억제는 필수 관측 누락으로 실패했으며 원격 반영 전에 모두 되돌렸다. 원인으로 확인되지 않은 저장본 대체 분류 변경도 폐기했다. 최종6188ad2e는 guard·단위 회귀·CASE-045 세 파일만 변경한다. 앱 오류 수집·DB 데이터·manifest 관측을 유지한다. BUG-100, BUG-101.
main 승격의 full 재실행 판정
staging 배포와 main 후보 tree dae0856a6c7af414aa55fedc0dd9ca776ffa5abb의 일치는 확인됐다. full 성공 check-run 102760013981도 존재한다. 다만 원 full 실행의 현재 GitHub API pull_requests가 빈 배열이라 기존 validRun의 PR 출처 조건을 만족하지 못해 full 경로로 돌아갔다. API가 빈 배열을 반환한 이유까지 확정하지 않았다. 재사용 성공이나 수동 재실행으로 보고하지 않으며, 검사기 변경을 이번 승격에 추가하지 않았다.
Phase 3-4 — CASE-052 피드 재조회와 재진입
두 번째 main 후보 full34450767470에서 CASE-052가 실패했다. 좋아요·댓글·관계의 실패 복구, DB·API 결과는 맞았으나 팔로우 재연결 뒤 피드 조회가 시작된 지84ms 후 테스트의 새로고침에 취소됐다. 새 문서 시작과 취소는1ms 이내로 겹쳤다. 기존 c.waitForObservedRequests(page)를 재진입 직전에 한 줄 추가했다. 앱·DB·Edge·오류 관측을 변경하지 않는다. BUG-102. 사전 검증 누락: 관계 결과 확인을 브라우저 후속 조회 종료로 취급했다.
고정df114232의 local-1789026448509-103528은 CASE-052 첫시도1 pass/0 fail/skip/flaky·증거·정리 성공(2분59초), check3391 pass/0 fail/35 조건부skip을 확인했다. 부분 실행이며 전체 CI 통과를 뜻하지 않는다.
Phase 3-5 — 통계 게시 후 옛 번들 재진입
CASE-052 수리 후보의 staging full34452460088에서는 CASE-039만 실패했다. 새로고침 뒤 옛 v0.17.1 번들의 통계 복구가 home을 재적용하면서 진행 중 volume 읽기를 취소했다. 불변 옛 번들을 바꾸지 않고, 재진입 준비에서 실제 requested_version의 게시를 기존 로컬 worker 검증 도구로 확인한다. 원본 저장·두 번의503·세 번째 한 번의 성공·옛새탭 공존을 검증한 뒤의 준비 조건이며, 운영 원본이나 RPC응답을 변경하지 않는다. BUG-103.
고정c38e6bf3의 local-1789027724480-35812는 CASE-039 첫시도1 pass/0 fail/skip/flaky·증거·정리 성공(2분48초), check3391 pass/0 fail/35 조건부skip이다. 사전 검증 누락: 저장·RPC완료를 비동기 통계게시 완료로 취급했다. 옛 번들의 과거 읽기 결함을 고쳤다는 뜻은 아니다.
Phase 3-6 — 부팅 요청 완료 후 온보딩 재진입
세 번째 main 후보34455577226에서는 CASE-009만 실패했다. 홈프로필이 보이는 동안 그룹·PR·피드 등 부팅 요청이 진행 중인데 테스트가 reload하여 일부 요청이 취소되고 다음문서에서 그룹 오류를 기록했다. 첫탭 close에도 미종료요청1건이 있었다. 두 문서 교체 앞에 기존 요청완료 확인2줄을 추가했다. 고정092d4661의 CASE-009는 첫시도·증거·정리 성공(2분37초), check3391 pass/0 fail/35 조건부skip이다. BUG-104. 사전 검증 누락: 홈표시·원본확인을 전체 부팅읽기 완료로 취급했다.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| 같은 날짜 겹침 단위 재현 | 취소 신호 true로 실패 → 취소 없이 최신 결과 유지 |
| 달력 store 단위 | 10건 → 12건 통과 |
| CASE-047 수정 후 집중 실행 | 1 pass, 0 fail/skip/flaky, 전체 준비·정리 3분32초 |
| npm run check | 3390 pass / 0 fail / 35 기존 조건부 skip |
| release precheck | static/build·단위 3390 pass / 0 fail / 35 conditional skip, 2분4초 |
| release Merge Check | 34441786720 성공, a7436af1 병합 |
| release→staging full | 34441853080 성공, 8분23초. 브라우저53 + 화면22 첫 시도·정리 완료 |
| staging 배포·smoke | 34442504712 성공, 3분38초. f830b2ce의 DB·Edge·앱·CRUD |
| main 승격 첫 full | 34442861296 CASE-029 volume 조회 취소로 실패 |
| 홈 갱신 중 리포트 요청 브라우저 회귀 | 신규 2건 수리 전 실패 → 관련 전체 6건 통과 |
| CASE-029 고정 커밋 집중 실행 | local-1789020979313-105528, 첫 시도1 pass /0 fail/skip/flaky, 증거·정리 성공, 준비 포함4분17초 |
| 추가 수리 release precheck | static/build·3390 pass /0 fail /35 conditional skip,2분9초 |
| 추가 수리 Merge Check | 34444860376 성공, 2b6e1725 병합 |
| 새 staging 후보 첫 full | 34445030426 CASE-040 옛 탭의 새로고침 요청 취소로 실패. CASE-029·047 통과 |
| CASE-040 수리 검증 | 요청 수명 Chromium7/7, 고정커밋 CASE 첫시도1 pass·증거·정리 성공(4분8초), check3390 pass, precheck2분7초 |
| CASE-040 수리 Merge Check | 34446445617, release ca411faf |
| 세 번째 staging 후보 full | 34446524759 CASE-013·045 실패, CASE-029·040·047 통과 |
| CASE-013·045 최종 집중 검증 | local-1789024239736-106364, 첫시도2 pass/0 fail/skip/flaky·증거·정리 성공,5분21초 |
| 최종 수리 check·precheck | 3391 pass/0 fail/35 기존 조건부 skip, clean release precheck2분39초 |
| 최종 수리 Merge Check | 34449360435 성공, #1519 release31aa1fa9 |
| 최신 staging 후보 full | 34449444246 성공,9분31초, 브라우저53+화면22·단위·DB·증거·verify |
| 최신 staging 배포 | 34450322116 성공,3분59초, 91594e94 DB·Edge·앱·CRUD smoke |
| 두 번째 main 후보 검사 | 34450767470 CASE-052 재진입 취소로 실패. 합성7743778e와 staging은 tree312355ed로 동일, 원 full의 PR 출처가 빈 배열이라 전체 검사 실행 |
| CASE-052 수리 Merge Check | 34452319877 성공, #1524 release6dceeacc, precheck2분4초 |
| 다섯 번째 staging full | #1525, full34452460088 CASE-039 옛 번들의 통계복구 중 리포트 취소로 실패. CASE-052 통과 |
| CASE-039 수리 Merge Check | 34454211459 성공, #1526 release1b911c5b, precheck2분2초 |
| 최종 staging full | 34454288109 성공,9분6초. #1525 head1b911c5b, 브라우저53+화면22·단위·DB·증거집계 |
| 최종 staging 배포 | 34455153112 성공,3분24초. merge7df3ef62의 DB·Edge·앱·CRUD smoke |
| 세 번째 main 후보 | 34455577226 CASE-009 부팅 중 문서교체로 그룹오류 실패. 합성37bce5ab와 staged tree d029cd66 일치 |
| CASE-009 수리 Merge Check | 34457036868 성공, #1527 release da6fccd2, precheck1분57초 |
| 최종 staging 후보 | 34457196061 성공, #1528 head da6fccd2, 브라우저53+화면22·정적·단위·DB·증거·verify |
| 최종 staging 배포 | 34458173263 성공,3분42초. merge6b678da4의 DB·Edge·앱·CRUD smoke |
| 최종 main 후보 | 34458636028 성공. 브라우저53+화면22·정적·단위·DB·증거·verify. 합성d3c7928f와 staging6b678da4·실제mainf7195e8c의 tree9c78ba86 일치 |
| Production | 34459520846의 DB·Edge·앱·CRUD smoke 12/12·태그 성공, mainf7195e8c. 별도 브라우저 사후검사 1 pass/14 fail |
집중 실행 local-1789018029465-106000은 수정된 작업 트리의 실제 빌드를 사용했다. 로그의 Git SHA는 수정 전 기준점이며 dirty diff와 함께 보존했다. 이 부분 검사는 전체 CI 성공 증거가 아니다. 최초 #1512 수리는 migration·DB 코드·E2E 시나리오 파일을 변경하지 않아 사전검증의 추가 DB 대상에 해당하지 않았다. 이후 CASE-040·045의 실제 수정은 각 Phase에 별도로 기록했다.
6. 이번 개선으로 향상된 것
저장 후 같은 날짜 재조회가 겹쳐도 이전 요청을 불필요하게 중단하지 않으며, 늦은 결과가 최신 표시를 덮는 것은 계속 차단한다. 집계 잡은 새 runner에서도 실행 의존성을 갖춘다. 실제 운영 반영 여부는 각 배포 SHA·smoke·태그로 별도 확인한다.
검증 범위와 한계
승격·Production 배포·CRUD smoke·태그는 완료됐다. 별도 브라우저 사후검사는 실패했으므로 운영 브라우저 전체 성공을 뜻하지 않는다. 이번 승격 요청을 별도 수리·추가 이슈로 확대하지 않고 확인된 결과와 미확인 원인을 아래에 남긴다.
최종 운영 증거
- main 병합: 2026-09-10 09:14:11 UTC,
f7195e8c768c88614b99f2ef2f46f3ee2f6cf870. 검증 합성·staging·main의 tree는9c78ba86b8a770a3119ac7b16f8cd1b7c0482a23로 동일하다. - Production: DB 09:15:36, Edge 09:16:09, 앱·공개 주소 09:17:24, CRUD smoke 09:17:59, 태그 09:18:22 UTC 성공.
v0.17.9태그는 위 main 커밋을 가리킨다. - 랜딩 연결: 공개
/landing과 독립 랜딩 원본이 HTTP 200이며 같은 HTML 제목·스크립트를 제공한다./landing/assets/index-B-WIlj67.js도 양쪽 HTTP 200, 168701 bytes, SHA-2561e9dae164cb7e59fd69941b390fdc3c216a8b3a7d2f9401cb09d68045e23cd21로 일치한다. - 별도 Production 브라우저 여정은 기존 정책상 배포·태그와 독립된 사후검사다. 실제 결과를 아래에 구분한다.
Production 브라우저 사후검사
실행34459520846, mainf7195e8c, 첫 시도에서 1 pass / 14 fail / 0 skip / 0 flaky. 실제 테스트3분42.6초, 준비·업로드 포함 잡4분47초. 배포 workflow 전체는 success이나 browser journeys 잡은 failure이며 자동 경고 보고도 실행됐다. 이는 기존 #1485의 사후검사 정책에 따른 결과이지 브라우저 성공 판정이 아니다. 별도 수동 smoke나 통과를 위한 재실행을 하지 않았다.
| 결과 | 관측 |
|---|---|
| 통과 | CASE-016 |
| UI·데이터 대기 실패 | CASE-001·002·004·005·008·011 |
| 통계 worker 실패 단언 | CASE-006, CASE-008의 추가 오류 |
| 부팅 preload 관측 누락 | CASE-010 |
| 필수·예상 밖 오류 관측 실패 | CASE-012·013·014·015 |
| 로컬 전용 준비 조건 실패 | CASE-029·039가 local Supabase service-role client를 요구. 요청 관측기가 설치되기 전 종료되어 cleanup·관측기 부재 오류도 함께 보고됨 |
이 목록은 이번 artifact의 관측 분류다. 14건 모두 제품 회귀라거나 모두 검사 환경 때문이라고 단정하지 않는다. Production 브라우저에서 실패한 정확한 원인은 이번 승격 범위에서 확정하지 않았다. 최종 release→staging과 staging→main의 격리 DB 전체 CI는 각각 브라우저53+화면22를 통과했다. 실제 운영 CRUD smoke는12 pass/0 fail/0 skip이다.
문서 기록은 최초 PR #39와 최종 PR #48에 나눠 게시한다. 문서 저장소 병합과 문서 사이트 배포는 별개이며 이 기록은 사이트 배포 성공을 주장하지 않는다.