slash command rerun UX: emoji semantics + result writeback (#24802)

This commit is contained in:
Liangsheng Yin
2026-05-09 03:19:24 -07:00
committed by GitHub
parent f4b7e73699
commit ba625d5290
3 changed files with 194 additions and 21 deletions
+47
View File
@@ -44,6 +44,16 @@ on:
required: false
type: string
default: "false"
reply_comment_id:
description: "Reply comment ID to write back result to"
required: false
type: string
default: ""
reply_marker:
description: "Per-batch marker for locating the line in reply comment"
required: false
type: string
default: ""
env:
SGLANG_IS_IN_CI: true
@@ -196,3 +206,40 @@ jobs:
done
total_elapsed=$(( SECONDS - suite_start ))
echo "All $total test(s) passed in ${total_elapsed}s"
write-back-result:
needs: [rerun-test-cuda, rerun-test-cpu]
if: always() && inputs.reply_comment_id != '' && inputs.reply_marker != ''
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
concurrency:
group: rerun-test-writeback-${{ inputs.reply_comment_id }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install requests
- name: Write back result to reply comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ needs.rerun-test-cuda.result }}" == "success" || "${{ needs.rerun-test-cpu.result }}" == "success" ]]; then
STATUS=success
else
STATUS=failure
fi
python3 scripts/ci/utils/write_rerun_test_result.py \
--comment-id "${{ inputs.reply_comment_id }}" \
--marker "${{ inputs.reply_marker }}" \
--status "$STATUS" \
--repo "${{ github.repository }}"
+43 -21
View File
@@ -404,7 +404,7 @@ def handle_rerun_stage(
print("Error: No stage name provided")
comment.create_reaction("confused")
pr.create_issue_comment(
f"❌ Please specify a stage name: `/rerun-stage <stage-name>`\n\n"
f"⛔ Please specify a stage name: `/rerun-stage <stage-name>`\n\n"
f"Examples: `/rerun-stage unit-test-backend-4-gpu`, `/rerun-stage accuracy-test-1-gpu`"
)
return False
@@ -456,7 +456,7 @@ def handle_rerun_stage(
if stage_name not in valid_stages:
comment.create_reaction("confused")
pr.create_issue_comment(
f"❌ Stage `{stage_name}` doesn't support isolated runs yet.\n\n"
f"⛔ Stage `{stage_name}` doesn't support isolated runs yet.\n\n"
f"**NVIDIA stages:**\n"
+ "\n".join(f"- `{s}`" for s in nvidia_stages)
+ "\n\n**AMD stages:**\n"
@@ -573,13 +573,13 @@ def handle_rerun_stage(
)
if run_url:
pr.create_issue_comment(
f"✅ Triggered `{stage_name}` to run independently"
f"🚀 Triggered `{stage_name}` to run independently"
f" (skipping dependencies)."
f" [View workflow run]({run_url})"
)
else:
pr.create_issue_comment(
f"✅ Triggered `{stage_name}` to run independently"
f"🚀 Triggered `{stage_name}` to run independently"
f" (skipping dependencies).\n"
f"⚠️ Could not retrieve workflow run URL. "
f"Check the [Actions tab](https://github.com/{gh_repo.full_name}/actions) for progress."
@@ -593,7 +593,7 @@ def handle_rerun_stage(
print(f"Error triggering workflow_dispatch: {e}")
comment.create_reaction("confused")
pr.create_issue_comment(
f"❌ Failed to trigger workflow: {str(e)}\n\n"
f"⛔ Failed to trigger workflow: {str(e)}\n\n"
f"Please check the logs or contact maintainers."
)
return False
@@ -908,7 +908,7 @@ def _resolve_test_spec(test_spec):
}
def _dispatch_batch(gh_repo, pr, batch, token):
def _dispatch_batch(gh_repo, pr, batch, token, reply_comment_id="", reply_marker=""):
"""
Dispatch a single workflow run for a batch of resolved test specs
that share the same (runner_label, use_deepep, is_cpu).
@@ -951,6 +951,8 @@ def _dispatch_batch(gh_repo, pr, batch, token):
"use_deepep": str(use_deepep).lower(),
"is_cpu": str(is_cpu).lower(),
"install_diffusion": str(install_diffusion).lower(),
"reply_comment_id": str(reply_comment_id) if reply_comment_id else "",
"reply_marker": reply_marker,
}
if is_fork:
ref = "main"
@@ -998,6 +1000,7 @@ def _dispatch_batch(gh_repo, pr, batch, token):
"test_commands": test_commands,
"runner_label": runner_label,
"run_url": run_url,
"reply_marker": reply_marker,
}
except Exception as e:
@@ -1023,7 +1026,7 @@ def _check_rerun_test_permissions(gh_repo, pr, comment, user_perms, command_name
print(f"Permission denied: /{command_name} on fork PR by {commenter}.")
comment.create_reaction("confused")
pr.create_issue_comment(
f"❌ `/{command_name}` is not available for fork PRs unless the commenter "
f"⛔ `/{command_name}` is not available for fork PRs unless the commenter "
"has write permission on the repo.\n\n"
"Please ask a maintainer to run this command, or use the normal CI flow."
)
@@ -1055,7 +1058,7 @@ def handle_rerun_test(
if not test_specs:
comment.create_reaction("confused")
pr.create_issue_comment(
"❌ Please specify a test: `/rerun-test <file>::<TestClass.test_method>`\n\n"
"⛔ Please specify a test: `/rerun-test <file>::<TestClass.test_method>`\n\n"
"Examples:\n"
"- `/rerun-test test/registered/core/test_srt_endpoint.py::TestSRTEndpoint.test_simple_decode`\n"
"- `/rerun-test registered/core/test_srt_endpoint.py::TestSRTEndpoint`\n"
@@ -1091,12 +1094,30 @@ def handle_rerun_test(
)
groups.setdefault(key, []).append(r)
# Phase 3: Dispatch one workflow per group
dispatch_results = []
for batch in groups.values():
dispatch_results.append(_dispatch_batch(gh_repo, pr, batch, token))
# Phase 3a: Create placeholder reply comment so we have its ID before
# dispatching workflows. This lets each dispatched run write its
# success/failure result back to the right line in this comment.
reply_comment = pr.create_issue_comment("🚀 Dispatching rerun-test workflow(s)...")
# Build consolidated comment
# Phase 3b: Dispatch one workflow per group, with a unique per-batch
# marker each. The marker is an HTML comment that the writeback step
# uses to locate the line and replace 🚀 with ✅/❌.
dispatch_results = []
for idx, batch in enumerate(groups.values()):
marker = f"<!--rrt:{idx}-->"
dispatch_results.append(
_dispatch_batch(
gh_repo,
pr,
batch,
token,
reply_comment_id=reply_comment.id,
reply_marker=marker,
)
)
# Build consolidated comment body (markers placed at line ends so the
# writeback step can locate and update each line).
lines = []
for dr in dispatch_results:
if dr["success"]:
@@ -1113,25 +1134,26 @@ def handle_rerun_test(
cmds = "\n".join(
f"cd test/ && python3 {cmd}" for cmd in dr["test_commands"]
)
marker = dr.get("reply_marker", "")
if dr.get("run_url"):
lines.append(
f"✅ `{dr['runner_label']}` ({len(dr['test_commands'])} test{'s' if len(dr['test_commands']) > 1 else ''}): "
f"[View workflow run]({dr['run_url']})\n"
f"🚀 `{dr['runner_label']}` ({len(dr['test_commands'])} test{'s' if len(dr['test_commands']) > 1 else ''}): "
f"⏳ [View workflow run]({dr['run_url']}) {marker}\n"
f"```\n{cmds}\n```"
)
else:
lines.append(
f"✅ `{dr['runner_label']}` ({len(dr['test_commands'])} test{'s' if len(dr['test_commands']) > 1 else ''}):\n"
f"🚀 `{dr['runner_label']}` ({len(dr['test_commands'])} test{'s' if len(dr['test_commands']) > 1 else ''}): ⏳ {marker}\n"
f"```\n{cmds}\n```\n"
f"⚠️ Could not retrieve workflow run URL. "
f"Check the [Actions tab](https://github.com/{gh_repo.full_name}/actions) for progress."
)
else:
specs_str = ", ".join(f"`{s}`" for s in dr["specs"])
lines.append(f"❌ {specs_str}: {dr['error']}")
lines.append(f"⛔ {specs_str}: {dr['error']}")
for r in resolve_failures:
lines.append(f"❌ `{r['spec']}`: {r['error']}")
lines.append(f"⛔ `{r['spec']}`: {r['error']}")
body = "\n\n".join(lines)
@@ -1141,7 +1163,7 @@ def handle_rerun_test(
if not successes and (resolve_failures or dispatch_results):
comment.create_reaction("confused")
pr.create_issue_comment(body)
reply_comment.edit(body)
return len(successes) > 0
@@ -1158,7 +1180,7 @@ def handle_rerun_group(gh_repo, pr, comment, user_perms, group_names, token):
if not group_names:
comment.create_reaction("confused")
pr.create_issue_comment(
"❌ Please specify a test group: `/rerun-group <group>`\n\n"
"⛔ Please specify a test group: `/rerun-group <group>`\n\n"
"Example:\n"
"- `/rerun-group hicache`"
)
@@ -1180,7 +1202,7 @@ def handle_rerun_group(gh_repo, pr, comment, user_perms, group_names, token):
if failures:
comment.create_reaction("confused")
lines = [f"❌ `{group}`: {err}" for group, err in failures]
lines = [f"⛔ `{group}`: {err}" for group, err in failures]
pr.create_issue_comment("\n\n".join(lines))
return False
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Write back the result of a Rerun Test workflow run into its parent reply
comment, by replacing the hourglass placeholder on the marker's line with
a success/failure icon. The leading rocket on the line is preserved as a
visual anchor that this line came from a slash-command trigger.
Concurrent dispatches against the same reply comment are serialized at the
GitHub Actions layer via job-level `concurrency`, so a simple
read-modify-write here is sufficient. Marker is rewritten to a `:done`
variant to make the operation idempotent against accidental reruns.
"""
import argparse
import os
import sys
import time
import requests
# When the marker isn't found, retry a few times before giving up. The
# usual cause is a brief race where the dispatched workflow finishes its
# writeback step before the handler edits the placeholder comment with the
# final body containing markers. If retries don't help (e.g. the handler
# failed to edit the placeholder at all), we still return 0 — failing here
# would amplify a single placeholder-edit failure into N noisy finalizer
# job failures, one per dispatched batch.
RETRY_DELAYS_SEC = [0, 5, 15]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--comment-id", required=True, type=int)
ap.add_argument(
"--marker", required=True, help="Per-batch marker, e.g. <!--rrt:0-->"
)
ap.add_argument("--status", required=True, choices=["success", "failure"])
ap.add_argument("--repo", required=True, help="owner/repo")
args = ap.parse_args()
token = os.environ.get("GITHUB_TOKEN")
if not token:
print("ERROR: GITHUB_TOKEN not set")
return 1
icon = "✅" if args.status == "success" else "❌"
done_marker = args.marker.replace("-->", ":done-->")
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
url = f"https://api.github.com/repos/{args.repo}/issues/comments/{args.comment_id}"
body = None
for attempt, delay in enumerate(RETRY_DELAYS_SEC):
if delay:
time.sleep(delay)
resp = requests.get(url, headers=headers, timeout=15)
if resp.status_code != 200:
print(f"GET failed: {resp.status_code} {resp.text}")
return 1
body = resp.json().get("body") or ""
if done_marker in body:
print(f"Marker {done_marker} already present; nothing to do.")
return 0
if args.marker in body:
break
print(
f"Marker {args.marker} not found "
f"(attempt {attempt + 1}/{len(RETRY_DELAYS_SEC)}); will retry."
)
else:
print(
f"WARNING: marker {args.marker} not found after "
f"{len(RETRY_DELAYS_SEC)} attempts; skipping writeback. "
f"The handler may have failed to edit the placeholder comment."
)
return 0
new_lines = []
for line in body.splitlines(keepends=True):
if args.marker in line:
line = line.replace("⏳", icon, 1)
line = line.replace(args.marker, done_marker)
new_lines.append(line)
new_body = "".join(new_lines)
patch = requests.patch(
url,
headers=headers,
json={"body": new_body},
timeout=15,
)
if patch.status_code != 200:
print(f"PATCH failed: {patch.status_code} {patch.text}")
return 1
print(f"Updated comment {args.comment_id}: {args.marker} -> {icon}")
return 0
if __name__ == "__main__":
sys.exit(main())