Skip to content

Commit b3db26d

Browse files
authored
fix(migration-ci): add context to benchmark comments (#123)
1 parent d70027c commit b3db26d

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

.github/workflows/migration-ci.yml

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/unit/test_migration_ci_workflow.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,17 @@ def test_migration_ci_collects_incomplete_evidence_for_non_crane_prs() -> None:
3434
assert "Python behavior contract tests are incomplete in collection mode." in text
3535
assert "Go parity tests are incomplete in collection mode." in text
3636
assert "Upstream APM freshness/contract coverage is incomplete in collection mode." in text
37+
38+
39+
def test_benchmark_pr_comment_includes_iteration_context() -> None:
40+
text = _workflow_text()
41+
42+
assert "Download parity evidence" in text
43+
assert "migration-benchmark-context.md" in text
44+
assert "### What changed" in text
45+
assert "latest non-trigger commit" in text
46+
assert "### Parity snapshot" in text
47+
assert "Blocking gates" in text
48+
assert "### Next work" in text
49+
assert "upstream_freshness" in text
50+
assert "upstream_contracts" in text

0 commit comments

Comments
 (0)