Skip to content

Commit a53733b

Browse files
authored
Add SMS channel for crisis alerts
1 parent 7e00af6 commit a53733b

8 files changed

Lines changed: 172 additions & 12 deletions

.github/workflows/sync-cloud-run-env.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ jobs:
6767
CRISIS_ALERT_EMAIL_SMTP_HOST: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_HOST }}
6868
CRISIS_ALERT_EMAIL_SMTP_PORT: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_PORT }}
6969
CRISIS_ALERT_EMAIL_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_EMAIL_SMTP_SECURITY }}
70+
CRISIS_ALERT_SMS_RECIPIENTS: ${{ vars.CRISIS_ALERT_SMS_RECIPIENTS }}
71+
CRISIS_ALERT_SMS_PROVIDER: ${{ vars.CRISIS_ALERT_SMS_PROVIDER }}
72+
CRISIS_ALERT_SMS_ACCOUNT_ID: ${{ vars.CRISIS_ALERT_SMS_ACCOUNT_ID }}
73+
CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME }}
74+
CRISIS_ALERT_SMS_SENDER: ${{ vars.CRISIS_ALERT_SMS_SENDER }}
75+
CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID }}
76+
CRISIS_ALERT_SMS_API_BASE_URL: ${{ vars.CRISIS_ALERT_SMS_API_BASE_URL }}
77+
CRISIS_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_SMS_BODY_MAX_CHARS }}
7078
FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
7179
FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
7280
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
@@ -76,6 +84,7 @@ jobs:
7684
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
7785
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
7886
CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }}
87+
CRISIS_ALERT_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }}
7988
FIRSTRADE_USERNAME: ${{ secrets.FIRSTRADE_USERNAME }}
8089
FIRSTRADE_PASSWORD: ${{ secrets.FIRSTRADE_PASSWORD }}
8190
FIRSTRADE_MFA_SECRET: ${{ secrets.FIRSTRADE_MFA_SECRET }}
@@ -457,6 +466,13 @@ jobs:
457466
add_optional_env CRISIS_ALERT_EMAIL_SMTP_HOST
458467
add_optional_env CRISIS_ALERT_EMAIL_SMTP_PORT
459468
add_optional_env CRISIS_ALERT_EMAIL_SMTP_SECURITY
469+
add_optional_env CRISIS_ALERT_SMS_RECIPIENTS
470+
add_optional_env CRISIS_ALERT_SMS_PROVIDER
471+
add_optional_env CRISIS_ALERT_SMS_ACCOUNT_ID
472+
add_optional_env CRISIS_ALERT_SMS_SENDER
473+
add_optional_env CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID
474+
add_optional_env CRISIS_ALERT_SMS_API_BASE_URL
475+
add_optional_env CRISIS_ALERT_SMS_BODY_MAX_CHARS
460476
add_optional_env FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
461477
add_optional_env FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
462478
add_optional_env INCOME_THRESHOLD_USD
@@ -467,6 +483,7 @@ jobs:
467483
468484
add_optional_secret TELEGRAM_TOKEN TELEGRAM_TOKEN_SECRET_NAME TELEGRAM_TOKEN
469485
add_optional_secret CRISIS_ALERT_EMAIL_SENDER_PASSWORD CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME CRISIS_ALERT_EMAIL_SENDER_PASSWORD
486+
add_optional_secret CRISIS_ALERT_SMS_AUTH_TOKEN CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME CRISIS_ALERT_SMS_AUTH_TOKEN
470487
add_optional_secret FIRSTRADE_USERNAME FIRSTRADE_USERNAME_SECRET_NAME FIRSTRADE_USERNAME
471488
add_optional_secret FIRSTRADE_PASSWORD FIRSTRADE_PASSWORD_SECRET_NAME FIRSTRADE_PASSWORD
472489
add_optional_secret FIRSTRADE_MFA_SECRET FIRSTRADE_MFA_SECRET_SECRET_NAME FIRSTRADE_MFA_SECRET

