@@ -324,19 +324,207 @@ jobs:
324324 cat "$RUNNER_TEMP/migration-cli-benchmark.md" >> "$GITHUB_STEP_SUMMARY"
325325 fi
326326
327+ - name : Download parity evidence
328+ if : always()
329+ continue-on-error : true
330+ uses : actions/download-artifact@v4
331+ with :
332+ name : migration-parity-evidence
333+ path : ${{ runner.temp }}/migration-parity-evidence
334+
327335 - name : Post benchmark PR comment
328336 if : always() && github.event_name == 'pull_request'
329337 env :
330338 GH_TOKEN : ${{ github.token }}
331339 PR_NUMBER : ${{ github.event.pull_request.number }}
332340 HEAD_SHA : ${{ github.event.pull_request.head.sha }}
341+ PARITY_RESULT : ${{ needs.parity.result }}
333342 RUN_URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
334343 run : |
335344 if [ ! -f "$RUNNER_TEMP/migration-cli-benchmark.md" ]; then
336345 echo "No migration benchmark markdown found; skipping PR comment."
337346 exit 0
338347 fi
339348
349+ SCORE_PATH="$RUNNER_TEMP/migration-parity-evidence/migration-score.json"
350+ export SCORE_PATH
351+
352+ python - <<'PY' > "$RUNNER_TEMP/migration-benchmark-context.md"
353+ from __future__ import annotations
354+
355+ import json
356+ import os
357+ import subprocess
358+ from pathlib import Path
359+
360+
361+ def gh_json(path: str) -> object | None:
362+ try:
363+ completed = subprocess.run(
364+ ["gh", "api", path],
365+ check=True,
366+ capture_output=True,
367+ encoding="utf-8",
368+ )
369+ except subprocess.CalledProcessError:
370+ return None
371+ return json.loads(completed.stdout)
372+
373+
374+ def subject(message: str) -> str:
375+ return message.splitlines()[0] if message else "(no commit subject)"
376+
377+
378+ def body_lines(message: str, limit: int = 5) -> list[str]:
379+ items: list[str] = []
380+ current: list[str] = []
381+ for raw_line in message.splitlines()[1:]:
382+ line = raw_line.strip()
383+ if not line or line.startswith("Co-authored-by:"):
384+ continue
385+ if line.startswith(("Fixes ", "Run:")):
386+ continue
387+ is_bullet = line.startswith(("-", "*"))
388+ cleaned = line.lstrip("-* ").strip()
389+ if is_bullet:
390+ if current:
391+ items.append(" ".join(current))
392+ current = [cleaned]
393+ elif current:
394+ current.append(cleaned)
395+ else:
396+ current = [cleaned]
397+ if current:
398+ items.append(" ".join(current))
399+ return items[:limit]
400+
401+
402+ def short(sha: str | None) -> str:
403+ return (sha or "")[:7]
404+
405+
406+ def is_trigger_only(message: str) -> bool:
407+ return subject(message).strip().lower() in {"ci: trigger checks"}
408+
409+
410+ def bool_word(value: object) -> str:
411+ return "yes" if value is True else "no" if value is False else "unknown"
412+
413+
414+ def format_float(value: object) -> str:
415+ if isinstance(value, int | float):
416+ return f"{value:.3f}".rstrip("0").rstrip(".")
417+ return "unknown"
418+
419+
420+ repo = os.environ["GITHUB_REPOSITORY"]
421+ pr_number = os.environ["PR_NUMBER"]
422+ head_sha = os.environ["HEAD_SHA"]
423+ parity_result = os.environ.get("PARITY_RESULT", "unknown")
424+ score_path = Path(os.environ["SCORE_PATH"])
425+
426+ commits = gh_json(f"repos/{repo}/pulls/{pr_number}/commits?per_page=100")
427+ commits = commits if isinstance(commits, list) else []
428+ head_commit = next((item for item in commits if item.get("sha") == head_sha), None)
429+ if head_commit is None and commits:
430+ head_commit = commits[-1]
431+ head_message = ((head_commit or {}).get("commit") or {}).get("message", "")
432+
433+ change_commit = head_commit
434+ if is_trigger_only(head_message):
435+ for item in reversed(commits[:-1]):
436+ message = (item.get("commit") or {}).get("message", "")
437+ if not is_trigger_only(message):
438+ change_commit = item
439+ break
440+
441+ change_sha = (change_commit or {}).get("sha") or head_sha
442+ change_message = ((change_commit or {}).get("commit") or {}).get("message", "")
443+ commit_detail = gh_json(f"repos/{repo}/commits/{change_sha}") or {}
444+ files = [item.get("filename", "") for item in commit_detail.get("files", [])]
445+ files = [filename for filename in files if filename]
446+
447+ print("### What changed")
448+ print()
449+ print(f"- **PR head**: `{short(head_sha)}` -- {subject(head_message)}")
450+ if change_sha != head_sha:
451+ print(
452+ f"- **Change commit**: `{short(change_sha)}` -- "
453+ f"{subject(change_message)} (latest non-trigger commit)"
454+ )
455+ notes = body_lines(change_message)
456+ if notes:
457+ print("- **Commit notes**:")
458+ for line in notes:
459+ print(f" - {line}")
460+ if files:
461+ shown = files[:10]
462+ extra = len(files) - len(shown)
463+ suffix = f", +{extra} more" if extra > 0 else ""
464+ print(f"- **Files touched**: {', '.join(f'`{name}`' for name in shown)}{suffix}")
465+ print()
466+
467+ score: dict[str, object] = {}
468+ if score_path.is_file():
469+ score = json.loads(score_path.read_text(encoding="utf-8"))
470+
471+ print("### Parity snapshot")
472+ print()
473+ if score:
474+ gates = score.get("gates") or []
475+ failing = [
476+ str(gate.get("name"))
477+ for gate in gates
478+ if isinstance(gate, dict) and gate.get("passing") is False
479+ ]
480+ parity_passing = score.get("parity_passing", "?")
481+ parity_total = score.get("parity_total", "?")
482+ print(f"- **Score**: {format_float(score.get('migration_score'))}")
483+ print(f"- **Progress**: {format_float(score.get('progress'))}")
484+ print(f"- **Parity**: {parity_passing}/{parity_total}")
485+ print(
486+ "- **Tests**: "
487+ f"Go {score.get('target_tests_passing', '?')}, "
488+ f"Python {score.get('source_tests_passing', '?')}"
489+ )
490+ print(f"- **Deletion-grade ready**: {bool_word(score.get('deletion_grade_ready'))}")
491+ print(f"- **Blocking gates**: {', '.join(failing) if failing else 'none'}")
492+ else:
493+ failing = []
494+ print(f"- **Parity job result**: {parity_result}")
495+ print("- **Score artifact**: unavailable")
496+ print()
497+
498+ print("### Next work")
499+ print()
500+ failing_set = set(failing)
501+ if score and not failing_set and score.get("deletion_grade_ready") is True:
502+ print("- No benchmark or parity follow-up is needed; proceed to the completion gate.")
503+ elif failing_set & {"upstream_freshness", "upstream_contracts"}:
504+ print(
505+ "- Refresh the upstream APM baseline/reviewed SHA and repair upstream "
506+ "contract coverage until `upstream_freshness` and `upstream_contracts` pass."
507+ )
508+ elif failing_set & {
509+ "surface_parity",
510+ "help_parity",
511+ "option_parity",
512+ "functional_contracts",
513+ "state_diff_contracts",
514+ "python_behavior_contracts",
515+ }:
516+ print(
517+ "- Fix the listed Python/Go contract drift, add or update parity coverage, "
518+ "and rerun migration CI."
519+ )
520+ elif failing_set & {"benchmarks_pass"}:
521+ print("- Investigate the benchmark regression and restore Go/Python return-code parity.")
522+ elif score:
523+ print("- Inspect the failing gate artifacts and turn the first failing gate into the next Crane task.")
524+ else:
525+ print("- Open the parity evidence artifact; the score summary was not available to this job.")
526+ PY
527+
340528 marker="<!-- apm-migration-benchmark:${HEAD_SHA} -->"
341529 {
342530 echo "$marker"
@@ -345,6 +533,8 @@ jobs:
345533 echo "- **Commit**: \`${HEAD_SHA}\`"
346534 echo "- **Run**: ${RUN_URL}"
347535 echo
536+ cat "$RUNNER_TEMP/migration-benchmark-context.md"
537+ echo
348538 cat "$RUNNER_TEMP/migration-cli-benchmark.md"
349539 } > "$RUNNER_TEMP/migration-benchmark-pr-comment.md"
350540
0 commit comments