From 276ea7880f39e3c2e607563e59cdf6750f81dda4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 26 May 2026 00:03:41 +0800 Subject: [PATCH] Add push crisis alert runtime config --- .github/workflows/sync-cloud-run-env.yml | 82 +++++++++++++++++++++++ README.md | 14 ++-- requirements.txt | 4 +- runtime_config_support.py | 22 ++++++ tests/test_runtime_config_support.py | 41 ++++++++++++ tests/test_sync_cloud_run_env_workflow.sh | 32 +++++++++ 6 files changed, 187 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 1c7e223..a32534f 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -51,6 +51,7 @@ jobs: LONGBRIDGE_MIN_RESERVED_CASH_USD: ${{ vars.LONGBRIDGE_MIN_RESERVED_CASH_USD }} LONGBRIDGE_RESERVED_CASH_RATIO: ${{ vars.LONGBRIDGE_RESERVED_CASH_RATIO }} LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }} + CRISIS_ALERT_CHANNELS: ${{ vars.CRISIS_ALERT_CHANNELS }} CRISIS_ALERT_EMAIL_RECIPIENTS: ${{ vars.CRISIS_ALERT_EMAIL_RECIPIENTS }} CRISIS_ALERT_EMAIL_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_EMAIL }} CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }} @@ -65,6 +66,15 @@ jobs: CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID }} CRISIS_ALERT_SMS_API_BASE_URL: ${{ vars.CRISIS_ALERT_SMS_API_BASE_URL }} CRISIS_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_SMS_BODY_MAX_CHARS }} + CRISIS_ALERT_PUSH_RECIPIENTS: ${{ vars.CRISIS_ALERT_PUSH_RECIPIENTS }} + CRISIS_ALERT_PUSH_PROVIDER: ${{ vars.CRISIS_ALERT_PUSH_PROVIDER }} + CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME }} + CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME }} + CRISIS_ALERT_PUSH_API_BASE_URL: ${{ vars.CRISIS_ALERT_PUSH_API_BASE_URL }} + CRISIS_ALERT_PUSH_DEVICE: ${{ vars.CRISIS_ALERT_PUSH_DEVICE }} + CRISIS_ALERT_PUSH_PRIORITY: ${{ vars.CRISIS_ALERT_PUSH_PRIORITY }} + CRISIS_ALERT_PUSH_TAGS: ${{ vars.CRISIS_ALERT_PUSH_TAGS }} + CRISIS_ALERT_PUSH_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_PUSH_BODY_MAX_CHARS }} # Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults. INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }} QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }} @@ -75,6 +85,8 @@ jobs: TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }} CRISIS_ALERT_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }} + CRISIS_ALERT_PUSH_APP_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_APP_TOKEN }} + CRISIS_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_ACCESS_TOKEN }} steps: - name: Check whether env sync is enabled id: config @@ -363,6 +375,28 @@ jobs: remove_secret_vars+=("CRISIS_ALERT_SMS_AUTH_TOKEN") fi + if [ -n "${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + elif [ -n "${CRISIS_ALERT_PUSH_APP_TOKEN:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN}") + remove_secret_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + remove_secret_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + fi + + if [ -n "${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + elif [ -n "${CRISIS_ALERT_PUSH_ACCESS_TOKEN:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN}") + remove_secret_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + remove_secret_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + fi + secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest") remove_env_vars+=("LONGPORT_APP_KEY") @@ -417,6 +451,12 @@ jobs: remove_env_vars+=("LONGBRIDGE_RESERVED_CASH_RATIO") fi + if [ -n "${CRISIS_ALERT_CHANNELS:-}" ]; then + env_pairs+=("CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}") + else + remove_env_vars+=("CRISIS_ALERT_CHANNELS") + fi + if [ -n "${CRISIS_ALERT_EMAIL_RECIPIENTS:-}" ]; then env_pairs+=("CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}") else @@ -489,6 +529,48 @@ jobs: remove_env_vars+=("CRISIS_ALERT_SMS_BODY_MAX_CHARS") fi + if [ -n "${CRISIS_ALERT_PUSH_RECIPIENTS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_RECIPIENTS=${CRISIS_ALERT_PUSH_RECIPIENTS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_RECIPIENTS") + fi + + if [ -n "${CRISIS_ALERT_PUSH_PROVIDER:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_PROVIDER=${CRISIS_ALERT_PUSH_PROVIDER}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_PROVIDER") + fi + + if [ -n "${CRISIS_ALERT_PUSH_API_BASE_URL:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_API_BASE_URL=${CRISIS_ALERT_PUSH_API_BASE_URL}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_API_BASE_URL") + fi + + if [ -n "${CRISIS_ALERT_PUSH_DEVICE:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_DEVICE=${CRISIS_ALERT_PUSH_DEVICE}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_DEVICE") + fi + + if [ -n "${CRISIS_ALERT_PUSH_PRIORITY:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_PRIORITY=${CRISIS_ALERT_PUSH_PRIORITY}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_PRIORITY") + fi + + if [ -n "${CRISIS_ALERT_PUSH_TAGS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_TAGS=${CRISIS_ALERT_PUSH_TAGS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_TAGS") + fi + + if [ -n "${CRISIS_ALERT_PUSH_BODY_MAX_CHARS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS=${CRISIS_ALERT_PUSH_BODY_MAX_CHARS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") + fi + if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}") else diff --git a/README.md b/README.md index 4ef88ff..c79425a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Telegram notifications include structured execution and heartbeat messages, with | `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | No | Set to `true` to log raw LongBridge position quantity and available quantity for troubleshooting. | | `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | No | Optional LongBridge-side strategy plugin mount JSON. The plugin artifact controls mode; platform config must not set `mode`. | +| `CRISIS_ALERT_CHANNELS` | No | Optional crisis alert channel list: `email`, `sms`, and/or `push`. | | `CRISIS_ALERT_EMAIL_RECIPIENTS` | No | Comma/semicolon/newline-separated email-form recipients. Use a normal mailbox for email-only delivery, or a Google Voice-associated mailbox/address to also trigger Google Voice prompts. | | `CRISIS_ALERT_EMAIL_SENDER_EMAIL` | No | Sender email address used for crisis alert email. Gmail is the default transport, but the sender naming is provider-neutral. | | `CRISIS_ALERT_EMAIL_SENDER_PASSWORD` | No | Sender SMTP password or app password. For Cloud Run, prefer `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` in env sync. | @@ -86,8 +87,8 @@ Telegram notifications include structured execution and heartbeat messages, with Strategy allocation can still target fractional dollar values and fractional position weights. The LongBridge execution layer now keeps a whole-share-only rule for every broker order: sell sizing floors to whole shares, buy sizing floors to whole shares, and fractional orders are skipped rather than downgraded. When a target value is zero, sell sizing uses the sellable position quantity instead of re-deriving shares from current price, so liquidation targets do not leave a residual share because of quote drift. -When `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal Telegram cycle message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends an independent crisis email notification when the `CRISIS_ALERT_EMAIL_*` settings are complete. -Email alert results are written into the runtime report. Duplicate suppression uses stable plugin alert keys and stores markers under `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` when set, otherwise `EXECUTION_REPORT_GCS_URI`, with a local `/tmp` marker fallback. +When `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal Telegram cycle message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends independent crisis alerts through configured `CRISIS_ALERT_CHANNELS` channels. +Alert results are written into the runtime report. Duplicate suppression uses stable plugin alert keys and stores markers under `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` when set, otherwise `EXECUTION_REPORT_GCS_URI`, with a local `/tmp` marker fallback. Secret Manager must contain the secret named by `LONGPORT_SECRET_NAME` (default: `longport_token_paper`), where the **latest version = active access token**. The app refreshes it when expiry is within 30 days. @@ -154,7 +155,7 @@ Important: - GitHub now authenticates to Google Cloud with OIDC + Workload Identity Federation, so `GCP_SA_KEY` is no longer required for this workflow. - If you deploy with `gcloud run deploy --source` or a Cloud Run source trigger, also grant `roles/storage.objectViewer` on `gs://run-sources--` to the build service account, the deploy service account, and the default compute service account. Otherwise source deploy can fail before Cloud Build starts with `storage.objects.get` denied. - Here "shared" only means **shared inside this repository** between the `paper`, `HK`, and `SG` Cloud Run services. The Telegram token can still be shared, but LongPort app credentials should live in Secret Manager and be referenced by per-environment secret-name variables; they are not meant to be a global secret set reused by unrelated quant repos. -- If you want one cross-project shared layer across multiple quant repos, keep it small: `GLOBAL_TELEGRAM_CHAT_ID`, `NOTIFY_LANG`, and shared crisis email settings under `CRISIS_ALERT_EMAIL_*` are reasonable when the same alert policy applies; account credentials and deployment keys are not. +- If you want one cross-project shared layer across multiple quant repos, keep it small: `GLOBAL_TELEGRAM_CHAT_ID`, `NOTIFY_LANG`, `CRISIS_ALERT_CHANNELS`, and shared crisis alert settings under `CRISIS_ALERT_EMAIL_*`/`CRISIS_ALERT_PUSH_*` are reasonable when the same alert policy applies; account credentials, deployment keys, and alert secrets are not. ### Deployment unit and naming @@ -240,6 +241,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 | `LONGBRIDGE_DRY_RUN_ONLY` | 否 | 设为 `true` 时,该部署保持 dry-run。 | | `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT` | 否 | 设为 `true` 时输出 LongBridge 原始持仓数量和可卖数量,便于排查。 | | `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` | 否 | 可选的 LongBridge 侧策略插件挂载 JSON。插件 artifact 自带模式;平台配置不要设置 `mode`。 | +| `CRISIS_ALERT_CHANNELS` | 否 | 可选危机告警通道列表:`email`、`sms` 和/或 `push`。 | | `CRISIS_ALERT_EMAIL_RECIPIENTS` | 否 | 通知收件邮箱。普通邮箱只收邮件;关联 Google Voice 的邮箱/地址会额外触发 Google Voice 提醒。支持逗号、分号或换行分隔。 | | `CRISIS_ALERT_EMAIL_SENDER_EMAIL` | 否 | 通知发送方邮箱。默认传输走 Gmail SMTP,但命名不绑定 Gmail。 | | `CRISIS_ALERT_EMAIL_SENDER_PASSWORD` | 否 | 发送方 SMTP 密码或 app password。Cloud Run env sync 建议配置 `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME`。 | @@ -256,8 +258,8 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换 策略分配层仍然可以按目标金额和目标比例计算出小数仓位;LongBridge 执行层只提交整数股订单,因为实测账户的 OpenAPI `submit_order` 会拒绝碎股委托数量。目标市值为 0 时,卖出数量直接按可卖整数股持仓计算,不再用当前报价反推股数,避免因报价漂移留下 1 股残仓。 -如果 `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,只要 `CRISIS_ALERT_EMAIL_*` 配置完整,服务还会额外发送一条独立邮件危机通知。 -邮件告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 +如果 `LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,服务还会按 `CRISIS_ALERT_CHANNELS` 配置额外发送独立危机通知。 +告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `longport_token_paper`),**最新版本 = 当前有效的 access token**。Token 到期前 30 天会自动刷新。 @@ -325,7 +327,7 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo - GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`。 - 如果你用 `gcloud run deploy --source` 或 Cloud Run source trigger 部署,还要给 `gs://run-sources--` 这个 staging bucket 补 `roles/storage.objectViewer`,对象是 build service account、deploy service account、默认 compute service account。少了这层权限,部署会在 Cloud Build 启动前直接报 `storage.objects.get denied`。 - 这里的“共享”只是指 **同一个仓库里的 paper / HK / SG 服务共享**。Telegram token 可以继续共用,但 LongPort app 凭据建议放到 Secret Manager,并通过各自 Environment 里的 secret-name 变量引用,不建议把它们当成所有 quant 共用的全局 secrets。 -- 如果你真的要在多个 quant 仓库之间保留一层全局共享,建议只保留 `GLOBAL_TELEGRAM_CHAT_ID`、`NOTIFY_LANG`,以及同一套危机邮件策略下的 `CRISIS_ALERT_EMAIL_*` 这种低耦合配置。账户凭据和部署 key 不要做成全局共享。 +- 如果你真的要在多个 quant 仓库之间保留一层全局共享,建议只保留 `GLOBAL_TELEGRAM_CHAT_ID`、`NOTIFY_LANG`、`CRISIS_ALERT_CHANNELS`,以及同一套危机告警策略下的 `CRISIS_ALERT_EMAIL_*`/`CRISIS_ALERT_PUSH_*` 这种低耦合配置。账户凭据、部署 key 和告警 secret 不要做成全局共享。 ### 部署单元和命名建议 diff --git a/requirements.txt b/requirements.txt index cedfe05..a30c686 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@7183145b06f8b9292e1c8f3adba0a4f5dc0a5834 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@d13b248d279887a57eef5b7692c2c7bd33906348 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1050a678a17a997bf4776c2a4c4bef59de29e1c1 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@48f0267e3128eb7c6a8344ddefa08e0e80dc9d27 pandas requests pytz diff --git a/runtime_config_support.py b/runtime_config_support.py index 10e3ba4..af6e525 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -54,6 +54,7 @@ class PlatformRuntimeSettings: strategy_config_path: str | None = None strategy_config_source: str | None = None strategy_plugin_mounts_json: str | None = None + crisis_alert_channels: tuple[str, ...] = () crisis_alert_email_recipients: tuple[str, ...] = () crisis_alert_email_sender_email: str | None = None crisis_alert_email_sender_password: str | None = None @@ -68,6 +69,15 @@ class PlatformRuntimeSettings: crisis_alert_sms_messaging_service_id: str | None = None crisis_alert_sms_api_base_url: str | None = None crisis_alert_sms_body_max_chars: str | None = None + crisis_alert_push_recipients: tuple[str, ...] = () + crisis_alert_push_provider: str | None = None + crisis_alert_push_app_token: str | None = None + crisis_alert_push_access_token: str | None = None + crisis_alert_push_api_base_url: str | None = None + crisis_alert_push_device: str | None = None + crisis_alert_push_priority: str | None = None + crisis_alert_push_tags: str | None = None + crisis_alert_push_body_max_chars: str | None = None runtime_target: RuntimeTarget | None = None @@ -164,6 +174,7 @@ def load_platform_runtime_settings( os.getenv("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON") or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON") ), + crisis_alert_channels=_split_env_list(os.getenv("CRISIS_ALERT_CHANNELS")), crisis_alert_email_recipients=_split_env_list(os.getenv("CRISIS_ALERT_EMAIL_RECIPIENTS")), crisis_alert_email_sender_email=_first_non_empty(os.getenv("CRISIS_ALERT_EMAIL_SENDER_EMAIL")), crisis_alert_email_sender_password=_first_non_empty( @@ -186,6 +197,17 @@ def load_platform_runtime_settings( crisis_alert_sms_body_max_chars=_first_non_empty( os.getenv("CRISIS_ALERT_SMS_BODY_MAX_CHARS") ), + crisis_alert_push_recipients=_split_env_list(os.getenv("CRISIS_ALERT_PUSH_RECIPIENTS")), + crisis_alert_push_provider=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_PROVIDER")), + crisis_alert_push_app_token=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_APP_TOKEN")), + crisis_alert_push_access_token=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_ACCESS_TOKEN")), + crisis_alert_push_api_base_url=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_API_BASE_URL")), + crisis_alert_push_device=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_DEVICE")), + crisis_alert_push_priority=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_PRIORITY")), + crisis_alert_push_tags=_first_non_empty(os.getenv("CRISIS_ALERT_PUSH_TAGS")), + crisis_alert_push_body_max_chars=_first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") + ), runtime_target=runtime_target, ) diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index db55cfe..f6eaf69 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -119,6 +119,7 @@ def test_load_platform_runtime_settings_uses_defaults_with_explicit_strategy_pro self.assertIsNone(settings.feature_snapshot_path) self.assertIsNone(settings.strategy_config_path) self.assertIsNone(settings.strategy_plugin_mounts_json) + self.assertEqual(settings.crisis_alert_channels, ()) self.assertEqual(settings.crisis_alert_email_recipients, ()) self.assertIsNone(settings.crisis_alert_email_sender_email) self.assertIsNone(settings.crisis_alert_email_sender_password) @@ -133,6 +134,15 @@ def test_load_platform_runtime_settings_uses_defaults_with_explicit_strategy_pro self.assertIsNone(settings.crisis_alert_sms_messaging_service_id) self.assertIsNone(settings.crisis_alert_sms_api_base_url) self.assertIsNone(settings.crisis_alert_sms_body_max_chars) + self.assertEqual(settings.crisis_alert_push_recipients, ()) + self.assertIsNone(settings.crisis_alert_push_provider) + self.assertIsNone(settings.crisis_alert_push_app_token) + self.assertIsNone(settings.crisis_alert_push_access_token) + self.assertIsNone(settings.crisis_alert_push_api_base_url) + self.assertIsNone(settings.crisis_alert_push_device) + self.assertIsNone(settings.crisis_alert_push_priority) + self.assertIsNone(settings.crisis_alert_push_tags) + self.assertIsNone(settings.crisis_alert_push_body_max_chars) def test_load_platform_runtime_settings_prefers_runtime_target_json(self): with patch.dict( @@ -330,6 +340,37 @@ def test_crisis_alert_sms_config_is_loaded_from_env(self): self.assertEqual(settings.crisis_alert_sms_api_base_url, "https://twilio.example.test") self.assertEqual(settings.crisis_alert_sms_body_max_chars, "160") + def test_crisis_alert_channels_and_push_config_are_loaded_from_env(self): + with patch.dict( + os.environ, + { + "RUNTIME_TARGET_JSON": runtime_target_json(SAMPLE_STRATEGY_PROFILE), + "CRISIS_ALERT_CHANNELS": "email;push", + "CRISIS_ALERT_PUSH_RECIPIENTS": "risk-topic; backup-topic", + "CRISIS_ALERT_PUSH_PROVIDER": "ntfy", + "CRISIS_ALERT_PUSH_APP_TOKEN": "app-token", + "CRISIS_ALERT_PUSH_ACCESS_TOKEN": "access-token", + "CRISIS_ALERT_PUSH_API_BASE_URL": "https://ntfy.example.test", + "CRISIS_ALERT_PUSH_DEVICE": "iphone", + "CRISIS_ALERT_PUSH_PRIORITY": "5", + "CRISIS_ALERT_PUSH_TAGS": "warning", + "CRISIS_ALERT_PUSH_BODY_MAX_CHARS": "300", + }, + clear=True, + ): + settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") + + self.assertEqual(settings.crisis_alert_channels, ("email", "push")) + self.assertEqual(settings.crisis_alert_push_recipients, ("risk-topic", "backup-topic")) + self.assertEqual(settings.crisis_alert_push_provider, "ntfy") + self.assertEqual(settings.crisis_alert_push_app_token, "app-token") + self.assertEqual(settings.crisis_alert_push_access_token, "access-token") + self.assertEqual(settings.crisis_alert_push_api_base_url, "https://ntfy.example.test") + self.assertEqual(settings.crisis_alert_push_device, "iphone") + self.assertEqual(settings.crisis_alert_push_priority, "5") + self.assertEqual(settings.crisis_alert_push_tags, "warning") + self.assertEqual(settings.crisis_alert_push_body_max_chars, "300") + def test_income_layer_overrides_are_loaded_from_env(self): with patch.dict( os.environ, diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 6a036cf..2ae48d4 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -38,6 +38,8 @@ grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workfl grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_APP_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_APP_TOKEN }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_ACCESS_TOKEN }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}' "$workflow_file" grep -Fq 'LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }}' "$workflow_file" grep -Fq 'LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }}' "$workflow_file" @@ -49,6 +51,7 @@ grep -Fq 'LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_P grep -Fq 'LONGBRIDGE_MIN_RESERVED_CASH_USD: ${{ vars.LONGBRIDGE_MIN_RESERVED_CASH_USD }}' "$workflow_file" grep -Fq 'LONGBRIDGE_RESERVED_CASH_RATIO: ${{ vars.LONGBRIDGE_RESERVED_CASH_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_CHANNELS: ${{ vars.CRISIS_ALERT_CHANNELS }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_RECIPIENTS: ${{ vars.CRISIS_ALERT_EMAIL_RECIPIENTS }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_EMAIL }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }}' "$workflow_file" @@ -63,6 +66,15 @@ grep -Fq 'CRISIS_ALERT_SMS_SENDER: ${{ vars.CRISIS_ALERT_SMS_SENDER }}' "$workfl grep -Fq 'CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_API_BASE_URL: ${{ vars.CRISIS_ALERT_SMS_API_BASE_URL }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_SMS_BODY_MAX_CHARS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_RECIPIENTS: ${{ vars.CRISIS_ALERT_PUSH_RECIPIENTS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PROVIDER: ${{ vars.CRISIS_ALERT_PUSH_PROVIDER }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_API_BASE_URL: ${{ vars.CRISIS_ALERT_PUSH_API_BASE_URL }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_DEVICE: ${{ vars.CRISIS_ALERT_PUSH_DEVICE }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PRIORITY: ${{ vars.CRISIS_ALERT_PUSH_PRIORITY }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_TAGS: ${{ vars.CRISIS_ALERT_PUSH_TAGS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_PUSH_BODY_MAX_CHARS }}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file" @@ -93,6 +105,8 @@ grep -Fq '&& [ -z "${LONGBRIDGE_STRATEGY_CONFIG_PATH:-}" ]; then' "$workflow_fil grep -Fq 'secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("CRISIS_ALERT_SMS_AUTH_TOKEN=${CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'secret_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'secret_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("LONGPORT_APP_SECRET=${LONGPORT_APP_SECRET_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}' "$workflow_file" @@ -106,6 +120,8 @@ grep -Fq 'LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${LONGBRIDGE_SAFE_ grep -Fq 'remove_env_vars+=("LONGBRIDGE_MIN_RESERVED_CASH_USD")' "$workflow_file" grep -Fq 'remove_env_vars+=("LONGBRIDGE_RESERVED_CASH_RATIO")' "$workflow_file" grep -Fq 'remove_env_vars+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")' "$workflow_file" +grep -Fq 'CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_CHANNELS")' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_EMAIL=${CRISIS_ALERT_EMAIL_SENDER_EMAIL}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD}' "$workflow_file" @@ -119,6 +135,13 @@ grep -Fq 'CRISIS_ALERT_SMS_SENDER=${CRISIS_ALERT_SMS_SENDER}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID=${CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_API_BASE_URL=${CRISIS_ALERT_SMS_API_BASE_URL}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_BODY_MAX_CHARS=${CRISIS_ALERT_SMS_BODY_MAX_CHARS}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_RECIPIENTS=${CRISIS_ALERT_PUSH_RECIPIENTS}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PROVIDER=${CRISIS_ALERT_PUSH_PROVIDER}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_API_BASE_URL=${CRISIS_ALERT_PUSH_API_BASE_URL}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_DEVICE=${CRISIS_ALERT_PUSH_DEVICE}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PRIORITY=${CRISIS_ALERT_PUSH_PRIORITY}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_TAGS=${CRISIS_ALERT_PUSH_TAGS}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_BODY_MAX_CHARS=${CRISIS_ALERT_PUSH_BODY_MAX_CHARS}' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_RECIPIENTS")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD")' "$workflow_file" @@ -133,6 +156,15 @@ grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_SENDER")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_API_BASE_URL")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_BODY_MAX_CHARS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_RECIPIENTS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_PROVIDER")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_API_BASE_URL")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_DEVICE")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_PRIORITY")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_TAGS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS")' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_TO"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"' "$workflow_file"