Skip to content

Commit 8a561f7

Browse files
authored
Merge pull request #276 from QuantStrategyLab/codex/runtime-target-lifecycle
feat: publish Firstrade runtime target lifecycle
2 parents 2de8d2f + 7cbbfc2 commit 8a561f7

3 files changed

Lines changed: 197 additions & 5 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
name: Runtime Target Lifecycle
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "37 * * * *"
7+
8+
env:
9+
GCP_PROJECT_ID: firstradequant
10+
GCP_WORKLOAD_IDENTITY_PROVIDER: projects/1088907247379/locations/global/workloadIdentityPools/github-actions/providers/github-main
11+
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: firstrade-platform-deploy@firstradequant.iam.gserviceaccount.com
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref_name }}
15+
cancel-in-progress: false
16+
17+
jobs:
18+
lifecycle:
19+
name: Publish Firstrade target lifecycle
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 15
22+
permissions:
23+
contents: read
24+
id-token: write
25+
env:
26+
RUNTIME_TARGET_ENABLED: ${{ vars.RUNTIME_TARGET_ENABLED }}
27+
RUNTIME_TARGET_JSON: ${{ secrets.RUNTIME_TARGET_JSON }}
28+
FIRSTRADE_DRY_RUN_ONLY: ${{ vars.FIRSTRADE_DRY_RUN_ONLY }}
29+
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
30+
CLOUD_RUN_SERVICE: ${{ secrets.CLOUD_RUN_SERVICE }}
31+
CLOUD_RUN_SERVICES: ${{ secrets.CLOUD_RUN_SERVICES }}
32+
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ secrets.CLOUD_RUN_SERVICE_TARGETS_JSON }}
33+
RUNTIME_GUARD_LOOKBACK_MINUTES: ${{ vars.RUNTIME_GUARD_LOOKBACK_MINUTES || '180' }}
34+
RUNTIME_GUARD_REQUIRE_SUCCESS: ${{ vars.RUNTIME_GUARD_REQUIRE_SUCCESS || 'false' }}
35+
RUNTIME_HEARTBEAT_GCS_URIS: ${{ vars.RUNTIME_HEARTBEAT_GCS_URIS || vars.EXECUTION_REPORT_GCS_URI }}
36+
RUNTIME_HEARTBEAT_LOOKBACK_HOURS: ${{ vars.RUNTIME_HEARTBEAT_LOOKBACK_HOURS || '36' }}
37+
RUNTIME_HEARTBEAT_ACCEPT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_ACCEPT_STAGES }}
38+
RUNTIME_HEARTBEAT_REJECT_STAGES: ${{ vars.RUNTIME_HEARTBEAT_REJECT_STAGES }}
39+
RUNTIME_HEARTBEAT_MARKET_AWARE: ${{ vars.RUNTIME_HEARTBEAT_MARKET_AWARE || 'true' }}
40+
RUNTIME_HEARTBEAT_MARKET_CALENDAR: ${{ vars.FIRSTRADE_MARKET_CALENDAR }}
41+
RUNTIME_HEARTBEAT_MARKET_TIMEZONE: ${{ vars.FIRSTRADE_MARKET_TIMEZONE }}
42+
RUNTIME_HEARTBEAT_PUBLICATION_GRACE_MINUTES: ${{ vars.RUNTIME_HEARTBEAT_PUBLICATION_GRACE_MINUTES || '30' }}
43+
RUNTIME_HEARTBEAT_SCHEDULER_AWARE: ${{ vars.RUNTIME_HEARTBEAT_SCHEDULER_AWARE || 'true' }}
44+
RUNTIME_HEARTBEAT_SCHEDULER_LOCATION: ${{ vars.RUNTIME_HEARTBEAT_SCHEDULER_LOCATION || vars.CLOUD_RUN_REGION || 'us-central1' }}
45+
EXECUTION_EVIDENCE_SYNC_URL: ${{ vars.EXECUTION_EVIDENCE_SYNC_URL }}
46+
steps:
47+
- name: Checkout repository
48+
uses: actions/checkout@v6
49+
50+
- name: Resolve declared no-order target state
51+
id: target_state
52+
run: |
53+
set -euo pipefail
54+
python3 - <<'PY' >> "$GITHUB_OUTPUT"
55+
import json
56+
import os
57+
58+
target = {}
59+
raw_target = (os.environ.get("RUNTIME_TARGET_JSON") or "").strip()
60+
if raw_target:
61+
try:
62+
decoded = json.loads(raw_target)
63+
except json.JSONDecodeError as error:
64+
raise SystemExit(f"RUNTIME_TARGET_JSON is invalid: {error}") from error
65+
if isinstance(decoded, dict):
66+
target = decoded
67+
68+
enabled = (os.environ.get("RUNTIME_TARGET_ENABLED") or target.get("runtime_target_enabled") or target.get("RUNTIME_TARGET_ENABLED") or "true").strip().lower()
69+
if enabled not in {"true", "false"}:
70+
raise SystemExit("RUNTIME_TARGET_ENABLED must be true or false")
71+
72+
mode = str(target.get("execution_mode") or "").strip()
73+
if mode not in {"dry_run", "paper", "live"}:
74+
mode = "dry_run" if (os.environ.get("FIRSTRADE_DRY_RUN_ONLY") or "").strip().lower() == "true" else "live"
75+
76+
print(f"configured_state={'enabled' if enabled == 'true' else 'disabled'}")
77+
print(f"execution_mode={mode}")
78+
PY
79+
80+
- name: Authenticate to Google Cloud
81+
uses: google-github-actions/auth@v3
82+
with:
83+
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
84+
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
85+
86+
- name: Set up gcloud
87+
uses: google-github-actions/setup-gcloud@v3
88+
with:
89+
project_id: ${{ env.GCP_PROJECT_ID }}
90+
91+
- name: Check Cloud Run runtime without notification side effects
92+
id: runtime_guard
93+
env:
94+
RUNTIME_GUARD_NAME: FirstradePlatform lifecycle
95+
RUNTIME_GUARD_FAIL_WORKFLOW_ON_ALERT: "true"
96+
run: |
97+
set -uo pipefail
98+
python scripts/cloud_run_runtime_guard.py > runtime-guard.log 2>&1
99+
exit_code=$?
100+
cat runtime-guard.log
101+
if [ "$exit_code" -eq 0 ]; then
102+
status=pass
103+
elif grep -Eqi 'log query failed|internal error|network|timed out|unable to list' runtime-guard.log; then
104+
status=unavailable
105+
else
106+
status=attention
107+
fi
108+
echo "status=$status" >> "$GITHUB_OUTPUT"
109+
exit 0
110+
111+
- name: Install market calendar for enabled heartbeat
112+
if: ${{ steps.target_state.outputs.configured_state == 'enabled' }}
113+
run: >-
114+
python -m pip install --disable-pip-version-check
115+
--retries 3 --timeout 30 "pandas-market-calendars==5.4.0"
116+
117+
- name: Check enabled execution heartbeat without notification side effects
118+
id: execution_heartbeat
119+
env:
120+
RUNTIME_HEARTBEAT_NAME: FirstradePlatform lifecycle
121+
RUNTIME_HEARTBEAT_REQUIRED_SERVICES: ${{ secrets.CLOUD_RUN_SERVICE }}
122+
RUNTIME_HEARTBEAT_FAIL_WORKFLOW_ON_ALERT: "true"
123+
run: |
124+
set -uo pipefail
125+
if [ "${{ steps.target_state.outputs.configured_state }}" = "disabled" ]; then
126+
echo "status=not_applicable" >> "$GITHUB_OUTPUT"
127+
echo "Execution heartbeat is not applicable because this target is deliberately disabled."
128+
exit 0
129+
fi
130+
131+
python scripts/execution_report_heartbeat.py > execution-heartbeat.log 2>&1
132+
exit_code=$?
133+
cat execution-heartbeat.log
134+
if [ "$exit_code" -eq 0 ] && grep -qi 'heartbeat skipped' execution-heartbeat.log; then
135+
status=not_due
136+
elif [ "$exit_code" -eq 0 ]; then
137+
status=pass
138+
elif grep -Eqi 'gcloud|storage|network|timed out|internal error|unable to list' execution-heartbeat.log; then
139+
status=unavailable
140+
else
141+
status=attention
142+
fi
143+
echo "status=$status" >> "$GITHUB_OUTPUT"
144+
exit 0
145+
146+
- name: Detect unified control-plane ingress
147+
id: lifecycle_ingress
148+
env:
149+
EXECUTION_EVIDENCE_SYNC_TOKEN: ${{ secrets.EXECUTION_EVIDENCE_SYNC_TOKEN }}
150+
run: |
151+
set -euo pipefail
152+
if [ -n "${EXECUTION_EVIDENCE_SYNC_URL:-}" ] && [ -n "${EXECUTION_EVIDENCE_SYNC_TOKEN:-}" ]; then
153+
echo "enabled=true" >> "$GITHUB_OUTPUT"
154+
else
155+
echo "enabled=false" >> "$GITHUB_OUTPUT"
156+
fi
157+
158+
- name: Publish lifecycle to the unified control plane
159+
if: ${{ steps.lifecycle_ingress.outputs.enabled == 'true' }}
160+
uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-target-lifecycle@6d71e785dbf21bac6c829a1fc72f3a4492d36f54
161+
with:
162+
source-id: firstrade.primary
163+
target-id: firstrade.primary
164+
platform: firstrade
165+
configured-state: ${{ steps.target_state.outputs.configured_state }}
166+
execution-mode: ${{ steps.target_state.outputs.execution_mode }}
167+
runtime-guard: ${{ steps.runtime_guard.outputs.status }}
168+
execution-heartbeat: ${{ steps.execution_heartbeat.outputs.status }}
169+
sync-url: ${{ env.EXECUTION_EVIDENCE_SYNC_URL }}
170+
env:
171+
EXECUTION_EVIDENCE_SYNC_TOKEN: ${{ secrets.EXECUTION_EVIDENCE_SYNC_TOKEN }}
172+
173+
- name: Explain missing lifecycle ingress configuration
174+
if: ${{ steps.lifecycle_ingress.outputs.enabled != 'true' }}
175+
run: |
176+
echo "Lifecycle was checked but not centrally published: configure EXECUTION_EVIDENCE_SYNC_URL and EXECUTION_EVIDENCE_SYNC_TOKEN." >&2
177+
178+
- name: Summarize no-order lifecycle
179+
run: |
180+
{
181+
echo "## Runtime target lifecycle"
182+
echo
183+
echo "| Field | Value |"
184+
echo "| --- | --- |"
185+
echo "| Target | firstrade.primary |"
186+
echo "| Configured state | ${{ steps.target_state.outputs.configured_state }} |"
187+
echo "| Intended lane | ${{ steps.target_state.outputs.execution_mode }} |"
188+
echo "| Runtime guard | ${{ steps.runtime_guard.outputs.status }} |"
189+
echo "| Execution heartbeat | ${{ steps.execution_heartbeat.outputs.status }} |"
190+
echo
191+
echo "This workflow is read-only: it cannot enable a target, alter its lane, or submit an order."
192+
} >> "$GITHUB_STEP_SUMMARY"

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"pytest",
1919
"pytz",
2020
"requests",
21-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@be45863ad75316e3c993338828da356ddfb42f65",
21+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@cc8891a66cf58d9d339f5d257b412f3f7608a149",
2222
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@26c9b908c486ca60123414eda42cd509c0f17c62",
2323
]
2424
license = "MIT"
@@ -82,5 +82,5 @@ show_missing = true
8282

8383
[tool.uv]
8484
override-dependencies = [
85-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@be45863ad75316e3c993338828da356ddfb42f65",
85+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@cc8891a66cf58d9d339f5d257b412f3f7608a149",
8686
]

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)