Skip to content

Commit d5ef766

Browse files
committed
Add QPK publish dependency
1 parent 95b3c6b commit d5ef766

7 files changed

Lines changed: 88 additions & 36 deletions

File tree

.github/workflows/monthly_publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
python-version: "3.11"
5252

5353
- name: Authenticate to Google Cloud
54-
uses: google-github-actions/auth@v2
54+
uses: google-github-actions/auth@v3
5555
with:
5656
workload_identity_provider: "projects/677468735457/locations/global/workloadIdentityPools/github-actions/providers/github-main"
5757
service_account: "codex-gcp-operator@binancequant.iam.gserviceaccount.com"

requirements-lock.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d786c1140967f0e96e35599d057f0655e5a9ba25
12
pandas==3.0.3
23
numpy==2.4.6
34
requests==2.34.2

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d786c1140967f0e96e35599d057f0655e5a9ba25
12
pandas>=3.0.3
23
numpy>=2.4.6,<2.5
34
requests>=2.34.2

scripts/gate_codex_app_review.py

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ def env(name: str, default: str = "") -> str:
3333

3434

3535
def env_int(name: str, default: int) -> int:
36-
try: return int(env(name, str(default)))
37-
except ValueError: return default
36+
try:
37+
return int(env(name, str(default)))
38+
except ValueError:
39+
return default
3840

3941

