Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ jobs:
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_GMAIL_USER: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }}
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 }}
# 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 }}
IB_GATEWAY_IP_MODE: ${{ vars.IB_GATEWAY_IP_MODE }}
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}
CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD }}
steps:
- name: Check whether env sync is enabled
id: config
Expand Down Expand Up @@ -269,6 +272,8 @@ jobs:
"IB_GATEWAY_MODE"
"CRISIS_ALERT_GOOGLE_VOICE_TO"
"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD"
"CRISIS_ALERT_SMTP_FROM"
"CRISIS_ALERT_SMTP_HOST"
"CRISIS_ALERT_SMTP_PORT"
Expand All @@ -277,7 +282,7 @@ jobs:
"CRISIS_ALERT_SMTP_STARTTLS"
"CRISIS_ALERT_SMTP_SSL"
)
remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD")
remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD" "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep legacy GV password secret until replacement exists

When a repo is still configured with CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD/..._SECRET_NAME and has not yet added the new CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD* settings, this workflow now removes the old Cloud Run secret binding unconditionally but never re-adds an equivalent value under the new name. In that migration state, the sync job succeeds yet Google Voice alert sending loses its SMTP credential on the next deploy, causing production alert delivery to stop silently.

Useful? React with 👍 / 👎.


if [ -n "${TELEGRAM_TOKEN_SECRET_NAME:-}" ]; then
secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")
Expand All @@ -286,15 +291,15 @@ jobs:
remove_secret_vars+=("TELEGRAM_TOKEN")
fi

if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME:-}" ]; then
secret_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME}:latest")
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
elif [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD:-}" ]; then
env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD}")
remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
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")
else
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
remove_secret_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
fi

if [ -n "${IB_GATEWAY_ZONE:-}" ]; then
Expand Down Expand Up @@ -375,10 +380,28 @@ jobs:
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")
fi

if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER:-}" ]; then
env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER=${CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER}")
if [ -n "${CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL:-}" ]; then
env_pairs+=("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL=${CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL}")
else
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_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}")
else
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_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}")
else
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_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}")
else
remove_env_vars+=("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY")
fi

if [ -n "${EXECUTION_REPORT_GCS_URI:-}" ]; then
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,11 @@ For IBKR, keep `paper` as a single account-group entry. If you later add live ac
| `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_GMAIL_USER` | No | Gmail address used as the sender for Google Voice notification mail. |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | No | Gmail App Password for the sender account. For Cloud Run, prefer `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME` in env sync. |
| `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. |

The selected account-group entry must provide at least:

Expand Down Expand Up @@ -232,7 +235,7 @@ Recommended setup:
- `NOTIFY_LANG`
- **Repository Secrets**
- `TELEGRAM_TOKEN` (fallback only when `TELEGRAM_TOKEN_SECRET_NAME` is not set)
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` (fallback only when `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_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)
- **Optional transition Variables**
- `IB_GATEWAY_ZONE`
- `IB_GATEWAY_IP_MODE`
Expand Down Expand Up @@ -359,8 +362,11 @@ IBKR 账户
| `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 |
| `NOTIFY_LANG` | 否 | `en`(默认)或 `zh` |
| `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS` | 否 | 通知收件邮箱。普通邮箱只收邮件;Google Voice 邮箱/地址会额外触发 GV 提醒。支持逗号、分号或换行分隔。 |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER` | 否 | Google Voice 通知使用的 Gmail 地址。 |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | 否 | 发送方 Gmail 账号的 App Password。Cloud Run env sync 建议配置 `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME`。 |
| `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`。 |

选中的账号组配置里,至少要有:

Expand Down Expand Up @@ -444,7 +450,7 @@ Google Voice 告警结果会写入 runtime report。重复发送抑制使用稳
- `NOTIFY_LANG`
- **仓库级 Secrets**
- `TELEGRAM_TOKEN`(仅在没设置 `TELEGRAM_TOKEN_SECRET_NAME` 时作为 fallback)
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD`(仅在没设置 `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME` 时作为 fallback)
- `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD`(仅在没设置 `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME` 时作为 fallback)
- **可选过渡 Variables**
- `IB_GATEWAY_ZONE`
- `IB_GATEWAY_IP_MODE`
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@705667fbb88b743eb83e858b1cc42fe9ebc3a87a
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@aca4e1449e9e87e0222b8a1a5e0dd84f822d516c
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@43b2ecb9dc7b1a70e52fe038ce321d79f5f7987a
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@a3f1899079d8d6c11601dddc40cb6f3020b6fc82
pandas
numpy
requests
Expand Down
24 changes: 18 additions & 6 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ class PlatformRuntimeSettings:
notify_lang: str = "en"
strategy_plugin_mounts_json: str | None = None
crisis_alert_google_voice_recipients: tuple[str, ...] = ()
crisis_alert_google_voice_gmail_user: str | None = None
crisis_alert_google_voice_gmail_app_password: str | None = None
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
runtime_target: RuntimeTarget | None = None


Expand Down Expand Up @@ -186,11 +189,20 @@ def load_platform_runtime_settings(
crisis_alert_google_voice_recipients=split_env_list(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")
),
crisis_alert_google_voice_gmail_user=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")
crisis_alert_google_voice_sender_email=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL")
),
crisis_alert_google_voice_gmail_app_password=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
crisis_alert_google_voice_sender_password=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
),
crisis_alert_google_voice_smtp_host=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST")
),
crisis_alert_google_voice_smtp_port=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT")
),
crisis_alert_google_voice_smtp_security=first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY")
),
runtime_target=runtime_target,
)
Expand Down
7 changes: 5 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ 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_GMAIL_USER": None,
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD": 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,
}
defaults.update(env_overrides)
if "RUNTIME_TARGET_JSON" not in defaults:
Expand Down
21 changes: 15 additions & 6 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,11 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch):
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_gmail_user is None
assert settings.crisis_alert_google_voice_gmail_app_password is None
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


def test_load_platform_runtime_settings_prefers_runtime_target_json(monkeypatch):
Expand Down Expand Up @@ -208,14 +211,20 @@ def test_load_platform_runtime_settings_reads_crisis_alert_google_voice_config(m
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_GMAIL_USER", "sender@gmail.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD", "secret")
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")

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_gmail_user == "sender@gmail.com"
assert settings.crisis_alert_google_voice_gmail_app_password == "secret"
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"


def test_load_platform_runtime_settings_uses_whole_share_quantity_step(monkeypatch):
Expand Down
Loading