From 54e4fe42134f335e867da31d88b0d8e4d71ac08a Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Sun, 13 Sep 2026 14:27:03 +0900 Subject: [PATCH] fix: sync image migration inventory to 0031 --- docs/container-operations.md | 14 ++++++++------ docs/data-model.md | 2 +- docs/project-structure.md | 4 ++-- scripts/image_release.py | 4 ++-- tests/release/test_image_release.py | 8 ++++---- tests/release/test_release_contracts.py | 2 +- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/container-operations.md b/docs/container-operations.md index 1750e5c..179be43 100644 --- a/docs/container-operations.md +++ b/docs/container-operations.md @@ -82,30 +82,32 @@ The existing database variable names remain `TIMESCALEDB_*`; v0.2.1 deliberately Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations. -## One-shot migration and 0029 -> 0030 policy +## One-shot migration and 0029 -> 0031 policy -The installed image contains exactly **30** migration files and one Alembic head, `20260820_0030`. The application default command does not invoke Alembic. Migration is a separate pre-start operation using the exact same digest and database environment: +The installed image contains exactly **31** migration files and one Alembic head, `20260913_0031`. The application default command does not invoke Alembic. Migration is a separate pre-start operation using the exact same digest and database environment: 1. back up the database and record the current immutable image digest and `alembic_version`; 2. require the normal upgrade origin `20260814_0029`; for an older revision, review and apply the full ordered chain first; 3. stop writes or otherwise establish an operator-approved maintenance boundary; 4. execute `pfhub migrate-db` once with restart disabled; 5. stop rollout on any nonzero result and preserve sanitized logs; -6. run `pfhub --format json health-check` and require both `database_revision` and `expected_revision` to equal `20260820_0030`; +6. run `pfhub --format json health-check` and require both `database_revision` and `expected_revision` to equal `20260913_0031`; 7. start application replicas and verify `/health`, `/openapi.json`, authenticated caller paths, and strategy data; -8. a repeated `pfhub migrate-db` is expected to be idempotent and remain at `0030`, but it is verification—not an application-start hook. +8. a repeated `pfhub migrate-db` is expected to be idempotent and remain at `0031`, but it is verification—not an application-start hook. Migration `0029` is additive/idempotent around the strategy catalog: it preserves existing strategy IDs and portfolio references, reuses the canonical `target_value`/`{}` strategy, and backfills catalog data. Before migration, inspect duplicate or invalid strategy references and take a backup. After migration, verify strategy listing and portfolio active-strategy fields before resuming writes. v0.2.1 includes migration `0030`, an idempotent index on `(portfolio_id, recorded_at DESC, ledger_entry_id DESC)` for unified-ledger keyset reads. Operators at `0029` must run the same-image one-shot migration before rollout; it advances to `20260820_0030` without mutating ledger rows. -A pre-Alembic legacy schema is detected only when all three tables `strategy_sets`, `portfolios`, and `portfolio_ledger` exist; the CLI stamps `0027` and then runs real upgrades through `0030`. This compatibility path must be used only after backup and schema review. A partially matching schema is not auto-stamped and must fail for operator investigation. +Migration `0031` is additive corporate-action storage. Operators must complete the same-image migration through `20260913_0031` before using the corporate-action endpoint; the endpoint is not available for traffic until readiness reports that head. + +A pre-Alembic legacy schema is detected only when all three tables `strategy_sets`, `portfolios`, and `portfolio_ledger` exist; the CLI stamps `0027` and then runs real upgrades through `0031`. This compatibility path must be used only after backup and schema review. A partially matching schema is not auto-stamped and must fail for operator investigation. Image rollback is **not** database rollback. Selecting older application bytes does not reverse schema/data changes, tokens, or side effects. Database restore, downgrade, or forward-fix requires separate review and authorization; no automatic downgrade is provided. ## API verification and known caller blockers -v0.2.1 preserves Bearer authentication and adds all-source unified-ledger reads. The release gate runs the complete portfolio-hub source test suite (including auth success/failure, strategy catalog/data, portfolio and ledger APIs), then verifies the built image's health/OpenAPI surface, exact 30-file migration inventory, isolated PostgreSQL migration twice, readiness at `0030`, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it. +v0.2.1 preserves Bearer authentication and adds all-source unified-ledger reads. The release gate runs the complete portfolio-hub source test suite (including auth success/failure, strategy catalog/data, portfolio and ledger APIs), then verifies the built image's health/OpenAPI surface, exact 31-file migration inventory, isolated PostgreSQL migration twice, readiness at `0031`, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it. The strategy catalog API remains additive: strategy responses expose `nick`, admin create/update requests may set it, and portfolio responses expose the current strategy as `strategy_nick`. Existing clients may omit `nick`; supplied values are trimmed and blank-only values are rejected. trex-web v0.2.3 displays `strategy_nick`, but that dependency does not resolve or waive the separate caller blockers below. diff --git a/docs/data-model.md b/docs/data-model.md index 18b04c1..7134eec 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -372,7 +372,7 @@ - Alembic revisions in migrations/versions/ are the ordered schema history for persistent environments. - app/repositories/postgres.py includes ensure_schema() for bootstrap safety, but long-term schema evolution should run through Alembic. -- Current Alembic head is `20260820_0030`; the preceding `0029` strategy columns, default seed, and constraints and the `0030` unified-ledger keyset index are guarded for ensure_schema-first and repeatable execution. +- Current Alembic head is `20260913_0031`; migration `0031` additively stores corporate actions for the corporate-action endpoint. Operators must migrate an environment to `0031` before using that endpoint. The preceding `0029` strategy columns, `0030` unified-ledger keyset index, and `0031` corporate-action storage are guarded for ensure_schema-first and repeatable execution. - Comments, tags, auth tokens, and ledger rows are normalized into dedicated tables keyed to users or portfolios. ## Current Gaps / Planned Changes diff --git a/docs/project-structure.md b/docs/project-structure.md index 3acf709..52f083c 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -138,13 +138,13 @@ |-----------|-----------|------| | ensure_schema() 전용 | `idx_auth_tokens_service_name_unique` (partial index, BAT-006/REQ-017: `AND revoked_at IS NULL` 조건 추가) | Alembic revision 20260520_0022에도 CONCURRENTLY 버전 존재 | | Alembic 전용 | `portfolios.market`, `auth_tokens.runtime_id`, `idx_auth_tokens_runtime_id_unique`, `broker_credentials`, `ledger_trades` | ensure_schema()에 없음 — v1 API 전제 조건 | -| 양쪽 공유 (IF NOT EXISTS 보호) | users, auth_tokens, strategy_sets (`status`, `revision`, `updated_at` 포함), portfolios, portfolio_ledger, portfolio_comments, portfolio_tags + 대부분의 인덱스 | ensure_schema() 우선 실행 후 Alembic `20260820_0030`까지 실행 시 충돌 없음; 기본 전략 seed/constraints와 unified-ledger keyset index도 존재 여부 guard 적용 | +| 양쪽 공유 (IF NOT EXISTS 보호) | users, auth_tokens, strategy_sets (`status`, `revision`, `updated_at` 포함), portfolios, portfolio_ledger, portfolio_comments, portfolio_tags + 대부분의 인덱스 | ensure_schema() 우선 실행 후 Alembic `20260913_0031`까지 실행 시 충돌 없음; 기본 전략 seed/constraints, unified-ledger keyset index, additive corporate-action storage도 존재 여부 guard 적용 | ### 허용 실행 순서 1. `init-db` 선행 → App 시작 → 정상 운영 (권장) 2. App 시작 (ensure_schema() 선행) → `migrate-db` 후속 실행 (legacy stamp 경로) -3. 이미 Alembic 관리 DB → `migrate-db` 증분 upgrade +3. 이미 Alembic 관리 DB → `migrate-db` 증분 upgrade through `20260913_0031` before using the corporate-action endpoint ### 운영 제약 diff --git a/scripts/image_release.py b/scripts/image_release.py index 497a82d..56729a4 100755 --- a/scripts/image_release.py +++ b/scripts/image_release.py @@ -25,8 +25,8 @@ COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") VERSION_REF_RE = re.compile(r"^refs/tags/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") IMAGE_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$") DIGEST_RE = re.compile(r"(?:^|\s)digest:\s*(sha256:[0-9a-f]{64})(?:\s|$)") -EXPECTED_MIGRATION_COUNT = 30 -EXPECTED_ALEMBIC_HEAD = "20260820_0030" +EXPECTED_MIGRATION_COUNT = 31 +EXPECTED_ALEMBIC_HEAD = "20260913_0031" @dataclass(frozen=True) class Result: diff --git a/tests/release/test_image_release.py b/tests/release/test_image_release.py index aa8625a..f07cfc0 100644 --- a/tests/release/test_image_release.py +++ b/tests/release/test_image_release.py @@ -33,8 +33,8 @@ class RecordingRunner: if command[:3] == ["docker", "run", "--rm"] and "urllib.request" in command[-1]: enabled = "TARGET:8000" not in command[-1] and "plain" not in command[-1] return image_release.Result('{"status":"ok","db_enabled":' + str(enabled).lower() + '}') - if command[:3] == ["docker", "run", "--rm"] and "alembic_version" in command[-1]: return image_release.Result("20260820_0030\n") - if command[:3] == ["docker", "run", "--rm"] and "health-check" in command: return image_release.Result('{"database_revision":"20260820_0030"}\n') + if command[:3] == ["docker", "run", "--rm"] and "alembic_version" in command[-1]: return image_release.Result("20260913_0031\n") + if command[:3] == ["docker", "run", "--rm"] and "health-check" in command: return image_release.Result('{"database_revision":"20260913_0031"}\n') return image_release.Result() def artifacts(tmp_path: Path): @@ -74,12 +74,12 @@ def test_project_version_must_match_release_tag(tmp_path): def test_installed_smoke_has_import_cli_exact_inventory_and_cleanup(): inventory = tuple( [f"202601{i:02d}_{i:04d}_migration.py" for i in range(1, 30)] - + ["20260820_0030_portfolio_ledger_keyset_index.py"] + + ["20260820_0030_portfolio_ledger_keyset_index.py", "20260913_0031_corporate_actions.py"] ) runner = RecordingRunner(); image_release.smoke_installed_image("candidate", inventory, runner, 17) runs = [x[0] for x in runner.commands if x[0][:2] == ["docker","run"]] assert len(runs) == 3 and all("--read-only" in x and "none" in x for x in runs) - assert "migration_inventory" in runs[-1][-1] and "20260820_0030" in runs[-1][-1] + assert "migration_inventory" in runs[-1][-1] and "20260913_0031" in runs[-1][-1] assert len([x for x in runner.commands if x[0][:3] == ["docker","rm","--force"]]) == 3 def _assert_exact_startup_cleanup(commands: list[list[str]]) -> None: diff --git a/tests/release/test_release_contracts.py b/tests/release/test_release_contracts.py index f847763..f3e7149 100644 --- a/tests/release/test_release_contracts.py +++ b/tests/release/test_release_contracts.py @@ -50,7 +50,7 @@ def test_workflow_actions_are_commit_pinned_and_verify_version_is_project_derive assert 'RELEASE_VERSION=v0.1.3' not in verify -def test_release_version_and_migration_contract_are_v021_head_0030(): +def test_release_version_and_migration_contract_has_current_head_0031(): project = (REPO / "pyproject.toml").read_text() config = (REPO / "app/core/config.py").read_text() migrations = sorted(