diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index aa5f1ee..76a9acd 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -34,7 +34,9 @@ jobs: IBKR_MIN_RESERVED_CASH_USD: ${{ vars.IBKR_MIN_RESERVED_CASH_USD }} IBKR_RESERVED_CASH_RATIO: ${{ vars.IBKR_RESERVED_CASH_RATIO }} IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }} + CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }} CRISIS_ALERT_EMAIL_TO: ${{ vars.CRISIS_ALERT_EMAIL_TO }} + CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }} CRISIS_ALERT_EMAIL_FROM: ${{ vars.CRISIS_ALERT_EMAIL_FROM }} CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }} CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }} @@ -364,12 +366,24 @@ jobs: remove_env_vars+=("IBKR_RESERVED_CASH_RATIO") fi + if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_TO:-}" ]; then + env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_TO=${CRISIS_ALERT_GOOGLE_VOICE_TO}") + else + remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_TO") + fi + if [ -n "${CRISIS_ALERT_EMAIL_TO:-}" ]; then env_pairs+=("CRISIS_ALERT_EMAIL_TO=${CRISIS_ALERT_EMAIL_TO}") else remove_env_vars+=("CRISIS_ALERT_EMAIL_TO") fi + if [ -n "${CRISIS_ALERT_SMTP_FROM:-}" ]; then + env_pairs+=("CRISIS_ALERT_SMTP_FROM=${CRISIS_ALERT_SMTP_FROM}") + else + remove_env_vars+=("CRISIS_ALERT_SMTP_FROM") + fi + if [ -n "${CRISIS_ALERT_EMAIL_FROM:-}" ]; then env_pairs+=("CRISIS_ALERT_EMAIL_FROM=${CRISIS_ALERT_EMAIL_FROM}") else diff --git a/README.md b/README.md index 66723a6..e01eb47 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,11 @@ For IBKR, keep `paper` as a single account-group entry. If you later add live ac | `TELEGRAM_TOKEN` | Yes | Telegram bot token. For Cloud Run, prefer a Secret Manager reference instead of a literal env var. | | `GLOBAL_TELEGRAM_CHAT_ID` | Yes | Telegram chat ID used by this service. | | `NOTIFY_LANG` | No | `en` (default) or `zh` | -| `CRISIS_ALERT_EMAIL_TO` | No | Comma/semicolon/newline-separated recipients for escalated crisis-plugin email alerts. Email is skipped when unset. | -| `CRISIS_ALERT_EMAIL_FROM` | No | Sender address for crisis-plugin email alerts. | -| `CRISIS_ALERT_SMTP_HOST` | No | SMTP host for crisis-plugin email alerts. | +| `CRISIS_ALERT_GOOGLE_VOICE_TO` | No | Comma/semicolon/newline-separated Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com`. | +| `CRISIS_ALERT_EMAIL_TO` | No | Optional ordinary email recipients that receive the same escalated alert; also accepted as a legacy recipient list. | +| `CRISIS_ALERT_SMTP_FROM` | No | SMTP sender address for Google Voice alerts. Falls back to `CRISIS_ALERT_EMAIL_FROM`. | +| `CRISIS_ALERT_EMAIL_FROM` | No | Legacy SMTP sender alias; prefer `CRISIS_ALERT_SMTP_FROM`. | +| `CRISIS_ALERT_SMTP_HOST` | No | SMTP host for Google Voice alerts. | | `CRISIS_ALERT_SMTP_PORT` | No | SMTP port; defaults to `587`. | | `CRISIS_ALERT_SMTP_USERNAME` | No | Optional SMTP username. | | `CRISIS_ALERT_SMTP_PASSWORD` | No | Optional SMTP password. For Cloud Run, prefer `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME` in env sync. | @@ -215,8 +217,8 @@ Current behavior is fail-fast: - missing account-group config source → startup error - missing key fields in the selected group (`ib_gateway_instance_name`, `ib_gateway_mode`, `ib_client_id`) → startup error -When `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal strategy-cycle Telegram 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 when the `CRISIS_ALERT_*` SMTP 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 `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal strategy-cycle Telegram 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 Google Voice notification when the `CRISIS_ALERT_*` SMTP settings are complete. +Google Voice 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. ### GitHub-managed Cloud Run env sync @@ -233,7 +235,7 @@ Recommended setup: - `ACCOUNT_GROUP` (recommended: `paper`) - `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` - Optional: `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON`, `IBKR_MIN_RESERVED_CASH_USD`, `IBKR_RESERVED_CASH_RATIO`, `IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` - - Optional crisis email alerts: `CRISIS_ALERT_EMAIL_TO`, `CRISIS_ALERT_EMAIL_FROM`, `CRISIS_ALERT_SMTP_HOST`, `CRISIS_ALERT_SMTP_PORT`, `CRISIS_ALERT_SMTP_USERNAME`, `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`, `CRISIS_ALERT_SMTP_STARTTLS`, `CRISIS_ALERT_SMTP_SSL` + - Optional crisis Google Voice alerts: `CRISIS_ALERT_GOOGLE_VOICE_TO`, `CRISIS_ALERT_EMAIL_TO`, `CRISIS_ALERT_SMTP_FROM`, `CRISIS_ALERT_EMAIL_FROM`, `CRISIS_ALERT_SMTP_HOST`, `CRISIS_ALERT_SMTP_PORT`, `CRISIS_ALERT_SMTP_USERNAME`, `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`, `CRISIS_ALERT_SMTP_STARTTLS`, `CRISIS_ALERT_SMTP_SSL` - `GLOBAL_TELEGRAM_CHAT_ID` - `NOTIFY_LANG` - **Repository Secrets** @@ -364,9 +366,11 @@ IBKR 账户 | `TELEGRAM_TOKEN` | 是 | Telegram 机器人 Token。Cloud Run 上更推荐走 Secret Manager 引用,不要直接写成明文 env。 | | `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 | | `NOTIFY_LANG` | 否 | `en`(默认)或 `zh` | -| `CRISIS_ALERT_EMAIL_TO` | 否 | 危机插件升级邮件收件人,支持逗号、分号或换行分隔。不配置则跳过邮件。 | -| `CRISIS_ALERT_EMAIL_FROM` | 否 | 危机插件升级邮件发件人。 | -| `CRISIS_ALERT_SMTP_HOST` | 否 | 危机插件升级邮件的 SMTP host。 | +| `CRISIS_ALERT_GOOGLE_VOICE_TO` | 否 | Google Voice 短信网关收件人,通常以 `@txt.voice.google.com` 结尾,支持逗号、分号或换行分隔。 | +| `CRISIS_ALERT_EMAIL_TO` | 否 | 可选普通邮件收件人,会收到同一份升级告警;也作为旧版收件人配置兼容。 | +| `CRISIS_ALERT_SMTP_FROM` | 否 | Google Voice 告警的 SMTP 发件人;未设置时回退到 `CRISIS_ALERT_EMAIL_FROM`。 | +| `CRISIS_ALERT_EMAIL_FROM` | 否 | 旧版 SMTP 发件人别名;优先使用 `CRISIS_ALERT_SMTP_FROM`。 | +| `CRISIS_ALERT_SMTP_HOST` | 否 | Google Voice 告警的 SMTP host。 | | `CRISIS_ALERT_SMTP_PORT` | 否 | SMTP 端口,默认 `587`。 | | `CRISIS_ALERT_SMTP_USERNAME` | 否 | 可选 SMTP 用户名。 | | `CRISIS_ALERT_SMTP_PASSWORD` | 否 | 可选 SMTP 密码。Cloud Run env sync 建议配置 `CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`。 | @@ -433,8 +437,8 @@ IB_GATEWAY_IP_MODE=internal - 没有账号组配置来源 → 启动直接报错 - 选中的账号组缺少关键字段(`ib_gateway_instance_name`、`ib_gateway_mode`、`ib_client_id`)→ 启动直接报错 -如果 `IBKR_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_*` SMTP 配置完整,服务还会额外发一封独立危机邮件。 -邮件告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 +如果 `IBKR_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_*` SMTP 配置完整,服务还会额外发一封独立 Google Voice 危机通知。 +Google Voice 告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 ### GitHub 统一管理 Cloud Run 环境变量 @@ -451,7 +455,7 @@ IB_GATEWAY_IP_MODE=internal - `ACCOUNT_GROUP`(建议设为 `paper`) - `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` - 可选:`IBKR_STRATEGY_PLUGIN_MOUNTS_JSON`、`IBKR_MIN_RESERVED_CASH_USD`、`IBKR_RESERVED_CASH_RATIO`、`IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD` - - 可选危机插件邮件告警:`CRISIS_ALERT_EMAIL_TO`、`CRISIS_ALERT_EMAIL_FROM`、`CRISIS_ALERT_SMTP_HOST`、`CRISIS_ALERT_SMTP_PORT`、`CRISIS_ALERT_SMTP_USERNAME`、`CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`、`CRISIS_ALERT_SMTP_STARTTLS`、`CRISIS_ALERT_SMTP_SSL` + - 可选危机插件 Google Voice 告警:`CRISIS_ALERT_GOOGLE_VOICE_TO`、`CRISIS_ALERT_EMAIL_TO`、`CRISIS_ALERT_SMTP_FROM`、`CRISIS_ALERT_EMAIL_FROM`、`CRISIS_ALERT_SMTP_HOST`、`CRISIS_ALERT_SMTP_PORT`、`CRISIS_ALERT_SMTP_USERNAME`、`CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME`、`CRISIS_ALERT_SMTP_STARTTLS`、`CRISIS_ALERT_SMTP_SSL` - `GLOBAL_TELEGRAM_CHAT_ID` - `NOTIFY_LANG` - **仓库级 Secrets** diff --git a/main.py b/main.py index 0361546..95a6efd 100644 --- a/main.py +++ b/main.py @@ -25,10 +25,10 @@ from decision_mapper import map_strategy_decision from entrypoints.cloud_run import is_market_open_today from notifications.telegram import build_strategy_display_name, build_translator, send_telegram_message -from quant_platform_kit.notifications.strategy_plugin_email import ( - StrategyPluginEmailAlertMarkerStore, - build_strategy_plugin_alert_context_label as build_email_alert_context_label, - publish_strategy_plugin_email_alerts, +from quant_platform_kit.notifications.strategy_plugin_google_voice import ( + StrategyPluginGoogleVoiceAlertMarkerStore, + build_strategy_plugin_alert_context_label as build_google_voice_alert_context_label, + publish_strategy_plugin_google_voice_alerts, ) from quant_platform_kit.common.runtime_assembly import build_runtime_assembly from quant_platform_kit.common.runtime_reports import ( @@ -478,7 +478,7 @@ def build_strategy_plugin_alert_messages(signals): def build_strategy_plugin_alert_store(): - return StrategyPluginEmailAlertMarkerStore( + return StrategyPluginGoogleVoiceAlertMarkerStore( local_dir=os.getenv("STRATEGY_PLUGIN_ALERT_STATE_DIR") or "/tmp/quant_strategy_plugin_alerts", gcs_prefix_uri=os.getenv("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI") or os.getenv("EXECUTION_REPORT_GCS_URI"), gcp_project_id=PROJECT_ID, @@ -486,7 +486,7 @@ def build_strategy_plugin_alert_store(): def build_strategy_plugin_alert_context_label() -> str: - return build_email_alert_context_label( + return build_google_voice_alert_context_label( platform_id="ibkr", strategy_profile=STRATEGY_PROFILE, account_scope=ACCOUNT_GROUP, @@ -495,15 +495,15 @@ def build_strategy_plugin_alert_context_label() -> str: ) -def attach_strategy_plugin_alert_email_result(report, result) -> None: - report.setdefault("summary", {})["strategy_plugin_alert_email_sent_count"] = result.sent_count +def attach_strategy_plugin_alert_google_voice_result(report, result) -> None: + report.setdefault("summary", {})["strategy_plugin_alert_google_voice_sent_count"] = result.sent_count report.setdefault("diagnostics", {}).update(result.to_report_fields()) def publish_strategy_plugin_alerts(signals, *, report=None): - result = publish_strategy_plugin_email_alerts( + result = publish_strategy_plugin_google_voice_alerts( signals, - email_settings=RUNTIME_SETTINGS, + google_voice_settings=RUNTIME_SETTINGS, translator=t, strategy_label=STRATEGY_PROFILE, context_label=build_strategy_plugin_alert_context_label(), @@ -511,7 +511,7 @@ def publish_strategy_plugin_alerts(signals, *, report=None): log_message=print, ) if report is not None: - attach_strategy_plugin_alert_email_result(report, result) + attach_strategy_plugin_alert_google_voice_result(report, result) return result diff --git a/requirements.txt b/requirements.txt index 8517a38..c272b13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ba67541711228f5a72a294def0e5cc24cc5479f3 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@305f2cc0748ec08d001deabc3add6c4eff7fe7ba +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8ed13d9122f52c35425b0802d22467bb6664dcd3 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@e89ea43181f687d3454636b4b2d99ab7771546f4 pandas numpy requests diff --git a/runtime_config_support.py b/runtime_config_support.py index 9c5e8f7..aa0f520 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -72,6 +72,8 @@ class PlatformRuntimeSettings: tg_chat_id: str | None = None notify_lang: str = "en" strategy_plugin_mounts_json: str | None = None + crisis_alert_google_voice_to: tuple[str, ...] = () + crisis_alert_smtp_from: str | None = None crisis_alert_email_to: tuple[str, ...] = () crisis_alert_email_from: str | None = None crisis_alert_smtp_host: str | None = None @@ -188,6 +190,11 @@ def load_platform_runtime_settings( os.getenv("IBKR_STRATEGY_PLUGIN_MOUNTS_JSON") or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON") ), + crisis_alert_google_voice_to=split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_TO")), + crisis_alert_smtp_from=first_non_empty( + os.getenv("CRISIS_ALERT_SMTP_FROM"), + os.getenv("CRISIS_ALERT_EMAIL_FROM"), + ), crisis_alert_email_to=split_env_list(os.getenv("CRISIS_ALERT_EMAIL_TO")), crisis_alert_email_from=first_non_empty(os.getenv("CRISIS_ALERT_EMAIL_FROM")), crisis_alert_smtp_host=first_non_empty(os.getenv("CRISIS_ALERT_SMTP_HOST")), diff --git a/tests/conftest.py b/tests/conftest.py index f417047..7dae0b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,9 @@ def load_strategy_module(**env_overrides): ), "IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME": None, "GLOBAL_TELEGRAM_CHAT_ID": None, + "CRISIS_ALERT_GOOGLE_VOICE_TO": None, "CRISIS_ALERT_EMAIL_TO": None, + "CRISIS_ALERT_SMTP_FROM": None, "CRISIS_ALERT_EMAIL_FROM": None, "CRISIS_ALERT_SMTP_HOST": None, "CRISIS_ALERT_SMTP_PORT": None, diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 611bfb2..e8f76e0 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -54,15 +54,15 @@ def fake_publish(signals, **kwargs): return types.SimpleNamespace( sent_count=1, to_report_fields=lambda: { - "strategy_plugin_alert_email_attempted_count": 1, - "strategy_plugin_alert_email_sent_count": 1, - "strategy_plugin_alert_email_skipped_count": 0, - "strategy_plugin_alert_email_failed_count": 0, - "strategy_plugin_alert_email_deliveries": [], + "strategy_plugin_alert_google_voice_attempted_count": 1, + "strategy_plugin_alert_google_voice_sent_count": 1, + "strategy_plugin_alert_google_voice_skipped_count": 0, + "strategy_plugin_alert_google_voice_failed_count": 0, + "strategy_plugin_alert_google_voice_deliveries": [], }, ) - monkeypatch.setattr(strategy_module, "publish_strategy_plugin_email_alerts", fake_publish) + monkeypatch.setattr(strategy_module, "publish_strategy_plugin_google_voice_alerts", fake_publish) monkeypatch.setattr(strategy_module, "run_strategy_core", lambda **_kwargs: "OK - executed") with strategy_module.app.test_request_context("/", method="POST"): diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 360479c..e45b32d 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -135,6 +135,8 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch): assert settings.tg_token is None assert settings.tg_chat_id is None assert settings.strategy_plugin_mounts_json is None + assert settings.crisis_alert_google_voice_to == () + assert settings.crisis_alert_smtp_from is None assert settings.crisis_alert_email_to == () assert settings.crisis_alert_email_from is None assert settings.crisis_alert_smtp_host is None @@ -208,11 +210,13 @@ def test_load_platform_runtime_settings_supports_explicit_group_config_values(mo assert settings.notify_lang == "zh" -def test_load_platform_runtime_settings_reads_crisis_alert_email_config(monkeypatch): +def test_load_platform_runtime_settings_reads_crisis_alert_google_voice_config(monkeypatch): monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json(SAMPLE_STRATEGY_PROFILE)) monkeypatch.setenv("ACCOUNT_GROUP", "paper") monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON) + monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_TO", "gateway@txt.voice.google.com") monkeypatch.setenv("CRISIS_ALERT_EMAIL_TO", "risk@example.com;ops@example.com,risk@example.com") + monkeypatch.setenv("CRISIS_ALERT_SMTP_FROM", "smtp-from@example.com") monkeypatch.setenv("CRISIS_ALERT_EMAIL_FROM", "bot@example.com") monkeypatch.setenv("CRISIS_ALERT_SMTP_HOST", "smtp.example.com") monkeypatch.setenv("CRISIS_ALERT_SMTP_PORT", "465") @@ -223,6 +227,8 @@ def test_load_platform_runtime_settings_reads_crisis_alert_email_config(monkeypa settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") + assert settings.crisis_alert_google_voice_to == ("gateway@txt.voice.google.com",) + assert settings.crisis_alert_smtp_from == "smtp-from@example.com" assert settings.crisis_alert_email_to == ("risk@example.com", "ops@example.com") assert settings.crisis_alert_email_from == "bot@example.com" assert settings.crisis_alert_smtp_host == "smtp.example.com" diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 7df8182..bfa58a9 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -45,7 +45,9 @@ grep -Fq 'IBKR_PAPER_LIQUIDATE_ONLY: ${{ vars.IBKR_PAPER_LIQUIDATE_ONLY }}' "$wo grep -Fq 'IBKR_MIN_RESERVED_CASH_USD: ${{ vars.IBKR_MIN_RESERVED_CASH_USD }}' "$workflow_file" grep -Fq 'IBKR_RESERVED_CASH_RATIO: ${{ vars.IBKR_RESERVED_CASH_RATIO }}' "$workflow_file" grep -Fq 'IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_TO: ${{ vars.CRISIS_ALERT_EMAIL_TO }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_FROM: ${{ vars.CRISIS_ALERT_EMAIL_FROM }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }}' "$workflow_file" @@ -96,7 +98,9 @@ grep -Fq 'env_pairs+=("IBKR_PAPER_LIQUIDATE_ONLY=${IBKR_PAPER_LIQUIDATE_ONLY}")' grep -Fq 'env_pairs+=("IBKR_MIN_RESERVED_CASH_USD=${IBKR_MIN_RESERVED_CASH_USD}")' "$workflow_file" grep -Fq 'env_pairs+=("IBKR_RESERVED_CASH_RATIO=${IBKR_RESERVED_CASH_RATIO}")' "$workflow_file" grep -Fq 'env_pairs+=("IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_TO=${CRISIS_ALERT_GOOGLE_VOICE_TO}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_TO=${CRISIS_ALERT_EMAIL_TO}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_SMTP_FROM=${CRISIS_ALERT_SMTP_FROM}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_FROM=${CRISIS_ALERT_EMAIL_FROM}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_SMTP_HOST=${CRISIS_ALERT_SMTP_HOST}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_SMTP_PORT=${CRISIS_ALERT_SMTP_PORT}")' "$workflow_file" @@ -114,7 +118,9 @@ grep -Fq 'remove_env_vars+=("IBKR_PAPER_LIQUIDATE_ONLY")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_MIN_RESERVED_CASH_USD")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_RESERVED_CASH_RATIO")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_TO")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_TO")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_FROM")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_FROM")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_HOST")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMTP_PORT")' "$workflow_file"