|
| 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 }} |
0 commit comments