diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index f475aba..13373e7 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -34,12 +34,12 @@ 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_RECIPIENTS: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS }} - CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL }} - CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME }} - CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST }} - CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT }} - CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY }} + 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 }} + CRISIS_ALERT_EMAIL_SMTP_HOST: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_HOST }} + CRISIS_ALERT_EMAIL_SMTP_PORT: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_PORT }} + CRISIS_ALERT_EMAIL_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_SECURITY }} # Strategy-owned defaults continue to come from UsEquityStrategies; this workflow only syncs platform/runtime inputs. EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }} IB_GATEWAY_ZONE: ${{ vars.IB_GATEWAY_ZONE }} @@ -47,7 +47,7 @@ jobs: GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }} NOTIFY_LANG: ${{ vars.NOTIFY_LANG }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} - CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD }} + CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }} steps: - name: Check whether env sync is enabled id: config @@ -274,6 +274,12 @@ jobs: "CRISIS_ALERT_GOOGLE_VOICE_GATEWAY" "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER" "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" + "CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS" + "CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL" + "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD" + "CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST" + "CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT" + "CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY" "CRISIS_ALERT_SMTP_FROM" "CRISIS_ALERT_SMTP_HOST" "CRISIS_ALERT_SMTP_PORT" @@ -282,7 +288,11 @@ jobs: "CRISIS_ALERT_SMTP_STARTTLS" "CRISIS_ALERT_SMTP_SSL" ) - remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD" "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD") + remove_secret_vars=( + "CRISIS_ALERT_SMTP_PASSWORD" + "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" + "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD" + ) if [ -n "${TELEGRAM_TOKEN_SECRET_NAME:-}" ]; then secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest") @@ -291,15 +301,15 @@ jobs: remove_secret_vars+=("TELEGRAM_TOKEN") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME:-}" ]; then - secret_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME}:latest") - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD") - elif [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD}") - remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD") + if [ -n "${CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD") + elif [ -n "${CRISIS_ALERT_EMAIL_SENDER_PASSWORD:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD}") + remove_secret_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD") - remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD") + remove_secret_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD") fi if [ -n "${IB_GATEWAY_ZONE:-}" ]; then @@ -374,34 +384,34 @@ jobs: remove_env_vars+=("IBKR_RESERVED_CASH_RATIO") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS=${CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS}") + if [ -n "${CRISIS_ALERT_EMAIL_RECIPIENTS:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS") + remove_env_vars+=("CRISIS_ALERT_EMAIL_RECIPIENTS") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL}") + if [ -n "${CRISIS_ALERT_EMAIL_SENDER_EMAIL:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL=${CRISIS_ALERT_EMAIL_SENDER_EMAIL}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST}") + if [ -n "${CRISIS_ALERT_EMAIL_SMTP_HOST:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_HOST=${CRISIS_ALERT_EMAIL_SMTP_HOST}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_HOST") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT}") + if [ -n "${CRISIS_ALERT_EMAIL_SMTP_PORT:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_PORT=${CRISIS_ALERT_EMAIL_SMTP_PORT}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_PORT") fi - if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY:-}" ]; then - env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY}") + if [ -n "${CRISIS_ALERT_EMAIL_SMTP_SECURITY:-}" ]; then + env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_SECURITY=${CRISIS_ALERT_EMAIL_SMTP_SECURITY}") else - remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY") + remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_SECURITY") fi if [ -n "${EXECUTION_REPORT_GCS_URI:-}" ]; then diff --git a/README.md b/README.md index 31524e7..a6b7217 100644 --- a/README.md +++ b/README.md @@ -112,12 +112,12 @@ 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_GOOGLE_VOICE_RECIPIENTS` | No | Comma/semicolon/newline-separated email-form recipients. Use a normal mailbox for email-only delivery, or a Google Voice mailbox/address to also trigger GV prompts. | -| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL` | No | Sender email address used for Google Voice notification mail. Gmail is the default transport, but the sender naming is provider-neutral. | -| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD` | No | Sender SMTP password or app password. For Cloud Run, prefer `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME` in env sync. | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST` | No | Optional SMTP host override. Defaults to Gmail SMTP when unset. | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT` | No | Optional SMTP port override. Defaults to `465` when unset. | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY` | No | Optional SMTP security override: `ssl`, `starttls`, or `none`. Defaults to `ssl` when unset. | +| `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. | +| `CRISIS_ALERT_EMAIL_SMTP_HOST` | No | Optional SMTP host override. Defaults to Gmail SMTP when unset. | +| `CRISIS_ALERT_EMAIL_SMTP_PORT` | No | Optional SMTP port override. Defaults to `465` when unset. | +| `CRISIS_ALERT_EMAIL_SMTP_SECURITY` | No | Optional SMTP security override: `ssl`, `starttls`, or `none`. Defaults to `ssl` when unset. | The selected account-group entry must provide at least: @@ -182,7 +182,7 @@ GLOBAL_TELEGRAM_CHAT_ID= NOTIFY_LANG=zh ``` -This shared-config mode is only for the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). It is not meant to become a global secret bundle for unrelated quant repos. Across multiple quant projects, the only broadly reusable runtime settings are usually `GLOBAL_TELEGRAM_CHAT_ID` and `NOTIFY_LANG`. +This shared-config mode is only for the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). It is not meant to become a global secret bundle for unrelated quant repos. Across multiple quant projects, broadly reusable runtime settings are usually limited to `GLOBAL_TELEGRAM_CHAT_ID`, `NOTIFY_LANG`, and the generic `CRISIS_ALERT_EMAIL_*` crisis mail contract when the same alert policy applies. Recommended account-group config payload: @@ -213,8 +213,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 Google Voice notification when the `CRISIS_ALERT_GOOGLE_VOICE_*` 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. +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 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. ### GitHub-managed Cloud Run env sync @@ -231,11 +231,13 @@ 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_ALERT_EMAIL_RECIPIENTS`, `CRISIS_ALERT_EMAIL_SENDER_EMAIL`, `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` + - Optional: `CRISIS_ALERT_EMAIL_SMTP_HOST`, `CRISIS_ALERT_EMAIL_SMTP_PORT`, `CRISIS_ALERT_EMAIL_SMTP_SECURITY` - `GLOBAL_TELEGRAM_CHAT_ID` - `NOTIFY_LANG` - **Repository Secrets** - `TELEGRAM_TOKEN` (fallback only when `TELEGRAM_TOKEN_SECRET_NAME` is not set) - - `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD` (fallback only when `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME` is not set) + - `CRISIS_ALERT_EMAIL_SENDER_PASSWORD` (fallback only when `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` is not set) - **Optional transition Variables** - `IB_GATEWAY_ZONE` - `IB_GATEWAY_IP_MODE` @@ -247,7 +249,7 @@ On every push to `main`, the workflow updates the existing Cloud Run service wit Important: - The workflow only becomes strict when `ENABLE_GITHUB_ENV_SYNC=true`. If this variable is unset, the sync job is skipped. When enabled, it resolves the selected profile's snapshot/config requirements from `scripts/print_strategy_profile_status.py --json` instead of a hard-coded strategy-name list. -- Here "shared config" still only means the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). `TELEGRAM_TOKEN` and `TELEGRAM_TOKEN_SECRET_NAME` remain repository-specific. +- Here "shared config" still only means the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). `TELEGRAM_TOKEN` and `TELEGRAM_TOKEN_SECRET_NAME` remain repository-specific. If crisis mail recipients and sender policy are shared across platforms, manage `CRISIS_ALERT_EMAIL_*` with GitHub Organization Variables/Secrets or GCP Secret Manager. - If `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` is set, the Cloud Run runtime needs Secret Manager access to that secret. - 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, the staging bucket `gs://run-sources--` also needs `roles/storage.objectViewer` for the build service account, the deploy service account, and the default compute service account. Missing this binding fails the deploy before Cloud Build starts with `storage.objects.get` denied. @@ -361,12 +363,12 @@ IBKR 账户 | `TELEGRAM_TOKEN` | 是 | Telegram 机器人 Token。Cloud Run 上更推荐走 Secret Manager 引用,不要直接写成明文 env。 | | `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 | | `NOTIFY_LANG` | 否 | `en`(默认)或 `zh` | -| `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS` | 否 | 通知收件邮箱。普通邮箱只收邮件;Google Voice 邮箱/地址会额外触发 GV 提醒。支持逗号、分号或换行分隔。 | -| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL` | 否 | Google Voice 通知邮件的发送方邮箱。默认传输走 Gmail SMTP,但命名不绑定 Gmail。 | -| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD` | 否 | 发送方 SMTP 密码或 app password。Cloud Run env sync 建议配置 `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME`。 | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST` | 否 | 可选 SMTP host 覆盖。不设置时默认 Gmail SMTP。 | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT` | 否 | 可选 SMTP port 覆盖。不设置时默认 `465`。 | -| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY` | 否 | 可选 SMTP 加密方式:`ssl`、`starttls` 或 `none`。不设置时默认 `ssl`。 | +| `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`。 | +| `CRISIS_ALERT_EMAIL_SMTP_HOST` | 否 | 可选 SMTP host 覆盖。不设置时默认 Gmail SMTP。 | +| `CRISIS_ALERT_EMAIL_SMTP_PORT` | 否 | 可选 SMTP port 覆盖。不设置时默认 `465`。 | +| `CRISIS_ALERT_EMAIL_SMTP_SECURITY` | 否 | 可选 SMTP 加密方式:`ssl`、`starttls` 或 `none`。不设置时默认 `ssl`。 | 选中的账号组配置里,至少要有: @@ -397,7 +399,7 @@ IB_GATEWAY_ZONE=us-central1-c IB_GATEWAY_IP_MODE=internal ``` -这里说的“共享配置”只针对 **IBKR 这一组系统**,也就是 `InteractiveBrokersPlatform` 和 `IBKRGatewayManager` 之间共享。它不是让所有 quant 仓库都共用一套 secrets。对多个量化仓库来说,通常只有 `GLOBAL_TELEGRAM_CHAT_ID` 和 `NOTIFY_LANG` 适合做跨项目共享。 +这里说的“共享配置”只针对 **IBKR 这一组系统**,也就是 `InteractiveBrokersPlatform` 和 `IBKRGatewayManager` 之间共享。它不是让所有 quant 仓库都共用一套 secrets。对多个量化仓库来说,`GLOBAL_TELEGRAM_CHAT_ID`、`NOTIFY_LANG`,以及同一套危机邮件策略下的 `CRISIS_ALERT_EMAIL_*` 适合提升到组织级配置;发送方密码仍应放在 GitHub Secret 或 GCP Secret Manager。 推荐的账号组配置 JSON: @@ -428,8 +430,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_GOOGLE_VOICE_*` 配置完整,服务还会额外发送一条独立 Google Voice 危机通知。 -Google Voice 告警结果会写入 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_EMAIL_*` 配置完整,服务还会额外发送一条独立邮件危机通知。 +邮件告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 ### GitHub 统一管理 Cloud Run 环境变量 @@ -446,11 +448,13 @@ Google Voice 告警结果会写入 runtime report。重复发送抑制使用稳 - `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_RECIPIENTS`、`CRISIS_ALERT_EMAIL_SENDER_EMAIL`、`CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` + - 可选:`CRISIS_ALERT_EMAIL_SMTP_HOST`、`CRISIS_ALERT_EMAIL_SMTP_PORT`、`CRISIS_ALERT_EMAIL_SMTP_SECURITY` - `GLOBAL_TELEGRAM_CHAT_ID` - `NOTIFY_LANG` - **仓库级 Secrets** - `TELEGRAM_TOKEN`(仅在没设置 `TELEGRAM_TOKEN_SECRET_NAME` 时作为 fallback) - - `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD`(仅在没设置 `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME` 时作为 fallback) + - `CRISIS_ALERT_EMAIL_SENDER_PASSWORD`(仅在没设置 `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` 时作为 fallback) - **可选过渡 Variables** - `IB_GATEWAY_ZONE` - `IB_GATEWAY_IP_MODE` @@ -462,7 +466,7 @@ Google Voice 告警结果会写入 runtime report。重复发送抑制使用稳 注意: - 只有在 `ENABLE_GITHUB_ENV_SYNC=true` 时,这个 workflow 才会严格校验并执行同步。没打开时会直接跳过。打开后,它会通过 `scripts/print_strategy_profile_status.py --json` 动态解析目标策略需要的 snapshot/config 输入,不再维护硬编码策略名列表。 -- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**。`TELEGRAM_TOKEN` 和 `TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。 +- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**。`TELEGRAM_TOKEN` 和 `TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。危机邮件通知如果确实跨平台共用同一套收件人和发送方,可以用 GitHub Organization Variables/Secrets 管理 `CRISIS_ALERT_EMAIL_*`。 - 如果设置了 `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`,Cloud Run 运行时还需要有对应 Secret 的访问权限。 - GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`。 - 如果你用 `gcloud run deploy --source` 或 Cloud Run source trigger 部署,还要给 staging bucket `gs://run-sources--` 补 `roles/storage.objectViewer`,对象包括 build service account、deploy service account、默认 compute service account。少了这层权限,会在 Cloud Build 启动前直接报 `storage.objects.get denied`。 diff --git a/main.py b/main.py index 95a6efd..0361546 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_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.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.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 StrategyPluginGoogleVoiceAlertMarkerStore( + return StrategyPluginEmailAlertMarkerStore( 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_google_voice_alert_context_label( + return build_email_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_google_voice_result(report, result) -> None: - report.setdefault("summary", {})["strategy_plugin_alert_google_voice_sent_count"] = result.sent_count +def attach_strategy_plugin_alert_email_result(report, result) -> None: + report.setdefault("summary", {})["strategy_plugin_alert_email_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_google_voice_alerts( + result = publish_strategy_plugin_email_alerts( signals, - google_voice_settings=RUNTIME_SETTINGS, + email_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_google_voice_result(report, result) + attach_strategy_plugin_alert_email_result(report, result) return result diff --git a/requirements.txt b/requirements.txt index 920d439..0f2b30b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@43b2ecb9dc7b1a70e52fe038ce321d79f5f7987a -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@a3f1899079d8d6c11601dddc40cb6f3020b6fc82 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3c8f52b16a8ebf98b9fdfa4af5e96be43e6144fe +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b84bebb784248bd2bb3d2c589db9ea0ab35ee95d pandas numpy requests diff --git a/runtime_config_support.py b/runtime_config_support.py index ac09abb..518309a 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -72,12 +72,12 @@ class PlatformRuntimeSettings: tg_chat_id: str | None = None notify_lang: str = "en" strategy_plugin_mounts_json: str | None = None - crisis_alert_google_voice_recipients: tuple[str, ...] = () - crisis_alert_google_voice_sender_email: str | None = None - crisis_alert_google_voice_sender_password: str | None = None - crisis_alert_google_voice_smtp_host: str | None = None - crisis_alert_google_voice_smtp_port: str | None = None - crisis_alert_google_voice_smtp_security: str | None = None + crisis_alert_email_recipients: tuple[str, ...] = () + crisis_alert_email_sender_email: str | None = None + crisis_alert_email_sender_password: str | None = None + crisis_alert_email_smtp_host: str | None = None + crisis_alert_email_smtp_port: str | None = None + crisis_alert_email_smtp_security: str | None = None runtime_target: RuntimeTarget | None = None @@ -186,23 +186,23 @@ def load_platform_runtime_settings( os.getenv("IBKR_STRATEGY_PLUGIN_MOUNTS_JSON") or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON") ), - crisis_alert_google_voice_recipients=split_env_list( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS") + crisis_alert_email_recipients=split_env_list( + os.getenv("CRISIS_ALERT_EMAIL_RECIPIENTS") ), - crisis_alert_google_voice_sender_email=first_non_empty( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL") + crisis_alert_email_sender_email=first_non_empty( + os.getenv("CRISIS_ALERT_EMAIL_SENDER_EMAIL") ), - crisis_alert_google_voice_sender_password=first_non_empty( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD") + crisis_alert_email_sender_password=first_non_empty( + os.getenv("CRISIS_ALERT_EMAIL_SENDER_PASSWORD") ), - crisis_alert_google_voice_smtp_host=first_non_empty( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST") + crisis_alert_email_smtp_host=first_non_empty( + os.getenv("CRISIS_ALERT_EMAIL_SMTP_HOST") ), - crisis_alert_google_voice_smtp_port=first_non_empty( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT") + crisis_alert_email_smtp_port=first_non_empty( + os.getenv("CRISIS_ALERT_EMAIL_SMTP_PORT") ), - crisis_alert_google_voice_smtp_security=first_non_empty( - os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY") + crisis_alert_email_smtp_security=first_non_empty( + os.getenv("CRISIS_ALERT_EMAIL_SMTP_SECURITY") ), runtime_target=runtime_target, ) diff --git a/tests/conftest.py b/tests/conftest.py index 2a7f72a..faa73f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,12 +30,12 @@ def load_strategy_module(**env_overrides): ), "IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME": None, "GLOBAL_TELEGRAM_CHAT_ID": None, - "CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS": None, - "CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL": None, - "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD": None, - "CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST": None, - "CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT": None, - "CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY": None, + "CRISIS_ALERT_EMAIL_RECIPIENTS": None, + "CRISIS_ALERT_EMAIL_SENDER_EMAIL": None, + "CRISIS_ALERT_EMAIL_SENDER_PASSWORD": None, + "CRISIS_ALERT_EMAIL_SMTP_HOST": None, + "CRISIS_ALERT_EMAIL_SMTP_PORT": None, + "CRISIS_ALERT_EMAIL_SMTP_SECURITY": None, } defaults.update(env_overrides) if "RUNTIME_TARGET_JSON" not in defaults: diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index e8f76e0..611bfb2 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_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": [], + "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": [], }, ) - monkeypatch.setattr(strategy_module, "publish_strategy_plugin_google_voice_alerts", fake_publish) + monkeypatch.setattr(strategy_module, "publish_strategy_plugin_email_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 8ae8e21..245d87f 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -135,12 +135,12 @@ 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_recipients == () - assert settings.crisis_alert_google_voice_sender_email is None - assert settings.crisis_alert_google_voice_sender_password is None - assert settings.crisis_alert_google_voice_smtp_host is None - assert settings.crisis_alert_google_voice_smtp_port is None - assert settings.crisis_alert_google_voice_smtp_security is None + assert settings.crisis_alert_email_recipients == () + assert settings.crisis_alert_email_sender_email is None + assert settings.crisis_alert_email_sender_password is None + assert settings.crisis_alert_email_smtp_host is None + assert settings.crisis_alert_email_smtp_port is None + assert settings.crisis_alert_email_smtp_security is None def test_load_platform_runtime_settings_prefers_runtime_target_json(monkeypatch): @@ -206,25 +206,25 @@ 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_google_voice_config(monkeypatch): +def test_load_platform_runtime_settings_reads_crisis_alert_email_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_RECIPIENTS", "alerts@example.com; voice@example.com") - monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL", "sender@example.com") - monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD", "secret") - monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST", "smtp.example.com") - monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT", "587") - monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY", "starttls") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_RECIPIENTS", "alerts@example.com; voice@example.com") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_SENDER_EMAIL", "sender@example.com") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_SENDER_PASSWORD", "secret") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_SMTP_HOST", "smtp.example.com") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_SMTP_PORT", "587") + monkeypatch.setenv("CRISIS_ALERT_EMAIL_SMTP_SECURITY", "starttls") settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - assert settings.crisis_alert_google_voice_recipients == ("alerts@example.com", "voice@example.com") - assert settings.crisis_alert_google_voice_sender_email == "sender@example.com" - assert settings.crisis_alert_google_voice_sender_password == "secret" - assert settings.crisis_alert_google_voice_smtp_host == "smtp.example.com" - assert settings.crisis_alert_google_voice_smtp_port == "587" - assert settings.crisis_alert_google_voice_smtp_security == "starttls" + assert settings.crisis_alert_email_recipients == ("alerts@example.com", "voice@example.com") + assert settings.crisis_alert_email_sender_email == "sender@example.com" + assert settings.crisis_alert_email_sender_password == "secret" + assert settings.crisis_alert_email_smtp_host == "smtp.example.com" + assert settings.crisis_alert_email_smtp_port == "587" + assert settings.crisis_alert_email_smtp_security == "starttls" def test_load_platform_runtime_settings_uses_whole_share_quantity_step(monkeypatch): diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 93396ce..edd8bc5 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -45,18 +45,18 @@ 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_RECIPIENTS: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY }}' "$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" +grep -Fq 'CRISIS_ALERT_EMAIL_SMTP_HOST: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_HOST }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_EMAIL_SMTP_PORT: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_PORT }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_EMAIL_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_SECURITY }}' "$workflow_file" grep -Fq 'IB_GATEWAY_ZONE: ${{ vars.IB_GATEWAY_ZONE }}' "$workflow_file" grep -Fq 'IB_GATEWAY_IP_MODE: ${{ vars.IB_GATEWAY_IP_MODE }}' "$workflow_file" grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" grep -Fq 'NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" -grep -Fq 'CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }}' "$workflow_file" grep -Fq "Skipping Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" grep -Fq "Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN' "$workflow_file" @@ -94,12 +94,12 @@ 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_RECIPIENTS=${CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS}")' "$workflow_file" -grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL}")' "$workflow_file" -grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD}")' "$workflow_file" -grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST}")' "$workflow_file" -grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT}")' "$workflow_file" -grep -Fq 'env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY=${CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL=${CRISIS_ALERT_EMAIL_SENDER_EMAIL}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_HOST=${CRISIS_ALERT_EMAIL_SMTP_HOST}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_PORT=${CRISIS_ALERT_EMAIL_SMTP_PORT}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SMTP_SECURITY=${CRISIS_ALERT_EMAIL_SMTP_SECURITY}")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_FEATURE_SNAPSHOT_PATH")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_STRATEGY_CONFIG_PATH")' "$workflow_file" @@ -110,19 +110,25 @@ 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_RECIPIENTS")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT")' "$workflow_file" -grep -Fq 'remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY")' "$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" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_HOST")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_PORT")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SMTP_SECURITY")' "$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" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT"' "$workflow_file" +grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY"' "$workflow_file" grep -Fq '"CRISIS_ALERT_SMTP_HOST"' "$workflow_file" grep -Fq 'secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file" -grep -Fq 'secret_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_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 'env_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN}")' "$workflow_file" grep -Fq 'remove_env_vars+=("IB_GATEWAY_ZONE")' "$workflow_file" grep -Fq 'remove_env_vars+=("IB_GATEWAY_IP_MODE")' "$workflow_file"