chore(release): prepare v0.7.0
This commit is contained in:
+7
-1
@@ -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
|
||||
|
||||
@@ -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**를 남긴다.
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
Executable
+37
@@ -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
|
||||
|
||||
@@ -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__}"
|
||||
@@ -1 +1,5 @@
|
||||
"""trex-core — shared core for the trex product family."""
|
||||
|
||||
from trex_core.version import __version__
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Installed package version exposed to runtime entry points."""
|
||||
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version("trex-core")
|
||||
Reference in New Issue
Block a user