chore(release): prepare v0.7.0

This commit is contained in:
Hermes Agent
2026-09-13 17:01:20 +09:00
parent 3569d864c9
commit adf131f19c
14 changed files with 145 additions and 14 deletions
+7 -1
View File
@@ -1,8 +1,11 @@
FROM python:3.12-slim
ARG SOURCE_COMMIT
ARG SOURCE_VERSION
LABEL org.opencontainers.image.source="https://github.com/fourblocks2020/trex-core" \
org.opencontainers.image.revision="${SOURCE_COMMIT}"
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.version="${SOURCE_VERSION}" \
org.opencontainers.image.title="trex-core"
WORKDIR /opt/trex-core
@@ -16,6 +19,9 @@ RUN test "${#SOURCE_COMMIT}" -eq 40 \
COPY pyproject.toml uv.lock ./
COPY trex_core/ ./trex_core/
RUN test "$(python -c 'import pathlib,tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')" = "${SOURCE_VERSION}" \
|| { echo "SOURCE_VERSION must match pyproject.toml" >&2; exit 1; }
RUN python -m pip install --no-cache-dir "uv==0.11.14" \
&& uv sync --locked --no-dev --no-editable \
&& useradd --create-home --uid 10001 trex
+7
View File
@@ -40,6 +40,13 @@ pkill -TERM -f trex-core-daemon
- `config/.env`는 **라이브 KIS 키 + portfolio_hub 토큰 + 라이브 PostgreSQL DSN**을 담는다 — 커밋 금지, 라이브 DB에 파괴적 작업 금지.
- 테스트는 `TREX_BROKER=paper` + 파생 `trex_core_test` DSN.
### Release image build
Release tag의 clean checkout에서 `./scripts/build_release_image.sh trex-core:<version>`을
실행한다. 이 경로는 project version과 image tag를 대조하고 OCI version/revision labels,
non-root user 및 container `--version`을 검증한다. 전체 명령과 registry 사용법은
[`docs/ops/release-image-build.md`](ops/release-image-build.md)를 따른다.
### 배포/복구/수동 재기동 provenance gate (issue #87)
배포, 복구, 수동 stop/start/recreate 전에 아래 gate를 실행해 **검증 가능한 release record**를 남긴다.
+25
View File
@@ -0,0 +1,25 @@
# Release image build
운영팀은 release tag의 clean checkout에서 아래 명령으로 이미지를 빌드한다.
```bash
git checkout v0.7.0
./scripts/build_release_image.sh trex-core:0.7.0
```
스크립트는 `pyproject.toml`의 canonical version과 image tag가 일치하는지 확인하고 현재
commit을 OCI `org.opencontainers.image.revision` label로, package version을
`org.opencontainers.image.version` label로 기록한다. Dirty checkout, version이 다른 image
tag, label 불일치, non-root user 불일치 또는 container `--version` smoke test 실패 시 종료
코드 0을 반환하지 않는다.
Registry 경로를 사용할 때도 version tag를 유지한다.
```bash
./scripts/build_release_image.sh registry.example/trex-core:0.7.0
docker push registry.example/trex-core:0.7.0
```
배포 전 `docker image inspect`에서 나온 revision과 image digest를
[`release-provenance-standard.md`](release-provenance-standard.md)의 PRE/POST gate에 기록한다.
이 저장소는 registry publication을 자동화하지 않으므로 push 대상과 권한은 운영팀이 관리한다.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "trex-core"
version = "0.6.0"
version = "0.7.0"
description = "Shared core for the trex product family; first domain: slot lifecycle (storage-agnostic pure domain)"
requires-python = ">=3.12"
dependencies = [
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$repo_root"
version=$(python3 -c 'import pathlib,tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')
image_ref=${1:-trex-core:$version}
source_commit=$(git rev-parse HEAD)
if [ -n "$(git status --porcelain)" ]; then
echo "release image build requires a clean checkout" >&2
exit 2
fi
case "$image_ref" in
*:"$version") ;;
*) echo "image reference must use the project version tag :$version" >&2; exit 2 ;;
esac
docker build \
--build-arg "SOURCE_COMMIT=$source_commit" \
--build-arg "SOURCE_VERSION=$version" \
--tag "$image_ref" \
.
actual_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image_ref")
actual_version=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image_ref")
actual_user=$(docker image inspect --format '{{ .Config.User }}' "$image_ref")
[ "$actual_revision" = "$source_commit" ]
[ "$actual_version" = "$version" ]
[ "$actual_user" = "10001:10001" ]
docker run --rm "$image_ref" --version
printf 'image=%s\nversion=%s\nrevision=%s\nimage_id=%s\n' \
"$image_ref" "$actual_version" "$actual_revision" \
"$(docker image inspect --format '{{ .Id }}' "$image_ref")"
@@ -94,7 +94,8 @@ def test_client_errors_are_permanent(status):
def test_outcome_parses_cash_quantity_after_on_new():
"""dcy-018 / REQ-017: option D authoritative cash/quantity from 201 response."""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(201, json={"cash_balance_after": 300000.0,
return httpx.Response(201, json={"status": "new",
"cash_balance_after": 300000.0,
"asset_quantity_after": 10})
out = PortfolioHubLedger("https://portfolio-hub.test", client=_client(handler)).record_trade(_trade())
@@ -102,12 +103,21 @@ def test_outcome_parses_cash_quantity_after_on_new():
assert out.cash_balance_after == 300000.0 and out.asset_quantity_after == 10.0
def test_outcome_parses_after_on_duplicate():
@pytest.mark.parametrize(
"duplicate_marker",
[
{"status": "duplicate"}, # portfolio-hub canonical response
{"duplicate": True}, # backward-compatible legacy response
],
)
def test_outcome_parses_after_on_duplicate(duplicate_marker):
"""duplicate(200) also carries current authoritative values (OQ-2 b)."""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"duplicate": True,
return httpx.Response(200, json={
**duplicate_marker,
"cash_balance_after": 50000.0,
"asset_quantity_after": 3})
"asset_quantity_after": 3,
})
out = PortfolioHubLedger("https://portfolio-hub.test", client=_client(handler)).record_trade(_trade())
assert out.result is LedgerWriteResult.DUPLICATE_OK
+30
View File
@@ -0,0 +1,30 @@
import subprocess
import sys
import tomllib
from pathlib import Path
from trex_core import __version__
ROOT = Path(__file__).resolve().parents[1]
def test_runtime_version_matches_project_metadata():
project = tomllib.loads((ROOT / "pyproject.toml").read_text())
assert __version__ == project["project"]["version"]
def test_all_console_modules_display_the_release_version():
for module, command in (
("trex_core.runtime.cli", "trex-core"),
("trex_core.runtime.daemon", "trex-core-daemon"),
("trex_core.runtime.reset", "trex-core-reset"),
):
result = subprocess.run(
[sys.executable, "-m", module, "--version"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
assert result.stdout.strip() == f"{command} {__version__}"
+4
View File
@@ -1 +1,5 @@
"""trex-core — shared core for the trex product family."""
from trex_core.version import __version__
__all__ = ["__version__"]
+7 -6
View File
@@ -4,7 +4,8 @@ Live portfolio-hub contract (verified against openapi, REQ-017 CHG-04):
POST /v1/portfolios/{portfolio_id}/ledger/trades
body keys: side, quantity, price, fee_amount, tax_amount, executed_at,
execution_id, order_id
- 2xx (201) => recorded (or duplicate accepted idempotently)
- 201/status=new => recorded
- 200/status=duplicate => duplicate accepted idempotently
- 409 => LedgerConflictError (operator resolution)
- 404/400/422 => LedgerPermanentError (dcy-015/REQ-017 — retry can't fix:
portfolio not registered / malformed trade)
@@ -94,18 +95,18 @@ class PortfolioHubLedger:
raise LedgerRetryableError(f"portfolio-hub network error: {exc!s}") from exc
if 200 <= resp.status_code < 300:
# orbit convention: response body may carry a "duplicate" flag when
# the server has already recorded this execution_id idempotently.
# portfolio-hub returns status="duplicate" for an idempotent replay.
# Keep the former boolean marker compatible with older deployments.
try:
body = resp.json() if resp.content else {}
except ValueError:
body = {}
if not isinstance(body, dict):
body = {}
result = (
LedgerWriteResult.DUPLICATE_OK if body.get("duplicate") is True
else LedgerWriteResult.NEW
is_duplicate = (
body.get("status") == "duplicate" or body.get("duplicate") is True
)
result = LedgerWriteResult.DUPLICATE_OK if is_duplicate else LedgerWriteResult.NEW
# dcy-018 / REQ-017 (option D, portfolio-hub#8): root flat authoritative
# cash/quantity after the write — present on new(201) + duplicate(200).
return LedgerWriteOutcome(
+2
View File
@@ -55,6 +55,7 @@ from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.ledger_outbox import LedgerWriter, OutboxOutcome
from trex_core.application.ledger_port import LedgerTrade
from trex_core.application.ledger_skip import ledger_skip_alarm_fields, publish_ledger_skip_alarm
from trex_core.version import __version__
from trex_core.runtime.config import (
DaemonConfig,
DaemonInstanceConfig,
@@ -563,6 +564,7 @@ def _add_intent_guards(p) -> None:
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="trex-core", description="trex-core admin CLI")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
sub = parser.add_subparsers(dest="cmd", required=True)
p_pause = sub.add_parser("pause", help="Pause a portfolio_id")
+2
View File
@@ -600,8 +600,10 @@ def main() -> None: # pragma: no cover - process entrypoint
import argparse
from trex_core.runtime.logging_setup import configure_logging
from trex_core.version import __version__
configure_logging(level=logging.INFO) # REQ-084: ISO8601(KST)+level+json+cid
parser = argparse.ArgumentParser(prog="trex-core-daemon")
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument(
"--config", required=True,
help="per-instance TOML name (resolved within --config-path) or explicit "
+2
View File
@@ -160,6 +160,7 @@ def main(argv: Optional[list[str]] = None) -> int: # pragma: no cover - CLI she
from trex_core.runtime.config import DaemonConfig, resolve_config_toml
from trex_core.runtime.logging_setup import configure_logging
from trex_core.version import __version__
configure_logging(level=logging.INFO) # REQ-084
parser = argparse.ArgumentParser(
@@ -168,6 +169,7 @@ def main(argv: Optional[list[str]] = None) -> int: # pragma: no cover - CLI she
"the DB to the asset-centric schema (dry-run by default). "
"Destructive and DB-wide — back up first.",
)
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument("--config", required=True, help="instance TOML name/path (DSN source)")
parser.add_argument("--config-path", default=None)
parser.add_argument("--env-file", default=None)
+5
View File
@@ -0,0 +1,5 @@
"""Installed package version exposed to runtime entry points."""
from importlib.metadata import version
__version__ = version("trex-core")
Generated
+1 -1
View File
@@ -289,7 +289,7 @@ wheels = [
[[package]]
name = "trex-core"
version = "0.6.0"
version = "0.7.0"
source = { editable = "." }
dependencies = [
{ name = "httpx" },