Skip to content

Commit 916a4f0

Browse files
committed
Use strategy Telegram alert target for workflow alerts
1 parent d8df855 commit 916a4f0

6 files changed

Lines changed: 184 additions & 73 deletions

.github/workflows/execution-report-heartbeat.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ jobs:
5252
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
5353
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
5454
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
55+
STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS }}
56+
STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN }}
57+
STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME }}
5558
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
5659
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
5760
steps:

.github/workflows/runtime-guard.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ jobs:
5555
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
5656
CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}
5757
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
58+
STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS }}
59+
STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN }}
60+
STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME }}
5861
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
5962
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
6063
steps:

scripts/cloud_run_runtime_guard.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,6 @@ def _summarize(entry: dict[str, Any]) -> str:
182182
suffix = f" {text}" if text else ""
183183
return f"- {timestamp} {target or '<unknown>'} severity={severity}{status_text}{suffix}"
184184

185-
186-
187-
188185
def _telegram_secret_project() -> str | None:
189186
return (
190187
os.environ.get("RUNTIME_HEARTBEAT_GCP_PROJECT_ID")
@@ -194,42 +191,76 @@ def _telegram_secret_project() -> str | None:
194191
)
195192

196193

197-
def _load_telegram_token_from_secret() -> str:
198-
secret_name = (os.environ.get("TELEGRAM_TOKEN_SECRET_NAME") or "").strip()
194+
def _load_telegram_token_from_secret(secret_env_name: str) -> str:
195+
secret_name = (os.environ.get(secret_env_name) or "").strip()
199196
if not secret_name:
200197
return ""
201-
command = ["gcloud", "secrets", "versions", "access", "latest", "--secret", secret_name]
198+
command = [
199+
"gcloud",
200+
"secrets",
201+
"versions",
202+
"access",
203+
"latest",
204+
"--secret",
205+
secret_name,
206+
]
202207
project = _telegram_secret_project()
203208
if project:
204209
command.extend(["--project", project])
205210
result = _run_gcloud(command)
206211
if result.returncode != 0:
207212
detail = (result.stderr or result.stdout or "").strip()
208213
print(
209-
f"Unable to read Telegram token from Secret Manager: {detail or 'gcloud failed'}",
214+
f"Unable to read Telegram token from {secret_env_name}: "
215+
f"{detail or 'gcloud failed'}",
210216
file=sys.stderr,
211217
)
212218
return ""
213219
return result.stdout.strip()
214220

215221

