chore(release): prepare v0.3.4
This commit is contained in:
@@ -39,7 +39,7 @@ jobs:
|
|||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- run: python -m pip install "uv==0.11.14"
|
- run: python -m pip install "uv==0.11.14"
|
||||||
- run: uv sync --extra dev --locked
|
- run: uv sync --extra dev --locked
|
||||||
- run: uv run pytest -q
|
- run: uv run python -m pytest -q
|
||||||
- name: Honest scoped lint
|
- 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
|
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
|
- name: Reject Ruff findings introduced by v0.3.0 or later patches
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ jobs:
|
|||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- run: python -m pip install "uv==0.11.14"
|
- run: python -m pip install "uv==0.11.14"
|
||||||
- run: uv sync --extra dev --locked
|
- 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
|
- name: Verify corporate-action atomicity and guarded recovery in PostgreSQL
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: change-me
|
POSTGRES_PASSWORD: change-me
|
||||||
run: |
|
run: |
|
||||||
export PORTFOLIOHUB_TEST_POSTGRES_DSN="postgresql://portfolio_hub_user:${POSTGRES_PASSWORD}@127.0.0.1:5432/postgres"
|
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
|
- 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
|
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
|
- 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.0
|
||||||
uv run python scripts/ruff_ratchet.py --base v0.3.1
|
uv run python scripts/ruff_ratchet.py --base v0.3.1
|
||||||
- name: Validate container contracts without registry writes
|
- 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
|
- name: Build and smoke exact pull-request or main source image
|
||||||
run: |
|
run: |
|
||||||
uv run python -c 'from app.db.migrations import migration_inventory; print("\n".join(migration_inventory()))' > migration-inventory.txt
|
uv run python -c 'from app.db.migrations import migration_inventory; print("\n".join(migration_inventory()))' > migration-inventory.txt
|
||||||
|
|||||||
+1
-1
@@ -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.3.3"
|
app_version: str = "0.3.4"
|
||||||
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")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# PortfolioHub v0.3.3 container contract
|
# PortfolioHub v0.3.4 container contract
|
||||||
|
|
||||||
## Publication is not deployment
|
## 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:<source-commit-first-12>`
|
- `ghcr.io/fourblocks2020/portfolio-hub:<source-commit-first-12>`
|
||||||
- `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:
|
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.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
|
## 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.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
|
### 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 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.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.
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portfoliohub"
|
name = "portfoliohub"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
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"
|
||||||
|
|||||||
@@ -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
|
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"):
|
for forbidden in ("packages: write","docker login","docker push","REGISTRY_TOKEN","image_release.py publish"):
|
||||||
assert forbidden not in text
|
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 "Verify corporate-action atomicity and guarded recovery in PostgreSQL" in text
|
||||||
assert "tests/test_corporate_action_atomic_recovery_postgres.py" in text
|
assert "tests/test_corporate_action_atomic_recovery_postgres.py" in text
|
||||||
assert "${POSTGRES_PASSWORD}@127.0.0.1:5432/postgres" 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")
|
for path in (REPO / "app/db/alembic/versions").glob("*.py")
|
||||||
if path.name != "__init__.py"
|
if path.name != "__init__.py"
|
||||||
)
|
)
|
||||||
assert 'version = "0.3.3"' in project
|
assert 'version = "0.3.4"' in project
|
||||||
assert 'app_version: str = "0.3.3"' in config
|
assert 'app_version: str = "0.3.4"' in config
|
||||||
assert len(migrations) == 31
|
assert len(migrations) == 31
|
||||||
assert migrations[-1].startswith("20260913_0031_")
|
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",
|
"Image rollback is **not** database rollback",
|
||||||
"read-only root filesystem",
|
"read-only root filesystem",
|
||||||
"10001:10001",
|
"10001:10001",
|
||||||
"PortfolioHub v0.3.3 container contract",
|
"PortfolioHub v0.3.4 container contract",
|
||||||
"v0.2.1 includes migration `0030`",
|
"v0.2.1 includes migration `0030`",
|
||||||
"trex-web v0.2.3 displays `strategy_nick`",
|
"trex-web v0.2.3 displays `strategy_nick`",
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -592,7 +592,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portfoliohub"
|
name = "portfoliohub"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
|
|||||||
Reference in New Issue
Block a user