Skip to content

Commit 23bdf08

Browse files
authored
[CI] add /revert slash command and fix cherry-pick PR refresh bug (vllm-project#11220)
- Add pr_revert_command.yml workflow to revert merged PRs via /revert command - Fix cherry-pick workflow failing when re-running /cherry-pick on the same PR (gh pr create 'already exists' error was not handled) - Fix git revert not aborting conflicted state before fallback retry - Ensure create_failed status triggers generic failure comment in both workflows - Register revert command in slash_command_dispatch.yml - Document /revert command in slash-commands.md --- - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@dc68bd8 Signed-off-by: wangxiyuan <wangxiyuan1007@gmail.com>
1 parent 031799f commit 23bdf08

4 files changed

Lines changed: 391 additions & 8 deletions

File tree

.github/workflows/pr_cherry_pick_command.yml

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,33 +198,54 @@ jobs:
198198
# Push the new branch to the PAT_TOKEN user's fork
199199
git push -f fork "$CHERRY_BRANCH"
200200
201-
# Check if a cherry-pick PR already exists for this target branch
201+
# Check if a cherry-pick PR already exists for this target branch.
202+
# gh pr list --head requires "owner:branch" format for fork branches,
203+
# but it can be unreliable. We also add `2>/dev/null || true` to ensure
204+
# the step doesn't fail even if the query returns nothing.
202205
EXISTING_PR_URL=$(gh pr list \
203206
--repo "$REPO" \
204207
--head "${TOKEN_USER}:${CHERRY_BRANCH}" \
205208
--base "$TARGET_BRANCH" \
206209
--state open \
207-
--json url --jq '.[0].url // empty')
210+
--json url --jq '.[0].url // empty' 2>/dev/null || true)
208211
209212
if [ -n "$EXISTING_PR_URL" ]; then
210213
# PR already exists — branch has been force-pushed, just notify
211214
echo "Cherry-pick PR already exists: $EXISTING_PR_URL"
212215
echo "cherry_status=refreshed" >> "$GITHUB_OUTPUT"
213216
echo "existing_pr_url=$EXISTING_PR_URL" >> "$GITHUB_OUTPUT"
214217
else
215-
# Create a new PR from the fork branch
218+
# Create a new PR from the fork branch.
219+
# If the PR already exists (gh pr list missed it), gh pr create will
220+
# fail — we parse the error to extract the existing PR URL instead of
221+
# letting the step crash.
216222
NEW_PR_TITLE="[Cherry-pick][${TARGET_BRANCH}]${PR_TITLE} (from #${PR_NUMBER})"
217223
NEW_PR_BODY="Cherry-pick of PR #${PR_NUMBER} onto \`${TARGET_BRANCH}\`."$'\n'$'\n'"Original PR: #${PR_NUMBER}"$'\n'"Original author: @${PR_AUTHOR}"$'\n'$'\n'"---"$'\n'"${PR_BODY}"
218224
219-
NEW_PR_URL=$(gh pr create \
225+
CREATE_LOG=/tmp/gh_pr_create.log
226+
if NEW_PR_URL=$(gh pr create \
220227
--repo "$REPO" \
221228
--title "$NEW_PR_TITLE" \
222229
--body "$NEW_PR_BODY" \
223230
--base "$TARGET_BRANCH" \
224-
--head "${TOKEN_USER}:${CHERRY_BRANCH}")
225-
226-
echo "new_pr_url=$NEW_PR_URL" >> "$GITHUB_OUTPUT"
227-
echo "cherry_status=success" >> "$GITHUB_OUTPUT"
231+
--head "${TOKEN_USER}:${CHERRY_BRANCH}" 2>"$CREATE_LOG"); then
232+
echo "new_pr_url=$NEW_PR_URL" >> "$GITHUB_OUTPUT"
233+
echo "cherry_status=success" >> "$GITHUB_OUTPUT"
234+
else
235+
# If PR already exists, gh pr create fails with "already exists"
236+
# and includes the existing PR URL in the message.
237+
EXISTING_PR_URL=$(grep -oE 'https://github.com/[^ ]+/pull/[0-9]+' "$CREATE_LOG" | head -1 || true)
238+
if [ -n "$EXISTING_PR_URL" ]; then
239+
echo "Cherry-pick PR already exists: $EXISTING_PR_URL"
240+
echo "cherry_status=refreshed" >> "$GITHUB_OUTPUT"
241+
echo "existing_pr_url=$EXISTING_PR_URL" >> "$GITHUB_OUTPUT"
242+
else
243+
echo "::error::Failed to create PR:"
244+
cat "$CREATE_LOG"
245+
echo "cherry_status=create_failed" >> "$GITHUB_OUTPUT"
246+
exit 1
247+
fi
248+
fi
228249
fi
229250
230251
- name: Comment success result
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
#
2+
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# This file is a part of the vllm-ascend project.
16+
#
17+
18+
name: Handle /revert Command
19+
20+
on:
21+
repository_dispatch:
22+
types: [revert-command]
23+
24+
defaults:
25+
run:
26+
shell: bash -el {0}
27+
28+
permissions:
29+
contents: write
30+
actions: write
31+
pull-requests: write
32+
issues: write
33+
34+
jobs:
35+
revert:
36+
name: Revert merged PR
37+
runs-on: linux-amd64-cpu-4-hk
38+
container:
39+
image: swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/vllm-ascend/act-environments-ubuntu:act-22.04
40+
steps:
41+
- name: Install system dependencies
42+
run: |
43+
sudo mkdir -p -m 755 /etc/apt/keyrings
44+
curl -sL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
45+
sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
46+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
47+
sudo apt-get update -y
48+
sudo apt-get install gh -y
49+
50+
- name: Check user authorization
51+
id: auth
52+
env:
53+
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
54+
run: |
55+
ACTOR='${{ github.event.client_payload.github.payload.comment.user.login }}'
56+
PR_AUTHOR='${{ github.event.client_payload.pull_request.user.login }}'
57+
REPO='${{ github.event.client_payload.github.payload.repository.full_name }}'
58+
59+
if [ "$ACTOR" = "$PR_AUTHOR" ]; then
60+
echo "User $ACTOR is the PR author, authorized."
61+
echo "authorized=true" >> "$GITHUB_OUTPUT"
62+
else
63+
ROLE_NAME=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
64+
"https://api.github.com/repos/$REPO/collaborators/$ACTOR/permission" | jq -r '.role_name // empty')
65+
case "$ROLE_NAME" in
66+
triage|write|maintain|admin)
67+
echo "authorized=true" >> "$GITHUB_OUTPUT"
68+
;;
69+
*)
70+
echo "::error::User $ACTOR is not the PR author and does not have triage+ permission."
71+
echo "authorized=false" >> "$GITHUB_OUTPUT"
72+
;;
73+
esac
74+
fi
75+
76+
- name: Verify PR is merged
77+
id: check_merged
78+
env:
79+
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
80+
run: |
81+
PR_NUMBER='${{ github.event.client_payload.pull_request.number }}'
82+
REPO='${{ github.event.client_payload.github.payload.repository.full_name }}'
83+
84+
PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state,mergeCommit,baseRefName,title,body,number,author --jq '.')
85+
86+
STATE=$(echo "$PR_JSON" | jq -r '.state')
87+
MERGE_COMMIT=$(echo "$PR_JSON" | jq -r '.mergeCommit.oid // empty')
88+
BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName')
89+
PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')
90+
PR_BODY=$(echo "$PR_JSON" | jq -r '.body')
91+
PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.author.login')
92+
93+
if [ "$STATE" != "MERGED" ] || [ -z "$MERGE_COMMIT" ]; then
94+
echo "::error::PR #$PR_NUMBER is not merged (state: $STATE). Only merged PRs can be reverted."
95+
echo "is_merged=false" >> "$GITHUB_OUTPUT"
96+
else
97+
{
98+
echo "is_merged=true"
99+
echo "merge_commit=$MERGE_COMMIT"
100+
echo "base_branch=$BASE_BRANCH"
101+
echo "pr_title=$PR_TITLE"
102+
echo "pr_body<<PRBODYEOF"
103+
echo "$PR_BODY"
104+
echo "PRBODYEOF"
105+
echo "pr_author=$PR_AUTHOR"
106+
} >> "$GITHUB_OUTPUT"
107+
fi
108+
109+
- name: Post unauthorized comment
110+
if: steps.auth.outputs.authorized == 'false'
111+
uses: peter-evans/create-or-update-comment@v5
112+
with:
113+
token: ${{ secrets.PAT_TOKEN }}
114+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
115+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
116+
body: |
117+
[Bot]: revert command failed: you do not have permission. Only the PR author or users with triage+ permission can trigger /revert.
118+
reactions: confused
119+
120+
- name: Post not-merged comment
121+
if: steps.check_merged.outputs.is_merged == 'false'
122+
uses: peter-evans/create-or-update-comment@v5
123+
with:
124+
token: ${{ secrets.PAT_TOKEN }}
125+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
126+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
127+
body: |
128+
[Bot]: revert command failed: PR #${{ github.event.client_payload.pull_request.number }} is not merged. Only merged PRs can be reverted.
129+
reactions: confused
130+
131+
- name: Fail if unauthorized or not merged
132+
if: steps.auth.outputs.authorized == 'false' || steps.check_merged.outputs.is_merged == 'false'
133+
run: exit 1
134+
135+
- name: Checkout repo
136+
uses: actions/checkout@v7
137+
with:
138+
token: ${{ secrets.PAT_TOKEN }}
139+
fetch-depth: 0
140+
141+
- name: Revert and create PR
142+
id: revert
143+
env:
144+
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
145+
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
146+
run: |
147+
PR_NUMBER='${{ github.event.client_payload.pull_request.number }}'
148+
PR_TITLE='${{ steps.check_merged.outputs.pr_title }}'
149+
PR_BODY='${{ steps.check_merged.outputs.pr_body }}'
150+
PR_AUTHOR='${{ steps.check_merged.outputs.pr_author }}'
151+
MERGE_COMMIT='${{ steps.check_merged.outputs.merge_commit }}'
152+
BASE_BRANCH='${{ steps.check_merged.outputs.base_branch }}'
153+
REPO='${{ github.event.client_payload.github.payload.repository.full_name }}'
154+
155+
# Get the PAT_TOKEN owner's username
156+
TOKEN_USER=$(gh api user --jq '.login')
157+
echo "PAT_TOKEN user: $TOKEN_USER"
158+
159+
# Configure git for push
160+
git config user.name "github-actions[bot]"
161+
git config user.email "github-actions[bot]@users.noreply.github.com"
162+
# origin for fetching from the upstream repo
163+
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git"
164+
# fork remote for pushing revert branch
165+
git remote add fork "https://x-access-token:${GITHUB_TOKEN}@github.com/${TOKEN_USER}/vllm-ascend.git"
166+
167+
# Fetch the base branch
168+
git fetch origin "$BASE_BRANCH" 2>/dev/null || true
169+
170+
# Verify base branch exists
171+
if ! git rev-parse --verify "origin/$BASE_BRANCH" >/dev/null 2>&1; then
172+
echo "::error::Base branch '$BASE_BRANCH' does not exist."
173+
echo "revert_status=missing_branch" >> "$GITHUB_OUTPUT"
174+
exit 1
175+
fi
176+
177+
# Create revert branch from base
178+
SANITIZED_BASE=$(echo "$BASE_BRANCH" | tr '/' '-')
179+
REVERT_BRANCH="revert/pr-${PR_NUMBER}-to-${SANITIZED_BASE}"
180+
git checkout -b "$REVERT_BRANCH" "origin/$BASE_BRANCH"
181+
182+
# Verify the merge commit exists in history
183+
if ! git cat-file -e "$MERGE_COMMIT" 2>/dev/null; then
184+
echo "::error::Merge commit $MERGE_COMMIT not found in repository."
185+
echo "revert_status=missing_commit" >> "$GITHUB_OUTPUT"
186+
exit 1
187+
fi
188+
189+
# Perform the revert.
190+
# -m 1 means keep the first parent (the base branch), reverting
191+
# the changes introduced by the PR branch. For rebase-merged PRs,
192+
# the merge commit is not a real merge, so fall back to plain revert.
193+
REVERT_OUTPUT=$(git revert -m 1 "$MERGE_COMMIT" --no-edit 2>&1)
194+
REVERT_EXIT_CODE=$?
195+
196+
if [ $REVERT_EXIT_CODE -ne 0 ]; then
197+
# Abort the failed revert before trying again, otherwise git will
198+
# refuse to start a new revert on top of the conflicted state.
199+
git revert --abort 2>/dev/null || true
200+
if echo "$REVERT_OUTPUT" | grep -q "is not a merge"; then
201+
echo "Merge commit is not a merge (rebase-merge detected), trying plain revert..."
202+
if ! git revert "$MERGE_COMMIT" --no-edit; then
203+
echo "::error::git revert failed with conflicts."
204+
git revert --abort 2>/dev/null || true
205+
echo "revert_status=conflict" >> "$GITHUB_OUTPUT"
206+
exit 1
207+
fi
208+
else
209+
echo "::error::git revert failed. See details below:"
210+
echo "$REVERT_OUTPUT"
211+
git revert --abort 2>/dev/null || true
212+
echo "revert_status=conflict" >> "$GITHUB_OUTPUT"
213+
exit 1
214+
fi
215+
fi
216+
217+
# Push the revert branch to the PAT_TOKEN user's fork
218+
git push -f fork "$REVERT_BRANCH"
219+
220+
# Check if a revert PR already exists for this PR.
221+
# gh pr list --head requires "owner:branch" format for fork branches,
222+
# but it can be unreliable. We also add `2>/dev/null || true` to ensure
223+
# the step doesn't fail even if the query returns nothing.
224+
EXISTING_PR_URL=$(gh pr list \
225+
--repo "$REPO" \
226+
--head "${TOKEN_USER}:${REVERT_BRANCH}" \
227+
--base "$BASE_BRANCH" \
228+
--state open \
229+
--json url --jq '.[0].url // empty' 2>/dev/null || true)
230+
231+
if [ -n "$EXISTING_PR_URL" ]; then
232+
# PR already exists — branch has been force-pushed, just notify
233+
echo "Revert PR already exists: $EXISTING_PR_URL"
234+
echo "revert_status=refreshed" >> "$GITHUB_OUTPUT"
235+
echo "existing_pr_url=$EXISTING_PR_URL" >> "$GITHUB_OUTPUT"
236+
else
237+
# Create a new PR from the fork branch.
238+
# If the PR already exists (gh pr list missed it), gh pr create will
239+
# fail — we parse the error to extract the existing PR URL instead of
240+
# letting the step crash.
241+
NEW_PR_TITLE="[Revert] Revert \"${PR_TITLE}\" (#${PR_NUMBER})"
242+
NEW_PR_BODY="Revert of PR #${PR_NUMBER} (merged onto \`${BASE_BRANCH}\`)."$'\n'$'\n'"Original PR: #${PR_NUMBER}"$'\n'"Original author: @${PR_AUTHOR}"$'\n'"Merge commit: \`${MERGE_COMMIT}\`"$'\n'$'\n'"---"$'\n'"${PR_BODY}"
243+
244+
CREATE_LOG=/tmp/gh_pr_create.log
245+
if NEW_PR_URL=$(gh pr create \
246+
--repo "$REPO" \
247+
--title "$NEW_PR_TITLE" \
248+
--body "$NEW_PR_BODY" \
249+
--base "$BASE_BRANCH" \
250+
--head "${TOKEN_USER}:${REVERT_BRANCH}" 2>"$CREATE_LOG"); then
251+
echo "new_pr_url=$NEW_PR_URL" >> "$GITHUB_OUTPUT"
252+
echo "revert_status=success" >> "$GITHUB_OUTPUT"
253+
else
254+
# If PR already exists, gh pr create fails with "already exists"
255+
# and includes the existing PR URL in the message.
256+
EXISTING_PR_URL=$(grep -oE 'https://github.com/[^ ]+/pull/[0-9]+' "$CREATE_LOG" | head -1 || true)
257+
if [ -n "$EXISTING_PR_URL" ]; then
258+
echo "Revert PR already exists: $EXISTING_PR_URL"
259+
echo "revert_status=refreshed" >> "$GITHUB_OUTPUT"
260+
echo "existing_pr_url=$EXISTING_PR_URL" >> "$GITHUB_OUTPUT"
261+
else
262+
echo "::error::Failed to create PR:"
263+
cat "$CREATE_LOG"
264+
echo "revert_status=create_failed" >> "$GITHUB_OUTPUT"
265+
exit 1
266+
fi
267+
fi
268+
fi
269+
270+
- name: Comment success result
271+
if: steps.revert.outputs.revert_status == 'success'
272+
uses: peter-evans/create-or-update-comment@v5
273+
with:
274+
token: ${{ secrets.PAT_TOKEN }}
275+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
276+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
277+
body: |
278+
[Bot]: revert completed successfully. New PR created: ${{ steps.revert.outputs.new_pr_url }}
279+
reactions: hooray
280+
281+
- name: Comment refreshed result
282+
if: steps.revert.outputs.revert_status == 'refreshed'
283+
uses: peter-evans/create-or-update-comment@v5
284+
with:
285+
token: ${{ secrets.PAT_TOKEN }}
286+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
287+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
288+
body: |
289+
[Bot]: revert branch has been refreshed. Existing PR: ${{ steps.revert.outputs.existing_pr_url }}
290+
reactions: hooray
291+
292+
- name: Comment conflict failure
293+
if: failure() && steps.revert.outputs.revert_status == 'conflict'
294+
uses: peter-evans/create-or-update-comment@v5
295+
with:
296+
token: ${{ secrets.PAT_TOKEN }}
297+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
298+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
299+
body: |
300+
[Bot]: revert failed due to merge conflicts. Please revert manually.
301+
reactions: confused
302+
303+
- name: Comment missing branch failure
304+
if: failure() && steps.revert.outputs.revert_status == 'missing_branch'
305+
uses: peter-evans/create-or-update-comment@v5
306+
with:
307+
token: ${{ secrets.PAT_TOKEN }}
308+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
309+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
310+
body: |
311+
[Bot]: revert failed: base branch `${{ steps.check_merged.outputs.base_branch }}` does not exist.
312+
reactions: confused
313+
314+
- name: Comment missing commit failure
315+
if: failure() && steps.revert.outputs.revert_status == 'missing_commit'
316+
uses: peter-evans/create-or-update-comment@v5
317+
with:
318+
token: ${{ secrets.PAT_TOKEN }}
319+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
320+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
321+
body: |
322+
[Bot]: revert failed: merge commit `${{ steps.check_merged.outputs.merge_commit }}` not found in repository.
323+
reactions: confused
324+
325+
- name: Comment generic failure
326+
if: failure() && steps.revert.outputs.revert_status != 'conflict' && steps.revert.outputs.revert_status != 'missing_branch' && steps.revert.outputs.revert_status != 'missing_commit' && steps.revert.outputs.revert_status != 'success' && steps.revert.outputs.revert_status != 'refreshed'
327+
uses: peter-evans/create-or-update-comment@v5
328+
with:
329+
token: ${{ secrets.PAT_TOKEN }}
330+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
331+
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
332+
body: |
333+
[Bot]: revert command failed. Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
334+
reactions: confused

0 commit comments

Comments
 (0)