Skip to content

Commit 6eecae8

Browse files
Pigbibicodex
andcommitted
fix: harden Firstrade runtime monitoring and deployment controls
Co-Authored-By: Codex <noreply@openai.com>
1 parent f061aab commit 6eecae8

4 files changed

Lines changed: 123 additions & 25 deletions

File tree

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
name: Deploy Cloud Run
22

33
on:
4-
workflow_run:
5-
workflows: [CI]
6-
types: [completed]
7-
branches: [main]
84
workflow_dispatch:
95

106
permissions:
@@ -27,15 +23,8 @@ concurrency:
2723
jobs:
2824
deploy-cloud-run:
2925
name: Deploy Cloud Run
30-
if: >
31-
github.event_name == 'workflow_dispatch' ||
32-
(
33-
github.event_name == 'workflow_run' &&
34-
github.event.workflow_run.conclusion == 'success' &&
35-
github.event.workflow_run.event == 'push' &&
36-
!contains(github.event.workflow_run.head_commit.message, 'chore(deps): align QPK pin') &&
37-
!contains(github.event.workflow_run.head_commit.message, '[skip-cloud-run-deploy]')
38-
)
26+
# Production deployment and environment sync require an explicit operator dispatch.
27+
if: github.event_name == 'workflow_dispatch'
3928
runs-on: ubuntu-latest
4029
timeout-minutes: 20
4130
permissions:
@@ -190,7 +179,7 @@ jobs:
190179
if: steps.deploy_config.outputs.enabled == 'true'
191180
uses: actions/checkout@v6
192181
with:
193-
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
182+
ref: ${{ github.sha }}
194183

195184
- name: Validate deploy inputs
196185
if: steps.deploy_config.outputs.enabled == 'true'

scripts/cloud_run_runtime_guard.py

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import subprocess
1111
import sys
12+
import time
1213
import urllib.parse
1314
import urllib.request
1415
from typing import Any
@@ -24,6 +25,26 @@
2425
"URL_UNREACHABLE",
2526
)
2627
SCHEDULER_CLOUD_RUN_DEDUP_SECONDS = 120
28+
DEFAULT_LOG_QUERY_MAX_ATTEMPTS = 3
29+
DEFAULT_LOG_QUERY_RETRY_SECONDS = 1.0
30+
_RETRYABLE_LOG_QUERY_MARKERS = (
31+
"http 429",
32+
"http 500",
33+
"http 502",
34+
"http 503",
35+
"http 504",
36+
'"code": 429',
37+
'"code": 500',
38+
'"code": 502',
39+
'"code": 503',
40+
'"code": 504',
41+
"internal error",
42+
"unavailable",
43+
"timed out",
44+
"network connectivity",
45+
"connection reset",
46+
"rate limit",
47+
)
2748

2849

2950
def _split_values(raw: str | None) -> list[str]:
@@ -200,6 +221,31 @@ def _run_gcloud_json(args: list[str], context: str) -> Any:
200221
raise RuntimeError(f"gcloud {context} returned invalid JSON: {exc}") from exc
201222

202223

