chore(release): prepare v0.7.0
This commit is contained in:
+7
-1
@@ -1,8 +1,11 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
ARG SOURCE_COMMIT
|
ARG SOURCE_COMMIT
|
||||||
|
ARG SOURCE_VERSION
|
||||||
LABEL org.opencontainers.image.source="https://github.com/fourblocks2020/trex-core" \
|
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
|
WORKDIR /opt/trex-core
|
||||||
|
|
||||||
@@ -16,6 +19,9 @@ RUN test "${#SOURCE_COMMIT}" -eq 40 \
|
|||||||
COPY pyproject.toml uv.lock ./
|
COPY pyproject.toml uv.lock ./
|
||||||
COPY trex_core/ ./trex_core/
|
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" \
|
RUN python -m pip install --no-cache-dir "uv==0.11.14" \
|
||||||
&& uv sync --locked --no-dev --no-editable \
|
&& uv sync --locked --no-dev --no-editable \
|
||||||
&& useradd --create-home --uid 10001 trex
|
&& useradd --create-home --uid 10001 trex
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ pkill -TERM -f trex-core-daemon
|
|||||||
- `config/.env`는 **라이브 KIS 키 + portfolio_hub 토큰 + 라이브 PostgreSQL DSN**을 담는다 — 커밋 금지, 라이브 DB에 파괴적 작업 금지.
|
- `config/.env`는 **라이브 KIS 키 + portfolio_hub 토큰 + 라이브 PostgreSQL DSN**을 담는다 — 커밋 금지, 라이브 DB에 파괴적 작업 금지.
|
||||||
- 테스트는 `TREX_BROKER=paper` + 파생 `trex_core_test` DSN.
|
- 테스트는 `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)
|
### 배포/복구/수동 재기동 provenance gate (issue #87)
|
||||||
|
|
||||||
배포, 복구, 수동 stop/start/recreate 전에 아래 gate를 실행해 **검증 가능한 release record**를 남긴다.
|
배포, 복구, 수동 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]
|
[project]
|
||||||
name = "trex-core"
|
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)"
|
description = "Shared core for the trex product family; first domain: slot lifecycle (storage-agnostic pure domain)"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
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():
|
def test_outcome_parses_cash_quantity_after_on_new():
|
||||||
"""dcy-018 / REQ-017: option D authoritative cash/quantity from 201 response."""
|
"""dcy-018 / REQ-017: option D authoritative cash/quantity from 201 response."""
|
||||||
def handler(request: httpx.Request) -> httpx.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})
|
"asset_quantity_after": 10})
|
||||||
|
|
||||||
out = PortfolioHubLedger("https://portfolio-hub.test", client=_client(handler)).record_trade(_trade())
|
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
|
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)."""
|
"""duplicate(200) also carries current authoritative values (OQ-2 b)."""
|
||||||
def handler(request: httpx.Request) -> httpx.Response:
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
return httpx.Response(200, json={"duplicate": True,
|
return httpx.Response(200, json={
|
||||||
"cash_balance_after": 50000.0,
|
**duplicate_marker,
|
||||||
"asset_quantity_after": 3})
|
"cash_balance_after": 50000.0,
|
||||||
|
"asset_quantity_after": 3,
|
||||||
|
})
|
||||||
|
|
||||||
out = PortfolioHubLedger("https://portfolio-hub.test", client=_client(handler)).record_trade(_trade())
|
out = PortfolioHubLedger("https://portfolio-hub.test", client=_client(handler)).record_trade(_trade())
|
||||||
assert out.result is LedgerWriteResult.DUPLICATE_OK
|
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."""
|
"""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
|
POST /v1/portfolios/{portfolio_id}/ledger/trades
|
||||||
body keys: side, quantity, price, fee_amount, tax_amount, executed_at,
|
body keys: side, quantity, price, fee_amount, tax_amount, executed_at,
|
||||||
execution_id, order_id
|
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)
|
- 409 => LedgerConflictError (operator resolution)
|
||||||
- 404/400/422 => LedgerPermanentError (dcy-015/REQ-017 — retry can't fix:
|
- 404/400/422 => LedgerPermanentError (dcy-015/REQ-017 — retry can't fix:
|
||||||
portfolio not registered / malformed trade)
|
portfolio not registered / malformed trade)
|
||||||
@@ -94,18 +95,18 @@ class PortfolioHubLedger:
|
|||||||
raise LedgerRetryableError(f"portfolio-hub network error: {exc!s}") from exc
|
raise LedgerRetryableError(f"portfolio-hub network error: {exc!s}") from exc
|
||||||
|
|
||||||
if 200 <= resp.status_code < 300:
|
if 200 <= resp.status_code < 300:
|
||||||
# orbit convention: response body may carry a "duplicate" flag when
|
# portfolio-hub returns status="duplicate" for an idempotent replay.
|
||||||
# the server has already recorded this execution_id idempotently.
|
# Keep the former boolean marker compatible with older deployments.
|
||||||
try:
|
try:
|
||||||
body = resp.json() if resp.content else {}
|
body = resp.json() if resp.content else {}
|
||||||
except ValueError:
|
except ValueError:
|
||||||
body = {}
|
body = {}
|
||||||
if not isinstance(body, dict):
|
if not isinstance(body, dict):
|
||||||
body = {}
|
body = {}
|
||||||
result = (
|
is_duplicate = (
|
||||||
LedgerWriteResult.DUPLICATE_OK if body.get("duplicate") is True
|
body.get("status") == "duplicate" or body.get("duplicate") is True
|
||||||
else LedgerWriteResult.NEW
|
|
||||||
)
|
)
|
||||||
|
result = LedgerWriteResult.DUPLICATE_OK if is_duplicate else LedgerWriteResult.NEW
|
||||||
# dcy-018 / REQ-017 (option D, portfolio-hub#8): root flat authoritative
|
# dcy-018 / REQ-017 (option D, portfolio-hub#8): root flat authoritative
|
||||||
# cash/quantity after the write — present on new(201) + duplicate(200).
|
# cash/quantity after the write — present on new(201) + duplicate(200).
|
||||||
return LedgerWriteOutcome(
|
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_outbox import LedgerWriter, OutboxOutcome
|
||||||
from trex_core.application.ledger_port import LedgerTrade
|
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.application.ledger_skip import ledger_skip_alarm_fields, publish_ledger_skip_alarm
|
||||||
|
from trex_core.version import __version__
|
||||||
from trex_core.runtime.config import (
|
from trex_core.runtime.config import (
|
||||||
DaemonConfig,
|
DaemonConfig,
|
||||||
DaemonInstanceConfig,
|
DaemonInstanceConfig,
|
||||||
@@ -563,6 +564,7 @@ def _add_intent_guards(p) -> None:
|
|||||||
|
|
||||||
def _build_parser() -> argparse.ArgumentParser:
|
def _build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(prog="trex-core", description="trex-core admin CLI")
|
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)
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
p_pause = sub.add_parser("pause", help="Pause a portfolio_id")
|
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
|
import argparse
|
||||||
|
|
||||||
from trex_core.runtime.logging_setup import configure_logging
|
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
|
configure_logging(level=logging.INFO) # REQ-084: ISO8601(KST)+level+json+cid
|
||||||
parser = argparse.ArgumentParser(prog="trex-core-daemon")
|
parser = argparse.ArgumentParser(prog="trex-core-daemon")
|
||||||
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config", required=True,
|
"--config", required=True,
|
||||||
help="per-instance TOML name (resolved within --config-path) or explicit "
|
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.config import DaemonConfig, resolve_config_toml
|
||||||
from trex_core.runtime.logging_setup import configure_logging
|
from trex_core.runtime.logging_setup import configure_logging
|
||||||
|
from trex_core.version import __version__
|
||||||
|
|
||||||
configure_logging(level=logging.INFO) # REQ-084
|
configure_logging(level=logging.INFO) # REQ-084
|
||||||
parser = argparse.ArgumentParser(
|
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). "
|
"the DB to the asset-centric schema (dry-run by default). "
|
||||||
"Destructive and DB-wide — back up first.",
|
"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", required=True, help="instance TOML name/path (DSN source)")
|
||||||
parser.add_argument("--config-path", default=None)
|
parser.add_argument("--config-path", default=None)
|
||||||
parser.add_argument("--env-file", 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