Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions .github/workflows/nightly-rebase.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# Nightly Dev-to-Main Rebase
# Automatically rebases dev branch onto main to keep them in sync
name: Nightly Rebase

on:
schedule:
# Run at 3 AM UTC daily
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (no push)'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'

jobs:
rebase-dev:
name: Rebase dev onto main
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'

- name: Attempt Rebase
id: rebase
run: |
echo "🔄 Starting nightly rebase of dev onto main..."

# Fetch latest changes
git fetch origin main dev

# Checkout dev branch
git checkout dev

# Get commit counts for logging
MAIN_COMMITS=$(git rev-list --count origin/main)
DEV_COMMITS=$(git rev-list --count origin/dev)
DIVERGED=$(git rev-list --count origin/main..origin/dev)

echo "📊 Branch statistics:"
echo " - Main has $MAIN_COMMITS commits"
echo " - Dev has $DEV_COMMITS commits"
echo " - Dev is $DIVERGED commits ahead of main"
Comment on lines +51 to +56
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The variable name 'DIVERGED' is misleading as it represents commits ahead, not actual divergence. Consider renaming to 'AHEAD_COMMITS' or 'DEV_AHEAD' for clarity.

Suggested change
DIVERGED=$(git rev-list --count origin/main..origin/dev)
echo "📊 Branch statistics:"
echo " - Main has $MAIN_COMMITS commits"
echo " - Dev has $DEV_COMMITS commits"
echo " - Dev is $DIVERGED commits ahead of main"
AHEAD_COMMITS=$(git rev-list --count origin/main..origin/dev)
echo "📊 Branch statistics:"
echo " - Main has $MAIN_COMMITS commits"
echo " - Dev has $DEV_COMMITS commits"
echo " - Dev is $AHEAD_COMMITS commits ahead of main"

Copilot uses AI. Check for mistakes.

# Attempt rebase
if git rebase origin/main; then
echo "✅ Rebase successful!"
echo "rebase_success=true" >> $GITHUB_OUTPUT

# Check if this is a dry run
if [[ "${{ github.event.inputs.dry_run }}" != "true" ]]; then
# Push the rebased dev branch
git push origin dev --force-with-lease
echo "✅ Pushed rebased dev branch"
else
echo "🔍 Dry run - not pushing changes"
git log --oneline -10
fi
else
echo "❌ Rebase failed due to conflicts"
echo "rebase_success=false" >> $GITHUB_OUTPUT

# Abort the rebase
git rebase --abort

# Get conflict information
echo "conflict_files=$(git diff --name-only origin/main origin/dev | head -20 | tr '\n' ' ')" >> $GITHUB_OUTPUT
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The magic number '20' for limiting conflict files should be defined as a variable or constant at the top of the file for better maintainability.

Suggested change
echo "conflict_files=$(git diff --name-only origin/main origin/dev | head -20 | tr '\n' ' ')" >> $GITHUB_OUTPUT
echo "conflict_files=$(git diff --name-only origin/main origin/dev | head -${{ env.MAX_CONFLICT_FILES }} | tr '\n' ' ')" >> $GITHUB_OUTPUT

Copilot uses AI. Check for mistakes.
fi

- name: Create Conflict Resolution PR
if: steps.rebase.outputs.rebase_success == 'false'
uses: actions/github-script@v7
with:
script: |
const conflictFiles = '${{ steps.rebase.outputs.conflict_files }}'.trim().split(' ').filter(Boolean);

// Check if a rebase PR already exists
const { data: existingPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:rebase-dev-${new Date().toISOString().split('T')[0]}`
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The head parameter format is incorrect for checking existing PRs. It should be just the branch name without the owner prefix when checking PRs in the same repository.

Suggested change
head: `${context.repo.owner}:rebase-dev-${new Date().toISOString().split('T')[0]}`
head: `rebase-dev-${new Date().toISOString().split('T')[0]}`

Copilot uses AI. Check for mistakes.
});

if (existingPRs.length > 0) {
console.log('Rebase PR already exists, skipping creation');
return;
}

// Create a new branch for conflict resolution
const branchName = `rebase-dev-${new Date().toISOString().split('T')[0]}`;

try {
// Create branch from dev
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${branchName}`,
sha: (await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/dev'
})).data.object.sha
});

