Skip to content

Commit 48a29c6

Browse files
Pigbibicodex
andcommitted
fix: harden IBKR runtime monitoring and deployment controls
Co-Authored-By: Codex <noreply@openai.com>
1 parent 037a653 commit 48a29c6

4 files changed

Lines changed: 121 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
inputs:
106
target:
@@ -59,15 +55,8 @@ concurrency:
5955
jobs:
6056
sync-cloud-run-env:
6157
name: Deploy / Sync Cloud Run
62-
if: >
63-
github.event_name == 'workflow_dispatch' ||
64-
(
65-
github.event_name == 'workflow_run' &&
66-
github.event.workflow_run.conclusion == 'success' &&
67-
github.event.workflow_run.event == 'push' &&
68-
!contains(github.event.workflow_run.head_commit.message, 'chore(deps): align QPK pin') &&
69-
!contains(github.event.workflow_run.head_commit.message, '[skip-cloud-run-deploy]')
70-
)
58+
# Production deployment and environment sync require an explicit operator dispatch.
59+
if: github.event_name == 'workflow_dispatch'
7160
runs-on: ubuntu-latest
7261
timeout-minutes: 20
7362
permissions:
@@ -238,7 +227,7 @@ jobs:
238227
if: steps.config.outputs.enabled == 'true'
239228
uses: actions/checkout@v6
240229
with:
241-
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
230+
ref: ${{ github.sha }}
242231

243232
- name: Apply HK verify-only dispatch defaults
244233
if: steps.config.outputs.enabled == 'true' && github.event_name == 'workflow_dispatch' && inputs.target == 'hk-verify'

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
@@ -57,6 +57,54 @@ def fake_run_gcloud(command):
5757
]
5858

5959

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

tests/test_sync_cloud_run_env_workflow.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ set -euo pipefail
44
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"
55
workflow_file="$repo_dir/.github/workflows/sync-cloud-run-env.yml"
66

7+
if grep -Fq 'workflow_run:' "$workflow_file"; then
8+
echo "deploy workflow must require a manual dispatch" >&2
9+
exit 1
10+
fi
11+
grep -Fq "if: github.event_name == 'workflow_dispatch'" "$workflow_file"
12+
713
grep -Fq 'GCP_WORKLOAD_IDENTITY_PROVIDER: projects/303168642265/locations/global/workloadIdentityPools/github-actions/providers/github-main' "$workflow_file"
814
grep -Fq 'GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: ibkr-platform-deploy@interactivebrokersquant.iam.gserviceaccount.com' "$workflow_file"
915
grep -Fq 'GCP_SCHEDULER_SERVICE_ACCOUNT: ibkr-platform-scheduler@interactivebrokersquant.iam.gserviceaccount.com' "$workflow_file"

0 commit comments

Comments
 (0)