b6040e23fd8490ae4ada8bceb147aa28be30313f jnavarr5 Wed Aug 12 15:33:02 2026 -0700 Quote the saved review file's absolute path in daily code review emails so authors can point a tool at it, refs #36890 Co-Authored-By: Claude Opus 5 (1M context) diff --git src/utils/codeReviewAi.py src/utils/codeReviewAi.py index 85116b44d60..c6fd9879cdf 100755 --- src/utils/codeReviewAi.py +++ src/utils/codeReviewAi.py @@ -1964,32 +1964,40 @@ if os.path.exists(GMAIL_TOKEN_PATH): creds = Credentials.from_authorized_user_file(GMAIL_TOKEN_PATH, GMAIL_SCOPES) if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) with open(GMAIL_TOKEN_PATH, 'w') as f: f.write(creds.to_json()) else: print("ERROR: Gmail token not found or invalid. Run the MLQ automation script first to authenticate.") sys.exit(1) return google_build('gmail', 'v1', credentials=creds, cache_discovery=False) -def send_review_email(gmail_service, to_email, author_name, review_text, cc=None): - """Send a code review email to the author""" +def send_review_email(gmail_service, to_email, author_name, review_text, cc=None, + review_file=None): + """Send a code review email to the author. + review_file is the path to the saved text copy of this same review; it is quoted + at the bottom so the author can point a tool at the file instead of copying the + body out of the email.""" + if review_file: + review_text = (review_text.rstrip('\n') + "\n\n" + "-" * 70 + "\n" + + "Text version of this review, readable on hgwdev:\n" + + f" {review_file}\n") message = MIMEText(review_text) message['To'] = to_email message['From'] = 'gbauto@ucsc.edu' message['Subject'] = f'Daily Code Review - {author_name} - {datetime.now().strftime("%Y-%m-%d")}' if cc: message['Cc'] = cc encoded = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8') gmail_service.users().messages().send( userId='me', body={'raw': encoded} ).execute() def send_alert_email(gmail_service, to_email, failures, window_label, log_dir, @@ -2340,31 +2348,34 @@ review, temp_files, error = review_daily_author( data['name'], data['commits'], log_dir, window_label=window_label, file_suffix=file_suffix) all_temp_files.extend(temp_files) reviews[author_email] = { 'name': data['name'], 'email': author_email, 'review': review, 'num_commits': len(data['commits']), 'error': error, } # Save review to log_dir safe_name = re.sub(r'[^a-zA-Z0-9]', '_', data['name']) - filepath = os.path.join(log_dir, f"daily_review_{safe_name}_{file_suffix}.txt") + # Absolute, because this path is quoted in the author's email and has to be + # usable from wherever they happen to be sitting. + filepath = os.path.abspath( + os.path.join(log_dir, f"daily_review_{safe_name}_{file_suffix}.txt")) with open(filepath, 'w') as f: f.write(review) reviews[author_email]['file'] = filepath print(f" Saved: {filepath}") # Collect any failures (broken/auth-failed reviews). A failed review is # never emailed to an author; instead we alert the maintainer below. failures = [(d['name'], d['error']) for d in reviews.values() if d['error'] and not d.get('skipped')] # Authors the run never got to. Their own category: nothing is broken and no token has # expired, the night simply ran out, so they need different words in the alert. skipped = [(d['name'], d['error']) for d in reviews.values() if d.get('skipped')] # A split that only got through some of an author's commits is not an outright # failure - the author still gets the reviews that worked, and the digest is @@ -2396,31 +2407,33 @@ print(f" {data['name']} <{author_email}>: {verdict_of(data)} - {data['file']}") else: gmail_service = get_gmail_service() for author_email, data in reviews.items(): if data['error']: print(f" {data['name']}: FAILED - skipping author email (will alert maintainer)") continue if not digest_wants_email(data['review']): print(f" {data['name']}: APPROVED - skipping email") continue if digest_overall_status(data['review']) is None: print(f" {data['name']}: WARNING - could not read OVERALL STATUS, " f"emailing anyway rather than dropping a possible FEEDBACK") print(f" Emailing {data['name']} <{author_email}> (FEEDBACK)...") try: - send_review_email(gmail_service, author_email, data['name'], data['review'], cc=cc_address) + send_review_email(gmail_service, author_email, data['name'], + data['review'], cc=cc_address, + review_file=data.get('file')) print(f" SENT") except Exception as e: # This email is the only channel that tells an author a commit of # theirs went unreviewed, so a send failure has to reach the # maintainer rather than being swallowed into the log. print(f" FAILED: {e}") failures.append((data['name'], f"Review completed but the email could " f"not be sent: {e}")) # Phase 3b: Alert the maintainer if anything failed, so it does not fail silently if failures or incomplete or skipped: print(f"\nPhase 3b: {len(failures)} review(s) FAILED, " f"{len(incomplete)} incomplete, {len(skipped)} not started " f"- alerting maintainer...") for name, err in failures + incomplete + skipped: