Sync dev to main #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync dev to main | |
| # Automatically merge dev → main daily at 8 AM PST (16:00 UTC) | |
| # Uses github-actions[bot] with bypass permissions to skip PR approval requirements | |
| # Handles conflicts gracefully (logs warning, skips merge, doesn't fail) | |
| on: | |
| schedule: | |
| # Run daily at 8:00 AM PST (16:00 UTC) | |
| - cron: '0 16 * * *' | |
| workflow_dispatch: # Allow manual triggering | |
| permissions: | |
| contents: write # Push to main branch | |
| issues: write # Create issues on conflict | |
| actions: read # Check workflow status (optional) | |
| env: | |
| SOURCE_BRANCH: dev | |
| TARGET_BRANCH: main | |
| # Power Automate webhook for Teams notifications | |
| TEAMS_WEBHOOK_URL: "https://default097499ff179d4959ab0286d364125b.fc.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/a6dd70a2ec694cf6957a2620e1d89c23/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=B_MxbOdUi92JYpARbYIcaDNwUdSqILgO1ZV6T7vsQXU" | |
| jobs: | |
| sync-branches: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history for merge | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Check if dev has new commits | |
| id: check-new-commits | |
| run: | | |
| git fetch origin ${{ env.SOURCE_BRANCH }} | |
| git fetch origin ${{ env.TARGET_BRANCH }} | |
| # Get commit counts | |
| COMMITS_AHEAD=$(git rev-list --count origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }}) | |
| echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT | |
| if [ "$COMMITS_AHEAD" -eq 0 ]; then | |
| echo "ℹ️ No new commits in ${{ env.SOURCE_BRANCH }} - nothing to sync" | |
| echo "has_new_commits=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "✅ Found $COMMITS_AHEAD new commit(s) in ${{ env.SOURCE_BRANCH }}" | |
| echo "has_new_commits=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Check latest dev build status (non-blocking) | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' | |
| continue-on-error: true # Don't fail if this step fails | |
| run: | | |
| echo "ℹ️ Checking latest build status on ${{ env.SOURCE_BRANCH }} (informational only)..." | |
| # Get latest build workflow run on dev | |
| BUILD_STATUS=$(gh run list --workflow="build-and-deploy.yml" \ | |
| --branch ${{ env.SOURCE_BRANCH }} --limit 1 --json conclusion,status \ | |
| --jq '.[0] | "\(.status):\(.conclusion)"' 2>/dev/null || echo "unknown:unknown") | |
| echo "Latest build status: $BUILD_STATUS" | |
| if [[ "$BUILD_STATUS" == *"failure"* ]]; then | |
| echo "⚠️ WARNING: Latest ${{ env.SOURCE_BRANCH }} build failed" | |
| echo "⚠️ Proceeding with merge anyway (bypass enabled)" | |
| elif [[ "$BUILD_STATUS" == *"in_progress"* ]]; then | |
| echo "ℹ️ Build is still running on ${{ env.SOURCE_BRANCH }}" | |
| echo "ℹ️ Proceeding with merge anyway (bypass enabled)" | |
| else | |
| echo "✅ Latest build status: $BUILD_STATUS" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Attempt merge | |
| id: merge | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' | |
| run: | | |
| echo "🔄 Attempting to merge ${{ env.SOURCE_BRANCH }} → ${{ env.TARGET_BRANCH }}..." | |
| # Checkout target branch | |
| git checkout ${{ env.TARGET_BRANCH }} | |
| # Attempt merge (don't fail on conflict) | |
| set +e | |
| git merge --no-edit --no-ff origin/${{ env.SOURCE_BRANCH }} 2>&1 | tee merge_output.txt | |
| MERGE_EXIT_CODE=$? | |
| set -e | |
| if [ $MERGE_EXIT_CODE -ne 0 ]; then | |
| echo "❌ Merge conflict detected" | |
| echo "conflict=true" >> $GITHUB_OUTPUT | |
| # Get conflicted files | |
| git status --short | grep '^UU\|^AA\|^DD' > conflicts.txt || echo "Unable to detect specific files" > conflicts.txt | |
| # Abort the merge | |
| git merge --abort | |
| echo "⚠️ Skipping automated merge due to conflicts" | |
| exit 0 # Exit success to allow graceful handling | |
| else | |
| echo "✅ Merge completed successfully" | |
| echo "conflict=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Push to main | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false' | |
| run: | | |
| echo "📤 Pushing merged changes to ${{ env.TARGET_BRANCH }}..." | |
| # Push directly to main (bypass branch protection via github-actions[bot]) | |
| git push origin ${{ env.TARGET_BRANCH }} | |
| echo "✅ Successfully synced ${{ env.SOURCE_BRANCH }} → ${{ env.TARGET_BRANCH }}" | |
| - name: Get merge details for notification | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false' | |
| id: merge-details | |
| run: | | |
| # Get commit details | |
| COMMIT_COUNT=${{ steps.check-new-commits.outputs.commits_ahead }} | |
| COMMIT_SHA=$(git rev-parse HEAD | cut -c1-7) | |
| # Get commit messages | |
| git log --oneline origin/${{ env.TARGET_BRANCH }}~$COMMIT_COUNT..origin/${{ env.TARGET_BRANCH }} | head -10 > commits.txt | |
| echo "commit_count=$COMMIT_COUNT" >> $GITHUB_OUTPUT | |
| echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT | |
| - name: Send success notification to Teams | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false' | |
| run: | | |
| # Get commit messages (truncate if too long) | |
| COMMITS=$(cat commits.txt | head -5 | sed 's/^/ - /') | |
| curl -H "Content-Type: application/json" -d '{ | |
| "@type": "MessageCard", | |
| "@context": "https://schema.org/extensions", | |
| "summary": "Dev → Main Sync Successful", | |
| "themeColor": "28a745", | |
| "title": "✅ Automated Sync: dev → main", | |
| "sections": [{ | |
| "activityTitle": "Successfully merged '${{ steps.check-new-commits.outputs.commits_ahead }}' commits", | |
| "activitySubtitle": "Repository: netwrix/docs", | |
| "facts": [ | |
| {"name": "Commits merged:", "value": "${{ steps.merge-details.outputs.commit_count }}"}, | |
| {"name": "Latest commit:", "value": "${{ steps.merge-details.outputs.commit_sha }}"}, | |
| {"name": "Triggered by:", "value": "${{ github.event_name }}"}, | |
| {"name": "Workflow:", "value": "${{ github.workflow }}"} | |
| ], | |
| "text": "**Recent commits:**\n\n'"$(echo "$COMMITS" | sed 's/$/\\n/' | tr -d '\n')"'" | |
| }], | |
| "potentialAction": [{ | |
| "@type": "OpenUri", | |
| "name": "View Workflow Run", | |
| "targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}] | |
| }, { | |
| "@type": "OpenUri", | |
| "name": "View Repository", | |
| "targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}"}] | |
| }] | |
| }' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification" | |
| - name: Create GitHub issue for conflict | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const conflictedFiles = require('fs').readFileSync('conflicts.txt', 'utf8'); | |
| const mergeOutput = require('fs').readFileSync('merge_output.txt', 'utf8'); | |
| const issue = await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '⚠️ Automated dev→main sync blocked by merge conflict', | |
| labels: ['automated-sync', 'merge-conflict'], | |
| body: '## Automated Sync Conflict\n\n' + | |
| 'The automated sync from `dev` to `main` encountered merge conflicts and was skipped.\n\n' + | |
| '**Workflow Run:** ' + context.serverUrl + '/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + '\n' + | |
| '**Triggered by:** ' + context.eventName + '\n' + | |
| '**Date:** ' + new Date().toISOString() + '\n\n' + | |
| '### Conflicted Files\n\n' + | |
| '```\n' + | |
| conflictedFiles + '\n' + | |
| '```\n\n' + | |
| '### Merge Output\n\n' + | |
| '```\n' + | |
| mergeOutput + '\n' + | |
| '```\n\n' + | |
| '### Resolution Steps\n\n' + | |
| '1. Manually resolve conflicts:\n' + | |
| ' ```bash\n' + | |
| ' git checkout main\n' + | |
| ' git pull origin main\n' + | |
| ' git merge origin/dev\n' + | |
| ' # Resolve conflicts in your editor\n' + | |
| ' git add .\n' + | |
| ' git commit\n' + | |
| ' git push origin main\n' + | |
| ' ```\n\n' + | |
| '2. Or create a PR from dev → main and resolve conflicts there\n\n' + | |
| '3. Once resolved, the next scheduled sync will proceed normally\n\n' + | |
| '---\n' + | |
| '*This issue was created automatically by the sync-dev-to-main workflow.*' | |
| }); | |
| console.log('Created issue #' + issue.data.number); | |
| - name: Send conflict notification to Teams | |
| if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'true' | |
| run: | | |
| curl -H "Content-Type: application/json" -d '{ | |
| "@type": "MessageCard", | |
| "@context": "https://schema.org/extensions", | |
| "summary": "Dev → Main Sync Blocked by Conflict", | |
| "themeColor": "ff9800", | |
| "title": "⚠️ Automated Sync Blocked: Merge Conflict", | |
| "sections": [{ | |
| "activityTitle": "Manual intervention required", | |
| "activitySubtitle": "Repository: netwrix/docs", | |
| "facts": [ | |
| {"name": "Source branch:", "value": "${{ env.SOURCE_BRANCH }}"}, | |
| {"name": "Target branch:", "value": "${{ env.TARGET_BRANCH }}"}, | |
| {"name": "Commits waiting:", "value": "${{ steps.check-new-commits.outputs.commits_ahead }}"}, | |
| {"name": "Status:", "value": "Merge conflict detected"} | |
| ], | |
| "text": "The automated sync encountered merge conflicts. A GitHub issue has been created with details. Please resolve the conflicts manually." | |
| }], | |
| "potentialAction": [{ | |
| "@type": "OpenUri", | |
| "name": "View Workflow Run", | |
| "targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}] | |
| }, { | |
| "@type": "OpenUri", | |
| "name": "View Issues", | |
| "targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/issues?q=is:issue+is:open+label:merge-conflict"}] | |
| }] | |
| }' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification" | |
| - name: Send skip notification to Teams | |
| if: steps.check-new-commits.outputs.has_new_commits == 'false' | |
| run: | | |
| curl -H "Content-Type: application/json" -d '{ | |
| "@type": "MessageCard", | |
| "@context": "https://schema.org/extensions", | |
| "summary": "No Changes to Sync", | |
| "themeColor": "0078D4", | |
| "title": "ℹ️ No Changes to Sync", | |
| "sections": [{ | |
| "activityTitle": "Branches are already in sync", | |
| "activitySubtitle": "Repository: netwrix/docs", | |
| "text": "No new commits found in dev branch. Nothing to sync." | |
| }] | |
| }' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification" | |
| - name: Workflow summary | |
| if: always() | |
| run: | | |
| echo "## Sync Dev to Main - Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.check-new-commits.outputs.has_new_commits }}" == "false" ]; then | |
| echo "ℹ️ **Status:** No new commits to sync" >> $GITHUB_STEP_SUMMARY | |
| echo "- Source: \`${{ env.SOURCE_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Target: \`${{ env.TARGET_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ steps.merge.outputs.conflict }}" == "true" ]; then | |
| echo "⚠️ **Status:** Merge conflict detected" >> $GITHUB_STEP_SUMMARY | |
| echo "- Commits waiting: ${{ steps.check-new-commits.outputs.commits_ahead }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Action: GitHub issue created" >> $GITHUB_STEP_SUMMARY | |
| echo "- Resolution: Manual merge required" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "✅ **Status:** Successfully synced" >> $GITHUB_STEP_SUMMARY | |
| echo "- Commits merged: ${{ steps.merge-details.outputs.commit_count }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Latest commit: \`${{ steps.merge-details.outputs.commit_sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Pushed to: \`${{ env.TARGET_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "*Automated by github-actions[bot] with branch protection bypass*" >> $GITHUB_STEP_SUMMARY |