application/rebalance_service.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
build_strategy_plugin_alert_context_label as build_email_alert_context_label,
5757
publish_strategy_plugin_email_alerts,
5858
)
59+
from quant_platform_kit.notifications.strategy_plugin_sms import (
60+
StrategyPluginSmsAlertMarkerStore,
61+
publish_strategy_plugin_sms_alerts,
62+
)
5963
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
6064
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
6165
from strategy_runtime import load_strategy_runtime
@@ -238,6 +242,35 @@ def build_strategy_plugin_alert_store(
238242
)
239243

240244

245+
def build_strategy_plugin_sms_alert_store(
246+
settings: PlatformRuntimeSettings,
247+
*,
248+
env_reader: Callable[[str, str | None], str | None] = os.getenv,
249+
):
250+
explicit_gcs_uri = env_reader("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI", None)
251+
report_gcs_uri = env_reader("EXECUTION_REPORT_GCS_URI", None)
252+
state_bucket = env_reader("FIRSTRADE_GCS_STATE_BUCKET", None)
253+
state_prefix = env_reader("FIRSTRADE_STATE_PREFIX", "firstrade-platform") or "firstrade-platform"
254+
state_gcs_uri = f"gs://{state_bucket}/{state_prefix}" if state_bucket else None
255+
return StrategyPluginSmsAlertMarkerStore(
256+
local_dir=env_reader("STRATEGY_PLUGIN_ALERT_STATE_DIR", None) or "/tmp/quant_strategy_plugin_alerts",
257+
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri or state_gcs_uri,
258+
gcp_project_id=settings.project_id,
259+
)
260+
261+
262+
class StrategyPluginAlertPublishResults:
263+
def __init__(self, *, email_result, sms_result):
264+
self.email_result = email_result
265+
self.sms_result = sms_result
266+
267+
def to_report_fields(self) -> dict[str, Any]:
268+
fields: dict[str, Any] = {}
269+
fields.update(self.email_result.to_report_fields())
270+
fields.update(self.sms_result.to_report_fields())
271+
return fields
272+
273+
241274
def publish_strategy_plugin_alerts(
242275
signals,
243276
*,
@@ -246,7 +279,7 @@ def publish_strategy_plugin_alerts(
246279
log_message: Callable[..., Any] = print,
247280
env_reader: Callable[[str, str | None], str | None] = os.getenv,
248281
):
249-
return publish_strategy_plugin_email_alerts(
282+
email_result = publish_strategy_plugin_email_alerts(
250283
signals,
251284
email_settings=settings,
252285
translator=translator,
@@ -255,6 +288,16 @@ def publish_strategy_plugin_alerts(
255288
alert_store=build_strategy_plugin_alert_store(settings, env_reader=env_reader),
256289
log_message=log_message,
257290
)
291+
sms_result = publish_strategy_plugin_sms_alerts(
292+
signals,
293+
sms_settings=settings,
294+
translator=translator,
295+
strategy_label=settings.strategy_profile,
296+
context_label=build_strategy_plugin_alert_context_label(settings),
297+
alert_store=build_strategy_plugin_sms_alert_store(settings, env_reader=env_reader),
298+
log_message=log_message,
299+
)
300+
return StrategyPluginAlertPublishResults(email_result=email_result, sms_result=sms_result)
258301

259302

260303
def _runtime_metadata_with_execution_policy(
@@ -385,6 +428,11 @@ def run_strategy_cycle(
385428
"strategy_plugin_alert_email_skipped_count": 0,
386429
"strategy_plugin_alert_email_failed_count": 0,
387430
"strategy_plugin_alert_email_deliveries": [],
431+
"strategy_plugin_alert_sms_attempted_count": 0,
432+
"strategy_plugin_alert_sms_sent_count": 0,
433+
"strategy_plugin_alert_sms_skipped_count": 0,
434+
"strategy_plugin_alert_sms_failed_count": 0,
435+
"strategy_plugin_alert_sms_deliveries": [],
388436
}
389437
return attach_strategy_plugin_result(
390438
result,
@@ -489,6 +537,11 @@ def run_strategy_cycle(
489537
"strategy_plugin_alert_email_skipped_count": 0,
490538
"strategy_plugin_alert_email_failed_count": 0,
491539
"strategy_plugin_alert_email_deliveries": [],
540+
"strategy_plugin_alert_sms_attempted_count": 0,
541+
"strategy_plugin_alert_sms_sent_count": 0,
542+
"strategy_plugin_alert_sms_skipped_count": 0,
543+
"strategy_plugin_alert_sms_failed_count": 0,
544+
"strategy_plugin_alert_sms_deliveries": [],
492545
}
493546
)
494547
if strategy_plugin_alert_email_error:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ authors = [
1414
]
1515
dependencies = [
1616
"firstrade==0.0.38",
17-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3c8f52b16a8ebf98b9fdfa4af5e96be43e6144fe",
18-
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b84bebb784248bd2bb3d2c589db9ea0ab35ee95d",
17+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d43800180aae1c7fe7051496a6af5d76f2c65879",
18+
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9661d8bb74e33466fa0ec1efef168b1d1bae8875",
1919
"google-cloud-storage",
2020
"requests",
2121
]

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
flask
22
gunicorn
33
firstrade==0.0.38
4-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3c8f52b16a8ebf98b9fdfa4af5e96be43e6144fe
5-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b84bebb784248bd2bb3d2c589db9ea0ab35ee95d
4+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d43800180aae1c7fe7051496a6af5d76f2c65879
5+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9661d8bb74e33466fa0ec1efef168b1d1bae8875
66
google-cloud-storage
77
requests
88
pytest

runtime_config_support.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ class PlatformRuntimeSettings:
6464
crisis_alert_email_smtp_host: str | None = None
6565
crisis_alert_email_smtp_port: str | None = None
6666
crisis_alert_email_smtp_security: str | None = None
67+
crisis_alert_sms_recipients: tuple[str, ...] = ()
68+
crisis_alert_sms_provider: str | None = None
69+
crisis_alert_sms_account_id: str | None = None
70+
crisis_alert_sms_auth_token: str | None = None
71+
crisis_alert_sms_sender: str | None = None
72+
crisis_alert_sms_messaging_service_id: str | None = None
73+
crisis_alert_sms_api_base_url: str | None = None
74+
crisis_alert_sms_body_max_chars: str | None = None
6775
runtime_target: RuntimeTarget | None = None
6876

6977

@@ -161,6 +169,18 @@ def load_platform_runtime_settings(
161169
crisis_alert_email_smtp_security=_first_non_empty(
162170
os.getenv("CRISIS_ALERT_EMAIL_SMTP_SECURITY")
163171
),
172+
crisis_alert_sms_recipients=_split_env_list(os.getenv("CRISIS_ALERT_SMS_RECIPIENTS")),
173+
crisis_alert_sms_provider=_first_non_empty(os.getenv("CRISIS_ALERT_SMS_PROVIDER")),
174+
crisis_alert_sms_account_id=_first_non_empty(os.getenv("CRISIS_ALERT_SMS_ACCOUNT_ID")),
175+
crisis_alert_sms_auth_token=_first_non_empty(os.getenv("CRISIS_ALERT_SMS_AUTH_TOKEN")),
176+
crisis_alert_sms_sender=_first_non_empty(os.getenv("CRISIS_ALERT_SMS_SENDER")),
177+
crisis_alert_sms_messaging_service_id=_first_non_empty(
178+
os.getenv("CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID")
179+
),
180+
crisis_alert_sms_api_base_url=_first_non_empty(os.getenv("CRISIS_ALERT_SMS_API_BASE_URL")),
181+
crisis_alert_sms_body_max_chars=_first_non_empty(
182+
os.getenv("CRISIS_ALERT_SMS_BODY_MAX_CHARS")
183+
),
164184
runtime_target=runtime_target,
165185
)
166186

tests/test_rebalance_service.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,16 @@ def test_run_strategy_cycle_loads_strategy_plugin_report_and_sends_email(
233233
crisis_alert_email_sender_password="app-password",
234234
)
235235
messages = []
236-
observed_alerts = []
236+
observed_email_alerts = []
237+
observed_sms_alerts = []
237238

238239
monkeypatch.setattr(
239240
"application.rebalance_service.load_strategy_runtime",
240241
lambda *_args, **_kwargs: FakeStrategyRuntime(),
241242
)
242243

243-
def fake_publish(signals, **kwargs):
244-
observed_alerts.append((tuple(signals), kwargs))
244+
def fake_email_publish(signals, **kwargs):
245+
observed_email_alerts.append((tuple(signals), kwargs))
245246
return SimpleNamespace(
246247
sent_count=1,
247248
to_report_fields=lambda: {
@@ -255,7 +256,23 @@ def fake_publish(signals, **kwargs):
255256
},
256257
)
257258

258-
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_email_alerts", fake_publish)
259+
def fake_sms_publish(signals, **kwargs):
260+
observed_sms_alerts.append((tuple(signals), kwargs))
261+
return SimpleNamespace(
262+
sent_count=1,
263+
to_report_fields=lambda: {
264+
"strategy_plugin_alert_sms_attempted_count": 1,
265+
"strategy_plugin_alert_sms_sent_count": 1,
266+
"strategy_plugin_alert_sms_skipped_count": 0,
267+
"strategy_plugin_alert_sms_failed_count": 0,
268+
"strategy_plugin_alert_sms_deliveries": [
269+
{"subject": "Crisis plugin alert", "status": "sent"}
270+
],
271+
},
272+
)
273+
274+
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_email_alerts", fake_email_publish)
275+
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_sms_alerts", fake_sms_publish)
259276

260277
result = run_strategy_cycle(
261278
runtime_settings=settings,
@@ -267,13 +284,18 @@ def fake_publish(signals, **kwargs):
267284

268285
assert result["strategy_plugins"][0]["canonical_route"] == "true_crisis"
269286
assert result["strategy_plugin_alert_email_sent_count"] == 1
287+
assert result["strategy_plugin_alert_sms_sent_count"] == 1
270288
assert result["strategy_plugin_lines"] == (
271289
"🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend",
272290
)
273-
assert len(observed_alerts) == 1
274-
assert observed_alerts[0][0][0].canonical_route == "true_crisis"
275-
assert "firstrade" in observed_alerts[0][1]["context_label"]
291+
assert len(observed_email_alerts) == 1
292+
assert len(observed_sms_alerts) == 1
293+
assert observed_email_alerts[0][0][0].canonical_route == "true_crisis"
294+
assert observed_sms_alerts[0][0][0].canonical_route == "true_crisis"
295+
assert "firstrade" in observed_email_alerts[0][1]["context_label"]
296+
assert "firstrade" in observed_sms_alerts[0][1]["context_label"]
276297
assert result["strategy_plugin_alert_email_deliveries"][0]["status"] == "sent"
298+
assert result["strategy_plugin_alert_sms_deliveries"][0]["status"] == "sent"
277299
assert "🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend" in messages[0]
278300

279301

@@ -296,6 +318,7 @@ def test_run_strategy_cycle_strategy_plugin_load_error_is_non_blocking(monkeypat
296318
assert result["action_done"] is True
297319
assert result["strategy_plugin_error"].startswith("JSONDecodeError:")
298320
assert result["strategy_plugin_alert_email_sent_count"] == 0
321+
assert result["strategy_plugin_alert_sms_sent_count"] == 0
299322

300323

301324
def test_run_strategy_cycle_persists_strategy_run_state(monkeypatch):

tests/test_runtime_config_support.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ def test_reserved_cash_policy_defaults_to_zero(monkeypatch):
5959
assert settings.crisis_alert_email_smtp_host is None
6060
assert settings.crisis_alert_email_smtp_port is None
6161
assert settings.crisis_alert_email_smtp_security is None
62+
assert settings.crisis_alert_sms_recipients == ()
63+
assert settings.crisis_alert_sms_provider is None
64+
assert settings.crisis_alert_sms_account_id is None
65+
assert settings.crisis_alert_sms_auth_token is None
66+
assert settings.crisis_alert_sms_sender is None
67+
assert settings.crisis_alert_sms_messaging_service_id is None
68+
assert settings.crisis_alert_sms_api_base_url is None
69+
assert settings.crisis_alert_sms_body_max_chars is None
6270

6371

6472
def test_reserved_cash_policy_loads_from_env(monkeypatch):
@@ -91,6 +99,29 @@ def test_crisis_alert_email_settings_load_from_env(monkeypatch):
9199
assert settings.crisis_alert_email_smtp_security == "starttls"
92100

93101

102+
def test_crisis_alert_sms_settings_load_from_env(monkeypatch):
103+
monkeypatch.setenv("RUNTIME_TARGET_JSON", _target_json())
104+
monkeypatch.setenv("CRISIS_ALERT_SMS_RECIPIENTS", "+15165480265;(516) 548-0265")
105+
monkeypatch.setenv("CRISIS_ALERT_SMS_PROVIDER", "twilio")
106+
monkeypatch.setenv("CRISIS_ALERT_SMS_ACCOUNT_ID", "AC123")
107+
monkeypatch.setenv("CRISIS_ALERT_SMS_AUTH_TOKEN", "secret")
108+
monkeypatch.setenv("CRISIS_ALERT_SMS_SENDER", "+15551234567")
109+
monkeypatch.setenv("CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID", "MG123")
110+
monkeypatch.setenv("CRISIS_ALERT_SMS_API_BASE_URL", "https://twilio.example.test")
111+
monkeypatch.setenv("CRISIS_ALERT_SMS_BODY_MAX_CHARS", "160")
112+
113+
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
114+
115+
assert settings.crisis_alert_sms_recipients == ("+15165480265", "(516) 548-0265")
116+
assert settings.crisis_alert_sms_provider == "twilio"
117+
assert settings.crisis_alert_sms_account_id == "AC123"
118+
assert settings.crisis_alert_sms_auth_token == "secret"
119+
assert settings.crisis_alert_sms_sender == "+15551234567"
120+
assert settings.crisis_alert_sms_messaging_service_id == "MG123"
121+
assert settings.crisis_alert_sms_api_base_url == "https://twilio.example.test"
122+
assert settings.crisis_alert_sms_body_max_chars == "160"
123+
124+
94125
def test_reserved_cash_ratio_rejects_invalid_env(monkeypatch):
95126
monkeypatch.setenv("FIRSTRADE_RESERVED_CASH_RATIO", "1.25")
96127

tests/test_sync_cloud_run_env_workflow.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ def test_sync_cloud_run_env_workflow_syncs_crisis_alert_settings():
1313
"CRISIS_ALERT_EMAIL_SMTP_HOST",
1414
"CRISIS_ALERT_EMAIL_SMTP_PORT",
1515
"CRISIS_ALERT_EMAIL_SMTP_SECURITY",
16+
"CRISIS_ALERT_SMS_RECIPIENTS",
17+
"CRISIS_ALERT_SMS_PROVIDER",
18+
"CRISIS_ALERT_SMS_ACCOUNT_ID",
19+
"CRISIS_ALERT_SMS_SENDER",
20+
"CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID",
21+
"CRISIS_ALERT_SMS_API_BASE_URL",
22+
"CRISIS_ALERT_SMS_BODY_MAX_CHARS",
1623
):
1724
assert f"{name}: ${{{{ vars.{name} }}}}" in workflow
1825
assert f"add_optional_env {name}" in workflow
@@ -26,6 +33,15 @@ def test_sync_cloud_run_env_workflow_syncs_crisis_alert_settings():
2633
"add_optional_secret CRISIS_ALERT_EMAIL_SENDER_PASSWORD "
2734
"CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME CRISIS_ALERT_EMAIL_SENDER_PASSWORD"
2835
) in workflow
36+
assert (
37+
"CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME: "
38+
"${{ vars.CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME }}"
39+
) in workflow
40+
assert "CRISIS_ALERT_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }}" in workflow
41+
assert (
42+
"add_optional_secret CRISIS_ALERT_SMS_AUTH_TOKEN "
43+
"CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME CRISIS_ALERT_SMS_AUTH_TOKEN"
44+
) in workflow
2945
assert '"CRISIS_ALERT_GOOGLE_VOICE_TO"' in workflow
3046
assert '"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"' in workflow
3147
assert '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"' in workflow

0 commit comments

Comments
 (0)