chore(release): prepare portfolio-hub v0.1.3
Publish verified PortfolioHub image / verify (push) Canceled after 0s
Publish verified PortfolioHub image / publish (push) Canceled after 0s

This commit is contained in:
2026-08-16 16:57:26 +09:00
parent ec912fcde8
commit 02bfa60299
6 changed files with 26 additions and 26 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ if env_file.exists():
class Settings(BaseSettings): class Settings(BaseSettings):
app_name: str = "PortfolioHub" app_name: str = "PortfolioHub"
app_version: str = "0.1.2" app_version: str = "0.1.3"
environment: str = "development" environment: str = "development"
allowed_origins_raw: str = Field(default="http://localhost:3000", validation_alias="PORTFOLIOHUB_ALLOWED_ORIGINS") allowed_origins_raw: str = Field(default="http://localhost:3000", validation_alias="PORTFOLIOHUB_ALLOWED_ORIGINS")
bootstrap_username: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_USERNAME") bootstrap_username: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_USERNAME")
+12 -12
View File
@@ -1,11 +1,11 @@
# PortfolioHub v0.1.2 container contract # PortfolioHub v0.1.3 container contract
## Publication is not deployment ## Publication is not deployment
Only a requester-authorized push of the strict tag `v0.1.2` 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.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:
- `ghcr.io/fourblocks2020/portfolio-hub:<source-commit-first-12>` - `ghcr.io/fourblocks2020/portfolio-hub:<source-commit-first-12>`
- `ghcr.io/fourblocks2020/portfolio-hub:v0.1.2` - `ghcr.io/fourblocks2020/portfolio-hub:v0.1.3`
It never publishes `latest`. Both selectors must report the same registry digest; deployment and rollback must use the immutable identity: 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> 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.2`. 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.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.
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.2 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.1.3 supply-chain gap; they must not be added by rebuilding a different image in the publish job.
## Image runtime ## 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_BOOTSTRAP_PASSWORD` | optional; empty | **yes** | bootstrap password; inject temporarily and remove after first successful start |
| `PORTFOLIOHUB_ENVIRONMENT` | optional; `development` | no | reported environment metadata | | `PORTFOLIOHUB_ENVIRONMENT` | optional; `development` | no | reported environment metadata |
| `PORTFOLIOHUB_APP_NAME` | optional; `PortfolioHub` | no | application/OpenAPI display name | | `PORTFOLIOHUB_APP_NAME` | optional; `PortfolioHub` | no | application/OpenAPI display name |
| `PORTFOLIOHUB_APP_VERSION` | do not override for v0.1.2 | `0.1.2` | no | health/OpenAPI version metadata | | `PORTFOLIOHUB_APP_VERSION` | do not override for v0.1.3 | `0.1.3` | 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.2 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.1.3 application does not open Redis or Loki connections. They are not production dependencies of this image.
### Legacy and sibling mapping ### Legacy and sibling mapping
The existing database variable names remain `TIMESCALEDB_*`; v0.1.2 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.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.
## Liveness and readiness ## 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. - **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. - **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.2`. - **API identity:** `GET /openapi.json` is stable for smoke verification. Health version is `0.1.3`.
Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations. Do not route traffic from a static `/health` success alone. Readiness checks never apply, stamp, or repair migrations.
@@ -97,7 +97,7 @@ The installed image contains exactly **29** migration files and one Alembic head
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. 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.2 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.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.
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 `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.
@@ -105,9 +105,9 @@ Image rollback is **not** database rollback. Selecting older application bytes d
## API verification and known caller blockers ## API verification and known caller blockers
v0.1.2 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.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.
The strategy catalog API change is additive: strategy responses expose `nick`, and admin create/update requests may set it. Existing clients may omit `nick`; supplied values are trimmed and blank-only values are rejected. trex-web v0.2.2 requires this backend contract, but that dependency does not resolve or waive the separate caller blockers below. 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.
Route presence is **not** a positive cross-repository caller-compatibility claim. Direct source comparison found the following unresolved consumers, which the requester explicitly separated as BLOCKED follow-up rather than adding legacy shims to portfolio-hub: Route presence is **not** a positive cross-repository caller-compatibility claim. Direct source comparison found the following unresolved consumers, which the requester explicitly separated as BLOCKED follow-up rather than adding legacy shims to portfolio-hub:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "portfoliohub" name = "portfoliohub"
version = "0.1.2" version = "0.1.3"
description = "PortfolioHub API for portfolio management and ledger access" description = "PortfolioHub API for portfolio management and ledger access"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+4 -4
View File
@@ -65,11 +65,11 @@ def test_checkout_identity_is_exact():
def test_project_version_must_match_release_tag(tmp_path): def test_project_version_must_match_release_tag(tmp_path):
project = tmp_path / "pyproject.toml" project = tmp_path / "pyproject.toml"
project.write_text('[project]\nversion = "0.1.2"\n') project.write_text('[project]\nversion = "0.1.3"\n')
assert image_release.project_release_version(project) == "v0.1.2" assert image_release.project_release_version(project) == "v0.1.3"
assert image_release.validate_release_event("push", "refs/tags/v0.1.2", COMMIT, "v0.1.2") == "v0.1.2" assert image_release.validate_release_event("push", "refs/tags/v0.1.3", COMMIT, "v0.1.3") == "v0.1.3"
with pytest.raises(ValueError, match="project version"): with pytest.raises(ValueError, match="project version"):
image_release.validate_release_event("push", "refs/tags/v0.1.3", COMMIT, "v0.1.2") image_release.validate_release_event("push", "refs/tags/v0.1.4", COMMIT, "v0.1.3")
def test_installed_smoke_has_import_cli_exact_inventory_and_cleanup(): def test_installed_smoke_has_import_cli_exact_inventory_and_cleanup():
inventory = tuple( inventory = tuple(
+7 -7
View File
@@ -47,10 +47,10 @@ def test_workflow_actions_are_commit_pinned_and_verify_version_is_project_derive
assert len(revision) == 40 and all(char in "0123456789abcdef" for char in revision) assert len(revision) == 40 and all(char in "0123456789abcdef" for char in revision)
verify = VERIFY.read_text() verify = VERIFY.read_text()
assert "tomllib" in verify and 'RELEASE_VERSION=${RELEASE_VERSION}' in verify assert "tomllib" in verify and 'RELEASE_VERSION=${RELEASE_VERSION}' in verify
assert 'RELEASE_VERSION=v0.1.2' not in verify assert 'RELEASE_VERSION=v0.1.3' not in verify
def test_release_version_and_migration_contract_are_v012_head_0029(): def test_release_version_and_migration_contract_are_v013_head_0029():
project = (REPO / "pyproject.toml").read_text() project = (REPO / "pyproject.toml").read_text()
config = (REPO / "app/core/config.py").read_text() config = (REPO / "app/core/config.py").read_text()
migrations = sorted( migrations = sorted(
@@ -58,8 +58,8 @@ def test_release_version_and_migration_contract_are_v012_head_0029():
for path in (REPO / "app/db/alembic/versions").glob("*.py") for path in (REPO / "app/db/alembic/versions").glob("*.py")
if path.name != "__init__.py" if path.name != "__init__.py"
) )
assert 'version = "0.1.2"' in project assert 'version = "0.1.3"' in project
assert 'app_version: str = "0.1.2"' in config assert 'app_version: str = "0.1.3"' in config
assert len(migrations) == 29 assert len(migrations) == 29
assert migrations[-1].startswith("20260814_0029") assert migrations[-1].startswith("20260814_0029")
@@ -85,9 +85,9 @@ def test_operations_separate_publication_deployment_migration_and_db_rollback():
"Image rollback is **not** database rollback", "Image rollback is **not** database rollback",
"read-only root filesystem", "read-only root filesystem",
"10001:10001", "10001:10001",
"PortfolioHub v0.1.2 container contract", "PortfolioHub v0.1.3 container contract",
"v0.1.2 introduces no schema migration", "v0.1.3 introduces no schema migration",
"trex-web v0.2.2 requires this backend contract", "trex-web v0.2.3 displays `strategy_nick`",
): ):
assert value in text assert value in text
for forbidden in ( for forbidden in (
Generated
+1 -1
View File
@@ -592,7 +592,7 @@ wheels = [
[[package]] [[package]]
name = "portfoliohub" name = "portfoliohub"
version = "0.1.2" version = "0.1.3"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },