Skip to content

Commit a57b863

Browse files
committed
Apply audit remediation
1 parent c2ac3bd commit a57b863

8 files changed

Lines changed: 471 additions & 5 deletions

File tree

.github/workflows/manual-strategy-switch.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ jobs:
126126
switch:
127127
name: Build and apply runtime switch
128128
runs-on: ubuntu-latest
129+
timeout-minutes: 15
129130
environment: runtime-strategy-switch
130131
permissions:
131132
contents: read

.github/workflows/validate.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ on:
44
push:
55
pull_request:
66

7+
permissions:
8+
contents: read
9+
710
jobs:
811
validate:
912
runs-on: ubuntu-latest
13+
timeout-minutes: 15
1014
steps:
1115
- uses: actions/checkout@v6
1216
with:
@@ -30,6 +34,8 @@ jobs:
3034
run: python3 scripts/runtime_settings.py validate
3135
- name: Run unit tests
3236
run: python3 -m unittest discover -s tests -v
37+
- name: Report internal dependency matrix
38+
run: python3 scripts/check_internal_dependency_matrix.py --projects-root .. --json
3339
- name: Validate strategy switch web assets
3440
run: |
3541
set -euo pipefail

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

internal_dependency_matrix.json

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
{
2+
"schema_version": 1,
3+
"dependencies": [
4+
{
5+
"consumer_repo": "BinancePlatform",
6+
"path": "requirements.txt",
7+
"package": "quant-platform-kit",
8+
"source_repo": "QuantPlatformKit",
9+
"ref": "v0.7.35"
10+
},
11+
{
12+
"consumer_repo": "BinancePlatform",
13+
"path": "requirements.txt",
14+
"package": "crypto-strategies",
15+
"source_repo": "CryptoStrategies",
16+
"ref": "v0.4.8"
17+
},
18+
{
19+
"consumer_repo": "BinancePlatform",
20+
"path": "requirements-lock.txt",
21+
"package": "quant-platform-kit",
22+
"source_repo": "QuantPlatformKit",
23+
"ref": "v0.7.35"
24+
},
25+
{
26+
"consumer_repo": "BinancePlatform",
27+
"path": "requirements-lock.txt",
28+
"package": "crypto-strategies",
29+
"source_repo": "CryptoStrategies",
30+
"ref": "v0.4.8"
31+
},
32+
{
33+
"consumer_repo": "CharlesSchwabPlatform",
34+
"path": "requirements.txt",
35+
"package": "quant-platform-kit",
36+
"source_repo": "QuantPlatformKit",
37+
"ref": "3b6a0a9bedde72773e188041e0dc48516b38aadc"
38+
},
39+
{
40+
"consumer_repo": "CharlesSchwabPlatform",
41+
"path": "requirements.txt",
42+
"package": "us-equity-strategies",
43+
"source_repo": "UsEquityStrategies",
44+
"ref": "8278048366f1cd83e29e0c921e4048e7e25ae227"
45+
},
46+
{
47+
"consumer_repo": "CryptoStrategies",
48+
"path": "pyproject.toml",
49+
"package": "quant-platform-kit",
50+
"source_repo": "QuantPlatformKit",
51+
"ref": "v0.7.35"
52+
},
53+
{
54+
"consumer_repo": "FirstradePlatform",
55+
"path": "pyproject.toml",
56+
"package": "quant-platform-kit",
57+
"source_repo": "QuantPlatformKit",
58+
"ref": "3b6a0a9bedde72773e188041e0dc48516b38aadc"
59+
},
60+
{
61+
"consumer_repo": "FirstradePlatform",
62+
"path": "pyproject.toml",
63+
"package": "us-equity-strategies",
64+
"source_repo": "UsEquityStrategies",
65+
"ref": "8278048366f1cd83e29e0c921e4048e7e25ae227"
66+
},
67+
{
68+
"consumer_repo": "FirstradePlatform",
69+
"path": "requirements.txt",
70+
"package": "quant-platform-kit",
71+
"source_repo": "QuantPlatformKit",
72+
"ref": "3b6a0a9bedde72773e188041e0dc48516b38aadc"
73+
},
74+
{
75+
"consumer_repo": "FirstradePlatform",
76+
"path": "requirements.txt",
77+
"package": "us-equity-strategies",
78+
"source_repo": "UsEquityStrategies",
79+
"ref": "8278048366f1cd83e29e0c921e4048e7e25ae227"
80+
},
81+
{
82+
"consumer_repo": "HkEquityStrategies",
83+
"path": "pyproject.toml",
84+
"package": "quant-platform-kit",
85+
"source_repo": "QuantPlatformKit",
86+
"ref": "023641c88506c732624a7329e48b51b9dbbe3c2a"
87+
},
88+
{
89+
"consumer_repo": "InteractiveBrokersPlatform",
90+
"path": "requirements.txt",
91+
"package": "quant-platform-kit",
92+
"source_repo": "QuantPlatformKit",
93+
"ref": "3b6a0a9bedde72773e188041e0dc48516b38aadc"
94+
},
95+
{
96+
"consumer_repo": "InteractiveBrokersPlatform",
97+
"path": "requirements.txt",
98+
"package": "us-equity-strategies",
99+
"source_repo": "UsEquityStrategies",
100+
"ref": "8278048366f1cd83e29e0c921e4048e7e25ae227"
101+
},
102+
{
103+
"consumer_repo": "InteractiveBrokersPlatform",
104+
"path": "requirements.txt",
105+
"package": "hk-equity-strategies",
106+
"source_repo": "HkEquityStrategies",
107+
"ref": "b690fcfd1e26648840723a5ab8b12c873f038b9b"
108+
},
109+
{
110+
"consumer_repo": "LongBridgePlatform",
111+
"path": "requirements.txt",
112+
"package": "quant-platform-kit",
113+
"source_repo": "QuantPlatformKit",
114+
"ref": "023641c88506c732624a7329e48b51b9dbbe3c2a"
115+
},
116+
{
117+
"consumer_repo": "LongBridgePlatform",
118+
"path": "requirements.txt",
119+
"package": "us-equity-strategies",
120+
"source_repo": "UsEquityStrategies",
121+
"ref": "7d35772d1125b534d0bcca557cb6dbaf28914719"
122+
},
123+
{
124+
"consumer_repo": "LongBridgePlatform",
125+
"path": "requirements.txt",
126+
"package": "hk-equity-strategies",
127+
"source_repo": "HkEquityStrategies",
128+
"ref": "2e0075004239e7ede7ba256763a3441d4ec4ca73"
129+
},
130+
{
131+
"consumer_repo": "UsEquitySnapshotPipelines",
132+
"path": "pyproject.toml",
133+
"package": "quant-strategy-plugins",
134+
"source_repo": "QuantStrategyPlugins",
135+
"ref": "v0.1.4"
136+
},
137+
{
138+
"consumer_repo": "UsEquitySnapshotPipelines",
139+
"path": "pyproject.toml",
140+
"package": "us-equity-strategies",
141+
"source_repo": "UsEquityStrategies",
142+
"ref": "7d35772d1125b534d0bcca557cb6dbaf28914719"
143+
},
144+
{
145+
"consumer_repo": "UsEquityStrategies",
146+
"path": "pyproject.toml",
147+
"package": "quant-platform-kit",
148+
"source_repo": "QuantPlatformKit",
149+
"ref": "023641c88506c732624a7329e48b51b9dbbe3c2a"
150+
}
151+
]
152+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""Report QuantStrategyLab internal git dependency pin drift."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import re
9+
from dataclasses import dataclass
10+
from pathlib import Path
11+
from typing import Any
12+
13+
14+
ROOT = Path(__file__).resolve().parents[1]
15+
DEFAULT_MATRIX_PATH = ROOT / "internal_dependency_matrix.json"
16+
DEFAULT_PROJECTS_ROOT = ROOT.parent
17+
DEPENDENCY_PATTERN = re.compile(
18+
r"(?P<package>[A-Za-z0-9_.-]+)\s*@\s*"
19+
r"git\+https://github\.com/QuantStrategyLab/"
20+
r"(?P<source_repo>[A-Za-z0-9_.-]+)\.git@(?P<ref>[A-Za-z0-9_.-]+)"
21+
)
22+
23+
24+
@dataclass(frozen=True)
25+
class DependencyPin:
26+
consumer_repo: str
27+
path: str
28+
package: str
29+
source_repo: str
30+
ref: str
31+
32+
@property
33+
def key(self) -> tuple[str, str, str, str]:
34+
return (self.consumer_repo, self.path, self.package, self.source_repo)
35+
36+
def label(self) -> str:
37+
return f"{self.consumer_repo}/{self.path}:{self.package}->{self.source_repo}"
38+
39+
40+
@dataclass(frozen=True)
41+
class MatrixReport:
42+
checked_files: int
43+
missing_files: list[str]
44+
issues: list[str]
45+
46+
@property
47+
def ok(self) -> bool:
48+
return not self.issues
49+
50+
51+
def load_matrix(path: Path) -> list[DependencyPin]:
52+
payload = json.loads(path.read_text(encoding="utf-8"))
53+
if payload.get("schema_version") != 1:
54+
raise ValueError("internal dependency matrix schema_version must be 1")
55+
dependencies = payload.get("dependencies")
56+
if not isinstance(dependencies, list):
57+
raise ValueError("internal dependency matrix dependencies must be a list")
58+
pins: list[DependencyPin] = []
59+
for index, item in enumerate(dependencies):
60+
if not isinstance(item, dict):
61+
raise ValueError(f"dependencies[{index}] must be an object")
62+
pins.append(
63+
DependencyPin(
64+
consumer_repo=_required_string(item, "consumer_repo", index),
65+
path=_required_string(item, "path", index),
66+
package=_required_string(item, "package", index),
67+
source_repo=_required_string(item, "source_repo", index),
68+
ref=_required_string(item, "ref", index),
69+
)
70+
)
71+
return pins
72+
73+
74+
def _required_string(item: dict[str, Any], key: str, index: int) -> str:
75+
value = item.get(key)
76+
if not isinstance(value, str) or not value.strip():
77+
raise ValueError(f"dependencies[{index}].{key} must be a non-empty string")
78+
return value.strip()
79+
80+
81+
def parse_dependency_pins(consumer_repo: str, path: str, text: str) -> list[DependencyPin]:
82+
return [
83+
DependencyPin(
84+
consumer_repo=consumer_repo,
85+
path=path,
86+
package=match.group("package"),
87+
source_repo=match.group("source_repo"),
88+
ref=match.group("ref"),
89+
)
90+
for match in DEPENDENCY_PATTERN.finditer(text)
91+
]
92+
93+
94+
def check_matrix(*, matrix_pins: list[DependencyPin], projects_root: Path) -> MatrixReport:
95+
expected_by_file: dict[tuple[str, str], list[DependencyPin]] = {}
96+
for pin in matrix_pins:
97+
expected_by_file.setdefault((pin.consumer_repo, pin.path), []).append(pin)
98+
99+
issues: list[str] = []
100+
missing_files: list[str] = []
101+
checked_files = 0
102+
for (consumer_repo, relative_path), expected_pins in sorted(expected_by_file.items()):
103+
path = projects_root / consumer_repo / relative_path
104+
if not path.exists():
105+
missing_files.append(f"{consumer_repo}/{relative_path}")
106+
continue
107+
checked_files += 1
108+
actual_pins = parse_dependency_pins(consumer_repo, relative_path, path.read_text(encoding="utf-8"))
109+
actual_by_key = {pin.key: pin for pin in actual_pins}
110+
expected_by_key = {pin.key: pin for pin in expected_pins}
111+
112+
for key, expected in sorted(expected_by_key.items()):
113+
actual = actual_by_key.get(key)
114+
if actual is None:
115+
issues.append(f"missing {expected.label()} expected @{expected.ref}")
116+
elif actual.ref != expected.ref:
117+
issues.append(f"ref mismatch {expected.label()}: expected @{expected.ref}, found @{actual.ref}")
118+
119+
for key, actual in sorted(actual_by_key.items()):
120+
if key not in expected_by_key:
121+
issues.append(f"untracked internal dependency {actual.label()} @{actual.ref}")
122+
123+
return MatrixReport(checked_files=checked_files, missing_files=missing_files, issues=issues)
124+
125+
126+
def build_parser() -> argparse.ArgumentParser:
127+
parser = argparse.ArgumentParser(description="Report QuantStrategyLab internal dependency pin drift.")
128+
parser.add_argument("--matrix", type=Path, default=DEFAULT_MATRIX_PATH)
129+
parser.add_argument("--projects-root", type=Path, default=DEFAULT_PROJECTS_ROOT)
130+
parser.add_argument("--json", action="store_true", help="Print machine-readable report.")
131+
parser.add_argument("--strict", action="store_true", help="Exit non-zero when drift is detected.")
132+
return parser
133+
134+
135+
def main(argv: list[str] | None = None) -> int:
136+
args = build_parser().parse_args(argv)
137+
report = check_matrix(matrix_pins=load_matrix(args.matrix), projects_root=args.projects_root)
138+
if args.json:
139+
print(
140+
json.dumps(
141+
{
142+
"checked_files": report.checked_files,
143+
"missing_files": report.missing_files,
144+
"issues": report.issues,
145+
"ok": report.ok,
146+
},
147+
ensure_ascii=False,
148+
indent=2,
149+
)
150+
)
151+
else:
152+
print(f"checked_files={report.checked_files}")
153+
if report.missing_files:
154+
print("missing_files:")
155+
for item in report.missing_files:
156+
print(f"- {item}")
157+
if report.issues:
158+
print("issues:")
159+
for issue in report.issues:
160+
print(f"- {issue}")
161+
if report.ok:
162+
print("internal dependency matrix is current")
163+
return 1 if args.strict and not report.ok else 0
164+
165+
166+
if __name__ == "__main__":
167+
raise SystemExit(main())

tests/strategy_switch_worker_validation.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,32 @@ assert.equal(
111111
await __test.withTimeout(new Promise(() => {}), 1, "fallback"),
112112
"fallback",
113113
);
114+
const timeoutFetchResponse = await __test.fetchWithTimeout(
115+
"https://api.github.test/user",
116+
{ headers: { Accept: "application/json" } },
117+
100,
118+
async (_resource, init) => {
119+
assert.ok(init.signal instanceof AbortSignal);
120+
assert.equal(init.headers.Accept, "application/json");
121+
return new Response('{"ok":true}', { status: 200 });
122+
},
123+
);
124+
assert.equal(timeoutFetchResponse.status, 200);
125+
await assert.rejects(
126+
() => __test.fetchWithTimeout(
127+
"https://api.github.test/slow",
128+
{},
129+
1,
130+
(_resource, init) => new Promise((_resolve, reject) => {
131+
init.signal.addEventListener("abort", () => {
132+
const error = new Error("aborted");
133+
error.name = "AbortError";
134+
reject(error);
135+
});
136+
}),
137+
),
138+
/GitHub request timed out/,
139+
);
114140

115141
function captureError(fn) {
116142
try {

0 commit comments

Comments
 (0)