diff --git a/.github/workflows/nightly-rebase.yml b/.github/workflows/nightly-rebase.yml new file mode 100644 index 00000000..69f8f574 --- /dev/null +++ b/.github/workflows/nightly-rebase.yml @@ -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" + + # 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 + 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]}` + }); + + 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 + +# 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 + }); + + 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) + + 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 \ No newline at end of file