--- id: "dcy-0002" status: confirmed --- # Requester-versioned GHCR 이미지 게시 계약 ## Problem ### 배경 `portfolio-hub`도 다른 TREX 서비스처럼 검증된 OCI image를 GHCR에 게시하고, 운영자가 immutable digest로 배포·롤백할 수 있어야 한다. 비교 대상의 현재 상태는 서로 다르다. - `trex-core`: build-once, image ID, archive SHA-256, OCI revision, canary scan, no-rebuild publish, selector digest 검증은 강하지만 `main`과 `v*` push 모두 publication하며 runtime dependency 설치가 `uv.lock`에 묶이지 않는다. - `kis-data-hub`: built-container `/health` liveness와 tested archive handoff가 있으나 `main`도 publication하고 CI/runtime install이 unlocked이며 image ID/checksum/canary/non-root/same-digest 검증이 약하다. - `trex-mega-swing-v2`: strict `vX.Y.Z` tag-only, locked CI/runtime, build-once, installed/startup smoke, image ID/checksum/revision/canary, no-rebuild publish, commit/version selector same-digest, non-root, digest deploy/rollback 경계를 구현했고 실제 `v0.1.0` hosted run이 성공했다. 후속 hardening 이슈도 같은 방향을 가리킨다. - `trex-core` #57: https://github.com/fourblocks2020/trex-core/issues/57 - `kis-data-hub` #30: https://github.com/fourblocks2020/kis-data-hub/issues/30 두 이슈는 현재 OPEN proposal이며, 기존 방식을 공통 표준으로 유지하는 근거가 아니라 `trex-mega-swing-v2`형 requester-versioned publication으로 이관하려는 근거다. ### 문제 정의 일반 `main` push마다 image를 게시하면 문서·governance-only 변경도 새 registry artifact를 만들며 requester의 release/version 선택과 publication이 분리되지 않는다. 반대로 image build만 성공했다고 운영 가능한 artifact가 되는 것도 아니다. `portfolio-hub`는 다음을 동시에 만족해야 한다. 1. publication authority가 명시적인 requester-selected version에만 귀속될 것 2. repository verification과 runtime dependency가 동일한 lock resolution을 사용할 것 3. verify job에서 시험한 exact image만 publish job으로 전달되고 rebuild되지 않을 것 4. source commit, image ID, archive checksum, registry digest가 연결될 것 5. secret·운영 설정이 build context, layer, history, archive, metadata에 포함되지 않을 것 6. installed image에 API, CLI 및 모든 Alembic migration payload가 존재할 것 7. HTTP liveness와 PostgreSQL readiness/migration 검증을 구분할 것 8. image rollback과 DB migration rollback을 구분할 것 ## Evidence ### 원문·승인·사후 문서화 경계 - 원문: https://github.com/fourblocks2020/portfolio-hub/issues/25 - 원문 제목: `discovery: requester-versioned GHCR 이미지 게시 계약` - 원문 상태: `OPEN` - Discovery 승인 댓글: https://github.com/fourblocks2020/portfolio-hub/issues/25#issuecomment-5277036560 - 승인 시각(KST): `2026-08-13T15:51:34+09:00` - 승인 선택: `OQ-1-A`, `OQ-2-B`, `OQ-3-A`, `OQ-4-A`, `OQ-5-A`, `OQ-6-A` - 요청자는 2026-08-13 이슈 #25 원문을 최대한 포함한 승인 상태의 Discovery와 승인된 REQ/AC를 새 Line에 사후 작성하도록 명시했다. - 이 Line은 이미 수행된 로컬 구현을 승인된 지속 사양으로 포착한다. #25 구현이 ProofLine 정식 시퀀스로 수행됐다는 주장, ProofLine delivery closure, push, publication, release 또는 deployment 완료를 소급 생성하지 않는다. ### 이슈 원문의 문서 성격 - 유형: **Discovery 제안** - 상태: **approved / requester confirmed** - 승인 시각(KST): `2026-08-13T15:51:34+09:00` - 승인 결정: `OQ-1-A`, `OQ-2-B`, `OQ-3-A`, `OQ-4-A`, `OQ-5-A`, `OQ-6-A` - 대상 저장소: `fourblocks2020/portfolio-hub` - 목적: `portfolio-hub`의 GHCR 운영 이미지 게시 방식을 결정하기 위한 문제·근거·경계·미결정 질문 정리 이 Discovery는 requester가 아래 OQ 선택으로 **승인했다**. 이 승인은 `portfolio-hub` 저장소 안의 후속 요구사항 작성과 bounded implementation 범위 제안을 허용하지만, PR 생성, Git tag 생성·push, GHCR publication, GitHub Release 생성, 운영 배포·재시작 또는 production DB migration 실행을 승인하지 않는다. 각 외부 write는 별도 requester 지시가 필요하다. ### 관찰된 `portfolio-hub` 현황 현재 `main` 기준: - Python `>=3.11`, FastAPI/uvicorn HTTP service다. - `uv.lock`이 존재하며 `uv lock --check`가 통과한다. - `/health`는 HTTP 200 응답과 `status`, `service`, `version`, `checked_at`, `db_enabled`를 반환한다. - `pfhub` CLI에 `init-db`, `migrate-db`, `health-check`가 있다. - PostgreSQL과 Alembic을 사용하며 `migrations/versions`에 28개 migration이 있다. - DB-enabled startup은 `CREDENTIAL_ENCRYPTION_KEY`를 요구하며, 설정에 따라 initial admin user를 생성할 수 있다. - Dockerfile, deny-by-default `.dockerignore`, image publication workflow, release helper/tests, container operations 문서는 아직 없다. #### 패키징 위험 현재 wheel을 실제 build해 inventory를 확인한 결과: ```text wheel: portfoliohub-0.1.0-py3-none-any.whl members: 61 migration_members: 0 alembic_ini: False pfhub_entrypoint: True ``` 현재 migration loader는 설치된 `app` 위치의 상위에서 `alembic.ini`와 `migrations/`를 찾는다. 따라서 `.venv`만 runtime stage로 옮기는 단순 multi-stage image는 API import와 `pfhub --help`는 통과하면서도 운영의 `pfhub migrate-db`가 실패할 수 있다. 이 문제는 첫 image publication 전에 fail-closed package/runtime contract로 해결해야 한다. ### 이슈 원문 근거 - `portfolio-hub/pyproject.toml` - `portfolio-hub/uv.lock` - `portfolio-hub/app/main.py` - `portfolio-hub/app/api/health.py` - `portfolio-hub/app/db/migrations.py` - `portfolio-hub/app/cli/commands/health.py` - `portfolio-hub/alembic.ini` - `portfolio-hub/migrations/versions/` - `trex-core/.github/workflows/publish-image.yml` - `trex-core/Dockerfile` - `kis-data-hub/.github/workflows/publish-image.yml` - `kis-data-hub/Dockerfile` - `kis-data-hub/docs/container-operations.md` - `trex-mega-swing-v2/.github/workflows/publish-image.yml` - `trex-mega-swing-v2/Dockerfile` - `trex-mega-swing-v2/scripts/image_release.py` - `trex-mega-swing-v2/tests/release/` - `trex-mega-swing-v2/docs/container-operations.md` - 성공한 `trex-mega-swing-v2 v0.1.0` publication run: https://github.com/fourblocks2020/trex-mega-swing-v2/actions/runs/31468042611 ### 구현·검증 증거 - 구현 브랜치: `feat/25-ghcr-image-publication` - 구현 기준: `origin/main` `35e85981af845489236c18d709d0545c830e7f03` - 로컬 구현 커밋: `96260b6302e050c6e564f3eca4e723558f26246f` - 구현 진행 댓글: https://github.com/fourblocks2020/portfolio-hub/issues/25#issuecomment-5277080186 - 구현 완료 댓글: https://github.com/fourblocks2020/portfolio-hub/issues/25#issuecomment-5277346458 - 최종 증거: release tests `23 passed`; 전체 pytest `621 passed, 6 skipped`; `uv lock --check`, scoped Ruff, `git diff --check`, 실제 Python 3.11 image build/canary scan, installed-image/PostgreSQL startup smoke 통과. - 독립 최초 리뷰의 MAJOR 1건과 MINOR 1건은 수정·회귀검증됐고 독립 재리뷰 PASS에서 모두 CLOSED됐다. - 두 댓글의 exact body는 이 Line의 `evidence/implementation-progress.md`와 `evidence/implementation-complete.md`에 비정식 증적으로 보존한다. ## Scope ### 승인된 방향 publication·공급망 계약은 `trex-mega-swing-v2`를 기준으로 채택하고, smoke와 payload 검증은 `portfolio-hub`에 맞춘다. #### 공통 publication 계약 후보 - strict `vX.Y.Z` Git tag push만 publication event로 허용 - `main`, 일반 branch, PR, `workflow_dispatch`, malformed/prerelease tag에서 registry write 금지 - 자동 version 증가 및 moving `latest` 금지 - exact tagged commit checkout 및 checked-out HEAD 일치 검증 - pinned `uv` + `uv sync --extra dev --locked` 검증 - runtime도 repository-owned `uv.lock`으로 `--locked --no-dev --no-editable` 설치 - full pytest와 새 release helper/contract test에 대한 정직한 lint gate - source image를 한 번만 build - OCI full revision, tested image ID, exported archive SHA-256 기록 - synthetic canary를 image filesystem, history, inspect metadata, archive 및 helper output에서 검사 - publish job은 checksum/image ID/revision을 확인한 동일 archive를 load하고 rebuild 없이 게시 - 다음 두 selector만 부여하고 동일 registry digest를 검증 - `ghcr.io/fourblocks2020/portfolio-hub:` - `ghcr.io/fourblocks2020/portfolio-hub:vX.Y.Z` - 실제 deploy/rollback identity는 `ghcr.io/fourblocks2020/portfolio-hub@sha256:` - 기본 runtime user는 non-root 고정 UID/GID, 운영 root filesystem은 read-only, 필요한 경로만 명시적으로 writable mount #### `portfolio-hub` 전용 verification 후보 1. **Installed payload smoke** - networkless/read-only/non-root container에서 `python -I -c 'import app'`, `pfhub --help`, uvicorn entrypoint 확인 2. **DB-disabled HTTP liveness** - 외부 dependency 없이 API를 기동하고 `/health`의 `status=ok`, `service=portfolio-hub`, `db_enabled=false` 확인 3. **Migration payload exact inventory** - source의 canonical migration 목록과 wheel/image 안의 목록이 non-empty이고 정확히 같은지 확인 - `alembic.ini` 및 resource lookup 계약 확인 4. **Isolated PostgreSQL migration/readiness smoke** - internal network의 synthetic PostgreSQL과 synthetic encryption key만 사용 - migration을 head까지 적용하고 Alembic revision 확인 - DB-enabled API 기동 후 `/health` liveness와 `pfhub health-check` DB ping을 별도로 확인 - production credential/DB 접속 금지, bootstrap-user side effect 억제 또는 격리 DB로 제한 - container/network/volume cleanup까지 PASS 조건에 포함 ### 원문 범위 후보 — In scope #### In scope - Dockerfile 및 deny-by-default `.dockerignore` - strict tag-only GHCR publication workflow - image release helper와 contract/regression tests - locked verification/runtime dependency 설치 - installed payload·HTTP·PostgreSQL/Alembic smoke - migration resource/package layout의 최소 수정 - immutable digest 중심의 container operations 문서 - secret, non-root, read-only filesystem, writable mount, migration/rollback 경계 ### 확정 결정(OQ) — 원문 전체 > **승인 기록:** requester가 각 OQ에서 하나씩 선택했으며, 아래 `[x]` 항목이 승인된 scope authority다. 미선택 항목은 구현 범위가 아니다. #### OQ-1 — migration payload 위치 (확정) 어떤 방식을 canonical runtime contract로 채택할 것인가? - [x] **OQ-1-A (승인):** migration과 Alembic 설정을 installable package data로 포함하고 `importlib.resources` 기반으로 조회 - [ ] **OQ-1-B:** runtime image의 고정 경로에 `alembic.ini`와 `migrations/`를 명시적으로 복사하고 loader가 그 경로를 사용 결정 근거: **A**는 wheel/image payload parity를 검증하기 쉽고 source checkout 경로에 덜 의존한다. 단, Alembic script layout과 CLI 호환성을 prototype으로 검증해야 한다. #### OQ-2 — publication smoke의 DB 깊이 (확정) - [ ] **OQ-2-A:** DB-disabled `/health`만 publication gate로 두고 PostgreSQL migration은 별도 CI에서 검증 - [x] **OQ-2-B (승인):** publication gate 안에서 isolated PostgreSQL migration + DB ping까지 필수로 검증 결정 근거: **B**. 이 서비스의 운영 가능성은 migration payload와 DB 연결에 의존하며 `/health`는 DB ping을 수행하지 않는다. #### OQ-3 — migration과 application startup 순서 (확정) 다음 운영 순서를 계약으로 고정할 것인가? 1. 별도 pre-start `pfhub migrate-db` 2. 성공 후 application container start 3. `/health` liveness와 별도 DB readiness 확인 - [x] **OQ-3-A (승인):** 위 pre-start migration 순서를 운영 계약으로 고정 - [ ] **OQ-3-B:** application startup 과정에서 migration을 수행 결정 근거: **A**는 application startup이 production schema mutation을 암묵적으로 수행하지 않게 한다. #### OQ-4 — runtime entrypoint (확정) - [x] **OQ-4-A (승인):** image 기본 command를 `uvicorn app.main:app ...`로 명시하고 shutdown/worker/root-path/forwarded-header 계약을 operations 문서와 smoke에 고정 - [ ] **OQ-4-B:** 명시적인 `portfolio-hub` server CLI/entrypoint를 추가하고 이를 image 기본 command로 사용 결정 근거: 첫 범위에서는 **A**로 publication 구축 범위를 제한한다. 별도 server CLI는 독립 필요성이 확인될 때 추가한다. #### OQ-5 — Python minor baseline (확정) - [x] **OQ-5-A (승인):** 현재 저장소 계약과 일치하도록 image와 hosted verification을 Python 3.11로 고정 - [ ] **OQ-5-B:** 이 범위에서 Python 3.12로 함께 승격 결정 근거: **A**. runtime 승격을 publication 구축에 묶지 않는다. #### OQ-6 — branch CI와 publication 분리 (확정) Tag-only publication과 별개로 일반 branch/main에서 어떤 검증을 수행할 것인가? - [x] **OQ-6-A (승인):** branch/main에서 registry write 없이 full pytest와 release/container contract 검증을 수행하고, strict version tag workflow만 `packages: write`와 publication을 수행 - [ ] **OQ-6-B:** branch/main에서는 기존 repository 검증만 수행하고 release/container contract와 publication 검증은 strict version tag workflow에서만 수행 결정 근거: **A**는 registry write 권한을 tag publication에만 제한하면서 publication 결함을 tag push 전에 발견할 수 있다. ### 승인 결정 요약 | OQ | 승인 선택 | 확정 내용 | |---|---|---| | OQ-1 | A | migration/Alembic 설정을 installable package data로 포함하고 `importlib.resources`로 조회 | | OQ-2 | B | publication gate에서 isolated PostgreSQL migration과 DB ping을 필수 검증 | | OQ-3 | A | `pfhub migrate-db` 성공 후 application을 시작하는 pre-start migration 순서 | | OQ-4 | A | 첫 범위에서는 명시적인 uvicorn command 사용, 운영 파라미터를 문서·smoke에 고정 | | OQ-5 | A | Python 3.11 고정 | | OQ-6 | A | branch/main은 registry write 없이 full pytest와 release/container contract 검증, strict version tag만 publication | ### Discovery 결정 게이트 다음 결정 게이트는 requester의 OQ 선택 및 본 Discovery 승인으로 확정되었다. - [x] OQ-1 migration payload/resource layout - [x] OQ-2 PostgreSQL publication smoke 깊이 - [x] OQ-3 pre-start migration 운영 순서 - [x] OQ-4 runtime entrypoint - [x] OQ-5 Python minor baseline - [x] OQ-6 branch CI와 publication workflow 분리 방식 - [x] image name 및 selector 규칙 - [x] production writable mount 및 secret injection 경계 - [x] 구현이 tag push/publication/deployment를 자동 승인하지 않는다는 외부-write 경계 ### Discovery 성공 조건 다음 산출이 완료되면 요구사항/implementation scope를 제안할 수 있다. - [x] 비교 대상 세 서비스의 현재 방식과 목표 방식 차이가 위 근거와 일치한다. - [x] `portfolio-hub`의 migration payload 실패 seam을 재현 가능한 contract로 정의한다. - [x] OQ-1~OQ-6이 requester 결정으로 해소된다. - [x] HTTP liveness, DB readiness, schema migration, publication, deployment의 상태가 서로 분리된다. - [x] secret·non-root·read-only·cleanup 및 immutable rollback 경계가 확정된다. - [ ] 후속 요구사항에서 구현 AC를 source/lock → build → smoke → artifact handoff → publish → operator handoff 전체에 추적한다. ### Acceptance admission 기존 active AC는 포트폴리오 ledger 목록 집계 계약으로 이 publication capability와 의미가 다르므로 `update` 또는 `satisfy` 대상으로 사용하지 않는다. 이 Line은 다음 일곱 가지 version-independent behavior를 새 AC로 `create`한다. - installable Alembic migration resource와 exact inventory parity - Python 3.11 locked non-root image와 deny-by-default build context - branch 검증과 strict version-tag publication authority 분리 - build-once tested-artifact identity·integrity·provenance - installed payload와 DB-disabled HTTP liveness - isolated PostgreSQL migration·readiness와 exact cleanup - immutable operator handoff와 deployment exclusion ## Out of Scope ### 원문 범위 후보 — Out of scope #### Out of scope - 이 Discovery에서 실제 version 결정 또는 tag push - GHCR image publication 및 package visibility 변경 - GitHub Release 생성 - 운영 서버/Dockge/Compose 배포 또는 재시작 - production credential·DB 사용 - production DB migration, downgrade, data repair, reset 또는 backup 실행 - application API/domain behavior 변경 - deployment orchestrator 선정·구축 - unrelated repository-wide lint debt 정리 - `trex-core` #57 또는 `kis-data-hub` #30 구현 ### 사후 ProofLine 경계 - 이 사후 사양 포착은 구현이 ProofLine 정식 시퀀스로 수행됐다는 주장이나 implementation/delivery/release/deployment lifecycle closure를 만들지 않는다. - 구현 진행·완료 댓글은 canonical lifecycle artifact가 아니라 출처가 명시된 비정식 evidence다. - 후속 승인 사양은 `req-0002`와 active `ac-0006`~`ac-0012`로 추적한다. 이는 원문의 미완료 성공 조건인 source/lock → build → smoke → artifact handoff → publish → operator handoff AC 추적을 사후 충족한다. - 기능 브랜치 push, PR, Git tag 생성·push, GHCR publication, GitHub Release, 운영 배포·재시작 및 production DB migration은 별도 requester 승인 없이는 수행하지 않는다.