40-
def github_request(token: str, method: str, path: str,
41-
payload: dict[str, Any] | None = None) -> Any:
42+
def github_request(
43+
token: str,
44+
method: str,
45+
path: str,
46+
payload: dict[str, Any] | None = None,
47+
) -> Any:
4248
url = f"{API_BASE}{path}" if not path.startswith("https://") else path
4349
data = json.dumps(payload).encode() if payload else None
4450
headers = {
@@ -47,7 +53,8 @@ def github_request(token: str, method: str, path: str,
4753
"X-GitHub-Api-Version": "2022-11-28",
4854
"User-Agent": "codex-review-gate",
4955
}
50-
if payload: headers["Content-Type"] = "application/json"
56+
if payload:
57+
headers["Content-Type"] = "application/json"
5158
req = urllib.request.Request(url, data=data, method=method, headers=headers)
5259
try:
5360
with urllib.request.urlopen(req, timeout=30) as resp:
@@ -69,8 +76,10 @@ def step_summary(text: str) -> None:
6976

7077
def load_policy() -> dict[str, Any]:
7178
if POLICY_PATH.exists():
72-
try: return json.loads(POLICY_PATH.read_text(encoding="utf-8"))
73-
except (OSError, json.JSONDecodeError): pass
79+
try:
80+
return json.loads(POLICY_PATH.read_text(encoding="utf-8"))
81+
except (OSError, json.JSONDecodeError):
82+
pass
7483
return {
7584
"version": 1,
7685
"blocked_path_patterns": [
@@ -85,8 +94,10 @@ def compile_patterns(policy: dict[str, Any]) -> list[re.Pattern[str]]:
8594
pp: list[re.Pattern[str]] = []
8695
for p in policy.get("blocked_path_patterns", []):
8796
if isinstance(p, str) and p.strip():
88-
try: pp.append(re.compile(p, re.IGNORECASE))
89-
except re.error: pass
97+
try:
98+
pp.append(re.compile(p, re.IGNORECASE))
99+
except re.error:
100+
pass
90101
return pp
91102

92103

@@ -111,8 +122,11 @@ def scan_diff(diff_text: str, path_patterns: list[re.Pattern[str]]) -> list[str]
111122
violations.append(f"**Blocked file**: `{current}` matches `{pat.pattern}`")
112123
break
113124
continue
114-
if line.startswith("+++ b/"): current = line[6:]; continue
115-
if not line.startswith("+") or line.startswith("+++"): continue
125+
if line.startswith("+++ b/"):
126+
current = line[6:]
127+
continue
128+
if not line.startswith("+") or line.startswith("+++"):
129+
continue
116130
m = _SENSITIVE.search(line[1:])
117131
if m:
118132
violations.append(f"**Hardcoded secret** in `{current}`: `{m.group(0)[:100]}`")
@@ -128,8 +142,10 @@ def check_metadata(files: list[dict[str, Any]], policy: dict[str, Any]) -> list[
128142
for f in files:
129143
fn = f.get("filename", "?")
130144
st = (f.get("status") or "").lower().strip()
131-
if st == "removed": issues.append(f"**File deleted**: `{fn}` — verify intentional")
132-
elif st == "renamed": issues.append(f"**File renamed**: `{f.get('previous_filename', '?')}` → `{fn}`")
145+
if st == "removed":
146+
issues.append(f"**File deleted**: `{fn}` — verify intentional")
147+
elif st == "renamed":
148+
issues.append(f"**File renamed**: `{f.get('previous_filename', '?')}` → `{fn}`")
133149
if len(files) > mx_f:
134150
issues.append(f"**Too many files**: {len(files)} changed (limit {mx_f})")
135151
if ta + td > mx_l:
@@ -144,12 +160,18 @@ def run_static_guard(token: str, repo: str, pr_number: int) -> int:
144160
page = 1
145161
while True:
146162
try:
147-
batch = github_request(token, "GET",
148-
f"/repos/{repo}/pulls/{pr_number}/files?per_page=100&page={page}")
149-
except RuntimeError: break
150-
if not isinstance(batch, list) or not batch: break
163+
batch = github_request(
164+
token,
165+
"GET",
166+
f"/repos/{repo}/pulls/{pr_number}/files?per_page=100&page={page}",
167+
)
168+
except RuntimeError:
169+
break
170+
if not isinstance(batch, list) or not batch:
171+
break
151172
files.extend(batch)
152-
if len(batch) < 100: break
173+
if len(batch) < 100:
174+
break
153175
page += 1
154176

155177
diff_text = ""
@@ -165,23 +187,29 @@ def run_static_guard(token: str, repo: str, pr_number: int) -> int:
165187
)
166188
with urllib.request.urlopen(req, timeout=30) as resp:
167189
diff_text = resp.read().decode("utf-8", errors="replace")
168-
except Exception: pass
190+
except Exception:
191+
pass
169192

170193
issues = check_metadata(files, policy) + scan_diff(diff_text, compile_patterns(policy))
171-
if not issues: return 0
194+
if not issues:
195+
return 0
172196

173197
print(f"STATIC → BLOCKED: {len(issues)} issue(s)")
174-
for i in issues: print(f" • {i}")
175-
step_summary(f"## Merge blocked: {len(issues)} static issue(s)\n\n" +
176-
"\n".join(f"- {i}" for i in issues))
198+
for i in issues:
199+
print(f" • {i}")
200+
step_summary(
201+
f"## Merge blocked: {len(issues)} static issue(s)\n\n"
202+
+ "\n".join(f"- {i}" for i in issues)
203+
)
177204
return 1
178205

179206

180207
# ─── app review ──────────────────────────────────────────────────────────────
181208

182209
def get_codex_review(token: str, repo: str, pr_number: int) -> dict[str, Any] | None:
183210
reviews = github_request(token, "GET", f"/repos/{repo}/pulls/{pr_number}/reviews?per_page=100")
184-
if not isinstance(reviews, list): return None
211+
if not isinstance(reviews, list):
212+
return None
185213
for r in reversed(reviews):
186214
if isinstance(r, dict) and (r.get("user") or {}).get("login") == BOT_LOGIN:
187215
return r
@@ -228,16 +256,20 @@ def main() -> int:
228256
pr_number = pr.get("number")
229257
head_sha = (pr.get("head") or {}).get("sha")
230258
if not pr_number or not head_sha:
231-
print(f"::warning::Cannot resolve PR context"); return 0
259+
print("::warning::Cannot resolve PR context")
260+
return 0
232261

233262
print(f"PR #{pr_number} sha={head_sha[:12]} event={event_name}")
234263

235264
# ── Phase 1: Static guard (skip on review-only events) ────────────
236265
if event_name != "pull_request_review":
237-
try: rc = run_static_guard(token, repo, pr_number)
266+
try:
267+
rc = run_static_guard(token, repo, pr_number)
238268
except RuntimeError as exc:
239-
print(f"::warning::Static guard error: {exc}"); rc = 0
240-
if rc != 0: return 1
269+
print(f"::warning::Static guard error: {exc}")
270+
rc = 0
271+
if rc != 0:
272+
return 1
241273
print("STATIC → clean")
242274

243275
# ── Phase 2: App review ───────────────────────────────────────────
@@ -250,8 +282,10 @@ def main() -> int:
250282
return rc
251283

252284
# WAIT: poll for existing or upcoming review
253-
try: existing = get_codex_review(token, repo, pr_number)
254-
except RuntimeError: existing = None
285+
try:
286+
existing = get_codex_review(token, repo, pr_number)
287+
except RuntimeError:
288+
existing = None
255289

256290
if existing is not None:
257291
rc, title, summary = app_decision(existing)
@@ -266,8 +300,10 @@ def main() -> int:
266300

267301
while time.time() < deadline:
268302
time.sleep(poll_s)
269-
try: review = get_codex_review(token, repo, pr_number)
270-
except RuntimeError: continue
303+
try:
304+
review = get_codex_review(token, repo, pr_number)
305+
except RuntimeError:
306+
continue
271307
if review is not None:
272308
rc, title, summary = app_decision(review)
273309
print(f"WAIT → found review → exit={rc}: {title}")

src/indicators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import math
4+
from typing import Any
45

56
import numpy as np
67
import pandas as pd
@@ -188,4 +189,3 @@ def rolling_beta(asset_returns: pd.Series, benchmark_returns: pd.Series, window:
188189

189190
def rolling_correlation(asset_returns: pd.Series, benchmark_returns: pd.Series, window: int = 60) -> pd.Series:
190191
return asset_returns.rolling(window, min_periods=window).corr(benchmark_returns)
191-

src/publish.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def ensure_publish_preflight(
187187
if settings.dry_run:
188188
return validation
189189
if not settings.project_id:
190-
raise ValueError("Publish preflight failed: CLOUD_PROJECT_ID is required for a real publish.")
190+
raise ValueError("Publish preflight failed: CLOUD_PROJECT_ID or GCP_PROJECT_ID is required for a real publish.")
191191
if not settings.cloud_bucket:
192192
raise ValueError("Publish preflight failed: CLOUD_BUCKET is required for a real publish.")
193193
if not str(settings.firestore_collection).strip():

tests/test_monthly_publish_workflow_config.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
PROJECT_ROOT = Path(__file__).resolve().parents[1]
88
WORKFLOW_PATH = PROJECT_ROOT / ".github" / "workflows" / "monthly_publish.yml"
99
README_ZH_PATH = PROJECT_ROOT / "README.zh-CN.md"
10+
QPK_DEPENDENCY = (
11+
"quant-platform-kit @ "
12+
"git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d786c1140967f0e96e35599d057f0655e5a9ba25"
13+
)
1014

1115

1216
class MonthlyPublishWorkflowConfigTests(unittest.TestCase):
@@ -19,10 +23,13 @@ def test_publish_targets_use_vars_only(self) -> None:
1923
self.assertIn("actions/upload-artifact@v7", workflow)
2024
self.assertIn("GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}", workflow)
2125
self.assertIn("GCS_BUCKET: ${{ vars.GCS_BUCKET }}", workflow)
22-
self.assertIn("credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}", workflow)
26+
self.assertIn("workload_identity_provider:", workflow)
27+
self.assertIn("service_account:", workflow)
2328
self.assertIn("issues: write", workflow)
2429
self.assertNotIn("secrets.GCP_PROJECT_ID", workflow)
2530
self.assertNotIn("secrets.GCS_BUCKET", workflow)
31+
self.assertNotIn("credentials_json:", workflow)
32+
self.assertNotIn("GCP_SERVICE_ACCOUNT_KEY", workflow)
2633

2734
def test_monthly_review_issue_creation_does_not_require_gh_cli(self) -> None:
2835
workflow = WORKFLOW_PATH.read_text(encoding="utf-8")
@@ -64,6 +71,13 @@ def test_monthly_review_issue_creation_does_not_require_gh_cli(self) -> None:
6471
self.assertNotIn("LEGACY_API_REVIEW_ENABLED", workflow)
6572
self.assertNotIn("/actions/workflows/ai_review.yml/dispatches", workflow)
6673

74+
def test_real_publish_dependency_is_locked(self) -> None:
75+
requirements = (PROJECT_ROOT / "requirements.txt").read_text(encoding="utf-8")
76+
requirements_lock = (PROJECT_ROOT / "requirements-lock.txt").read_text(encoding="utf-8")
77+
78+
self.assertIn(QPK_DEPENDENCY, requirements)
79+
self.assertIn(QPK_DEPENDENCY, requirements_lock)
80+
6781
def test_source_local_legacy_ai_workflows_are_removed(self) -> None:
6882
workflow_dir = PROJECT_ROOT / ".github" / "workflows"
6983

0 commit comments

Comments
 (0)