로컬·CI Postgres 이미지를 Production 버전에 고정 — 세 곳 제각각(.127/.156/.158)에서 상수 하나로 (2026-09-04)
- 기간: 2026-09-04 (세션 1개
b59a92ee, #1225 직후. 오너 질문 "버전 맞추는게 좋을까?" → "프로덕션을 올릴까? 전부 158로 통일?" → ".127이랑 .158은 차이 클까?" → "A로 이슈 만들어서 진행해줘") - 랜딩: PR #1234(Phase 1~3, squash 머지) — 마이그레이션·엣지 없음, Vercel 배포 없음(워크플로·스크립트·테스트·문서)
- 설계서: 이슈 #1233 본문("예상 효과·개선사항" 절 포함)
- 정본:
package.jsonsupabaseToolchain(cli+postgresVersion) — CI 두 워크플로와scripts/ci-local.mjs가 읽는 유일한 버전 상수 - 도구:
npm run ci:local(CLI 불일치·이미지 불일치를 종료 코드 3으로 잡는다) - 게이트:
tests/react/ciLocal.test.mjs(워크플로가 버전 숫자를 하드코딩하지 않고 상수를 참조·.temp/postgres-version을 쓰는지, 태그 비교 함수),tests/react/statsRefreshStatementTimeoutContract.test.mjs(이미지 검사 단계 앵커 갱신). CI 1회(migration-smoke 이미지 검사 단계가 17.6.1.127로 통과) - 버그리포트: 없음
- 계약:
docs/process/ci-local.md"Postgres 이미지와 CLI 버전은 Production에 맞춘다" 절,migration-landing.md0-1 샌드박스 준비
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 상수 + ci:local(CLI 핀 검사·.temp/postgres-version·이미지 태그 검사·--stop --no-backup) | ✅ PR #1234 |
| Phase 2 | CI 워크플로 두 개가 상수를 읽고, migration-smoke가 Production 이미지로 뜨고 검사 | ✅ PR #1234 |
| Phase 3 | 문서·작업 기록 | ✅ PR #1234 |
1. 배경
#1225로 npm run ci:local이 생기면서 로컬 Postgres 이미지(17.6.1.158)가 CI 핀(17.6.1.156)과 다르다는 참고 로그가 남았고, 오너가 "버전 맞추는 게 좋을까?"를 물었다. 실측하니 Production은 또 다른 17.6.1.127이었다.
2. 문제 제기
같은 테스트가 세 가지 빌드 위에서 돌았다
| 환경 | 이미지 | 정하는 것 |
|---|---|---|
| Production | 17.6.1.127 | Supabase 배정 |
| CI | ghcr.io/…:17.6.1.156 | 2026-08-13(#407) 고정한 CLI 2.111.0 |
| 이 PC | public.ecr.aws/…:17.6.1.158 | scoop CLI 2.113.0 기본값 |
.127 → .158 변경분(supabase/postgres 커밋 101개)을 대조하면 PostgreSQL 본체 17.6 동일, 우리가 쓰는 확장(pg_cron·pgcrypto·uuid-ossp·pg_stat_statements·vault·pgtap·plpgsql_check) 버전 변화 없음 — 당장 결과가 갈릴 여지는 작다. 그러나 다음에 진짜 차이가 생겼을 때 "어느 빌드에서 났나"를 알 수 없다.
버전을 정하는 곳이 세 곳이었다
워크플로 두 파일의 setup-cli 버전(3곳), 워크플로의 이미지 검사 문자열, 스크립트 상수. 어디 하나를 고치면 나머지가 어긋난다.
3. 해결 방안
원칙 (오너 결정)
- D1 (2026-09-04): "A로 이슈 만들어서 진행" — Production을 올리지 않고 로컬·CI를 Production 버전에 맞춘다. B(Production을 .158로 올려 통일)는 테스트를 위해 Production을 건드리는 순서 역전이라 기각.
접근
supabase start는 supabase/.temp/postgres-version이 있으면 그 태그의 이미지를 띄운다(supabase link가 링크된 프로젝트에 남기는 파일. 본 체크아웃에 17.6.1.127로 존재). 링크 없는 샌드박스와 CI에 이 파일만 써 주면 Production과 같은 이미지가 뜬다 — 실측: 샌드박스에 파일을 쓰고 start → public.ecr.aws/supabase/postgres:17.6.1.127. 그래서 상수 하나(package.json)에 CLI 버전과 이미지 태그를 두고 모두가 읽게 했다.
4. 적용한 내용
Phase 1 — 상수와 ci:local (PR #1234)
package.jsonsupabaseToolchain: { cli: "2.113.0", postgresVersion: "17.6.1.127" }.scripts/ci-local.mjs: 설치된 CLI가 핀과 다르면 종료 코드 3(설치 명령 안내,CI_LOCAL_ALLOW_CLI_MISMATCH=1로만 우회). 샌드박스 준비 시supabase/.temp/postgres-version에 핀을 쓴다. 떠 있는 스택이 다른 이미지면 멈추고--stop을 안내, 뜬 뒤에도 태그가 다르면 실패(레지스트리 차이는 허용).--stop은supabase stop --no-backup으로 볼륨까지 지운다.
Phase 2 — CI 워크플로 (PR #1234)
policy-contract.ymlmigration-smoke: 상수를 읽는 단계 →setup-cli버전 = 상수 →.temp/postgres-version쓰기 → 이미지 검사는*/supabase/postgres:<상수>패턴(하드코딩 제거).deploy.yml: 두 곳의setup-cli버전도 같은 상수에서.- 테스트:
ciLocal.test.mjs에 상수·워크플로 참조·태그 비교 3건 추가,statsRefreshStatementTimeoutContract.test.mjs의 버전 문자열 앵커를 상수 참조 형태로 갱신.
Phase 3 — 문서 (PR #1234)
docs/process/ci-local.md절 신설 + 종료 코드 3 표 두 행,migration-landing.md0-1 샌드박스 준비에.temp/postgres-version항목, 이 기록.
주요 결정과 그 근거
- Production에 맞춘다, Production을 올리지 않는다 — 테스트 환경은 실제 서비스가 도는 빌드를 따라가야 한다. 업그레이드는 보안 패치 등 그 자체의 이유가 생길 때 오너가 대시보드에서 결정하고, 그때 상수만 올린다.
- CLI 불일치는 경고가 아니라 실패 — 경고는 무시되어 다시 드리프트가 된다. 의식적 우회 환경변수만 남겼다.
- 태그만 비교 — CLI 2.111은 ghcr.io, 2.113은 public.ecr.aws에서 같은 태그를 받는다. 레지스트리까지 비교하면 CLI를 올릴 때마다 검사가 깨진다.
작업 중 드러난 것
- 8월 20일 로컬 스모크가 .127로 돌았던 것(메모리 기록)은 링크된 본 체크아웃의
.temp/postgres-version때문이었다 — 같은 CLI 2.113.0이라도 링크 유무로 이미지가 달랐다. - #1225의 빈 포트 검사가 이 PC에서 거짓 통과했다: Windows Docker Desktop은 컨테이너 포트를 자기 프록시로 묶어 두어 Node의 bind 검사(127.0.0.1)가 성공한다. 새 샌드박스가 다른 세션 스택(barbelic-e2wt, 5632x)과 같은 포트를 골라
supabase start가 "port is already allocated"로 죽었다. 검사에docker ps의 포트 매핑을 더하고, 우리 스택이 안 떠 있는데 포트가 잡혀 있으면 실패한 잔해 컨테이너를 지우고 포트를 새로 골라 config를 다시 쓰게 고쳤다. - CI 1회차 빨간불 — 이미지 차이가 아니라 시각 의존 테스트(CASE-030): 체중 80kg 기대, 75kg 저장. 로컬 .127 샌드박스에서 같은 모양으로 재현(사전 검증에는 걸리지 않았다 — 브라우저 저니 전체는 PR 전에 돌리지 않았고, 돌렸어도 KST 새벽이면 통과한다). 원인: 온보딩은
body_metrics에measured_at = 저장 시각을 넣고, CASE-030은 날짜만 넣는다.get_profile_workspace가coalesce(measured_at, date::timestamptz) desc로 최신 지표를 고르는데 DB가 UTC라 KST 날짜의date::timestamptz(그날 00:00 UTC)는 KST 09시 이후엔 온보딩 행보다 과거가 되어 75kg이 이긴다. main CI가 통과한 건 KST 새벽 5시(UTC 20:29)에 돌아서였다. 앱의 체중 저장(saveProfileBodyMetric)은 항상 측정 시각을 넣으므로 제품 결함은 아니고 테스트가 앱의 저장 모양과 달랐던 것 — CASE-030에measured_at을 넣어 고쳤다(이 PR에 포함). deploy.yml의 변경은 다음 릴리스 레인에서야 실제로 실행된다(이 PR의 CI는 policy-contract.yml만 돈다). 릴리스 PR에서 확인할 것.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 샌드박스가 뜨는 이미지 | 17.6.1.158 → 17.6.1.127(= Production). 새 샌드박스에서 ci:local --full --only db: public.ecr.aws/supabase/postgres:17.6.1.127 — 핀과 일치 · db reset 1분 11초 · pgTAP 102파일/1,629 assert 통과 16초 (총 2분 47초) |
| CI migration-smoke 이미지 검사 | ghcr.io/…:17.6.1.156 하드코딩 → 상수 태그 17.6.1.127. CI 1회차: 이미지 검사·마이그레이션 적용·pgTAP 통과, 브라우저 저니 CASE-030만 실패(시각 의존 테스트 결함, 위 §4). 2회차(run 33830161945) 통과 — 이미지 검사 ghcr.io/supabase/postgres:17.6.1.127(CI는 ghcr, 로컬은 public.ecr.aws로 같은 태그를 받는다 → 태그만 비교한 것이 맞았다), 마이그레이션 적용 65초, pgTAP 15초, 브라우저 저니 348초, 뷰포트 86초 |
| 버전 상수 위치 | 워크플로 3곳 + 검사 문자열 1곳 + 스크립트 1곳 → package.json 1곳 |
| CLI 불일치 | 조용히 다른 버전으로 실행 → 시작 시 종료 코드 3 |
| 단위 테스트 | ciLocal 14건·statsRefresh 대조 통과, npm run check 통과 |
deploy.yml의 상수 참조 | 다음 릴리스 레인에서 실제 실행 확인 (미검증) |
6. 이번 개선으로 향상된 것
테스트가 Production과 같은 빌드에서 돈다
로컬과 CI가 서로 다르고 둘 다 Production과 달랐던 상태가, 세 곳 모두 17.6.1.127 하나로 됐다. 결과가 갈리면 "빌드 차이"는 원인 후보에서 빠진다.
버전을 바꾸는 방법이 한 줄이 됐다
Production이 올라가면 postgresVersion 한 값, CLI를 올리면 cli 한 값. 나머지는 상수를 읽는다.
구조적으로 남는 것
package.jsonsupabaseToolchain정본과 그것을 참조하는지 검사하는 테스트.ci:local의 CLI·이미지 일치 검사(종료 코드 3).
남은 것
- Production의 Postgres 버전이 상수와 같은지 기계로 비교하는 검사(
check:remote-schema류)는 넣지 않았다. Supabase가 Production을 올리면 사람이 상수를 따라 올려야 한다. deploy.yml의 상수 참조는 다음 릴리스에서 실행으로 확인한다.