From 345aac63b3f4dad94d2c7cbe721bfe5c5cfa8b18 Mon Sep 17 00:00:00 2001 From: jsboige Date: Mon, 28 Sep 2026 00:15:52 +0200 Subject: [PATCH] fix(tooling): closure dossier organ resolves cross-repo PR references in their source repo _merged_referring_prs resolved every cross-referenced PR against the target repo, so a sister-repo reference crashed the organ with "Could not resolve to a PullRequest" -- measured on #17301 (MyIntelligenceAgency/Z3.Linq#31, merged 2026-09-23, cross-references the issue), which blocks the Lot D dispatch #18140: the --template call could not even render. Each cross-referenced PR is now resolved in its own repository (source.issue.repository.full_name, payload measured live; falls back to the target repo), and the row carries its repo. The cited-PR fast path is restricted to same-repo rows: a bare #N in a dossier designates a target- repo PR, never the same-numbered PR of a sister repo. Tests: 30 passed (2 new -- cross-repo resolution + fast-path guard). Co-Authored-By: Claude Sonnet 5 --- scripts/check_closure_dossier.py | 15 ++++- scripts/tests/test_check_closure_dossier.py | 65 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/scripts/check_closure_dossier.py b/scripts/check_closure_dossier.py index 7281d526fb..3bdbfc18a4 100644 --- a/scripts/check_closure_dossier.py +++ b/scripts/check_closure_dossier.py @@ -236,14 +236,20 @@ def _merged_referring_prs(repo: str, number: int) -> list[dict[str, Any]]: pr = src.get("pull_request") or {} if not pr.get("merged_at"): continue + # La reference croisee peut venir d'un DEPOT SOEUR (ex. MyIntelligenceAgency/Z3.Linq#31 + # referencant #17301) : resoudre dans le depot source, pas dans le depot cible, + # sinon gh rend "Could not resolve to a PullRequest" et l'organe crashe + # (mesure 2026-09-28, blocage Lot D #18140 sur #17301). + src_repo = ((src.get("repository") or {}).get("full_name")) or repo # Pas de --jq : gh l'ecrit en TEXTE BRUT, que json.loads refuse # (defaut mesure c.5849452860 -- tout temoin rendait UNKNOWN rc=2). row = gh_json([ - "pr", "view", str(src["number"]), "--repo", repo, + "pr", "view", str(src["number"]), "--repo", src_repo, "--json", "body", ]) body = row.get("body") if isinstance(row, dict) else None out.append({"number": src["number"], "merged_at": pr["merged_at"], + "repo": src_repo, "body": str(body) if body else ""}) return out @@ -397,8 +403,11 @@ def validate_dossier(dossier: Dossier, snapshot: dict[str, Any], f"{child['state']} -- a closed child does not keep the parent open" ) - # PRs citees dans les preuves : toutes MERGED. - merged_numbers = {pr["number"] for pr in snapshot["merged_prs"]} + # PRs citees dans les preuves : toutes MERGED. Le raccourci ne couvre que + # les PRs du MEME depot : un #N nu dans le dossier designe une PR du depot + # cible, jamais la PR de meme numero d'un depot soeur. + merged_numbers = {pr["number"] for pr in snapshot["merged_prs"] + if pr.get("repo", snapshot["repo"]) == snapshot["repo"]} for n in sorted(_cited_pr_numbers(dossier)): if n in merged_numbers: continue diff --git a/scripts/tests/test_check_closure_dossier.py b/scripts/tests/test_check_closure_dossier.py index dc06b2d46a..e18dce864b 100644 --- a/scripts/tests/test_check_closure_dossier.py +++ b/scripts/tests/test_check_closure_dossier.py @@ -402,3 +402,68 @@ def fake_run(args, **kwargs): assert len(prs) == 1 assert prs[0]["body"].startswith("Grain:") assert prs[0]["number"] == 17901 + + +def test_merged_referring_prs_resout_une_reference_cross_repo(monkeypatch): + # #17301, mesure 2026-09-28 : MyIntelligenceAgency/Z3.Linq#31 (merged) + # reference l'issue -> l'organe resout toute cross-ref dans le depot SOURCE, + # sinon gh rend "Could not resolve to a PullRequest" sur le depot cible et + # le template crash (blocage Lot D #18140). + import subprocess as _sp + import json as _json + # Payload reel mesure sur #17301 : le depot vit dans + # source.issue.repository.full_name (source.repository est None). + timeline = [_json.dumps([{ + "event": "cross-referenced", + "source": {"issue": {"number": 31, + "repository": {"full_name": "MyIntelligenceAgency/Z3.Linq"}, + "pull_request": {"merged_at": "2026-09-23T18:39:12Z"}}}, + }])] + seen = {} + + class _P: + def __init__(self, stdout, returncode=0): + self.stdout = stdout + self.returncode = returncode + + def fake_run(args, **kwargs): + tail = tuple(args[1:]) + if tail[0] == "api": + return _P("".join(timeline)) + if tail[:2] == ("pr", "view"): + seen[("--repo",)] = tail + assert "--repo" in tail and "MyIntelligenceAgency/Z3.Linq" in tail, ( + "la PR cross-ref doit etre resolue dans son depot source, " + "pas dans le depot cible") + return _P(_json.dumps({"body": "Grain: DEEP/lean -- lane myia-po-2024:CoursIA"})) + return _P("", returncode=1) + + monkeypatch.setattr(_sp, "run", fake_run) + prs = ccd._merged_referring_prs("jsboige/CoursIA", 17301) + assert len(prs) == 1 + assert prs[0]["number"] == 31 + assert prs[0]["repo"] == "MyIntelligenceAgency/Z3.Linq" + + +def test_pr_cross_repo_ne_satisfait_pas_le_raccourci_des_prs_citees(monkeypatch): + # Un #N nu dans le dossier designe une PR du depot CIBLE : une PR de meme + # numero venue d'un depot soeur ne doit pas court-circuiter la verification + # (le dossier citerait #31 sans preuve que la PR 31 du depot cible existe). + def fake_gh_json(args): + assert args[:2] == ["pr", "view"], f"appel gh inattendu: {args}" + assert args[args.index("--repo") + 1] == "o/r", ( + "le numero nu doit etre verifie dans le depot cible") + return {"state": "OPEN", "mergedAt": None} # PR 31 de o/r : non merged + + monkeypatch.setattr(ccd, "gh_json", fake_gh_json) + snap = _snapshot( + comments=[_comment(_dossier_body(items=("critere A -> PR #31",)))], + merged_prs=[ + {"number": 31, "merged_at": "2026-09-23T18:39:12Z", + "repo": "MyIntelligenceAgency/Z3.Linq", + "body": "Grain: DEEP/lean -- lane myia-po-2024:CoursIA"}, + ], + ) + verdict, errors, _ = evaluate(snap) + assert verdict == "REFUSED" + assert any("cited PR #31 is not MERGED" in e for e in errors)