Skip to content

Commit c8ea74e

Browse files
Pigbibicodex
andcommitted
ci: add qsl exception lifecycle report
Co-Authored-By: Codex <noreply@openai.com>
1 parent c3439db commit c8ea74e

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: QSL Exception Lifecycle
2+
3+
on:
4+
schedule:
5+
- cron: "17 3 * * 1"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: qsl-exception-lifecycle
13+
cancel-in-progress: false
14+
15+
jobs:
16+
report:
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 15
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v6
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v6
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Prepare QuantStrategyLab workspace
29+
id: workspace
30+
env:
31+
GH_TOKEN: ${{ secrets.RUNTIME_SETTINGS_GH_TOKEN || github.token }}
32+
run: |
33+
set -euo pipefail
34+
workspace="${RUNNER_TEMP}/qsl-workspace"
35+
rm -rf "$workspace"
36+
mkdir -p "$workspace"
37+
ln -s "$GITHUB_WORKSPACE" "$workspace/QuantRuntimeSettings"
38+
39+
python3 - <<'PY' > qsl-bundle-repos.tsv
40+
import os
41+
import tomllib
42+
from pathlib import Path
43+
44+
qsl_payload = tomllib.loads(Path("qsl.toml").read_text(encoding="utf-8"))
45+
qsl_config = qsl_payload.get("qsl", qsl_payload)
46+
bundle = qsl_config.get("bundle") or qsl_config.get("compat")
47+
if isinstance(bundle, dict):
48+
bundle = bundle.get("bundle")
49+
if not isinstance(bundle, str) or not bundle.strip():
50+
raise SystemExit("qsl.toml missing qsl.bundle")
51+
bundle = bundle.strip()
52+
bundle_path = Path("compat/bundles") / f"{bundle}.toml"
53+
if not bundle_path.exists():
54+
raise SystemExit(f"bundle manifest not found: {bundle_path}")
55+
payload = tomllib.loads(bundle_path.read_text(encoding="utf-8"))
56+
repos = payload.get("repos")
57+
if not isinstance(repos, dict) or not repos:
58+
raise SystemExit(f"bundle manifest missing [repos]: {bundle_path}")
59+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
60+
output.write(f"bundle={bundle}\n")
61+
output.write(f"workspace={os.environ['RUNNER_TEMP']}/qsl-workspace\n")
62+
for repo, ref in sorted(repos.items()):
63+
print(f"{repo}\t{ref}")
64+
PY
65+
66+
while IFS=$'\t' read -r repo ref; do
67+
[ -n "$repo" ] || continue
68+
if [ -e "$workspace/$repo" ]; then
69+
continue
70+
fi
71+
gh repo clone "QuantStrategyLab/$repo" "$workspace/$repo"
72+
git -C "$workspace/$repo" checkout --detach "$ref"
73+
done < qsl-bundle-repos.tsv
74+
75+
- name: Generate QSL compatibility report
76+
id: report
77+
run: |
78+
set -euo pipefail
79+
python3 python/scripts/qslctl.py report \
80+
--projects-root "${{ steps.workspace.outputs.workspace }}" \
81+
--compat-root . \
82+
--json > qsl-report.json
83+
python3 - <<'PY'
84+
import json
85+
import os
86+
from pathlib import Path
87+
88+
report = json.loads(Path("qsl-report.json").read_text(encoding="utf-8"))
89+
lines = [
90+
"## QSL Exception Lifecycle Report",
91+
f"- Bundle: `${os.environ['QSL_BUNDLE']}`",
92+
f"- Workspace: `${os.environ['QSL_WORKSPACE']}`",
93+
f"- Total repositories: `{report['total_repositories']}`",
94+
f"- Strict repositories: `{report['strict_repositories']}`",
95+
f"- Warning repositories: `{report['warning_repositories']}`",
96+
f"- Clean repositories: `{report['clean_repositories']}`",
97+
"",
98+
"### Repository status",
99+
]
100+
for repo in report["repositories"]:
101+
status = "strict" if repo["issues"] else "warning" if repo["warnings"] else "clean"
102+
lines.append(
103+
f"- `{repo['repo']}`: `{status}` "
104+
f"(issues={len(repo['issues'])}, warnings={len(repo['warnings'])})"
105+
)
106+
for message in repo["issues"] + repo["warnings"]:
107+
lines.append(f" - {message}")
108+
Path(os.environ["GITHUB_STEP_SUMMARY"]).write_text("\n".join(lines).strip() + "\n", encoding="utf-8")
109+
110+
if report["strict_repositories"] or report["warning_repositories"]:
111+
raise SystemExit(1)
112+
PY
113+
env:
114+
QSL_BUNDLE: ${{ steps.workspace.outputs.bundle }}
115+
QSL_WORKSPACE: ${{ steps.workspace.outputs.workspace }}

docs/qsl_version_control.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,28 @@ python3 python/scripts/qslctl.py generate-matrix --projects-root /Users/lisiyi/P
3535
- `qslctl report` is read-only. It groups the current workspace by ring, status, and bundle hotspot.
3636
- `qslctl plan` is read-only. It renders the ring-by-ring convergence order and highlights which repos should be fixed before the next ring starts.
3737
- Use `report` to answer “what is broken right now?” and `plan` to answer “what should we fix first?”
38+
39+
## QSL exception lifecycle check
40+
41+
Use the repository workflow `.github/workflows/qsl_exception_lifecycle.yml` to run a scheduled or manual report.
42+
43+
The workflow first reads the current bundle from `qsl.toml`, prepares a temporary workspace, links the current `QuantRuntimeSettings` checkout, clones every repo listed in that bundle manifest, checks each repo out to its pinned bundle SHA, and then runs `qslctl report` against that prepared workspace.
44+
45+
It checks each prepared QuantStrategyLab repo for:
46+
47+
- `enforce_bundle = false` exception metadata completeness
48+
- `owner`
49+
- `expires_at`
50+
- `next_action`
51+
- expired `expires_at`
52+
- any strict QSL issues or warning-level QSL issues
53+
54+
It does **not** scan the GitHub Actions checkout directory as a proxy for the full org workspace. If you need a broader multi-repo scan, prepare a local workspace or use external orchestration to clone the repositories first.
55+
56+
Run locally with:
57+
58+
```bash
59+
python3 python/scripts/qslctl.py report --projects-root /path/to/prepared-workspace --compat-root . --json
60+
```
61+
62+
The workflow writes the report to the GitHub Actions step summary and fails the job when any strict or warning repository is found. Public repositories can be cloned with the default GitHub token. If a repository becomes private, configure `RUNTIME_SETTINGS_GH_TOKEN` with read access; otherwise the clone step will fail instead of silently skipping it.

0 commit comments

Comments
 (0)