From 6e17a53b17d33010d0a84e086c182be756df5400 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 21 Aug 2026 17:11:24 +0900 Subject: [PATCH] chore(release): prepare portfolio-hub v0.2.0 --- app/core/config.py | 2 +- docs/container-operations.md | 34 ++++++++++++------------- pyproject.toml | 2 +- tests/release/test_release_contracts.py | 10 ++++---- uv.lock | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index b5fe252..83f517d 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -18,7 +18,7 @@ if env_file.exists(): class Settings(BaseSettings): app_name: str = "PortfolioHub" - app_version: str = "0.1.3" + app_version: str = "0.2.0" environment: str = "development" allowed_origins_raw: str = Field(default="http://localhost:3000", validation_alias="PORTFOLIOHUB_ALLOWED_ORIGINS") bootstrap_username: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_USERNAME") diff --git a/docs/container-operations.md b/docs/container-operations.md index 5680621..48672fe 100644 --- a/docs/container-operations.md +++ b/docs/container-operations.md @@ -1,11 +1,11 @@ -# PortfolioHub v0.1.3 container contract +# PortfolioHub v0.2.0 container contract ## Publication is not deployment -Only a requester-authorized push of the strict tag `v0.1.3` can publish this revision. The workflow checks that the tag equals `[project].version`, checks out `github.sha`, runs the complete Python suite, builds one `linux/amd64` image from `uv.lock`, and tests those exact bytes before publication. It gives the same image exactly two selectors: +Only a requester-authorized push of the strict tag `v0.2.0` can publish this revision. The workflow checks that the tag equals `[project].version`, checks out `github.sha`, runs the complete Python suite, builds one `linux/amd64` image from `uv.lock`, and tests those exact bytes before publication. It gives the same image exactly two selectors: - `ghcr.io/fourblocks2020/portfolio-hub:` -- `ghcr.io/fourblocks2020/portfolio-hub:v0.1.3` +- `ghcr.io/fourblocks2020/portfolio-hub:v0.2.0` It never publishes `latest`. Both selectors must report the same registry digest; deployment and rollback must use the immutable identity: @@ -13,9 +13,9 @@ It never publishes `latest`. Both selectors must report the same registry digest ghcr.io/fourblocks2020/portfolio-hub@sha256:<64-lowercase-hex-digest> ``` -OCI labels are fail-closed build inputs: `org.opencontainers.image.source`, the full 40-character `org.opencontainers.image.revision`, and strict `org.opencontainers.image.version=v0.1.3`. Publication does **not** deploy, replace, start, stop, or restart a production service. This repository provides no production Compose, Kubernetes, Helm, systemd, or Dockge configuration. +OCI labels are fail-closed build inputs: `org.opencontainers.image.source`, the full 40-character `org.opencontainers.image.revision`, and strict `org.opencontainers.image.version=v0.2.0`. Publication does **not** deploy, replace, start, stop, or restart a production service. This repository provides no production Compose, Kubernetes, Helm, systemd, or Dockge configuration. -The Dockerfile frontend, Python base image, uv image, and GitHub Actions are digest/commit pinned. The workflow intentionally uses `--provenance=false` to preserve the tested `docker save` archive and build-once/no-rebuild identity. Registry-attached provenance, SBOM, signing, and attestations are a nonblocking v0.1.3 supply-chain gap; they must not be added by rebuilding a different image in the publish job. +The Dockerfile frontend, Python base image, uv image, and GitHub Actions are digest/commit pinned. The workflow intentionally uses `--provenance=false` to preserve the tested `docker save` archive and build-once/no-rebuild identity. Registry-attached provenance, SBOM, signing, and attestations are a nonblocking v0.2.0 supply-chain gap; they must not be added by rebuilding a different image in the publish job. ## Image runtime @@ -66,46 +66,46 @@ Values are read at process start; changing any value requires a container restar | `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` | optional; empty | **yes** | bootstrap password; inject temporarily and remove after first successful start | | `PORTFOLIOHUB_ENVIRONMENT` | optional; `development` | no | reported environment metadata | | `PORTFOLIOHUB_APP_NAME` | optional; `PortfolioHub` | no | application/OpenAPI display name | -| `PORTFOLIOHUB_APP_VERSION` | do not override for v0.1.3 | `0.1.3` | no | health/OpenAPI version metadata | +| `PORTFOLIOHUB_APP_VERSION` | do not override for v0.2.0 | `0.2.0` | no | health/OpenAPI version metadata | -`REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_PASSWORD`, `LOKI_HOST`, `LOKI_PORT`, and `LOKI_ENABLED` are still parsed for source compatibility but the inspected v0.1.3 application does not open Redis or Loki connections. They are not production dependencies of this image. +`REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_PASSWORD`, `LOKI_HOST`, `LOKI_PORT`, and `LOKI_ENABLED` are still parsed for source compatibility but the inspected v0.2.0 application does not open Redis or Loki connections. They are not production dependencies of this image. ### Legacy and sibling mapping -The existing database variable names remain `TIMESCALEDB_*`; v0.1.3 deliberately does not invent a `DATABASE_URL` alias. Existing `PORTFOLIOHUB_{READ,TRADE_WRITE,ADMIN}_TOKENS` Bearer-token behavior remains compatible. `trex-core`'s `TREX_PORTFOLIO_HUB_BASE_URL` and TOML `portfolio_hub_token`, and `trex-web`'s server-only `PORTFOLIO_HUB_API_BASE_URL`, are **caller-side** settings: point them to `http://portfolio-hub:8000` (or the equivalent service URL) and do not inject them into this container. +The existing database variable names remain `TIMESCALEDB_*`; v0.2.0 deliberately does not invent a `DATABASE_URL` alias. Existing `PORTFOLIOHUB_{READ,TRADE_WRITE,ADMIN}_TOKENS` Bearer-token behavior remains compatible. `trex-core`'s `TREX_PORTFOLIO_HUB_BASE_URL` and TOML `portfolio_hub_token`, and `trex-web`'s server-only `PORTFOLIO_HUB_API_BASE_URL`, are **caller-side** settings: point them to `http://portfolio-hub:8000` (or the equivalent service URL) and do not inject them into this container. ## Liveness and readiness - **Liveness:** unauthenticated `GET /health` must return HTTP 200 with `status=ok`. It is intentionally process-only and does not query PostgreSQL. `db_enabled` only reports configuration; it is not readiness. - **Readiness:** run `pfhub --format json health-check` with the same runtime environment. With DB enabled it performs a repository ping, reads `alembic_version` without migration, compares it with the single packaged Alembic head, and exits nonzero on connectivity, missing schema, multiple heads, or revision mismatch. -- **API identity:** `GET /openapi.json` is stable for smoke verification. Health version is `0.1.3`. +- **API identity:** `GET /openapi.json` is stable for smoke verification. Health version is `0.2.0`. Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations. -## One-shot migration and 0028 -> 0029 policy +## One-shot migration and 0029 -> 0030 policy -The installed image contains exactly **29** migration files and one Alembic head, `20260814_0029`. 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 **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: 1. back up the database and record the current immutable image digest and `alembic_version`; -2. require the normal upgrade origin `20260715_0028`; for an older revision, review and apply the full ordered chain first; +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 `20260814_0029`; +6. run `pfhub --format json health-check` and require both `database_revision` and `expected_revision` to equal `20260820_0030`; 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 `0029`, but it is verification—not an application-start hook. +8. a repeated `pfhub migrate-db` is expected to be idempotent and remain at `0030`, 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.1.3 introduces no schema migration: the packaged inventory remains exactly 29 migration files and the single head remains `20260814_0029`. Operators already at `0029` must not expect or manufacture a new revision for this patch release. +v0.2.0 adds 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 `0028` and `0029`. 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. +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. 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.1.3 preserves Bearer authentication and the caller route surface observed in sibling source. 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 29-file migration inventory, isolated PostgreSQL migration twice, readiness at `0029`, persistent-volume restart, non-root/read-only operation, secret canary absence, and graceful SIGTERM. Publication consumes the tested archive without rebuilding it. +v0.2.0 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. 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/pyproject.toml b/pyproject.toml index 0bfb696..577c6d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "portfoliohub" -version = "0.1.3" +version = "0.2.0" description = "PortfolioHub API for portfolio management and ledger access" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/release/test_release_contracts.py b/tests/release/test_release_contracts.py index 45cdb79..9dbfed6 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_v013_head_0030(): +def test_release_version_and_migration_contract_are_v020_head_0030(): project = (REPO / "pyproject.toml").read_text() config = (REPO / "app/core/config.py").read_text() migrations = sorted( @@ -58,8 +58,8 @@ def test_release_version_and_migration_contract_are_v013_head_0030(): for path in (REPO / "app/db/alembic/versions").glob("*.py") if path.name != "__init__.py" ) - assert 'version = "0.1.3"' in project - assert 'app_version: str = "0.1.3"' in config + assert 'version = "0.2.0"' in project + assert 'app_version: str = "0.2.0"' in config assert len(migrations) == 30 assert migrations[-1].startswith("20260820_0030") @@ -85,8 +85,8 @@ def test_operations_separate_publication_deployment_migration_and_db_rollback(): "Image rollback is **not** database rollback", "read-only root filesystem", "10001:10001", - "PortfolioHub v0.1.3 container contract", - "v0.1.3 introduces no schema migration", + "PortfolioHub v0.2.0 container contract", + "v0.2.0 adds migration `0030`", "trex-web v0.2.3 displays `strategy_nick`", ): assert value in text diff --git a/uv.lock b/uv.lock index 292d2e1..b3413ce 100644 --- a/uv.lock +++ b/uv.lock @@ -592,7 +592,7 @@ wheels = [ [[package]] name = "portfoliohub" -version = "0.1.3" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "alembic" },