forked from quay/quay-docs
-
Notifications
You must be signed in to change notification settings - Fork 0
324 lines (281 loc) · 14.1 KB
/
cherry-pick.yml
File metadata and controls
324 lines (281 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
name: Cherry Pick
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
jobs:
cherry-pick:
runs-on: ubuntu-latest
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
(startsWith(github.event.comment.body, '/cherry-pick') || contains(github.event.comment.body, '/cherry-pick'))
steps:
- name: Check job condition
run: |
echo "Event name: ${{ github.event_name }}"
echo "Comment body: ${{ github.event.comment.body }}"
echo "Is PR: ${{ github.event.issue.pull_request != null }}"
echo "Comment starts with /cherry-pick: ${{ startsWith(github.event.comment.body, '/cherry-pick') }}"
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Debug event context
if: github.event_name == 'issue_comment'
run: |
echo "Event type: ${{ github.event_name }}"
echo "Comment body: ${{ github.event.comment.body }}"
echo "Issue number: ${{ github.event.issue.number }}"
echo "Is PR: ${{ github.event.issue.pull_request != null }}"
echo "PR URL: ${{ github.event.issue.pull_request.url }}"
- name: Get PR information
id: pr_info
continue-on-error: true
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issueNumber = context.payload.issue.number;
try {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: issueNumber,
});
core.setOutput('pr_number', issueNumber.toString());
core.setOutput('base_branch', pr.base.ref);
core.setOutput('head_branch', pr.head.ref);
core.setOutput('head_sha', pr.head.sha);
core.setOutput('title', pr.title || '');
core.setOutput('body', pr.body || '');
} catch (error) {
core.setOutput('pr_number', issueNumber.toString());
core.setFailed(`Failed to get PR information: ${error.message}`);
throw error;
}
- name: Extract target branch
id: extract
run: |
COMMENT_BODY="${{ github.event.comment.body }}"
TARGET_BRANCH=$(echo "$COMMENT_BODY" | sed -n 's|.*/cherry-pick[[:space:]]*\([^[:space:]]*\).*|\1|p')
if [ -z "$TARGET_BRANCH" ]; then
echo "❌ Error: No target branch specified. Usage: /cherry-pick <branch-name>"
exit 1
fi
echo "branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT
echo "Target branch: $TARGET_BRANCH"
- name: Validate target branch exists
id: validate_branch
run: |
TARGET_BRANCH="${{ steps.extract.outputs.branch }}"
# Remove any remote prefix if present
BRANCH_NAME=$(echo "$TARGET_BRANCH" | sed 's|^[^/]*/||')
# Check if branch exists in origin
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
echo "✅ Found branch: origin/$BRANCH_NAME"
echo "remote=origin" >> $GITHUB_OUTPUT
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
# Check if downstream remote exists and has the branch
elif git remote | grep -q "^downstream$"; then
if git ls-remote --heads downstream "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
echo "✅ Found branch: downstream/$BRANCH_NAME"
echo "remote=downstream" >> $GITHUB_OUTPUT
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
else
echo "❌ Error: Branch '$BRANCH_NAME' does not exist in downstream remote"
exit 1
fi
else
echo "❌ Error: Branch '$BRANCH_NAME' does not exist in origin"
exit 1
fi
- name: Create cherry-pick branch
id: create_branch
run: |
TARGET_BRANCH="${{ steps.validate_branch.outputs.branch_name }}"
REMOTE="${{ steps.validate_branch.outputs.remote }}"
PR_NUMBER="${{ steps.pr_info.outputs.pr_number }}"
# Create branch name: cherry-pick-<pr-number>-to-<target-branch>
# Replace any slashes with dashes for branch name
SAFE_BRANCH_NAME=$(echo "$TARGET_BRANCH" | tr '/' '-')
CHERRY_PICK_BRANCH="cherry-pick-${PR_NUMBER}-to-${SAFE_BRANCH_NAME}"
echo "branch_name=$CHERRY_PICK_BRANCH" >> $GITHUB_OUTPUT
# Fetch and checkout target branch
git fetch "$REMOTE" "$TARGET_BRANCH"
git checkout -b "$CHERRY_PICK_BRANCH" "${REMOTE}/${TARGET_BRANCH}"
echo "✅ Created branch: $CHERRY_PICK_BRANCH from ${REMOTE}/${TARGET_BRANCH}"
- name: Cherry-pick commits
id: cherry_pick
run: |
PR_NUMBER="${{ steps.pr_info.outputs.pr_number }}"
BASE_BRANCH="${{ steps.pr_info.outputs.base_branch }}"
# Fetch the PR branch
git fetch origin "pull/${PR_NUMBER}/head:pr-${PR_NUMBER}"
# Get all commits from the PR (compare base branch to PR branch)
COMMITS=$(git log --oneline --reverse origin/${BASE_BRANCH}..pr-${PR_NUMBER} | awk '{print $1}')
if [ -z "$COMMITS" ]; then
echo "❌ Error: No commits found to cherry-pick"
exit 1
fi
echo "Found commits to cherry-pick:"
echo "$COMMITS"
# Cherry-pick each commit
CHERRY_PICKED_COMMITS=""
for COMMIT in $COMMITS; do
echo "Cherry-picking commit: $COMMIT"
if git cherry-pick "$COMMIT"; then
CHERRY_PICKED_COMMITS="$CHERRY_PICKED_COMMITS $COMMIT"
echo "✅ Successfully cherry-picked $COMMIT"
else
echo "❌ Error: Failed to cherry-pick commit $COMMIT"
git cherry-pick --abort
exit 1
fi
done
echo "✅ Successfully cherry-picked all commits"
<<<<<<< Updated upstream
=======
echo "CHERRY_PICK_SUCCESS=true" >> $GITHUB_ENV
- name: Post error comment on failure
if: always() && (failure() || env.CHERRY_PICK_ERROR != '' || env.EXTRACT_ERROR != '' || env.VALIDATE_ERROR != '' || steps.pr_info.outcome == 'failure')
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = parseInt('${{ steps.pr_info.outputs.pr_number }}') || context.payload.issue.number;
const prInfoFailed = '${{ steps.pr_info.outcome }}' === 'failure';
const targetBranch = '${{ steps.validate_branch.outputs.branch_name }}' || '${{ steps.extract.outputs.branch }}' || 'unknown';
const extractError = '${{ env.EXTRACT_ERROR }}';
const validateError = '${{ env.VALIDATE_ERROR }}';
const cherryPickError = '${{ env.CHERRY_PICK_ERROR }}';
const failedCommit = '${{ env.FAILED_COMMIT }}' || 'unknown';
const failedCommitMsg = '${{ env.FAILED_COMMIT_MSG }}' || '';
const conflictFiles = '${{ env.CONFLICT_FILES }}';
const modifyDelete = '${{ env.MODIFY_DELETE }}';
const contentConflicts = '${{ env.CONTENT_CONFLICTS }}';
const errorDetails = `${{ env.CHERRY_PICK_ERROR_DETAILS }}` || cherryPickError || validateError || extractError;
let errorMessage = `❌ **Cherry-pick failed**\n\n`;
// Handle PR info failure
if (prInfoFailed) {
errorMessage += `**Error:** Failed to retrieve PR information.\n\n`;
errorMessage += `This might happen if:\n`;
errorMessage += `- The comment was made on an issue instead of a pull request\n`;
errorMessage += `- The PR number could not be determined\n`;
errorMessage += `- There was an API error\n\n`;
errorMessage += `Please ensure you're commenting on a pull request, not an issue.\n`;
}
// Handle different error types
else if (extractError) {
errorMessage += `**Error:** ${extractError}\n\n`;
errorMessage += `Please specify a target branch: \`/cherry-pick <branch-name>\`\n`;
} else if (validateError) {
errorMessage += `**Error:** ${validateError}\n\n`;
errorMessage += `Please verify that the branch exists and try again.\n`;
} else if (cherryPickError) {
errorMessage += `**Target branch:** \`${targetBranch}\`\n`;
errorMessage += `**Failed commit:** \`${failedCommit}\`\n`;
if (failedCommitMsg) {
errorMessage += `**Commit message:** ${failedCommitMsg}\n\n`;
}
// Format conflicts
if (modifyDelete || contentConflicts || conflictFiles) {
errorMessage += `## Merge Conflicts Detected\n\n`;
if (contentConflicts) {
const files = contentConflicts.split(',').filter(f => f);
if (files.length > 0) {
errorMessage += `### Content conflicts (files modified in both branches):\n`;
files.forEach(file => {
errorMessage += `- \`${file}\`\n`;
});
errorMessage += `\n`;
}
}
if (modifyDelete) {
const files = modifyDelete.split(',').filter(f => f);
if (files.length > 0) {
errorMessage += `### Modify/delete conflicts (files deleted in target branch but modified in PR):\n`;
files.forEach(file => {
errorMessage += `- \`${file}\`\n`;
});
errorMessage += `\n`;
}
}
errorMessage += `## Next Steps\n\n`;
errorMessage += `This cherry-pick requires manual resolution. You can:\n\n`;
errorMessage += `1. Create a manual cherry-pick branch from \`${targetBranch}\`\n`;
errorMessage += `2. Resolve the conflicts manually\n`;
errorMessage += `3. Create a PR with the resolved changes\n\n`;
errorMessage += `Or use the workflow run logs for detailed conflict information.\n`;
} else if (errorDetails) {
errorMessage += `\n**Error details:**\n\`\`\`\n${errorDetails.substring(0, 2000)}\n\`\`\`\n`;
}
} else {
errorMessage += `**Error:** An unexpected error occurred during cherry-pick.\n\n`;
if (errorDetails) {
errorMessage += `**Error details:**\n\`\`\`\n${errorDetails.substring(0, 2000)}\n\`\`\`\n`;
}
}
// Add link to workflow run
const workflowRunUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
errorMessage += `\n---\n`;
errorMessage += `📋 [View workflow run](${workflowRunUrl}) for full details`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: errorMessage,
});
// Fail the workflow
if (prInfoFailed || cherryPickError || validateError || extractError) {
core.setFailed('Cherry-pick failed');
}
>>>>>>> Stashed changes
- name: Push cherry-pick branch
run: |
CHERRY_PICK_BRANCH="${{ steps.create_branch.outputs.branch_name }}"
git push origin "$CHERRY_PICK_BRANCH"
- name: Create pull request
uses: actions/github-script@v7
with:
script: |
const targetBranch = '${{ steps.validate_branch.outputs.branch_name }}';
const cherryPickBranch = '${{ steps.create_branch.outputs.branch_name }}';
const prNumber = parseInt('${{ steps.pr_info.outputs.pr_number }}');
// Get original PR details
const { data: originalPR } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const title = `Cherry-pick #${prNumber} to ${targetBranch}: ${originalPR.title}`;
const body = `This PR cherry-picks #${prNumber} to \`${targetBranch}\`.\n\n` +
`**Original PR:** #${prNumber}\n` +
`**Target branch:** \`${targetBranch}\`\n\n` +
`---\n` +
`_This PR was created automatically by the cherry-pick workflow._`;
const { data: pr } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
head: cherryPickBranch,
base: targetBranch,
});
// Add a comment to the original PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `✅ Cherry-pick PR created: #${pr.number}\n\n` +
`**Target branch:** \`${targetBranch}\`\n` +
`**Cherry-pick PR:** #${pr.number}`,
});
console.log(`Created PR #${pr.number}`);