Skip to content

Auto Cherry-pick to Main #784

Auto Cherry-pick to Main

Auto Cherry-pick to Main #784

name: Auto Cherry-pick to Main
on:
push:
branches:
- canary
schedule:
# Run every hour to catch any missed commits
- cron: '0 * * * *'
workflow_dispatch:
jobs:
auto-cherry-pick:
runs-on: ubuntu-latest
if: github.repository == 'better-auth/better-auth'
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Find commits to cherry-pick
id: find-commits
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// Get all merged PRs targeting canary branch
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
base: 'canary',
per_page: 100,
sort: 'updated',
direction: 'desc'
});
// Filter for merged PRs without breaking change labels
const prsToProcess = prs.data.filter(pr => {
if (!pr.merged_at) return false;
// Skip if PR has breaking change label
const hasBreakingChange = pr.labels.some(label =>
label.name === 'breaking-change' ||
label.name === 'breaking' ||
label.name === 'major'
);
if (hasBreakingChange) {
console.log(`Skipping PR #${pr.number}: has breaking change label`);
return false;
}
// Skip if PR has merge-to-main or merge-to-main-failure label (handled by other workflow)
const hasManualLabel = pr.labels.some(label =>
label.name === 'merge-to-main' ||
label.name === 'merge-to-main-failure'
);
if (hasManualLabel) {
console.log(`Skipping PR #${pr.number}: handled by manual cherry-pick workflow`);
return false;
}
// Skip if PR has skip-cherry-pick label
const hasSkipLabel = pr.labels.some(label =>
label.name === 'skip-cherry-pick' ||
label.name === 'canary-only'
);
if (hasSkipLabel) {
console.log(`Skipping PR #${pr.number}: has skip label`);
return false;
}
// Skip if PR has already been auto-cherry-picked (prevents duplicates)
const hasAutoPickedLabel = pr.labels.some(label =>
label.name === 'auto-cherry-picked'
);
if (hasAutoPickedLabel) {
console.log(`Skipping PR #${pr.number}: already auto-cherry-picked`);
return false;
}
return true;
});
console.log(`Found ${prsToProcess.length} PRs eligible for auto cherry-pick`);
if (prsToProcess.length === 0) {
console.log('No PRs to process');
core.setOutput('has_prs', 'false');
return;
}
core.setOutput('has_prs', 'true');
core.setOutput('prs_to_process', JSON.stringify(prsToProcess.map(pr => ({
number: pr.number,
merge_commit_sha: pr.merge_commit_sha,
title: pr.title
}))));
- name: Cherry-pick to main
id: cherry-pick
if: steps.find-commits.outputs.has_prs == 'true'
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
PRS_JSON: ${{ steps.find-commits.outputs.prs_to_process }}
run: |
# Fetch all branches
git fetch origin main
git fetch origin canary
# Checkout main branch
git checkout -B main origin/main
set +e # Don't exit on error
echo "Processing PRs for auto cherry-pick..."
echo "$PRS_JSON" > prs.json
success_prs=""
conflict_prs=""
skipped_prs=""
success_count=0
conflict_count=0
skipped_count=0
for row in $(jq -r '.[] | @base64' prs.json); do
_jq() {
echo ${row} | base64 --decode | jq -r ${1}
}
PR_NUM=$(_jq '.number')
MERGE_COMMIT=$(_jq '.merge_commit_sha')
PR_TITLE=$(_jq '.title')
echo "----------------------------------------"
echo "Processing PR #$PR_NUM: $PR_TITLE"
echo "Merge commit: $MERGE_COMMIT"
# Attempt cherry-pick
if git cherry-pick -m 1 "$MERGE_COMMIT"; then
echo "✅ Successfully cherry-picked PR #$PR_NUM"
success_prs="$success_prs $PR_NUM"
success_count=$((success_count + 1))
else
# Check if it's an empty commit (already applied)
if git diff-index --quiet HEAD; then
echo "⏭️ PR #$PR_NUM changes already in main (empty commit)"
git cherry-pick --abort
skipped_prs="$skipped_prs $PR_NUM"
skipped_count=$((skipped_count + 1))
else
echo "❌ Conflict in PR #$PR_NUM"
conflict_prs="$conflict_prs $PR_NUM"
git cherry-pick --abort
conflict_count=$((conflict_count + 1))
fi
fi
done
set -e # Re-enable exit on error
echo "----------------------------------------"
echo "Summary:"
echo " Success: $success_count"
echo " Conflicts: $conflict_count"
echo " Already in main: $skipped_count"
# Push all successful cherry-picks
if [ $success_count -gt 0 ]; then
git push origin main
echo "✅ Pushed $success_count cherry-pick(s) to main"
fi
# Save results for next steps
echo "success_prs=$success_prs" >> $GITHUB_OUTPUT
echo "conflict_prs=$conflict_prs" >> $GITHUB_OUTPUT
echo "skipped_prs=$skipped_prs" >> $GITHUB_OUTPUT
echo "success_count=$success_count" >> $GITHUB_OUTPUT
echo "conflict_count=$conflict_count" >> $GITHUB_OUTPUT
echo "skipped_count=$skipped_count" >> $GITHUB_OUTPUT
- name: Label and comment on successful PRs
if: always() && steps.cherry-pick.outputs.success_prs != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const successPrs = '${{ steps.cherry-pick.outputs.success_prs }}'.trim().split(' ').filter(Boolean);
for (const prNumber of successPrs) {
console.log(`Processing successful PR #${prNumber}`);
// Add auto-cherry-picked label to prevent re-processing
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['auto-cherry-picked']
});
console.log(`✅ Added auto-cherry-picked label to PR #${prNumber}`);
} catch (error) {
console.log(`⚠️ Could not add label to PR #${prNumber}: ${error.message}`);
}
// Check if we've already commented about success
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const hasSuccessComment = comments.data.some(comment =>
comment.body.includes('Automatically cherry-picked to `main` branch')
);
if (hasSuccessComment) {
console.log(`⏭️ Skipping comment on PR #${prNumber} - success comment already exists`);
continue;
}
// Add success comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: '✅ Automatically cherry-picked to `main` branch!'
});
}
- name: Label skipped PRs
if: always() && steps.cherry-pick.outputs.skipped_prs != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const skippedPrs = '${{ steps.cherry-pick.outputs.skipped_prs }}'.trim().split(' ').filter(Boolean);
for (const prNumber of skippedPrs) {
console.log(`Marking skipped PR #${prNumber} as auto-cherry-picked`);
// Add auto-cherry-picked label to prevent re-processing
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['auto-cherry-picked']
});
console.log(`✅ Added auto-cherry-picked label to PR #${prNumber}`);
} catch (error) {
console.log(`⚠️ Could not add label to PR #${prNumber}: ${error.message}`);
}
}
- name: Handle PRs with conflicts
if: always() && steps.cherry-pick.outputs.conflict_prs != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const conflictPrs = '${{ steps.cherry-pick.outputs.conflict_prs }}'.trim().split(' ').filter(Boolean);
for (const prNumber of conflictPrs) {
console.log(`Checking PR #${prNumber} for existing conflict comment`);
// Check if we've already commented about conflicts
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const hasConflictComment = comments.data.some(comment =>
comment.body.includes('Auto cherry-pick to `main` failed due to conflicts')
);
if (hasConflictComment) {
console.log(`⏭️ Skipping PR #${prNumber} - conflict comment already exists`);
continue;
}
console.log(`Adding conflict comment and label to PR #${prNumber}`);
// Add skip-cherry-pick label to prevent retry attempts
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['skip-cherry-pick']
});
console.log(`✅ Added skip-cherry-pick label to PR #${prNumber}`);
} catch (error) {
console.log(`⚠️ Could not add label to PR #${prNumber}: ${error.message}`);
}
// Add conflict comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `⚠️ **Auto cherry-pick to \`main\` failed due to conflicts!**\n\nThis PR could not be automatically cherry-picked to the \`main\` branch. Please either:\n\n1. Manually cherry-pick and resolve conflicts, or\n2. Add the \`merge-to-main\` label to retry the cherry-pick\n\nThe \`skip-cherry-pick\` label has been added to prevent automatic retry attempts.`
});
}
- name: Workflow Summary
if: always() && steps.find-commits.outputs.has_prs == 'true'
run: |
echo "### Auto Cherry-pick Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Success: ${{ steps.cherry-pick.outputs.success_count }}" >> $GITHUB_STEP_SUMMARY
echo "- ⚠️ Conflicts: ${{ steps.cherry-pick.outputs.conflict_count }}" >> $GITHUB_STEP_SUMMARY
echo "- ℹ️ Already in main: ${{ steps.cherry-pick.outputs.skipped_count }}" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ steps.cherry-pick.outputs.success_prs }}" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Successful PRs:** ${{ steps.cherry-pick.outputs.success_prs }}" >> $GITHUB_STEP_SUMMARY
fi
if [ -n "${{ steps.cherry-pick.outputs.conflict_prs }}" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**PRs with conflicts:** ${{ steps.cherry-pick.outputs.conflict_prs }}" >> $GITHUB_STEP_SUMMARY
fi