@@ -13,34 +13,18 @@ permissions:
1313
1414jobs :
1515 preflight_backtests :
16- if : >-
17- github.event_name != 'pull_request' ||
18- github.event.pull_request.head.repo.full_name == github.repository
16+ if : github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
1917 runs-on : ubuntu-latest
20- timeout-minutes : 15
21- permissions :
22- contents : read
23- id-token : write
24- env :
25- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 : " true"
26- LIFECYCLE_PERFORMANCE_BUCKET : ${{ vars.LIFECYCLE_PERFORMANCE_BUCKET || '' }}
18+ timeout-minutes : 30
2719 steps :
2820 - name : Checkout
2921 uses : actions/checkout@v6
3022
31- - name : Require lifecycle bucket
32- run : |
33- set -euo pipefail
34- if [ -z "${LIFECYCLE_PERFORMANCE_BUCKET:-}" ]; then
35- echo "::error::LIFECYCLE_PERFORMANCE_BUCKET must be configured for drift preflight"
36- exit 1
37- fi
38-
3923 - name : Checkout QuantPlatformKit
4024 uses : actions/checkout@v6
4125 with :
4226 repository : QuantStrategyLab/QuantPlatformKit
43- ref : 335c7a22bc3f570bd5705427ccc40172eda6b289
27+ ref : 9bb8f31e898ea238a6446472f9f5e58133128d0c
4428 path : external/QuantPlatformKit
4529
4630 - name : Set up Python
@@ -55,10 +39,76 @@ jobs:
5539 python -m pip install -e . pandas
5640 python -m pip install --no-deps -e external/QuantPlatformKit
5741
58- - name : Build lifecycle backtests in staging
42+ - name : Download latest trusted lifecycle inputs
43+ env :
44+ GH_TOKEN : ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN }}
45+ INPUT_ROOT : ${{ runner.temp }}/crypto-lifecycle-inputs
46+ run : |
47+ set -euo pipefail
48+ if [ -z "${GH_TOKEN:-}" ]; then
49+ echo "::error::SNAPSHOT_REPOSITORY_TOKEN is required for lifecycle input artifact access"
50+ exit 1
51+ fi
52+ gh api --paginate --slurp "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts?per_page=100" > "${RUNNER_TEMP}/snapshot-artifacts.json"
53+ python - <<'PY' > "${RUNNER_TEMP}/snapshot-artifact-selection.txt"
54+ import json
55+ import os
56+ from pathlib import Path
57+ pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifacts.json").read_text())
58+ artifacts = [item for page in pages for item in page.get("artifacts", [])]
59+ candidates = [
60+ item for item in artifacts
61+ if not item.get("expired")
62+ and str(item.get("name", "")).startswith("crypto-lifecycle-inputs-")
63+ and item.get("workflow_run", {}).get("head_branch") == "main"
64+ ]
65+ if not candidates:
66+ raise SystemExit("no trusted crypto lifecycle input artifact is available")
67+ selected = max(candidates, key=lambda item: item["created_at"])
68+ print(selected["id"], selected["workflow_run"]["id"])
69+ PY
70+ read -r artifact_id workflow_run_id < "${RUNNER_TEMP}/snapshot-artifact-selection.txt"
71+ gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/runs/${workflow_run_id}" > "${RUNNER_TEMP}/snapshot-workflow-run.json"
72+ python - <<'PY'
73+ import json
74+ import os
75+ from pathlib import Path
76+ run = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-workflow-run.json").read_text())
77+ expected = {
78+ "conclusion": "success",
79+ "head_branch": "main",
80+ "path": ".github/workflows/publish-lifecycle-inputs.yml",
81+ }
82+ mismatches = {key: run.get(key) for key, value in expected.items() if run.get(key) != value}
83+ if run.get("head_repository", {}).get("full_name") != "QuantStrategyLab/CryptoLivePoolPipelines":
84+ mismatches["head_repository"] = run.get("head_repository", {}).get("full_name")
85+ if mismatches:
86+ raise SystemExit(f"lifecycle input provenance check failed: {mismatches}")
87+ PY
88+ gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts/${artifact_id}/zip" > "${RUNNER_TEMP}/snapshot-artifact.zip"
89+ python - <<'PY'
90+ import os
91+ import zipfile
92+ from pathlib import Path
93+ archive = Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifact.zip"
94+ target_root = Path(os.environ["INPUT_ROOT"])
95+ required = {"research_panel.csv.gz", "market_history.csv.gz", "manifest.json"}
96+ with zipfile.ZipFile(archive) as bundle:
97+ by_name = {Path(name).name: name for name in bundle.namelist() if Path(name).name in required}
98+ missing = sorted(required - set(by_name))
99+ if missing:
100+ raise SystemExit(f"crypto lifecycle artifact is missing: {', '.join(missing)}")
101+ target_root.mkdir(parents=True, exist_ok=True)
102+ for name, member in by_name.items():
103+ (target_root / name).write_bytes(bundle.read(member))
104+ PY
105+
106+ - name : Build lifecycle preflight bundle
59107 env :
60- LIFECYCLE_PREFLIGHT_STAGING_ROOT : ${{ runner.temp }}/lifecycle_preflight
108+ INPUT_ROOT : ${{ runner.temp }}/crypto-lifecycle-inputs
109+ LIFECYCLE_PREFLIGHT_BUNDLE_ROOT : ${{ runner.temp }}/lifecycle-preflight-bundle
61110 run : |
111+ set -euo pipefail
62112 python - <<'PY'
63113 import json
64114 import os
@@ -71,65 +121,61 @@ jobs:
71121 text=True,
72122 )
73123 )["profiles"]
74- staging_root = Path(os.environ["LIFECYCLE_PREFLIGHT_STAGING_ROOT"])
124+ input_root = Path(os.environ["INPUT_ROOT"])
125+ bundle_root = Path(os.environ["LIFECYCLE_PREFLIGHT_BUNDLE_ROOT"])
126+ store_root = bundle_root / "data" / "lifecycle_store"
75127 for profile in profiles:
128+ returns_output = (
129+ bundle_root
130+ / "external"
131+ / "CryptoLivePoolPipelines"
132+ / "data"
133+ / "output"
134+ / profile
135+ / "portfolio_and_tracker_returns.csv"
136+ )
76137 subprocess.check_call(
77138 [
78139 "python",
79140 "scripts/run_walk_forward_backtest.py",
80141 "--profile",
81142 profile,
143+ "--panel",
144+ str(input_root / "research_panel.csv.gz"),
145+ "--market-history",
146+ str(input_root / "market_history.csv.gz"),
82147 "--store-root",
83- str(staging_root),
148+ str(store_root),
149+ "--returns-output",
150+ str(returns_output),
84151 ]
85152 )
86153 PY
87154
88- - name : Promote staged lifecycle backtests
89- if : github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
90- env :
91- LIFECYCLE_PREFLIGHT_STAGING_ROOT : ${{ runner.temp }}/lifecycle_preflight
92- run : |
93- python - <<'PY'
94- import json
95- import os
96- import subprocess
97- from pathlib import Path
98-
99- from quant_platform_kit.strategy_lifecycle.performance_store import PerformanceStore
100-
101- domain = "crypto"
102- staging_root = Path(os.environ["LIFECYCLE_PREFLIGHT_STAGING_ROOT"])
103- source = PerformanceStore(local_root=staging_root)
104- target = PerformanceStore.from_env()
105- profiles = json.loads(
106- subprocess.check_output(
107- ["python", "scripts/run_walk_forward_backtest.py", "--list-profiles"],
108- text=True,
109- )
110- )["profiles"]
111- results = [source.load_latest_backtest(domain, profile) for profile in profiles]
112- if any(result is None for result in results):
113- raise RuntimeError("staged lifecycle backtests are incomplete")
114- for result in results:
115- target.save_backtest_result(result)
116- PY
155+ - name : Upload lifecycle preflight artifact
156+ uses : actions/upload-artifact@v4
157+ with :
158+ name : lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}
159+ path : ${{ runner.temp }}/lifecycle-preflight-bundle
160+ if-no-files-found : error
117161
118162 drift :
163+ if : github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
119164 needs : preflight_backtests
120165 permissions :
121166 contents : read
122167 issues : write
123168 id-token : write
124- uses : QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@17278db4e7aef0007346d853eb308b6c1bd8c859
169+ uses : QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@9bb8f31e898ea238a6446472f9f5e58133128d0c
125170 with :
126171 strategy_domain : crypto
127172 caller_event_name : ${{ github.event_name }}
128173 caller_pr_head_repository : ${{ github.event.pull_request.head.repo.full_name || '' }}
129174 snapshot_repository : QuantStrategyLab/CryptoLivePoolPipelines
130175 snapshot_checkout_path : external/CryptoLivePoolPipelines
131176 ai_gateway_service_url : ${{ vars.AI_GATEWAY_SERVICE_URL }}
132- lifecycle_performance_bucket : ${{ vars.LIFECYCLE_PERFORMANCE_BUCKET }}
177+ quant_platform_kit_ref : 9bb8f31e898ea238a6446472f9f5e58133128d0c
178+ lifecycle_preflight_artifact : lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}
133179 secrets :
134180 codex_audit_service_url : ${{ secrets.CODEX_AUDIT_SERVICE_URL }}
135181 snapshot_repository_token : ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN }}
0 commit comments