Skip to content

Commit 9c03b7f

Browse files
Pigbibicodex
andcommitted
chore: add dependency matrix sync and CI gates
Co-Authored-By: Codex <noreply@openai.com>
1 parent cab005d commit 9c03b7f

7 files changed

Lines changed: 142 additions & 0 deletions

File tree

.github/workflows/validate.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ permissions:
88
contents: read
99

1010
jobs:
11+
actionlint:
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 10
14+
steps:
15+
- uses: actions/checkout@v6
16+
- name: Actionlint
17+
uses: docker://rhysd/actionlint:1.7.12
18+
1119
python:
1220
runs-on: ubuntu-latest
1321
timeout-minutes: 15

docs/internal_dependency_pin_policy.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ QuantStrategyLab shares Python packages across platforms, strategies, and pipeli
1313
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --strict
1414
```
1515

16+
When drift is found repeatedly, you can regenerate the matrix from local consumer files in a single step:
17+
18+
```bash
19+
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --generate --json > /tmp/internal_dependency_matrix.json
20+
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --sync
21+
```
22+
1623
The checker compares matrix entries against consumer `requirements.txt`, `requirements-lock.txt`, and `pyproject.toml` files in sibling repositories. With `--strict`, ref mismatches fail CI even when sibling repos are not checked out locally.
1724

1825
## Pin formats

docs/internal_dependency_pin_policy.zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ QuantStrategyLab 通过 git URL pin 在平台、策略与 pipeline 之间共享
1313
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --strict
1414
```
1515

16+
若本地 consumer 文件与 matrix 漂移,可先从本地消费方依赖文件重建并同步:
17+
18+
```bash
19+
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --generate --json > /tmp/internal_dependency_matrix.json
20+
python3 scripts/check_internal_dependency_matrix.py --projects-root .. --sync
21+
```
22+
1623
该脚本会将 matrix 条目与各 consumer 仓库中的 `requirements.txt``requirements-lock.txt``pyproject.toml` 对比。启用 `--strict` 时,即使本地未 checkout sibling 仓库,ref 不一致也会导致 CI 失败。
1724

1825
## Pin 格式

python/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# quant-runtime-settings
2+
3+
Python helper package for QuantStrategyLab runtime target settings and validation scripts.

python/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name = "quant-runtime-settings"
33
version = "0.1.0"
44
description = "Declarative runtime target settings for QuantStrategyLab deployments"
5+
readme = "README.md"
56
requires-python = ">=3.11"
67

78
[tool.ruff]

python/scripts/check_internal_dependency_matrix.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
r"git\+https://github\.com/QuantStrategyLab/"
2020
r"(?P<source_repo>[A-Za-z0-9_.-]+)\.git@(?P<ref>[A-Za-z0-9_.-]+)"
2121
)
22+
TRACKED_DEPENDENCY_PATHS = ("requirements.txt", "requirements-lock.txt", "pyproject.toml")
23+
24+
25+
def _sort_dependency_pins(pins: list[DependencyPin]) -> list[DependencyPin]:
26+
return sorted(
27+
set(pins),
28+
key=lambda pin: (pin.consumer_repo, pin.path, pin.package, pin.source_repo, pin.ref),
29+
)
2230

2331

2432
@dataclass(frozen=True)
@@ -48,6 +56,33 @@ def ok(self) -> bool:
4856
return not self.issues
4957

5058

