Skip to content

Commit 0a9e6b8

Browse files
committed
Use platform Telegram token secrets for alerts
1 parent 142034f commit 0a9e6b8

6 files changed

Lines changed: 140 additions & 2 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ jobs:
4646
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
4747
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
4848
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
49+
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
4950
steps:
5051
- name: Checkout repository
5152
uses: actions/checkout@v6

.github/workflows/runtime-guard.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
5555
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
5656
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
57+
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
5758
steps:
5859
- name: Checkout repository
5960
uses: actions/checkout@v6

scripts/cloud_run_runtime_guard.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def _load_services() -> list[str]:
8585
return unique
8686

8787

88+
def _run_gcloud(args: list[str]) -> subprocess.CompletedProcess[str]:
89+
return subprocess.run(args, text=True, capture_output=True, check=False)
90+
91+
8892
def _run_gcloud_logging(project: str, log_filter: str, limit: int) -> list[dict[str, Any]]:
8993
command = [
9094
"gcloud",
@@ -166,10 +170,46 @@ def _summarize(entry: dict[str, Any]) -> str:
166170
return f"- {timestamp} {target or '<unknown>'} severity={severity}{status_text}{suffix}"
167171

168172

173+
174+
175+
def _telegram_secret_project() -> str | None:
176+
return (
177+
os.environ.get("RUNTIME_HEARTBEAT_GCP_PROJECT_ID")
178+
or os.environ.get("RUNTIME_GUARD_GCP_PROJECT_ID")
179+
or os.environ.get("GCP_PROJECT_ID")
180+
or os.environ.get("GOOGLE_CLOUD_PROJECT")
181+
)
182+
183+
184+
def _load_telegram_token_from_secret() -> str:
185+
secret_name = (os.environ.get("TELEGRAM_TOKEN_SECRET_NAME") or "").strip()
186+
if not secret_name:
187+
return ""
188+
command = ["gcloud", "secrets", "versions", "access", "latest", "--secret", secret_name]
189+
project = _telegram_secret_project()
190+
if project:
191+
command.extend(["--project", project])
192+
result = _run_gcloud(command)
193+
if result.returncode != 0:
194+
detail = (result.stderr or result.stdout or "").strip()
195+
print(
196+
f"Unable to read Telegram token from Secret Manager: {detail or 'gcloud failed'}",
197+
file=sys.stderr,
198+
)
199+
return ""
200+
return result.stdout.strip()
201+
202+
203+
def _telegram_token() -> str:
204+
direct_token = (os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN") or "").strip()
205+
if direct_token:
206+
return direct_token
207+
return _load_telegram_token_from_secret()
208+
169209
def _send_telegram(message: str) -> bool:
170210
targets: list[tuple[str, str]] = []
171211

172-
token = os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN")
212+
token = _telegram_token()
173213
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID")):
174214
if token:
175215
targets.append((token, chat_id))

scripts/execution_report_heartbeat.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,45 @@ def _is_accepted_report(payload: dict[str, Any]) -> tuple[bool, str]:
289289
return False, f"unaccepted status={status or '-'} stage={stage or '-'}"
290290

291291

292+
293+
294+
def _telegram_secret_project() -> str | None:
295+
return (
296+
os.environ.get("RUNTIME_HEARTBEAT_GCP_PROJECT_ID")
297+
or os.environ.get("RUNTIME_GUARD_GCP_PROJECT_ID")
298+
or os.environ.get("GCP_PROJECT_ID")
299+
or os.environ.get("GOOGLE_CLOUD_PROJECT")
300+
)
301+
302+
303+
def _load_telegram_token_from_secret() -> str:
304+
secret_name = (os.environ.get("TELEGRAM_TOKEN_SECRET_NAME") or "").strip()
305+
if not secret_name:
306+
return ""
307+
command = ["gcloud", "secrets", "versions", "access", "latest", "--secret", secret_name]
308+
project = _telegram_secret_project()
309+
if project:
310+
command.extend(["--project", project])
311+
result = _run_gcloud(command)
312+
if result.returncode != 0:
313+
detail = (result.stderr or result.stdout or "").strip()
314+
print(
315+
f"Unable to read Telegram token from Secret Manager: {detail or 'gcloud failed'}",
316+
file=sys.stderr,
317+
)
318+
return ""
319+
return result.stdout.strip()
320+
321+
322+
def _telegram_token() -> str:
323+
direct_token = (os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN") or "").strip()
324+
if direct_token:
325+
return direct_token
326+
return _load_telegram_token_from_secret()
327+
292328
def _send_telegram(message: str) -> bool:
293329
targets: list[tuple[str, str]] = []
294-
token = os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN")
330+
token = _telegram_token()
295331
for chat_id in _split_values(os.environ.get("GLOBAL_TELEGRAM_CHAT_ID")):
296332
if token:
297333
targets.append((token, chat_id))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
import subprocess
4+
5+
from scripts import cloud_run_runtime_guard as guard
6+
7+
8+
def test_telegram_token_falls_back_to_secret_manager(monkeypatch):
9+
monkeypatch.delenv("TELEGRAM_TOKEN", raising=False)
10+
monkeypatch.delenv("TG_TOKEN", raising=False)
11+
monkeypatch.setenv("TELEGRAM_TOKEN_SECRET_NAME", "platform-telegram-token")
12+
monkeypatch.setenv("GCP_PROJECT_ID", "firstradequant")
13+
observed = {}
14+
15+
def fake_run_gcloud(command):
16+
observed["command"] = command
17+
return subprocess.CompletedProcess(command, 0, stdout="secret-token\n", stderr="")
18+
19+
monkeypatch.setattr(guard, "_run_gcloud", fake_run_gcloud)
20+
21+
assert guard._telegram_token() == "secret-token"
22+
assert observed["command"] == [
23+
"gcloud",
24+
"secrets",
25+
"versions",
26+
"access",
27+
"latest",
28+
"--secret",
29+
"platform-telegram-token",
30+
"--project",
31+
"firstradequant",
32+
]

tests/test_execution_report_heartbeat.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import subprocess
34
import datetime as dt
45

56
from scripts import execution_report_heartbeat as heartbeat
@@ -24,3 +25,30 @@ def test_report_globs_include_sanitized_month_segments(monkeypatch):
2425
"gs://runtime-state/firstrade-platform/strategy-runs/**/2026-06/*.json",
2526
"gs://runtime-state/firstrade-platform/strategy-runs/**/2026_06/*.json",
2627
]
28+
29+
def test_telegram_token_falls_back_to_secret_manager(monkeypatch):
30+
monkeypatch.delenv("TELEGRAM_TOKEN", raising=False)
31+
monkeypatch.delenv("TG_TOKEN", raising=False)
32+
monkeypatch.setenv("TELEGRAM_TOKEN_SECRET_NAME", "platform-telegram-token")
33+
monkeypatch.setenv("GCP_PROJECT_ID", "firstradequant")
34+
observed = {}
35+
36+
def fake_run_gcloud(command):
37+
observed["command"] = command
38+
return subprocess.CompletedProcess(command, 0, stdout="secret-token\n", stderr="")
39+
40+
monkeypatch.setattr(heartbeat, "_run_gcloud", fake_run_gcloud)
41+
42+
assert heartbeat._telegram_token() == "secret-token"
43+
assert observed["command"] == [
44+
"gcloud",
45+
"secrets",
46+
"versions",
47+
"access",
48+
"latest",
49+
"--secret",
50+
"platform-telegram-token",
51+
"--project",
52+
"firstradequant",
53+
]
54+

0 commit comments

Comments
 (0)