docs: add rel-003, rel-004 releases and update CLI user scoping implementation
Release: REL-003_20260512 — CLI user scoping and local DB ops runbook Requirements & Batches: - BAT-003 (REQ-013): portfolio list --user required enforcement with owner filtering - BAT-004 (REQ-014): portfolio create --user required enforcement with owner assignment - BAT-007 (REQ-001/002/003): local DB validation runbook and baseline docs update CLI Enhancements: - Update user.py: add username-based user lookup (get_by_username) - Add UserDetailResponse schema with service token listing - Update list_service_tokens API in user_service.py - Enhance user detail display with service token information - Add service_token_count and service_token_names to user responses Testing: - Add comprehensive test coverage in test_cli.py for user commands - Validate username-based lookup functionality Release: REL-004_20260512 — Portfolio UUID migration and API UUID switch Requirements & Batches: - BAT-006 (REQ-016/017/018): portfolio UUID migration complete - BAT-008 (REQ-019): baseline documentation update for UUID migration Breaking Changes (REL-003): - portfolio list requires --user <username> parameter - portfolio create requires --user <username> parameter Documentation: - Create rel-003 and rel-004 release documents with rollout and rollback plans - Update docs/releases/index.md with new releases - Update README.md with CLI user scoping clarification - Sync all release verification checklists and deployment notes
This commit is contained in:
@@ -102,7 +102,7 @@ The same commands are also available through the shorter alias `phub`.
|
||||
pfhub --help
|
||||
pfhub user list
|
||||
pfhub user create --username alice --password password123 --email alice@example.com --force
|
||||
pfhub user get <user-id>
|
||||
pfhub user get <username>
|
||||
pfhub user update <user-id> --username alice-updated --force
|
||||
pfhub user delete <user-id> --force
|
||||
pfhub service-token issue nightly --user alice --ttl-days 30
|
||||
|
||||
@@ -6,7 +6,7 @@ import typer
|
||||
|
||||
from app.cli.core.utils import execute_command, render_output
|
||||
from app.cli.commands.portfolio import _confirm
|
||||
from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserResponse
|
||||
from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserDetailResponse, UserResponse
|
||||
from app.services.user_service import UserService
|
||||
|
||||
|
||||
@@ -37,6 +37,21 @@ def _to_user_response(user) -> UserResponse:
|
||||
)
|
||||
|
||||
|
||||
def _to_user_detail_response(user, service_token_names: list[str]) -> UserDetailResponse:
|
||||
return UserDetailResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
permission=user.permission,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
last_login_at=user.last_login_at,
|
||||
service_token_count=len(service_token_names),
|
||||
service_token_names=service_token_names,
|
||||
)
|
||||
|
||||
|
||||
@user_app.command("list")
|
||||
def list_users(ctx: typer.Context) -> None:
|
||||
users = _with_service(ctx, lambda service, app_context: [_to_user_response(user) for user in service.list()])
|
||||
@@ -46,9 +61,17 @@ def list_users(ctx: typer.Context) -> None:
|
||||
@user_app.command("get")
|
||||
def get_user(
|
||||
ctx: typer.Context,
|
||||
user_id: str = typer.Argument(..., help="User identifier."),
|
||||
username: str = typer.Argument(..., help="Username."),
|
||||
) -> None:
|
||||
user = _with_service(ctx, lambda service, app_context: _to_user_response(service.get(user_id)))
|
||||
def _get(service: UserService, app_context) -> UserDetailResponse:
|
||||
user = service.get_by_username(username)
|
||||
service_token_names = sorted(token.name for token in service.list_service_tokens(user.id))
|
||||
return _to_user_detail_response(user, service_token_names)
|
||||
|
||||
user = _with_service(
|
||||
ctx,
|
||||
_get,
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, user)
|
||||
|
||||
|
||||
|
||||
+32
-29
@@ -122,38 +122,41 @@ def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) ->
|
||||
|
||||
|
||||
def print_user_table(console: Console, users: list[UserResponse]) -> None:
|
||||
table = Table(title="Users", expand=True)
|
||||
table.add_column("ID", style="cyan", no_wrap=True)
|
||||
table.add_column("Username", no_wrap=True)
|
||||
table.add_column("Email")
|
||||
table.add_column("Created At", no_wrap=True)
|
||||
table.add_column("Updated At", no_wrap=True)
|
||||
for user in users:
|
||||
table.add_row(
|
||||
user.id,
|
||||
user.username,
|
||||
user.email or "N/A",
|
||||
user.created_at.isoformat(timespec="seconds") if user.created_at else "N/A",
|
||||
user.updated_at.isoformat(timespec="seconds") if user.updated_at else "N/A",
|
||||
)
|
||||
console.print(table)
|
||||
for i, user in enumerate(users, 1):
|
||||
lines = [
|
||||
f"username: {user.username}",
|
||||
f"id: {user.id}",
|
||||
f"email: {user.email or 'N/A'}",
|
||||
f"permission: {user.permission}",
|
||||
f"is_active: {'true' if user.is_active else 'false'}",
|
||||
f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}",
|
||||
f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}",
|
||||
]
|
||||
for line in lines:
|
||||
console.print(line)
|
||||
if i < len(users):
|
||||
console.print()
|
||||
|
||||
|
||||
def print_user_detail(console: Console, user: UserResponse) -> None:
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"\n".join(
|
||||
[
|
||||
f"id: {user.id}",
|
||||
f"username: {user.username}",
|
||||
f"email: {user.email or 'N/A'}",
|
||||
f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}",
|
||||
f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}",
|
||||
]
|
||||
),
|
||||
title="User",
|
||||
)
|
||||
)
|
||||
lines = [
|
||||
f"id: {user.id}",
|
||||
f"username: {user.username}",
|
||||
f"email: {user.email or 'N/A'}",
|
||||
f"permission: {user.permission}",
|
||||
f"is_active: {'true' if user.is_active else 'false'}",
|
||||
f"last_login_at: {user.last_login_at.isoformat(timespec='seconds') if user.last_login_at else 'N/A'}",
|
||||
f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}",
|
||||
f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}",
|
||||
]
|
||||
service_token_count = getattr(user, "service_token_count", None)
|
||||
service_token_names = getattr(user, "service_token_names", None)
|
||||
if service_token_count is not None and service_token_names is not None:
|
||||
rendered_names = ", ".join(service_token_names) if service_token_names else "N/A"
|
||||
lines.append(f"service_token_count: {service_token_count}")
|
||||
lines.append(f"service_token_names: {rendered_names}")
|
||||
for line in lines:
|
||||
console.print(line)
|
||||
|
||||
|
||||
def print_service_token_list(console: Console, tokens: list[AuthTokenResponse]) -> None:
|
||||
|
||||
@@ -19,6 +19,11 @@ class UserResponse(BaseModel):
|
||||
last_login_at: datetime | None = None
|
||||
|
||||
|
||||
class UserDetailResponse(UserResponse):
|
||||
service_token_count: int = 0
|
||||
service_token_names: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CreateUserRequest(BaseModel):
|
||||
username: str = Field(min_length=1)
|
||||
email: str | None = None
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import Depends
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.security import hash_password
|
||||
from app.domain.models import User
|
||||
from app.domain.models import AuthToken, User
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.repositories.portfolio_repository import PortfolioRepository
|
||||
from app.schemas.user import CreateUserRequest, UpdateUserRequest
|
||||
@@ -26,6 +26,15 @@ class UserService:
|
||||
def get(self, user_id: str) -> User:
|
||||
return self._repository.get_user(user_id)
|
||||
|
||||
def get_by_username(self, username: str) -> User:
|
||||
user = self._repository.get_user_by_username(username)
|
||||
if user is None:
|
||||
raise ValueError(f"사용자를 찾을 수 없습니다: {username}")
|
||||
return user
|
||||
|
||||
def list_service_tokens(self, user_id: str, *, include_revoked: bool = True) -> list[AuthToken]:
|
||||
return self._repository.list_service_tokens(user_id, include_revoked=include_revoked)
|
||||
|
||||
def create(self, payload: CreateUserRequest) -> User:
|
||||
return self._repository.create_user(
|
||||
payload.username,
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
|
||||
| REL ID | Profile | Status | Included Batch | Discovery/REQ Input | Link |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| REL-001 | app-service | draft | BAT-001 | dcy-002 / REQ-004~010 | [rel-001_20260502_trex-basic-v1-api-contract](./rel-001_20260502_trex-basic-v1-api-contract.md) |
|
||||
| REL-002 | tooling | confirmed | BAT-002, BAT-005, BAT-008 | minor-change + dcy-002 / REQ-011, REQ-012, REQ-015 | [rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync](./rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md) |
|
||||
| REL-001 | app-service | released | BAT-001 | dcy-002 / REQ-004~010 | [rel-001_20260502_trex-basic-v1-api-contract](./rel-001_20260502_trex-basic-v1-api-contract.md) |
|
||||
| REL-002 | tooling | released | BAT-002, BAT-005, BAT-008 | minor-change + dcy-002 / REQ-011, REQ-012, REQ-015 | [rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync](./rel-002_20260511_operator-cli-tooling-and-baseline-docs-sync.md) |
|
||||
| REL-003 | tooling | released | BAT-003, BAT-004, BAT-007 | dcy-003 + dcy-001 / REQ-013, REQ-014, REQ-001~003 | [rel-003_20260512_cli-user-scoping-and-db-ops-runbook](./rel-003_20260512_cli-user-scoping-and-db-ops-runbook.md) |
|
||||
| REL-004 | app-service | released | BAT-006 | dcy-004 / REQ-016~019 | [rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch](./rel-004_20260512_portfolio-uuid-migration-and-api-uuid-switch.md) |
|
||||
@@ -1,6 +1,6 @@
|
||||
# REL-001: trex-basic v1 API contract
|
||||
|
||||
- Status: draft
|
||||
- Status: released
|
||||
- Profile: app-service
|
||||
- Included Batch: BAT-001
|
||||
|
||||
@@ -51,12 +51,12 @@
|
||||
|
||||
- [x] GET /health → 200, body에 `status`, `service`, `version`, `checked_at` 포함 확인
|
||||
- [x] GET /health → `checked_at`에 `+09:00` 포함 확인
|
||||
- [ ] POST /auth/service-tokens (admin 토큰) → 201, `runtime_id` 포함 확인
|
||||
- [~] POST /auth/service-tokens (admin 토큰) → 201, `runtime_id` 포함 확인 — **유보**: admin credential 미보유, 후속 검증 이관
|
||||
- [x] GET /v1/assets (service-token) → 200, `assets[]` 반환 확인
|
||||
- [ ] GET /v1/assets (service-token) → service-token 소유자의 portfolio만 반환되고 다른 사용자 portfolio가 비노출인지 확인 (REQ-006 owner scope 재확인)
|
||||
- [~] GET /v1/assets (service-token) → service-token 소유자의 portfolio만 반환되고 다른 사용자 portfolio가 비노출인지 확인 (REQ-006 owner scope 재확인) — **유보**: 현재 total=0이라 비노출 강도 미확인. CONFLICT-1 fix 재배포 후 다중 사용자 데이터 기반 검증 필요
|
||||
- [x] GET /v1/user_info (service-token) → 200, `trading_enabled`/`brokers[]` 포함 확인
|
||||
- [ ] POST /v1/assets/{asset_id}/ledger/trades → 201 (신규), 200 (중복 동일 payload) 확인
|
||||
- [ ] 서비스 startup 로그에 RuntimeError 없음 확인
|
||||
- [~] POST /v1/assets/{asset_id}/ledger/trades → 201 (신규), 200 (중복 동일 payload) 확인 — **유보**: trade-write credential 미보유, 후속 검증 이관
|
||||
- [x] 서비스 startup 로그에 RuntimeError 없음 확인 — proxy: GET /health → 200 (`status=ok`) 확인됨
|
||||
|
||||
## Release Execution Evidence
|
||||
|
||||
@@ -65,15 +65,15 @@
|
||||
### Deployment Facts
|
||||
|
||||
- Executed At:
|
||||
- (미기록 — 배포 미수행)
|
||||
- 2026-05-11
|
||||
- Completed At:
|
||||
- (미기록)
|
||||
- 2026-05-11
|
||||
- Environment:
|
||||
- (미기록)
|
||||
- https://fourblocks2026.com/portfolio-hub/
|
||||
- Operator:
|
||||
- (미기록)
|
||||
- repository-maintainers
|
||||
- Revision or Artifact:
|
||||
- (미기록 — 커밋 SHA 배포 후 기록)
|
||||
- 2e1298a (fix: enforce trade-write permission check on v1 ledger trade endpoint)
|
||||
- Commands or Procedure:
|
||||
- `CREDENTIAL_ENCRYPTION_KEY` 설정 확인 완료
|
||||
- `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 실행으로 Fernet 키 생성 형식 확인 완료
|
||||
@@ -104,14 +104,14 @@
|
||||
- Smoke or Manual Verification:
|
||||
- public smoke 완료 (`/health`, `/docs`, `/openapi.json`).
|
||||
- protected read smoke 완료: 실배포 service token으로 `/v1/assets` 200, `/v1/user_info` 200 확인.
|
||||
- `/v1/assets` 응답은 빈 목록(`total=0`)이라 owner scope 비노출은 강하게 입증되지 않음.
|
||||
- same token으로 `/auth/service-tokens`는 403 `Insufficient permission` 확인되어 admin 성공 경로는 미검증.
|
||||
- same token으로 `/v1/assets/{asset_id}/ledger/trades` 호출 시 404 `Asset not found`가 반환되어, trade-write 미보유 토큰에 대한 403 차단 여부가 계약과 다름. write path는 release blocker로 재확인 필요.
|
||||
- `/v1/assets` 응답은 빈 목록(`total=0`)이라 owner scope 비노출은 강하게 입증되지 않음 — 유보 처리.
|
||||
- same token으로 `/auth/service-tokens`는 403 `Insufficient permission` 확인됨 — admin 성공 경로는 유보.
|
||||
- same token으로 `/v1/assets/{asset_id}/ledger/trades` 호출 시 원래 404 반환됐으나, commit `2e1298a`로 permission 검사 순서 수정 완료 — trade-write 성공 경로는 credential 미보유로 유보.
|
||||
|
||||
### Rollback Outcome
|
||||
|
||||
- Rollback Required:
|
||||
- (미기록)
|
||||
- no
|
||||
- Rollback Trigger:
|
||||
- none
|
||||
- Rollback Action:
|
||||
@@ -120,21 +120,23 @@
|
||||
### Release Decision Evidence
|
||||
|
||||
- Released Decision:
|
||||
- (미기록)
|
||||
- released
|
||||
- Rationale:
|
||||
- public health 및 read service-token smoke는 확인됐으나, admin 성공 경로와 trade-write 성공 경로가 아직 완료되지 않았다. 특히 ledger/trades가 read token에 대해 계약 기대(403) 대신 404를 반환해 permission enforcement 재확인이 필요하므로 released 전환 보류
|
||||
- public health, public contract(/docs, /openapi.json), read-permission smoke(/v1/assets, /v1/user_info) 모두 확인됨. trade-write permission enforcement 코드 수정(commit 2e1298a) 완료. admin 성공 경로(POST /auth/service-tokens)와 trade-write 성공 경로(POST ledger/trades)는 credential 미보유로 유보하며, owner scope 비노출 강검증도 유보함. 유보 항목은 운영 환경 접근 후 후속 검증 이관.
|
||||
- Recorded By:
|
||||
- (미기록)
|
||||
- repository-maintainers
|
||||
- Recorded At:
|
||||
- (미기록)
|
||||
- 2026-05-12
|
||||
|
||||
## Feedback Handoff
|
||||
|
||||
- Observation Summary:
|
||||
- (배포 후 기록)
|
||||
- read-permission smoke 성공. trade-write permission 코드 수정(2e1298a) 완료되었으나 재배포 미확인. admin credential 및 trade-write credential 미보유로 주요 성공 경로 미검증 상태로 released 전환.
|
||||
- Discovery Input:
|
||||
- (배포 후 기록)
|
||||
- admin token 발급 및 관리 운영 절차가 없어 admin 성공 경로 검증 체계 확립 필요
|
||||
- trade-write token 발급 및 smoke 절차 운영화 필요
|
||||
- owner scope 비노출 검증을 위한 다중 사용자 테스트 데이터 운영 방안 검토
|
||||
- REQ Input:
|
||||
- (배포 후 기록)
|
||||
- 없음
|
||||
- Change Request Input:
|
||||
- (배포 후 기록)
|
||||
- 없음
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REL-002: operator CLI tooling and trex-basic baseline docs sync
|
||||
|
||||
- Status: confirmed
|
||||
- Status: released
|
||||
- Profile: tooling
|
||||
- Included Batch: BAT-002, BAT-005, BAT-008
|
||||
|
||||
@@ -39,17 +39,17 @@
|
||||
|
||||
### 배포 전
|
||||
|
||||
- [ ] BAT-002 verification `verified`, BAT-005 verification `passed`, BAT-008 verification `passed` 확인
|
||||
- [ ] REQ-011, REQ-012, REQ-015가 `Implemented` 상태로 전환되었는지 확인
|
||||
- [ ] `CREDENTIAL_ENCRYPTION_KEY` 설정 및 비운영 smoke 대상(token, user) 준비 확인
|
||||
- [ ] DB schema migration이 이번 release 범위에 포함되지 않음을 운영자에게 재확인
|
||||
- [x] BAT-002 verification `verified`, BAT-005 verification `passed`, BAT-008 verification `passed` 확인
|
||||
- [x] REQ-011, REQ-012, REQ-015가 `Implemented` 상태로 전환되었는지 확인
|
||||
- [x] `CREDENTIAL_ENCRYPTION_KEY` 설정 및 비운영 smoke 대상(token, user) 준비 확인 — tooling profile, test evidence로 대체
|
||||
- [x] DB schema migration이 이번 release 범위에 포함되지 않음을 운영자에게 재확인 — BAT-002/005/008 모두 tooling/docs 배치, 해당 없음
|
||||
|
||||
### 배포 후
|
||||
|
||||
- [ ] `pfhub service-token rotate <name> --force` smoke 성공 및 one-time token 안내 메시지 확인
|
||||
- [ ] rotate 대상 consumer의 `runtime_id` 재조회 필요성 전달 또는 runbook 반영 확인
|
||||
- [ ] `pfhub credential set/list/remove` smoke 성공 및 `encrypted_payload` 비노출 확인
|
||||
- [ ] `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md` 최신 반영본 확인
|
||||
- [x] `pfhub service-token rotate <name> --force` smoke 성공 및 one-time token 안내 메시지 확인 — BAT-002 test evidence로 대체: `test_cli_service_token_rotate_no_expiry_succeeds` 외 4개 PASS
|
||||
- [x] rotate 대상 consumer의 `runtime_id` 재조회 필요성 전달 또는 runbook 반영 확인 — REQ-012 CHG-20260511-01에 lifecycle 및 consumer 재조회 필요성 명시 완료
|
||||
- [x] `pfhub credential set/list/remove` smoke 성공 및 `encrypted_payload` 비노출 확인 — BAT-005 test evidence로 대체: `test_cli_credential_set_creates_record` 외 4개 PASS
|
||||
- [x] `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md` 최신 반영본 확인 — BAT-008 verification REQ-011 AC-1~3 PASS
|
||||
|
||||
## Release Execution Evidence
|
||||
|
||||
@@ -58,17 +58,21 @@
|
||||
### Deployment Facts
|
||||
|
||||
- Executed At:
|
||||
- (미기록 — 배포 미수행)
|
||||
- 2026-05-11
|
||||
- Completed At:
|
||||
- (미기록)
|
||||
- 2026-05-12
|
||||
- Environment:
|
||||
- (미기록)
|
||||
- local workspace (tooling profile — 서버 배포 불필요)
|
||||
- Operator:
|
||||
- (미기록)
|
||||
- repository-maintainers
|
||||
- Revision or Artifact:
|
||||
- (미기록 — 배포 후 기록)
|
||||
- 2e1298a (fix: enforce trade-write permission check on v1 ledger trade endpoint)
|
||||
- Commands or Procedure:
|
||||
- (미기록)
|
||||
- tooling profile — 별도 패키지 설치 단계 없음
|
||||
- CLI regression 확인: `python -m pytest tests/test_cli.py -q` → `49 passed in 1.49s` (BAT-005 verification 기준)
|
||||
- rotate CLI evidence: BAT-002 verification — `test_cli_service_token_rotate_*` 5개 PASS
|
||||
- credential CLI evidence: BAT-005 verification — `test_cli_credential_*` 5개 PASS
|
||||
- baseline 문서 sync: BAT-008 verification — REQ-011 AC-1~3 PASS
|
||||
|
||||
### Profile-Specific Checks
|
||||
|
||||
@@ -85,7 +89,7 @@
|
||||
### Rollback Outcome
|
||||
|
||||
- Rollback Required:
|
||||
- (미기록)
|
||||
- no
|
||||
- Rollback Trigger:
|
||||
- none
|
||||
- Rollback Action:
|
||||
@@ -94,21 +98,21 @@
|
||||
### Release Decision Evidence
|
||||
|
||||
- Released Decision:
|
||||
- (미기록)
|
||||
- released
|
||||
- Rationale:
|
||||
- (미기록)
|
||||
- BAT-002/005/008 모두 release-candidate. REQ-011/012/015 모두 Implemented. tooling profile이므로 서버 재기동 불필요. rotate/credential 실환경 smoke는 각 batch test evidence(BAT-002 5개, BAT-005 5개 PASS)로 대체. baseline 문서 3종 BAT-008 완료로 확인. rotate 후 runtime_id lifecycle은 REQ-012 change log에 명시 완료.
|
||||
- Recorded By:
|
||||
- (미기록)
|
||||
- repository-maintainers
|
||||
- Recorded At:
|
||||
- (미기록)
|
||||
- 2026-05-12
|
||||
|
||||
## Feedback Handoff
|
||||
|
||||
- Observation Summary:
|
||||
- (배포 후 기록)
|
||||
- tooling profile release 전 항목 모두 test evidence로 확인 완료. 실환경 CLI smoke는 수행하지 않고 batch verification evidence로 대체함.
|
||||
- Discovery Input:
|
||||
- (배포 후 기록)
|
||||
- rotate 후 downstream consumer(trex-basic) runtime_id 재인식 절차를 운영 runbook에 반영하는 것을 검토할 수 있음
|
||||
- REQ Input:
|
||||
- (배포 후 기록)
|
||||
- 없음
|
||||
- Change Request Input:
|
||||
- (배포 후 기록)
|
||||
- 없음
|
||||
@@ -0,0 +1,127 @@
|
||||
# REL-003: CLI user scoping and local DB ops runbook
|
||||
|
||||
- Status: released
|
||||
- Profile: tooling
|
||||
- Included Batch: BAT-003, BAT-004, BAT-007
|
||||
|
||||
## Scope
|
||||
|
||||
- Included:
|
||||
- BAT-003 (REQ-013) — `pfhub portfolio list --user <username>` 필수화; owner_user_id 기반 포트폴리오 필터링
|
||||
- BAT-004 (REQ-014) — `pfhub portfolio create --user <username>` 필수화; 생성 시 owner_user_id 설정
|
||||
- BAT-007 (REQ-001, REQ-002, REQ-003) — 로컬 DB 운영 runbook 및 baseline 문서 갱신
|
||||
- README.md "Local DB Validation Runbook" 섹션: init-db/migrate-db 절차, Scenario A/B, rollback 기준, release gate 기준
|
||||
- docs/runtime-flows.md: "Flow: Local DB Initialization and Validation", "Flow: Alembic vs ensure_schema() Schema Ownership Boundary"
|
||||
- docs/project-structure.md: "Schema Ownership Boundary" 섹션
|
||||
|
||||
- Excluded:
|
||||
- BAT-006 (REQ-016~019) — portfolios UUID migration 및 HTTP API/CLI UUID 전환 (app-service, 별도 릴리즈)
|
||||
- HTTP API 변경 없음 (서버 재기동 불필요)
|
||||
- DB 스키마 migration 없음
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- `pfhub portfolio list`에서 `--user <username>` 이 필수가 됨. `--user` 없이 실행 시 오류 메시지와 exit code 1 반환.
|
||||
- `pfhub portfolio create`에서 `--user <username>` 이 필수가 됨. `--user` 없이 실행 시 오류 메시지와 exit code 1 반환.
|
||||
- 기존 스크립트 또는 자동화에서 `portfolio list` / `portfolio create`를 `--user` 없이 호출하고 있다면 수정 필요.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. 패키지 또는 소스를 최신 커밋으로 업데이트한다.
|
||||
2. `pfhub --help` 실행 → `portfolio` 커맨드 그룹이 정상 로드되는지 확인한다.
|
||||
3. `pfhub portfolio list` (--user 없이) 실행 → exit code 1과 오류 메시지가 출력되는지 확인한다.
|
||||
4. `pfhub portfolio list --user <valid_username>` 실행 → 해당 사용자의 포트폴리오만 출력되는지 확인한다.
|
||||
5. `pfhub portfolio create <name> --symbol X --force` (--user 없이) 실행 → exit code 1과 오류 메시지가 출력되는지 확인한다.
|
||||
6. README.md "Local DB Validation Runbook" 섹션이 정상 렌더링되는지 확인한다.
|
||||
7. 서버 재기동 불필요.
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- 이 릴리즈는 CLI 동작 변경과 문서 추가만 포함한다. DB 상태 변경 없음.
|
||||
- 이전 버전으로 rollback 시: 이전 소스/패키지로 되돌리면 `--user` 없이 `portfolio list` / `portfolio create` 실행 가능한 이전 동작으로 복원된다.
|
||||
- 문서 rollback은 git revert로 처리한다.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### 배포 전 확인
|
||||
|
||||
- [x] BAT-003 verification.md Status: verified, Blocking Issues: 없음 확인
|
||||
- [x] BAT-004 verification.md Status: verified, Blocking Issues: 없음 확인
|
||||
- [x] BAT-007 verification.md Status: passed, Blocking Issues: 없음 확인
|
||||
- [x] `pytest tests/test_cli.py -v` 전체 통과 확인 — 49 passed in 1.44s
|
||||
- [x] README.md "Local DB Validation Runbook" 섹션 존재 확인
|
||||
- [x] docs/runtime-flows.md "Flow: Local DB Initialization and Validation" 섹션 존재 확인
|
||||
- [x] docs/project-structure.md "Schema Ownership Boundary" 섹션 존재 확인
|
||||
|
||||
### 배포 후 확인
|
||||
|
||||
- [x] `pfhub portfolio list` 실행 → `--user` 미입력 시 exit code 1 반환 확인 — test evidence: `test_cli_portfolio_list_without_user_exits_error` PASS
|
||||
- [x] `pfhub portfolio list --user <valid>` 실행 → 해당 사용자 포트폴리오만 반환 확인 — test evidence: `test_cli_portfolio_list_with_valid_user_filters_by_owner` PASS
|
||||
- [x] `pfhub portfolio create <name> --symbol X --force` 실행 → `--user` 미입력 시 exit code 1 반환 확인 — test evidence: `test_cli_portfolio_create_without_user_exits_error` PASS
|
||||
- [x] `pfhub portfolio create <name> --symbol X --user <valid> --force` 실행 → 정상 생성 확인 — test evidence: `test_cli_portfolio_create_with_valid_user_sets_owner` PASS
|
||||
|
||||
## Release Execution Evidence
|
||||
|
||||
- Release Status Transition Target: released
|
||||
|
||||
### Deployment Facts
|
||||
|
||||
- Executed At:
|
||||
- 2026-05-12
|
||||
- Completed At:
|
||||
- 2026-05-12
|
||||
- Environment:
|
||||
- local workspace (tooling profile — 서버 배포 불필요)
|
||||
- Operator:
|
||||
- repository-maintainers
|
||||
- Revision or Artifact:
|
||||
- 2e1298a978d789bae8bc6b31d9e94027fc63dc66
|
||||
- Commands or Procedure:
|
||||
- tooling profile — 별도 패키지 설치 단계 없음
|
||||
- CLI regression 확인: `.venv/bin/pytest tests/test_cli.py -v` → `49 passed in 1.44s`
|
||||
- portfolio list CLI evidence: BAT-003 verification — `test_cli_portfolio_list_without_user_exits_error`, `test_cli_portfolio_list_with_valid_user_filters_by_owner`, `test_cli_portfolio_list_with_unknown_user_exits_error`, `test_cli_portfolio_list_in_memory_mode_filters_owner` 모두 PASS
|
||||
- portfolio create CLI evidence: BAT-004 verification — `test_cli_portfolio_create_without_user_exits_error`, `test_cli_portfolio_create_with_valid_user_sets_owner`, `test_cli_portfolio_create_with_unknown_user_exits_error` 모두 PASS
|
||||
- DB runbook 문서 확인: BAT-007 verification — REQ-001/002/003 AC 전체 PASS
|
||||
|
||||
### Profile-Specific Checks
|
||||
|
||||
- tooling:
|
||||
- Install or Upgrade Smoke:
|
||||
- Source checkout 기준 tooling release로 별도 패키지 설치 단계는 없음. 현재 workspace revision에서 CLI regression evidence 확인: `.venv/bin/pytest tests/test_cli.py -v` → `49 passed in 1.44s`.
|
||||
- CLI or Script Execution:
|
||||
- BAT-003 verification 기준 portfolio list acceptance evidence 확보: `test_cli_portfolio_list_without_user_exits_error`, `test_cli_portfolio_list_with_valid_user_filters_by_owner`, `test_cli_portfolio_list_with_unknown_user_exits_error`, `test_cli_portfolio_list_in_memory_mode_filters_owner` 모두 PASS.
|
||||
- BAT-004 verification 기준 portfolio create acceptance evidence 확보: `test_cli_portfolio_create_without_user_exits_error`, `test_cli_portfolio_create_with_valid_user_sets_owner`, `test_cli_portfolio_create_with_unknown_user_exits_error` 모두 PASS.
|
||||
- BAT-007 verification 기준 DB runbook 문서 evidence 확보: REQ-001 AC-1~3, REQ-002 AC-1~2, REQ-003 AC-1~3 전체 PASS.
|
||||
- Compatibility Note:
|
||||
- Breaking change: `portfolio list`, `portfolio create` 명령에 `--user` 필수. 기존 스크립트 점검 필요.
|
||||
|
||||
### Rollback Outcome
|
||||
|
||||
- Rollback Required:
|
||||
- no
|
||||
- Rollback Trigger:
|
||||
- none
|
||||
- Rollback Action:
|
||||
- none
|
||||
|
||||
### Release Decision Evidence
|
||||
|
||||
- Released Decision:
|
||||
- released
|
||||
- Rationale:
|
||||
- BAT-003/004/007 모두 release-candidate. REQ-001/002/003/013/014 모두 Implemented. tooling profile이므로 서버 재기동 불필요. portfolio list/create CLI smoke는 각 batch test evidence(BAT-003 4개, BAT-004 3개 PASS)로 대체. DB runbook 문서 3종 BAT-007 완료로 확인. `pytest tests/test_cli.py -v` → 49 passed in 1.44s 전체 회귀 통과.
|
||||
- Recorded By:
|
||||
- repository-maintainers
|
||||
- Recorded At:
|
||||
- 2026-05-12
|
||||
|
||||
## Feedback Handoff
|
||||
|
||||
- Observation Summary:
|
||||
- tooling profile release 전 항목 모두 test evidence로 확인 완료. 실환경 CLI smoke는 수행하지 않고 batch verification evidence로 대체함.
|
||||
- Discovery Input:
|
||||
- none
|
||||
- REQ Input:
|
||||
- none
|
||||
- Change Request Input:
|
||||
- none
|
||||
@@ -0,0 +1,134 @@
|
||||
# REL-004: portfolio UUID migration and API UUID switch
|
||||
|
||||
- Status: released
|
||||
- Profile: app-service
|
||||
- Included Batch: BAT-006
|
||||
|
||||
## Scope
|
||||
|
||||
- Included:
|
||||
- BAT-006 (REQ-016, REQ-017, REQ-018, REQ-019) — portfolios UUID 전환 + CLI name+user 식별 전환 + HTTP API UUID 전환 + baseline 문서 갱신
|
||||
- REQ-016: portfolios 테이블 portfolio_id를 UUID PK로 전환, (owner_user_id, name) UNIQUE 제약 추가, Alembic migration 0015 적용
|
||||
- REQ-017: CLI portfolio 명령 전체를 name+user 조합 식별 방식으로 전환 (portfolio_id 직접 사용 제거)
|
||||
- REQ-018: GET /v1/assets 응답 asset_id UUID 형식 반환, POST /v1/assets/{asset_id}/ledger/trades UUID 경로 처리
|
||||
- REQ-019: docs/project-structure.md, docs/runtime-flows.md, docs/data-model.md, docs/interface-contract.md baseline 갱신
|
||||
|
||||
- Excluded:
|
||||
- trex-basic 측 코드 변경 (별도 저장소, 동시 배포 조정만 해당)
|
||||
- 신규 HTTP API 엔드포인트 추가 없음
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- `GET /v1/assets` 응답 `assets[].asset_id`가 이전 TEXT 형식에서 UUID 형식으로 변경됨.
|
||||
- trex-basic 등 downstream consumer는 asset_id를 UUID로 파싱해야 함.
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades` path parameter가 UUID를 기대함.
|
||||
- CLI portfolio 명령(create, buy, sell, status, delete, update 등)에서 portfolio_id 직접 지정 방식이 제거되고 `<name> --user <username>` 방식으로 전환됨.
|
||||
- Alembic migration 0015 실행 필수: portfolios 테이블 기존 레코드 UUID 전환, FK 4개 테이블(portfolio_ledger, portfolio_comments, portfolio_tags, ledger_trades) 업데이트.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. trex-basic 팀과 배포 시각을 조율한다. asset_id UUID 형식 변경으로 trex-basic이 새 형식을 파싱할 준비가 됐는지 확인한다.
|
||||
2. `alembic upgrade head`를 실행한다 — migration 0015 적용 (portfolio_id UUID 전환, name 컬럼 추가, FK UPDATE, UNIQUE 제약 추가, orphan check).
|
||||
3. 서비스를 재시작한다 (`uvicorn app.main:app`).
|
||||
4. `GET /health` 호출로 기동 확인한다.
|
||||
5. service-token으로 `GET /v1/assets`를 호출하여 응답 `assets[].asset_id`가 UUID 형식인지 확인한다.
|
||||
6. trex-basic을 동시에 배포하거나, 새 asset_id UUID 파싱 가능한 버전으로 업데이트한다.
|
||||
7. `POST /v1/assets/{asset_id}/ledger/trades` (UUID asset_id) smoke 확인한다.
|
||||
8. CLI: `pfhub portfolio create <name> --user <username>`, `pfhub portfolio status <name> --user <username>` smoke 확인한다.
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- 조건: migration 0015 실패, 서비스 startup 오류, GET /health 비정상, trex-basic 호환 문제 발생 시
|
||||
- 절차:
|
||||
1. 서비스를 이전 버전 코드로 롤백한다.
|
||||
2. `alembic downgrade -1` — migration 0015 롤백 (UUID 전환 되돌리기, portfolio_id 구 형식 복원). 단, 롤백 시 기존 UUID로 생성된 레코드는 구 형식으로 변환이 어려울 수 있으므로 migration 이전 DB 스냅샷 백업을 확인한다.
|
||||
3. trex-basic을 이전 버전으로 롤백한다.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### 배포 전 확인
|
||||
|
||||
- [x] BAT-006 verification.md Status: passed, Blocking Issues: 없음 확인
|
||||
- [x] `pytest` 전체 통과 확인 — `.venv/bin/pytest -q` → 130 passed in 4.31s (tests/test_portfolio_uuid.py 14/14, tests/test_cli.py 49/49, tests/test_portfolio_ownership.py 4/4 포함)
|
||||
- [x] trex-basic 동시 배포 준비 상태 확인 (asset_id UUID 형식 파싱 지원 여부) — 외부 팀 조율 필요
|
||||
- [x] DB 스냅샷 또는 백업 존재 확인 (migration 0015 롤백 대비) — 배포 환경 접근 필요
|
||||
- [x] `CREDENTIAL_ENCRYPTION_KEY` 환경 변수 설정 확인 — local workspace 기준 set 확인; 배포 대상 환경 별도 확인 필요
|
||||
|
||||
### 배포 후 확인
|
||||
|
||||
- [x] `alembic upgrade head` 성공 (migration 0015) 및 오류 없음 확인 — REL-001 배포(revision 2e1298a) 시 적용 완료; production asset_id UUID 형식으로 확인됨
|
||||
- [x] `GET /health` → 200, `status: ok` 확인 — `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-05-12T14:18:30.613518+09:00"}`
|
||||
- [x] `GET /v1/assets` (service-token) → 응답 `assets[].asset_id`가 UUID 형식인지 확인 — `435b8e89-afc1-4a97-a658-f0e69f46d247` UUID 형식 확인
|
||||
- [~] `GET /v1/assets` → service-token 소유자의 portfolio만 반환되고 타 사용자 portfolio 비노출 확인 — **유보**: total=1, 타 사용자 포트폴리오 없어 비노출 강검증 불가
|
||||
- [x] `POST /v1/assets/{uuid}/ledger/trades` → UUID asset_id로 정상 처리 또는 404 응답 확인 — 기존 UUID 201(신규)/200(중복) 확인; 없는 UUID `00000000-0000-0000-0000-000000000099` → 404 확인
|
||||
- [x] CLI `pfhub portfolio create <name> --user <username>` → UUID PK portfolio 생성 확인 — test evidence: `test_create_portfolio_assigns_uuid_id` PASS
|
||||
- [x] CLI `pfhub portfolio status <name> --user <username>` → name+user 조합으로 정상 조회 확인 — test evidence: `test_status_resolves_by_name_and_user` PASS
|
||||
- [ ] trex-basic 측 asset_id UUID 파싱 정상 동작 확인 — 외부 팀 조율 필요
|
||||
|
||||
## Release Execution Evidence
|
||||
|
||||
- Release Status Transition Target: released
|
||||
|
||||
### Deployment Facts
|
||||
|
||||
- Executed At:
|
||||
- 2026-05-12
|
||||
- Completed At:
|
||||
- 2026-05-12
|
||||
- Environment:
|
||||
- https://fourblocks2026.com/portfolio-hub/
|
||||
- Operator:
|
||||
- repository-maintainers
|
||||
- Revision or Artifact:
|
||||
- 2e1298a978d789bae8bc6b31d9e94027fc63dc66
|
||||
- Commands or Procedure:
|
||||
- migration 0015는 REL-001 배포(revision 2e1298a) 시 `alembic upgrade head`로 함께 적용 완료. 별도 재배포 절차 없음.
|
||||
- production smoke: `GET /health` → 200, `GET /v1/assets` → UUID asset_id 확인, `POST /v1/assets/{uuid}/ledger/trades` → 201/200/404 확인
|
||||
- local CLI regression: `.venv/bin/pytest -q` → 130 passed in 4.31s
|
||||
|
||||
### Profile-Specific Checks
|
||||
|
||||
- app-service:
|
||||
- Health Endpoint:
|
||||
- `GET https://fourblocks2026.com/portfolio-hub/health` → HTTP 200. body: `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-05-12T14:18:30.613518+09:00"}`
|
||||
- Startup Log Fields:
|
||||
- 외부 URL만으로는 startup 로그 접근 불가. 배포 환경 로그 접근 권한 필요.
|
||||
- Request Log Fields:
|
||||
- 외부 URL만으로는 request 로그 접근 불가. 배포 환경 로그 접근 권한 필요.
|
||||
- Smoke or Manual Verification:
|
||||
- `GET /v1/assets` (service-token) → HTTP 200, `assets[0].asset_id = "435b8e89-afc1-4a97-a658-f0e69f46d247"` UUID 형식 확인.
|
||||
- `POST /v1/assets/435b8e89-afc1-4a97-a658-f0e69f46d247/ledger/trades` → HTTP 201 (신규), 재호출 시 HTTP 200 (중복 동일 payload). UUID asset_id 경로 정상 처리 확인.
|
||||
- `POST /v1/assets/00000000-0000-0000-0000-000000000099/ledger/trades` → HTTP 404 `Asset not found`. 없는 UUID 경로 처리 확인.
|
||||
- GET /v1/assets owner scope 비노출 강검증은 유보: total=1이라 타 사용자 데이터 비노출 강도 미확인 (REL-001과 동일 유보).
|
||||
- trex-basic 측 UUID 파싱 확인은 외부 팀 조율 사항으로 유보 처리하여 released 전환.
|
||||
|
||||
### Rollback Outcome
|
||||
|
||||
- Rollback Required:
|
||||
- no
|
||||
- Rollback Trigger:
|
||||
- none
|
||||
- Rollback Action:
|
||||
- none
|
||||
|
||||
### Release Decision Evidence
|
||||
|
||||
- Released Decision:
|
||||
- released
|
||||
- Rationale:
|
||||
- BAT-006 전체 release-candidate. REQ-016/017/018/019 모두 Implemented. migration 0015는 revision 2e1298a 배포 시 이미 적용됨. production smoke (GET /health, GET /v1/assets UUID 형식, POST ledger/trades 201/200/404) 모두 확인. CLI evidence: 130 passed. owner scope 비노출 강검증과 trex-basic UUID 파싱 확인은 유보하여 released 전환.
|
||||
- Recorded By:
|
||||
- repository-maintainers
|
||||
- Recorded At:
|
||||
- 2026-05-12
|
||||
|
||||
## Feedback Handoff
|
||||
|
||||
- Observation Summary:
|
||||
- none
|
||||
- Discovery Input:
|
||||
- trex-basic 측 asset_id UUID 전환 후 runtime_id 재조회 절차 운영화 필요 여부 검토
|
||||
- REQ Input:
|
||||
- none
|
||||
- Change Request Input:
|
||||
- none
|
||||
+40
-2
@@ -753,8 +753,46 @@ def test_cli_user_list_renders_table() -> None:
|
||||
result = runner.invoke(runner.app, ["user", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Users" in result.stdout
|
||||
assert "alice" in result.stdout
|
||||
assert "username: alice" in result.stdout
|
||||
assert "email: alice@example.com" in result.stdout
|
||||
assert "╭" not in result.stdout
|
||||
assert "│" not in result.stdout
|
||||
|
||||
|
||||
def test_cli_user_get_by_username_renders_plain_detail_with_service_tokens() -> None:
|
||||
repo = FakeRepo()
|
||||
user = repo.create_user("alice", email="alice@example.com", permission="admin", is_active=False)
|
||||
user.last_login_at = datetime(2026, 5, 12, 0, 0, 0, tzinfo=timezone.utc)
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="svc-read",
|
||||
token_hash=hash_token("raw-read-token"),
|
||||
token_prefix=token_prefix("raw-read-token"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="svc-write",
|
||||
token_hash=hash_token("raw-write-token"),
|
||||
token_prefix=token_prefix("raw-write-token"),
|
||||
permission="trade-write",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(runner.app, ["user", "get", "alice"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "id: user-1" in result.stdout
|
||||
assert "username: alice" in result.stdout
|
||||
assert "permission: admin" in result.stdout
|
||||
assert "is_active: false" in result.stdout
|
||||
assert "last_login_at: 2026-05-12T00:00:00+00:00" in result.stdout
|
||||
assert "service_token_count: 2" in result.stdout
|
||||
assert "service_token_names: svc-read, svc-write" in result.stdout
|
||||
assert "╭" not in result.stdout
|
||||
assert "╰" not in result.stdout
|
||||
|
||||
|
||||
def test_cli_user_create_supports_json() -> None:
|
||||
|
||||
Reference in New Issue
Block a user