Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/open-downstream-qpk-pin-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ jobs:
qpk_sha=$(tr -d '[:space:]' < QPK_PIN)
python3 scripts/merge_verified_strategy_qpk_pin_prs.py --qpk-sha "$qpk_sha" --close-superseded

- name: Report consumer QPK pin PR hygiene
if: steps.sync.outputs.missing_token != 'true'
env:
QSL_REPO_SYNC_TOKEN: ${{ secrets.QSL_REPO_SYNC_TOKEN }}
run: |
set -euo pipefail
qpk_sha=$(tr -d '[:space:]' < QPK_PIN)
python3 scripts/report_consumer_qpk_pin_prs.py --qpk-sha "$qpk_sha" >> "$GITHUB_STEP_SUMMARY"

- name: Summarize missing token
if: steps.sync.outputs.missing_token == 'true'
run: |
Expand Down
108 changes: 108 additions & 0 deletions scripts/report_consumer_qpk_pin_prs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Report stale generated QPK pin PRs in runtime-consumer repositories.

Consumer repositories may own deployment or broker workflows, so this command
is deliberately read-only. It exposes recognizable older generated PRs in
the QPK workflow summary without closing, merging, labelling, or changing any
consumer repository state.
"""

from __future__ import annotations

import argparse
import json
import os
import subprocess
from collections.abc import Iterable
from typing import Any

try: # Package import for tests and module execution.
from scripts.merge_verified_strategy_qpk_pin_prs import (
expected_branch,
superseded_pr_reason,
)
from scripts.open_downstream_qpk_pin_prs import CONSUMER_REPOS, RepoSpec
except ModuleNotFoundError: # Direct `python scripts/<file>.py` execution in Actions.
from merge_verified_strategy_qpk_pin_prs import expected_branch, superseded_pr_reason
from open_downstream_qpk_pin_prs import CONSUMER_REPOS, RepoSpec


def run(command: list[str], *, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, text=True, capture_output=True, check=True, env=env)


def generated_prs(repo: RepoSpec, *, env: dict[str, str]) -> list[dict[str, Any]]:
result = run(
[
"gh",
"pr",
"list",
"--repo",
f"QuantStrategyLab/{repo.name}",
"--state",
"open",
"--limit",
"100",
"--json",
"author,baseRefName,headRefName,isCrossRepository,isDraft,number,title,updatedAt,url",
],
env=env,
)
return json.loads(result.stdout)


def classify_generated_prs(
prs: Iterable[dict[str, Any]],
*,
current_branch: str,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Return current and recognizable stale generated PRs, ignoring all others."""

current: list[dict[str, Any]] = []
stale: list[dict[str, Any]] = []
for pr in prs:
reason = superseded_pr_reason(pr=pr, current_branch=current_branch)
if reason == "current_branch":
current.append(pr)
continue
if reason is None:
stale.append(pr)
return current, stale


def render_row(repo: RepoSpec, current: list[dict[str, Any]], stale: list[dict[str, Any]]) -> str:
current_refs = ", ".join(f"[#{item['number']}]({item['url']})" for item in current) or "—"
stale_refs = ", ".join(f"[#{item['number']}]({item['url']})" for item in stale) or "—"
return f"| {repo.name} | {current_refs} | {stale_refs} |"


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--qpk-sha", required=True)
parser.add_argument("--token-env", default="QSL_REPO_SYNC_TOKEN")
args = parser.parse_args()
qpk_sha = args.qpk_sha.strip()
if len(qpk_sha) != 40 or any(char not in "0123456789abcdef" for char in qpk_sha):
raise SystemExit("qpk_sha must be a full lowercase SHA")
token = os.environ.get(args.token_env, "").strip()
if not token:
raise SystemExit(f"missing required token env: {args.token_env}")

env = {**os.environ, "GH_TOKEN": token}
print("## Consumer QPK pin PR hygiene")
print()
print("Consumer repositories are report-only: they are never auto-merged or auto-closed.")
print()
print("| Repository | Current generated PR | Recognizable stale generated PRs |")
print("| --- | --- | --- |")
for repo in CONSUMER_REPOS:
current, stale = classify_generated_prs(
generated_prs(repo, env=env),
current_branch=expected_branch(repo, qpk_sha),
)
print(render_row(repo, current, stale))
return 0


if __name__ == "__main__":
raise SystemExit(main())
47 changes: 47 additions & 0 deletions tests/test_report_consumer_qpk_pin_prs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

from scripts.report_consumer_qpk_pin_prs import classify_generated_prs, render_row
from scripts.open_downstream_qpk_pin_prs import RepoSpec


TARGET = "8378e939d9324ea63a0f45c9f21ba0e2eeb1cfff"


def _pr(*, branch: str, number: int = 1) -> dict[str, object]:
return {
"author": {"login": "Pigbibi"},
"baseRefName": "main",
"headRefName": branch,
"isCrossRepository": False,
"isDraft": False,
"number": number,
"title": "chore(deps): align QPK pin to 8378e939d932",
"url": f"https://example.test/pr/{number}",
}


def test_classify_generated_consumer_prs_keeps_current_and_reports_only_recognized_stale() -> None:
current_branch = "auto/qpk-pin-sync-8378e939d932-longbridgeplatform"
current, stale = classify_generated_prs(
[
_pr(branch=current_branch, number=10),
_pr(branch="auto/qpk-pin-sync-37c81901160c-longbridgeplatform", number=9),
_pr(branch="codex/manual-update", number=8),
],
current_branch=current_branch,
)

assert [item["number"] for item in current] == [10]
assert [item["number"] for item in stale] == [9]


def test_render_row_includes_links_without_mutation_instruction() -> None:
row = render_row(
RepoSpec("LongBridgePlatform"),
[_pr(branch="auto/qpk-pin-sync-8378e939d932-longbridgeplatform", number=10)],
[_pr(branch="auto/qpk-pin-sync-37c81901160c-longbridgeplatform", number=9)],
)

assert "LongBridgePlatform" in row
assert "[#10](https://example.test/pr/10)" in row
assert "[#9](https://example.test/pr/9)" in row