216-
def _telegram_token() -> str:
217-
direct_token = (os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN") or "").strip()
218-
if direct_token:
219-
return direct_token
220-
return _load_telegram_token_from_secret()
222+
def _first_env_value(*names: str) -> str:
223+
for name in names:
224+
value = (os.environ.get(name) or "").strip()
225+
if value:
226+
return value
227+
return ""
221228

222-
def _send_telegram(message: str) -> bool:
223-
targets: list[tuple[str, str]] = []
224229

225-
token = _telegram_token()
226-
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID")):
227-
if token:
228-
targets.append((token, chat_id))
230+
def _telegram_targets() -> list[tuple[str, str]]:
231+
strategy_chat_ids = _split_values(
232+
os.environ.get("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS")
233+
)
234+
if strategy_chat_ids:
235+
strategy_token = _first_env_value("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN")
236+
if not strategy_token:
237+
strategy_token = _load_telegram_token_from_secret(
238+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME"
239+
)
240+
if strategy_token:
241+
return list(
242+
dict.fromkeys((strategy_token, chat_id) for chat_id in strategy_chat_ids)
243+
)
244+
return []
245+
246+
token = _first_env_value("TELEGRAM_TOKEN", "TG_TOKEN")
247+
if not token:
248+
token = _load_telegram_token_from_secret("TELEGRAM_TOKEN_SECRET_NAME")
249+
targets = [
250+
(token, chat_id)
251+
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID"))
252+
if token
253+
]
254+
return list(dict.fromkeys(targets))
255+
229256

230-
unique_targets = list(dict.fromkeys(targets))
257+
def _send_telegram(message: str) -> bool:
258+
unique_targets = _telegram_targets()
231259
if not unique_targets:
232-
print("No Telegram token/chat configured; unable to send runtime guard alert.", file=sys.stderr)
260+
print(
261+
"No Telegram token/chat configured; unable to send runtime guard alert.",
262+
file=sys.stderr,
263+
)
233264
return False
234265

235266
ok = True

scripts/execution_report_heartbeat.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,6 @@ def _is_accepted_report(payload: dict[str, Any]) -> tuple[bool, str]:
665665
return True, "report exists"
666666
return False, f"unaccepted status={status or '-'} stage={stage or '-'}"
667667

668-
669-
670-
671668
def _telegram_secret_project() -> str | None:
672669
return (
673670
os.environ.get("RUNTIME_HEARTBEAT_GCP_PROJECT_ID")
@@ -677,43 +674,80 @@ def _telegram_secret_project() -> str | None:
677674
)
678675

679676

680-
def _load_telegram_token_from_secret() -> str:
681-
secret_name = (os.environ.get("TELEGRAM_TOKEN_SECRET_NAME") or "").strip()
677+
def _load_telegram_token_from_secret(secret_env_name: str) -> str:
678+
secret_name = (os.environ.get(secret_env_name) or "").strip()
682679
if not secret_name:
683680
return ""
684-
command = ["gcloud", "secrets", "versions", "access", "latest", "--secret", secret_name]
681+
command = [
682+
"gcloud",
683+
"secrets",
684+
"versions",
685+
"access",
686+
"latest",
687+
"--secret",
688+
secret_name,
689+
]
685690
project = _telegram_secret_project()
686691
if project:
687692
command.extend(["--project", project])
688693
result = _run_gcloud(command)
689694
if result.returncode != 0:
690695
detail = (result.stderr or result.stdout or "").strip()
691696
print(
692-
f"Unable to read Telegram token from Secret Manager: {detail or 'gcloud failed'}",
697+
f"Unable to read Telegram token from {secret_env_name}: "
698+
f"{detail or 'gcloud failed'}",
693699
file=sys.stderr,
694700
)
695701
return ""
696702
return result.stdout.strip()
697703

698704

699-
def _telegram_token() -> str:
700-
direct_token = (os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN") or "").strip()
701-
if direct_token:
702-
return direct_token
703-
return _load_telegram_token_from_secret()
705+
def _first_env_value(*names: str) -> str:
706+
for name in names:
707+
value = (os.environ.get(name) or "").strip()
708+
if value:
709+
return value
710+
return ""
711+
712+
713+
def _telegram_targets() -> list[tuple[str, str]]:
714+
strategy_chat_ids = _split_values(
715+
os.environ.get("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS")
716+
)
717+
if strategy_chat_ids:
718+
strategy_token = _first_env_value("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN")
719+
if not strategy_token:
720+
strategy_token = _load_telegram_token_from_secret(
721+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME"
722+
)
723+
if strategy_token:
724+
return list(
725+
dict.fromkeys((strategy_token, chat_id) for chat_id in strategy_chat_ids)
726+
)
727+
return []
728+
729+
token = _first_env_value("TELEGRAM_TOKEN", "TG_TOKEN")
730+
if not token:
731+
token = _load_telegram_token_from_secret("TELEGRAM_TOKEN_SECRET_NAME")
732+
targets = [
733+
(token, chat_id)
734+
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID"))
735+
if token
736+
]
737+
return list(dict.fromkeys(targets))
738+
704739

705740
def _send_telegram(message: str) -> bool:
706-
targets: list[tuple[str, str]] = []
707-
token = _telegram_token()
708-
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID")):
709-
if token:
710-
targets.append((token, chat_id))
711-
unique_targets = list(dict.fromkeys(targets))
741+
unique_targets = _telegram_targets()
712742
if not unique_targets:
713-
print("No Telegram token/chat configured; unable to send heartbeat alert.", file=sys.stderr)
743+
print(
744+
"No Telegram token/chat configured; unable to send heartbeat alert.",
745+
file=sys.stderr,
746+
)
714747
return False
715-
base_url = "https://api.telegram.org"
748+
716749
ok = True
750+
base_url = "https://api.telegram.org"
717751
for token_value, chat_id in unique_targets:
718752
body = urllib.parse.urlencode({"chat_id": chat_id, "text": message}).encode()
719753
request = urllib.request.Request(
@@ -723,7 +757,9 @@ def _send_telegram(message: str) -> bool:
723757
)
724758
try:
725759
with urllib.request.urlopen(request, timeout=15) as response:
726-
ok = ok and response.status < 400
760+
if response.status >= 400:
761+
ok = False
762+
print(f"Telegram returned HTTP {response.status}", file=sys.stderr)
727763
except Exception as exc: # noqa: BLE001
728764
ok = False
729765
print(f"Telegram send failed: {type(exc).__name__}", file=sys.stderr)

tests/test_cloud_run_runtime_guard.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,48 @@ def test_scheduler_job_pattern_includes_service_alias():
1515
assert re.search(pattern, "interactive-brokers-live-u1599-tqqq-scheduler")
1616
assert not re.search(pattern, "interactive-brokers-live-u1660-soxl-scheduler")
1717

18-
def test_telegram_token_falls_back_to_secret_manager(monkeypatch):
18+
def test_telegram_targets_prefer_strategy_plugin_alert_secret(monkeypatch):
1919
monkeypatch.delenv("TELEGRAM_TOKEN", raising=False)
2020
monkeypatch.delenv("TG_TOKEN", raising=False)
21+
monkeypatch.delenv("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN", raising=False)
22+
monkeypatch.setenv(
23+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS",
24+
"strategy-chat; backup-chat",
25+
)
26+
monkeypatch.setenv(
27+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME",
28+
"strategy-plugin-telegram-token",
29+
)
30+
monkeypatch.setenv("GLOBAL_TELEGRAM_CHAT_ID", "platform-chat")
2131
monkeypatch.setenv("TELEGRAM_TOKEN_SECRET_NAME", "platform-telegram-token")
2232
monkeypatch.setenv("GCP_PROJECT_ID", "interactivebrokersquant")
23-
observed = {}
33+
observed = []
2434

2535
def fake_run_gcloud(command):
26-
observed["command"] = command
27-
return subprocess.CompletedProcess(command, 0, stdout="secret-token\n", stderr="")
36+
observed.append(command)
37+
return subprocess.CompletedProcess(
38+
command,
39+
0,
40+
stdout="strategy-token\n",
41+
stderr="",
42+
)
2843

2944
monkeypatch.setattr(guard, "_run_gcloud", fake_run_gcloud)
3045

31-
assert guard._telegram_token() == "secret-token"
32-
assert observed["command"] == [
33-
"gcloud",
34-
"secrets",
35-
"versions",
36-
"access",
37-
"latest",
38-
"--secret",
39-
"platform-telegram-token",
40-
"--project",
41-
"interactivebrokersquant",
46+
assert guard._telegram_targets() == [
47+
("strategy-token", "strategy-chat"),
48+
("strategy-token", "backup-chat"),
49+
]
50+
assert observed == [
51+
[
52+
"gcloud",
53+
"secrets",
54+
"versions",
55+
"access",
56+
"latest",
57+
"--secret",
58+
"strategy-plugin-telegram-token",
59+
"--project",
60+
"interactivebrokersquant",
61+
]
4262
]
43-

tests/test_execution_report_heartbeat.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -334,29 +334,48 @@ def test_main_skips_when_runtime_target_is_disabled(monkeypatch, capsys):
334334
assert "Execution report heartbeat skipped for Disabled runtime" in output
335335
assert "runtime target is disabled" in output
336336

337-
def test_telegram_token_falls_back_to_secret_manager(monkeypatch):
337+
def test_telegram_targets_prefer_strategy_plugin_alert_secret(monkeypatch):
338338
monkeypatch.delenv("TELEGRAM_TOKEN", raising=False)
339339
monkeypatch.delenv("TG_TOKEN", raising=False)
340+
monkeypatch.delenv("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN", raising=False)
341+
monkeypatch.setenv(
342+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS",
343+
"strategy-chat; backup-chat",
344+
)
345+
monkeypatch.setenv(
346+
"STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME",
347+
"strategy-plugin-telegram-token",
348+
)
349+
monkeypatch.setenv("GLOBAL_TELEGRAM_CHAT_ID", "platform-chat")
340350
monkeypatch.setenv("TELEGRAM_TOKEN_SECRET_NAME", "platform-telegram-token")
341351
monkeypatch.setenv("GCP_PROJECT_ID", "interactivebrokersquant")
342-
observed = {}
352+
observed = []
343353

344354
def fake_run_gcloud(command):
345-
observed["command"] = command
346-
return subprocess.CompletedProcess(command, 0, stdout="secret-token\n", stderr="")
355+
observed.append(command)
356+
return subprocess.CompletedProcess(
357+
command,
358+
0,
359+
stdout="strategy-token\n",
360+
stderr="",
361+
)
347362

348363
monkeypatch.setattr(heartbeat, "_run_gcloud", fake_run_gcloud)
349364

350-
assert heartbeat._telegram_token() == "secret-token"
351-
assert observed["command"] == [
352-
"gcloud",
353-
"secrets",
354-
"versions",
355-
"access",
356-
"latest",
357-
"--secret",
358-
"platform-telegram-token",
359-
"--project",
360-
"interactivebrokersquant",
365+
assert heartbeat._telegram_targets() == [
366+
("strategy-token", "strategy-chat"),
367+
("strategy-token", "backup-chat"),
368+
]
369+
assert observed == [
370+
[
371+
"gcloud",
372+
"secrets",
373+
"versions",
374+
"access",
375+
"latest",
376+
"--secret",
377+
"strategy-plugin-telegram-token",
378+
"--project",
379+
"interactivebrokersquant",
380+
]
361381
]
362-

0 commit comments

Comments
 (0)