59+
def matrix_payload(pins: list[DependencyPin]) -> dict[str, Any]:
60+
return {
61+
"schema_version": 1,
62+
"dependencies": [
63+
{
64+
"consumer_repo": pin.consumer_repo,
65+
"path": pin.path,
66+
"package": pin.package,
67+
"source_repo": pin.source_repo,
68+
"ref": pin.ref,
69+
}
70+
for pin in _sort_dependency_pins(pins)
71+
],
72+
}
73+
74+
75+
def collect_dependency_pins_from_projects(projects_root: Path) -> list[DependencyPin]:
76+
pins: list[DependencyPin] = []
77+
for project_dir in sorted(p for p in projects_root.iterdir() if p.is_dir() and not p.name.startswith(".")):
78+
for relative_path in TRACKED_DEPENDENCY_PATHS:
79+
path = project_dir / relative_path
80+
if not path.is_file():
81+
continue
82+
pins.extend(parse_dependency_pins(project_dir.name, relative_path, path.read_text(encoding="utf-8")))
83+
return _sort_dependency_pins(pins)
84+
85+
5186
def load_matrix(path: Path) -> list[DependencyPin]:
5287
payload = json.loads(path.read_text(encoding="utf-8"))
5388
if payload.get("schema_version") != 1:
@@ -127,6 +162,16 @@ def build_parser() -> argparse.ArgumentParser:
127162
parser = argparse.ArgumentParser(description="Report QuantStrategyLab internal dependency pin drift.")
128163
parser.add_argument("--matrix", type=Path, default=DEFAULT_MATRIX_PATH)
129164
parser.add_argument("--projects-root", type=Path, default=DEFAULT_PROJECTS_ROOT)
165+
parser.add_argument(
166+
"--generate",
167+
action="store_true",
168+
help="Generate internal dependency matrix payload from local consumer dependency files.",
169+
)
170+
parser.add_argument(
171+
"--sync",
172+
action="store_true",
173+
help="Overwrite --matrix with generated internal dependency payload.",
174+
)
130175
parser.add_argument("--json", action="store_true", help="Print machine-readable report.")
131176
parser.add_argument("--strict", action="store_true", help="Exit non-zero when drift is detected.")
132177
parser.add_argument(
@@ -139,6 +184,16 @@ def build_parser() -> argparse.ArgumentParser:
139184

140185
def main(argv: list[str] | None = None) -> int:
141186
args = build_parser().parse_args(argv)
187+
if args.generate or args.sync:
188+
generated_payload = matrix_payload(collect_dependency_pins_from_projects(projects_root=args.projects_root))
189+
rendered_payload = json.dumps(generated_payload, ensure_ascii=False, indent=2)
190+
print(rendered_payload)
191+
if args.sync:
192+
args.matrix.write_text(rendered_payload + "\n", encoding="utf-8")
193+
if not args.json:
194+
print(f"synced matrix -> {args.matrix}")
195+
return 0
196+
142197
report = check_matrix(matrix_pins=load_matrix(args.matrix), projects_root=args.projects_root)
143198
issues = list(report.issues)
144199
if args.require_consumer_files and report.missing_files:

python/tests/test_internal_dependency_matrix.py

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

33
import importlib.util
4+
import json
45
import sys
56
import unittest
67
from pathlib import Path
@@ -100,6 +101,66 @@ def test_require_consumer_files_treats_missing_paths_as_issues(self):
100101
self.assertEqual(report.missing_files, ["ExamplePlatform/requirements.txt"])
101102
self.assertEqual(report.issues, [])
102103

104+
def test_collect_dependency_pins_from_projects(self):
105+
projects_root = self._make_projects_root(
106+
{
107+
"ExampleA/pyproject.toml": "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@a11",
108+
"ExampleB/requirements.txt": "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b22",
109+
"ExampleB/requirements-lock.txt": "crypto-strategies @ git+https://github.com/QuantStrategyLab/CryptoStrategies.git@c33",
110+
}
111+
)
112+
113+
pins = check_internal_dependency_matrix.collect_dependency_pins_from_projects(projects_root)
114+
rows = [(pin.consumer_repo, pin.path, pin.package, pin.source_repo, pin.ref) for pin in pins]
115+
116+
self.assertEqual(rows, [
117+
("ExampleA", "pyproject.toml", "quant-platform-kit", "QuantPlatformKit", "a11"),
118+
("ExampleB", "requirements-lock.txt", "crypto-strategies", "CryptoStrategies", "c33"),
119+
("ExampleB", "requirements.txt", "us-equity-strategies", "UsEquityStrategies", "b22"),
120+
])
121+
122+
def test_sync_rewrites_matrix_with_stable_order(self):
123+
projects_root = self._make_projects_root(
124+
{
125+
"ExampleB/requirements.txt": "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b22",
126+
"ExampleA/pyproject.toml": "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@a11",
127+
}
128+
)
129+
matrix_path = projects_root / "internal_dependency_matrix.json"
130+
matrix_path.write_text(
131+
(
132+
"{\n"
133+
' "schema_version": 1,\n'
134+
' "dependencies": [\n'
135+
' {\n'
136+
' "consumer_repo": "ExampleB",\n'
137+
' "path": "requirements.txt",\n'
138+
' "package": "us-equity-strategies",\n'
139+
' "source_repo": "UsEquityStrategies",\n'
140+
' "ref": "b22"\n'
141+
" }\n"
142+
" ]\n"
143+
"}\n"
144+
),
145+
encoding="utf-8",
146+
)
147+
148+
projects_payload = check_internal_dependency_matrix.matrix_payload(
149+
check_internal_dependency_matrix.collect_dependency_pins_from_projects(projects_root)
150+
)
151+
exit_code = check_internal_dependency_matrix.main(
152+
["--sync", "--projects-root", str(projects_root), "--matrix", str(matrix_path)]
153+
)
154+
synced_payload = json.loads(matrix_path.read_text(encoding="utf-8"))
155+
156+
self.assertEqual(exit_code, 0)
157+
self.assertEqual(synced_payload["schema_version"], 1)
158+
self.assertEqual(synced_payload["dependencies"], projects_payload["dependencies"])
159+
self.assertLess(
160+
synced_payload["dependencies"][0]["path"],
161+
synced_payload["dependencies"][-1]["path"],
162+
)
163+
103164
def _make_projects_root(self, files: dict[str, str]) -> Path:
104165
import tempfile
105166

0 commit comments

Comments
 (0)