diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 30a7a1c..2827a82 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -39,7 +39,7 @@ jobs: python-version: "3.11" - run: python -m pip install "uv==0.11.14" - run: uv sync --extra dev --locked - - run: uv run pytest -q + - run: uv run python -m pytest -q - name: Honest scoped lint run: uv run ruff check --select F,I app/db/migrations.py scripts/image_release.py scripts/ruff_ratchet.py tests/release - name: Reject Ruff findings introduced by v0.3.0 or later patches diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5419eb2..4a7c82e 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -36,13 +36,13 @@ jobs: python-version: "3.11" - run: python -m pip install "uv==0.11.14" - run: uv sync --extra dev --locked - - run: uv run pytest -q + - run: uv run python -m pytest -q - name: Verify corporate-action atomicity and guarded recovery in PostgreSQL env: POSTGRES_PASSWORD: change-me run: | export PORTFOLIOHUB_TEST_POSTGRES_DSN="postgresql://portfolio_hub_user:${POSTGRES_PASSWORD}@127.0.0.1:5432/postgres" - uv run pytest -q tests/test_corporate_action_atomic_recovery_postgres.py + uv run python -m pytest -q tests/test_corporate_action_atomic_recovery_postgres.py - name: Honest scoped lint for release additions run: uv run ruff check --select F,I app/db/migrations.py scripts/image_release.py scripts/ruff_ratchet.py tests/release - name: Reject Ruff findings introduced by v0.3.0 or later patches @@ -51,7 +51,7 @@ jobs: uv run python scripts/ruff_ratchet.py --base v0.3.0 uv run python scripts/ruff_ratchet.py --base v0.3.1 - name: Validate container contracts without registry writes - run: uv run pytest -q tests/release + run: uv run python -m pytest -q tests/release - name: Build and smoke exact pull-request or main source image run: | uv run python -c 'from app.db.migrations import migration_inventory; print("\n".join(migration_inventory()))' > migration-inventory.txt diff --git a/app/core/config.py b/app/core/config.py index cd14593..df96e4b 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.3.3" + app_version: str = "0.3.4" 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 ddc1214..f7c60db 100644 --- a/docs/container-operations.md +++ b/docs/container-operations.md @@ -1,11 +1,11 @@ -# PortfolioHub v0.3.3 container contract +# PortfolioHub v0.3.4 container contract ## Publication is not deployment -Only a requester-authorized push of the strict tag `v0.3.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.3.4` 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.3.3` +- `ghcr.io/fourblocks2020/portfolio-hub:v0.3.4` 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.3.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.3.4`. 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.3.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.3.4 supply-chain gap; they must not be added by rebuilding a different image in the publish job. ## Image runtime @@ -66,19 +66,19 @@ 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.3.3 | `0.3.3` | no | health/OpenAPI version metadata | +| `PORTFOLIOHUB_APP_VERSION` | do not override for v0.3.4 | `0.3.4` | 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.3.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.3.4 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.3.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.3.4 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.3.3`. +- **API identity:** `GET /openapi.json` is stable for smoke verification. Health version is `0.3.4`. Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations. @@ -107,7 +107,7 @@ Image rollback is **not** database rollback. Selecting older application bytes d ## API verification and known caller blockers -v0.3.3 preserves Bearer authentication and all-source unified-ledger reads. Corporate-action quantity/reference-price updates, the complete ledger snapshot (including `total_value_after`), and the audit row execute in one explicit transaction. The guarded `resume_partial` path repairs only a verified quantity-only partial state without raw SQL. 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 changed-scope Ruff gate uses the locked linter and the ACMR Python path selector, compares normalized findings against v0.2.1, v0.3.0, and v0.3.1, and rejects every newly introduced finding. +v0.3.4 preserves Bearer authentication and all-source unified-ledger reads. Corporate-action quantity/reference-price updates, the complete ledger snapshot (including `total_value_after`), and the audit row execute in one explicit transaction. The guarded `resume_partial` path repairs only a verified quantity-only partial state without raw SQL. 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 changed-scope Ruff gate uses the locked linter and the ACMR Python path selector, compares normalized findings against v0.2.1, v0.3.0, and v0.3.1, and rejects every newly introduced finding. 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 10efeb9..27041f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "portfoliohub" -version = "0.3.3" +version = "0.3.4" 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 57e54c3..37aaa63 100644 --- a/tests/release/test_release_contracts.py +++ b/tests/release/test_release_contracts.py @@ -16,7 +16,7 @@ def test_branch_verification_has_no_registry_write_authority_or_path(): text=VERIFY.read_text(); assert "branches: [main]" in text and "pull_request:" in text for forbidden in ("packages: write","docker login","docker push","REGISTRY_TOKEN","image_release.py publish"): assert forbidden not in text - assert "uv sync --extra dev --locked" in text and "uv run pytest -q" in text + assert "uv sync --extra dev --locked" in text and "uv run python -m pytest -q" in text assert "Verify corporate-action atomicity and guarded recovery in PostgreSQL" in text assert "tests/test_corporate_action_atomic_recovery_postgres.py" in text assert "${POSTGRES_PASSWORD}@127.0.0.1:5432/postgres" in text @@ -62,8 +62,8 @@ def test_release_version_and_migration_contract_has_current_head_0031(): for path in (REPO / "app/db/alembic/versions").glob("*.py") if path.name != "__init__.py" ) - assert 'version = "0.3.3"' in project - assert 'app_version: str = "0.3.3"' in config + assert 'version = "0.3.4"' in project + assert 'app_version: str = "0.3.4"' in config assert len(migrations) == 31 assert migrations[-1].startswith("20260913_0031_") @@ -89,7 +89,7 @@ def test_operations_separate_publication_deployment_migration_and_db_rollback(): "Image rollback is **not** database rollback", "read-only root filesystem", "10001:10001", - "PortfolioHub v0.3.3 container contract", + "PortfolioHub v0.3.4 container contract", "v0.2.1 includes migration `0030`", "trex-web v0.2.3 displays `strategy_nick`", ): diff --git a/uv.lock b/uv.lock index fe85cef..7d5ef48 100644 --- a/uv.lock +++ b/uv.lock @@ -592,7 +592,7 @@ wheels = [ [[package]] name = "portfoliohub" -version = "0.3.3" +version = "0.3.4" source = { editable = "." } dependencies = [ { name = "alembic" },