224+
def _log_query_retry_config() -> tuple[int, float]:
225+
try:
226+
attempts = int(
227+
os.environ.get("RUNTIME_GUARD_LOG_QUERY_MAX_ATTEMPTS")
228+
or DEFAULT_LOG_QUERY_MAX_ATTEMPTS
229+
)
230+
except ValueError:
231+
attempts = DEFAULT_LOG_QUERY_MAX_ATTEMPTS
232+
try:
233+
retry_seconds = float(
234+
os.environ.get("RUNTIME_GUARD_LOG_QUERY_RETRY_SECONDS")
235+
or DEFAULT_LOG_QUERY_RETRY_SECONDS
236+
)
237+
except ValueError:
238+
retry_seconds = DEFAULT_LOG_QUERY_RETRY_SECONDS
239+
return max(1, min(attempts, 5)), max(0.0, min(retry_seconds, 10.0))
240+
241+
242+
def _is_retryable_log_query_error(detail: str) -> bool:
243+
normalized = detail.lower()
244+
if "403" in normalized or "permission_denied" in normalized:
245+
return False
246+
return any(marker in normalized for marker in _RETRYABLE_LOG_QUERY_MARKERS)
247+
248+
203249
def _run_gcloud_logging(project: str, log_filter: str, limit: int) -> list[dict[str, Any]]:
204250
command = [
205251
"gcloud",
@@ -211,17 +257,24 @@ def _run_gcloud_logging(project: str, log_filter: str, limit: int) -> list[dict[
211257
"--format=json",
212258
f"--limit={limit}",
213259
]
214-
result = subprocess.run(command, text=True, capture_output=True, check=False)
215-
if result.returncode != 0:
216-
detail = (result.stderr or result.stdout or "").strip()
217-
raise RuntimeError(detail or "gcloud logging read failed")
218-
if not result.stdout.strip():
219-
return []
220-
try:
221-
payload = json.loads(result.stdout)
222-
except json.JSONDecodeError as exc:
223-
raise RuntimeError(f"gcloud returned invalid JSON: {exc}") from exc
224-
return payload if isinstance(payload, list) else []
260+
max_attempts, retry_seconds = _log_query_retry_config()
261+
last_detail = ""
262+
for attempt in range(1, max_attempts + 1):
263+
result = _run_gcloud(command)
264+
if result.returncode == 0:
265+
if not result.stdout.strip():
266+
return []
267+
try:
268+
payload = json.loads(result.stdout)
269+
except json.JSONDecodeError as exc:
270+
raise RuntimeError(f"gcloud returned invalid JSON: {exc}") from exc
271+
return payload if isinstance(payload, list) else []
272+
last_detail = (result.stderr or result.stdout or "").strip()
273+
if attempt >= max_attempts or not _is_retryable_log_query_error(last_detail):
274+
break
275+
time.sleep(retry_seconds * attempt)
276+
suffix = f" after {max_attempts} attempt(s)" if max_attempts > 1 else ""
277+
raise RuntimeError((last_detail or "gcloud logging read failed") + suffix)
225278

226279

227280
def _parse_timestamp(value: Any) -> dt.datetime | None:

tests/test_cloud_run_runtime_guard.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,54 @@ def fake_run_gcloud(command):
5555
]
5656

5757

58+
def test_cloud_run_log_query_retries_transient_google_error(monkeypatch):
59+
attempts = []
60+
sleeps = []
61+
62+
def fake_run_gcloud(command):
63+
attempts.append(command)
64+
if len(attempts) == 1:
65+
return subprocess.CompletedProcess(
66+
command,
67+
1,
68+
stdout="",
69+
stderr='HttpError: {"error": {"code": 500, "status": "INTERNAL"}}',
70+
)
71+
return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="")
72+
73+
monkeypatch.setenv("RUNTIME_GUARD_LOG_QUERY_MAX_ATTEMPTS", "3")
74+
monkeypatch.setenv("RUNTIME_GUARD_LOG_QUERY_RETRY_SECONDS", "0")
75+
monkeypatch.setattr(guard, "_run_gcloud", fake_run_gcloud)
76+
monkeypatch.setattr(guard.time, "sleep", lambda seconds: sleeps.append(seconds))
77+
78+
assert guard._run_gcloud_logging("project-1", 'resource.type="cloud_run_revision"', 10) == []
79+
assert len(attempts) == 2
80+
assert sleeps == [0.0]
81+
82+
83+
def test_cloud_run_log_query_does_not_retry_permission_error(monkeypatch):
84+
attempts = []
85+
86+
def fake_run_gcloud(command):
87+
attempts.append(command)
88+
return subprocess.CompletedProcess(
89+
command,
90+
1,
91+
stdout="",
92+
stderr="ERROR: permission_denied (403)",
93+
)
94+
95+
monkeypatch.setattr(guard, "_run_gcloud", fake_run_gcloud)
96+
97+
try:
98+
guard._run_gcloud_logging("project-1", 'resource.type="cloud_run_revision"', 10)
99+
except RuntimeError as exc:
100+
assert "permission_denied" in str(exc)
101+
else:
102+
raise AssertionError("permission errors must fail without retry")
103+
assert len(attempts) == 1
104+
105+
58106
def test_cloud_run_log_since_uses_latest_ready_revision(monkeypatch):
59107
monkeypatch.setenv("CLOUD_RUN_REGION", "us-central1")
60108
observed = []

tests/test_sync_cloud_run_env_workflow.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
from pathlib import Path
44

55

6+
def test_sync_cloud_run_env_workflow_requires_manual_dispatch():
7+
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
8+
workflow = workflow_path.read_text(encoding="utf-8")
9+
10+
assert "workflow_run:" not in workflow
11+
assert "if: github.event_name == 'workflow_dispatch'" in workflow
12+
13+
614
def test_sync_cloud_run_env_workflow_uses_sync_plan_script():
715
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
816
workflow = workflow_path.read_text(encoding="utf-8")

0 commit comments

Comments
 (0)