Skip to content

Commit 5caea1e

Browse files
feat: add automated dev→main sync workflow
1 parent 3e7be45 commit 5caea1e

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
name: Sync dev to main
2+
3+
# Automatically merge dev → main daily at 8 AM PST (16:00 UTC)
4+
# Uses github-actions[bot] with bypass permissions to skip PR approval requirements
5+
# Handles conflicts gracefully (logs warning, skips merge, doesn't fail)
6+
7+
on:
8+
schedule:
9+
# Run daily at 8:00 AM PST (16:00 UTC)
10+
- cron: '0 16 * * *'
11+
12+
workflow_dispatch: # Allow manual triggering
13+
14+
permissions:
15+
contents: write # Push to main branch
16+
issues: write # Create issues on conflict
17+
actions: read # Check workflow status (optional)
18+
19+
env:
20+
SOURCE_BRANCH: dev
21+
TARGET_BRANCH: main
22+
# Power Automate webhook for Teams notifications
23+
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"
24+
25+
jobs:
26+
sync-branches:
27+
runs-on: ubuntu-latest
28+
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@v4
32+
with:
33+
fetch-depth: 0 # Full history for merge
34+
token: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Configure git
37+
run: |
38+
git config user.name "github-actions[bot]"
39+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
40+
41+
- name: Check if dev has new commits
42+
id: check-new-commits
43+
run: |
44+
git fetch origin ${{ env.SOURCE_BRANCH }}
45+
git fetch origin ${{ env.TARGET_BRANCH }}
46+
47+
# Get commit counts
48+
COMMITS_AHEAD=$(git rev-list --count origin/${{ env.TARGET_BRANCH }}..origin/${{ env.SOURCE_BRANCH }})
49+
50+
echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT
51+
52+
if [ "$COMMITS_AHEAD" -eq 0 ]; then
53+
echo "ℹ️ No new commits in ${{ env.SOURCE_BRANCH }} - nothing to sync"
54+
echo "has_new_commits=false" >> $GITHUB_OUTPUT
55+
else
56+
echo "✅ Found $COMMITS_AHEAD new commit(s) in ${{ env.SOURCE_BRANCH }}"
57+
echo "has_new_commits=true" >> $GITHUB_OUTPUT
58+
fi
59+
60+
- name: Check latest dev build status (non-blocking)
61+
if: steps.check-new-commits.outputs.has_new_commits == 'true'
62+
continue-on-error: true # Don't fail if this step fails
63+
run: |
64+
echo "ℹ️ Checking latest build status on ${{ env.SOURCE_BRANCH }} (informational only)..."
65+
66+
# Get latest build workflow run on dev
67+
BUILD_STATUS=$(gh run list --workflow="build-and-deploy.yml" \
68+
--branch ${{ env.SOURCE_BRANCH }} --limit 1 --json conclusion,status \
69+
--jq '.[0] | "\(.status):\(.conclusion)"' 2>/dev/null || echo "unknown:unknown")
70+
71+
echo "Latest build status: $BUILD_STATUS"
72+
73+
if [[ "$BUILD_STATUS" == *"failure"* ]]; then
74+
echo "⚠️ WARNING: Latest ${{ env.SOURCE_BRANCH }} build failed"
75+
echo "⚠️ Proceeding with merge anyway (bypass enabled)"
76+
elif [[ "$BUILD_STATUS" == *"in_progress"* ]]; then
77+
echo "ℹ️ Build is still running on ${{ env.SOURCE_BRANCH }}"
78+
echo "ℹ️ Proceeding with merge anyway (bypass enabled)"
79+
else
80+
echo "✅ Latest build status: $BUILD_STATUS"
81+
fi
82+
env:
83+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
84+
85+
- name: Attempt merge
86+
id: merge
87+
if: steps.check-new-commits.outputs.has_new_commits == 'true'
88+
run: |
89+
echo "🔄 Attempting to merge ${{ env.SOURCE_BRANCH }} → ${{ env.TARGET_BRANCH }}..."
90+
91+
# Checkout target branch
92+
git checkout ${{ env.TARGET_BRANCH }}
93+
94+
# Attempt merge (don't fail on conflict)
95+
set +e
96+
git merge --no-edit --no-ff origin/${{ env.SOURCE_BRANCH }} 2>&1 | tee merge_output.txt
97+
MERGE_EXIT_CODE=$?
98+
set -e
99+
100+
if [ $MERGE_EXIT_CODE -ne 0 ]; then
101+
echo "❌ Merge conflict detected"
102+
echo "conflict=true" >> $GITHUB_OUTPUT
103+
104+
# Get conflicted files
105+
git status --short | grep '^UU\|^AA\|^DD' > conflicts.txt || echo "Unable to detect specific files" > conflicts.txt
106+
107+
# Abort the merge
108+
git merge --abort
109+
110+
echo "⚠️ Skipping automated merge due to conflicts"
111+
exit 0 # Exit success to allow graceful handling
112+
else
113+
echo "✅ Merge completed successfully"
114+
echo "conflict=false" >> $GITHUB_OUTPUT
115+
fi
116+
117+
- name: Push to main
118+
if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false'
119+
run: |
120+
echo "📤 Pushing merged changes to ${{ env.TARGET_BRANCH }}..."
121+
122+
# Push directly to main (bypass branch protection via github-actions[bot])
123+
git push origin ${{ env.TARGET_BRANCH }}
124+
125+
echo "✅ Successfully synced ${{ env.SOURCE_BRANCH }} → ${{ env.TARGET_BRANCH }}"
126+
127+
- name: Get merge details for notification
128+
if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false'
129+
id: merge-details
130+
run: |
131+
# Get commit details
132+
COMMIT_COUNT=${{ steps.check-new-commits.outputs.commits_ahead }}
133+
COMMIT_SHA=$(git rev-parse HEAD | cut -c1-7)
134+
135+
# Get commit messages
136+
git log --oneline origin/${{ env.TARGET_BRANCH }}~$COMMIT_COUNT..origin/${{ env.TARGET_BRANCH }} | head -10 > commits.txt
137+
138+
echo "commit_count=$COMMIT_COUNT" >> $GITHUB_OUTPUT
139+
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
140+
141+
- name: Send success notification to Teams
142+
if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'false'
143+
run: |
144+
# Get commit messages (truncate if too long)
145+
COMMITS=$(cat commits.txt | head -5 | sed 's/^/ - /')
146+
147+
curl -H "Content-Type: application/json" -d '{
148+
"@type": "MessageCard",
149+
"@context": "https://schema.org/extensions",
150+
"summary": "Dev → Main Sync Successful",
151+
"themeColor": "28a745",
152+
"title": "✅ Automated Sync: dev → main",
153+
"sections": [{
154+
"activityTitle": "Successfully merged '${{ steps.check-new-commits.outputs.commits_ahead }}' commits",
155+
"activitySubtitle": "Repository: netwrix/docs",
156+
"facts": [
157+
{"name": "Commits merged:", "value": "${{ steps.merge-details.outputs.commit_count }}"},
158+
{"name": "Latest commit:", "value": "${{ steps.merge-details.outputs.commit_sha }}"},
159+
{"name": "Triggered by:", "value": "${{ github.event_name }}"},
160+
{"name": "Workflow:", "value": "${{ github.workflow }}"}
161+
],
162+
"text": "**Recent commits:**\n\n'"$(echo "$COMMITS" | sed 's/$/\\n/' | tr -d '\n')"'"
163+
}],
164+
"potentialAction": [{
165+
"@type": "OpenUri",
166+
"name": "View Workflow Run",
167+
"targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}]
168+
}, {
169+
"@type": "OpenUri",
170+
"name": "View Repository",
171+
"targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}"}]
172+
}]
173+
}' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification"
174+
175+
- name: Create GitHub issue for conflict
176+
if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'true'
177+
uses: actions/github-script@v7
178+
with:
179+
script: |
180+
const conflictedFiles = require('fs').readFileSync('conflicts.txt', 'utf8');
181+
const mergeOutput = require('fs').readFileSync('merge_output.txt', 'utf8');
182+
183+
const issue = await github.rest.issues.create({
184+
owner: context.repo.owner,
185+
repo: context.repo.repo,
186+
title: '⚠️ Automated dev→main sync blocked by merge conflict',
187+
labels: ['automated-sync', 'merge-conflict'],
188+
body: '## Automated Sync Conflict\n\n' +
189+
'The automated sync from `dev` to `main` encountered merge conflicts and was skipped.\n\n' +
190+
'**Workflow Run:** ' + context.serverUrl + '/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId + '\n' +
191+
'**Triggered by:** ' + context.eventName + '\n' +
192+
'**Date:** ' + new Date().toISOString() + '\n\n' +
193+
'### Conflicted Files\n\n' +
194+
'```\n' +
195+
conflictedFiles + '\n' +
196+
'```\n\n' +
197+
'### Merge Output\n\n' +
198+
'```\n' +
199+
mergeOutput + '\n' +
200+
'```\n\n' +
201+
'### Resolution Steps\n\n' +
202+
'1. Manually resolve conflicts:\n' +
203+
' ```bash\n' +
204+
' git checkout main\n' +
205+
' git pull origin main\n' +
206+
' git merge origin/dev\n' +
207+
' # Resolve conflicts in your editor\n' +
208+
' git add .\n' +
209+
' git commit\n' +
210+
' git push origin main\n' +
211+
' ```\n\n' +
212+
'2. Or create a PR from dev → main and resolve conflicts there\n\n' +
213+
'3. Once resolved, the next scheduled sync will proceed normally\n\n' +
214+
'---\n' +
215+
'*This issue was created automatically by the sync-dev-to-main workflow.*'
216+
});
217+
218+
console.log('Created issue #' + issue.data.number);
219+
220+
- name: Send conflict notification to Teams
221+
if: steps.check-new-commits.outputs.has_new_commits == 'true' && steps.merge.outputs.conflict == 'true'
222+
run: |
223+
curl -H "Content-Type: application/json" -d '{
224+
"@type": "MessageCard",
225+
"@context": "https://schema.org/extensions",
226+
"summary": "Dev → Main Sync Blocked by Conflict",
227+
"themeColor": "ff9800",
228+
"title": "⚠️ Automated Sync Blocked: Merge Conflict",
229+
"sections": [{
230+
"activityTitle": "Manual intervention required",
231+
"activitySubtitle": "Repository: netwrix/docs",
232+
"facts": [
233+
{"name": "Source branch:", "value": "${{ env.SOURCE_BRANCH }}"},
234+
{"name": "Target branch:", "value": "${{ env.TARGET_BRANCH }}"},
235+
{"name": "Commits waiting:", "value": "${{ steps.check-new-commits.outputs.commits_ahead }}"},
236+
{"name": "Status:", "value": "Merge conflict detected"}
237+
],
238+
"text": "The automated sync encountered merge conflicts. A GitHub issue has been created with details. Please resolve the conflicts manually."
239+
}],
240+
"potentialAction": [{
241+
"@type": "OpenUri",
242+
"name": "View Workflow Run",
243+
"targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}]
244+
}, {
245+
"@type": "OpenUri",
246+
"name": "View Issues",
247+
"targets": [{"os": "default", "uri": "${{ github.server_url }}/${{ github.repository }}/issues?q=is:issue+is:open+label:merge-conflict"}]
248+
}]
249+
}' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification"
250+
251+
- name: Send skip notification to Teams
252+
if: steps.check-new-commits.outputs.has_new_commits == 'false'
253+
run: |
254+
curl -H "Content-Type: application/json" -d '{
255+
"@type": "MessageCard",
256+
"@context": "https://schema.org/extensions",
257+
"summary": "No Changes to Sync",
258+
"themeColor": "0078D4",
259+
"title": "ℹ️ No Changes to Sync",
260+
"sections": [{
261+
"activityTitle": "Branches are already in sync",
262+
"activitySubtitle": "Repository: netwrix/docs",
263+
"text": "No new commits found in dev branch. Nothing to sync."
264+
}]
265+
}' "${{ env.TEAMS_WEBHOOK_URL }}" || echo "Failed to send Teams notification"
266+
267+
- name: Workflow summary
268+
if: always()
269+
run: |
270+
echo "## Sync Dev to Main - Summary" >> $GITHUB_STEP_SUMMARY
271+
echo "" >> $GITHUB_STEP_SUMMARY
272+
273+
if [ "${{ steps.check-new-commits.outputs.has_new_commits }}" == "false" ]; then
274+
echo "ℹ️ **Status:** No new commits to sync" >> $GITHUB_STEP_SUMMARY
275+
echo "- Source: \`${{ env.SOURCE_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY
276+
echo "- Target: \`${{ env.TARGET_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY
277+
elif [ "${{ steps.merge.outputs.conflict }}" == "true" ]; then
278+
echo "⚠️ **Status:** Merge conflict detected" >> $GITHUB_STEP_SUMMARY
279+
echo "- Commits waiting: ${{ steps.check-new-commits.outputs.commits_ahead }}" >> $GITHUB_STEP_SUMMARY
280+
echo "- Action: GitHub issue created" >> $GITHUB_STEP_SUMMARY
281+
echo "- Resolution: Manual merge required" >> $GITHUB_STEP_SUMMARY
282+
else
283+
echo "✅ **Status:** Successfully synced" >> $GITHUB_STEP_SUMMARY
284+
echo "- Commits merged: ${{ steps.merge-details.outputs.commit_count }}" >> $GITHUB_STEP_SUMMARY
285+
echo "- Latest commit: \`${{ steps.merge-details.outputs.commit_sha }}\`" >> $GITHUB_STEP_SUMMARY
286+
echo "- Pushed to: \`${{ env.TARGET_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY
287+
fi
288+
289+
echo "" >> $GITHUB_STEP_SUMMARY
290+
echo "---" >> $GITHUB_STEP_SUMMARY
291+
echo "*Automated by github-actions[bot] with branch protection bypass*" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)