|
| 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" |
0 commit comments