// Create PR
const prBody = `## 🔧 Nightly Rebase Conflict Resolution

The automated rebase of \`dev\` onto \`main\` has encountered conflicts that require manual resolution.

### Conflict Summary
${conflictFiles.length} files have conflicts:
${conflictFiles.map(f => `- \`${f}\``).join('\n')}

### Resolution Steps
1. Checkout this branch locally
2. Run \`git rebase origin/main\`
3. Resolve conflicts in each file
4. Continue rebase with \`git rebase --continue\`
5. Force push this branch
6. Merge this PR to update dev

### Alternative
If conflicts are too complex, consider:
- Creating focused PRs to main first
- Then rebasing dev after those are merged

---
*This PR was automatically created by the nightly rebase workflow*`;

const { data: pr } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🔧 Resolve rebase conflicts: dev <- main (${new Date().toLocaleDateString()})`,
body: prBody,
head: branchName,
base: 'dev',
draft: false
});

// Add labels
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['rebase-conflict', 'automated']
});

// Add comment with detailed instructions
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `### 📋 Detailed Conflict Resolution Guide

\`\`\`bash
# 1. Fetch and checkout this branch
git fetch origin
git checkout ${branchName}

# 2. Start the rebase
git rebase origin/main

# 3. For each conflict:
# - Edit the conflicted files
# - Remove conflict markers
# - Stage the resolved files
git add <resolved-files>

# 4. Continue the rebase
git rebase --continue

# 5. Push the resolved branch
git push origin ${branchName} --force-with-lease

# 6. Mark this PR as ready for review
\`\`\`

⚠️ **Important**: Ensure all tests pass after resolving conflicts!`
});

console.log(`Created PR #${pr.number} for conflict resolution`);
} catch (error) {
console.error('Failed to create conflict resolution PR:', error);
}

- name: Send Success Notification
if: steps.rebase.outputs.rebase_success == 'true' && github.event.inputs.dry_run != 'true'
uses: actions/github-script@v7
with:
script: |
// Get the last few commits that were rebased
const { data: commits } = await github.rest.repos.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
sha: 'dev',
per_page: 5
});

const commitList = commits.map(c =>
`- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}`
).join('\n');

// Create an issue to notify about successful rebase
const issueBody = `## ✅ Nightly Rebase Successful

The \`dev\` branch has been successfully rebased onto \`main\`.

### Recent commits on dev:
${commitList}

### Next Steps
- Continue development on \`dev\` branch
- Create PRs from feature branches to \`dev\`
- \`main\` remains stable and protected

---
*This notification was generated by the nightly rebase workflow*`;

// Check if notification issue exists for today
const today = new Date().toISOString().split('T')[0];
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'rebase-success',
state: 'open',
since: today
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'since' parameter expects an ISO 8601 timestamp, but 'today' is in YYYY-MM-DD format. This should be since: today + 'T00:00:00Z' to properly filter issues created today.

Copilot uses AI. Check for mistakes.
});

if (issues.length === 0) {
const { data: issue } = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `✅ Nightly rebase successful (${today})`,
body: issueBody,
labels: ['rebase-success', 'automated']
});

// Auto-close after creation
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed'
});
}

cleanup-old-rebase-branches:
name: Cleanup Old Rebase Branches
runs-on: ubuntu-latest
needs: rebase-dev
if: always()

steps:
- uses: actions/checkout@v4

- name: Delete Old Rebase Branches
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "🧹 Cleaning up old rebase branches..."

# Get all rebase branches older than 7 days
CUTOFF_DATE=$(date -d '7 days ago' +%Y-%m-%d)
Copy link

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date -d command is not portable and may fail on non-GNU systems like macOS. Since this runs on ubuntu-latest, it should work, but consider using a more portable date calculation or explicitly documenting the GNU date dependency.

Suggested change
CUTOFF_DATE=$(date -d '7 days ago' +%Y-%m-%d)
CUTOFF_DATE=$(node -e "console.log(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0])")

Copilot uses AI. Check for mistakes.

git ls-remote --heads origin | grep 'refs/heads/rebase-dev-' | while read sha ref; do
branch=${ref#refs/heads/}
branch_date=$(echo $branch | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}$' || true)

if [[ -n "$branch_date" ]] && [[ "$branch_date" < "$CUTOFF_DATE" ]]; then
echo "Deleting old rebase branch: $branch"
git push origin --delete "$branch" || echo "Failed to delete $branch"
fi
done
Loading