Skip to content

Commit f81b269

Browse files
markturanskyAmbient Code Botclaude
authored
chore(ci): sync alpha branch from main on every push (#1089)
## Summary - Adds `scripts/rebase-main-to-alpha.sh` — manual best-effort rebase script - Adds `.github/workflows/sync-alpha-from-main.yml` — automated trigger on every push to `main` On each `main` push the workflow: 1. Checks if `alpha` is already up to date (exits early if so) 2. Skips if an open sync PR already exists 3. Creates a timestamped branch off `alpha`, attempts rebase, falls back to merge on conflict 4. Opens a PR against `alpha` for human review and conflict resolution ## Test plan - [ ] Verify workflow triggers on next push to `main` - [ ] Verify PR is opened against `alpha` - [ ] Verify early-exit when alpha is already up to date - [ ] Verify deduplication — no second PR opened if one already exists 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8ac0f85 commit f81b269

2 files changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
name: Sync Alpha from Main
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
concurrency:
13+
group: sync-alpha-from-main
14+
cancel-in-progress: false
15+
16+
jobs:
17+
sync:
18+
name: Rebase main into alpha
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 15
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
with:
26+
fetch-depth: 0
27+
token: ${{ secrets.GITHUB_TOKEN }}
28+
29+
- name: Configure git
30+
run: |
31+
git config user.name "github-actions[bot]"
32+
git config user.email "github-actions[bot]@users.noreply.github.com"
33+
34+
- name: Check if alpha is already up to date
35+
id: check
36+
run: |
37+
MAIN_SHA="$(git rev-parse origin/main)"
38+
ALPHA_SHA="$(git rev-parse origin/alpha)"
39+
COMMIT_COUNT="$(git rev-list --count "${ALPHA_SHA}..${MAIN_SHA}")"
40+
41+
echo "main_sha=${MAIN_SHA}" >> "$GITHUB_OUTPUT"
42+
echo "alpha_sha=${ALPHA_SHA}" >> "$GITHUB_OUTPUT"
43+
echo "commit_count=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT"
44+
45+
if [ "${COMMIT_COUNT}" -eq 0 ]; then
46+
echo "needs_sync=false" >> "$GITHUB_OUTPUT"
47+
echo "alpha is already up to date with main"
48+
else
49+
echo "needs_sync=true" >> "$GITHUB_OUTPUT"
50+
echo "Commits in main not in alpha: ${COMMIT_COUNT}"
51+
fi
52+
53+
- name: Check for existing open sync PR
54+
if: steps.check.outputs.needs_sync == 'true'
55+
id: existing_pr
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
run: |
59+
EXISTING=$(gh pr list \
60+
--base alpha \
61+
--state open \
62+
--json headRefName \
63+
--jq '[.[] | select(.headRefName | startswith("chore/sync-alpha-from-main-"))] | length')
64+
65+
if [ "${EXISTING}" -gt 0 ]; then
66+
echo "Open sync PR already exists — skipping"
67+
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
68+
else
69+
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
70+
fi
71+
72+
- name: Create work branch off alpha
73+
if: steps.check.outputs.needs_sync == 'true' && steps.existing_pr.outputs.pr_exists == 'false'
74+
id: branch
75+
run: |
76+
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
77+
WORK_BRANCH="chore/sync-alpha-from-main-${TIMESTAMP}"
78+
echo "work_branch=${WORK_BRANCH}" >> "$GITHUB_OUTPUT"
79+
80+
git checkout -b "${WORK_BRANCH}" origin/alpha
81+
echo "Created ${WORK_BRANCH} from origin/alpha"
82+
83+
- name: Attempt rebase of main onto work branch
84+
if: steps.check.outputs.needs_sync == 'true' && steps.existing_pr.outputs.pr_exists == 'false'
85+
id: rebase
86+
env:
87+
WORK_BRANCH: ${{ steps.branch.outputs.work_branch }}
88+
MAIN_SHA: ${{ steps.check.outputs.main_sha }}
89+
ALPHA_SHA: ${{ steps.check.outputs.alpha_sha }}
90+
run: |
91+
MERGE_BASE="$(git merge-base "${ALPHA_SHA}" "${MAIN_SHA}")"
92+
93+
git rebase --onto "${WORK_BRANCH}" "${MERGE_BASE}" origin/main && {
94+
echo "rebase_clean=true" >> "$GITHUB_OUTPUT"
95+
git checkout -B "${WORK_BRANCH}"
96+
echo "Rebase completed cleanly"
97+
} || {
98+
echo "rebase_clean=false" >> "$GITHUB_OUTPUT"
99+
git rebase --abort 2>/dev/null || true
100+
101+
echo "Rebase had conflicts — falling back to merge"
102+
MERGE_MSG=$(cat <<'MSG'
103+
chore: merge main into alpha (conflict resolution required)
104+
105+
Automated merge of origin/main into origin/alpha.
106+
Rebase encountered conflicts; falling back to merge.
107+
A human must resolve conflict markers before merging this PR.
108+
MSG
109+
)
110+
git merge --no-ff --allow-unrelated-histories origin/main -m "${MERGE_MSG}" || {
111+
git add -A
112+
CONFLICT_MSG=$(cat <<'MSG'
113+
chore: best-effort merge main into alpha (conflicts present)
114+
115+
Automated merge of origin/main into origin/alpha.
116+
Both rebase and merge encountered conflicts. Conflict markers
117+
are present and must be resolved before this PR can be merged.
118+
MSG
119+
)
120+
git commit --no-verify -m "${CONFLICT_MSG}"
121+
}
122+
}
123+
124+
- name: Push work branch
125+
if: steps.check.outputs.needs_sync == 'true' && steps.existing_pr.outputs.pr_exists == 'false'
126+
env:
127+
WORK_BRANCH: ${{ steps.branch.outputs.work_branch }}
128+
run: |
129+
git push origin "${WORK_BRANCH}"
130+
131+
- name: Open PR against alpha
132+
if: steps.check.outputs.needs_sync == 'true' && steps.existing_pr.outputs.pr_exists == 'false'
133+
env:
134+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135+
WORK_BRANCH: ${{ steps.branch.outputs.work_branch }}
136+
COMMIT_COUNT: ${{ steps.check.outputs.commit_count }}
137+
REBASE_CLEAN: ${{ steps.rebase.outputs.rebase_clean }}
138+
MAIN_SHA: ${{ steps.check.outputs.main_sha }}
139+
ALPHA_SHA: ${{ steps.check.outputs.alpha_sha }}
140+
run: |
141+
if [ "${REBASE_CLEAN}" = "true" ]; then
142+
CONFLICT_NOTE="Rebase completed cleanly — no conflicts detected. This PR can be merged directly."
143+
else
144+
CONFLICT_NOTE="⚠️ **Conflicts detected.** Rebase fell back to merge. Search for \`<<<<<<<\` conflict markers and resolve before merging."
145+
fi
146+
147+
gh pr create \
148+
--base alpha \
149+
--head "${WORK_BRANCH}" \
150+
--title "chore: sync alpha from main ($(date +%Y-%m-%d))" \
151+
--body "## Summary
152+
153+
Automated sync of \`main\` into \`alpha\` triggered by push to \`main\`.
154+
155+
| | |
156+
|---|---|
157+
| Commits synced | ${COMMIT_COUNT} |
158+
| origin/main | \`${MAIN_SHA:0:8}\` |
159+
| origin/alpha | \`${ALPHA_SHA:0:8}\` |
160+
161+
## Status
162+
163+
${CONFLICT_NOTE}
164+
165+
## Review Instructions
166+
167+
1. Check for conflict markers (\`<<<<<<<\`) in changed files.
168+
2. Resolve any conflicts and push to this branch.
169+
3. Verify the build passes.
170+
4. Merge into \`alpha\`.
171+
172+
---
173+
*Auto-generated by \`.github/workflows/sync-alpha-from-main.yml\`*"
174+
175+
- name: Summary
176+
if: always()
177+
env:
178+
NEEDS_SYNC: ${{ steps.check.outputs.needs_sync }}
179+
PR_EXISTS: ${{ steps.existing_pr.outputs.pr_exists || 'false' }}
180+
COMMIT_COUNT: ${{ steps.check.outputs.commit_count || '0' }}
181+
REBASE_CLEAN: ${{ steps.rebase.outputs.rebase_clean || 'n/a' }}
182+
JOB_STATUS: ${{ job.status }}
183+
run: |
184+
if [ "${NEEDS_SYNC}" = "false" ]; then
185+
echo "## ✅ Already in sync" >> "$GITHUB_STEP_SUMMARY"
186+
echo "alpha is up to date with main — nothing to do." >> "$GITHUB_STEP_SUMMARY"
187+
elif [ "${PR_EXISTS}" = "true" ]; then
188+
echo "## ℹ️ Sync PR already open" >> "$GITHUB_STEP_SUMMARY"
189+
echo "An open sync PR already exists against alpha — skipped." >> "$GITHUB_STEP_SUMMARY"
190+
elif [ "${JOB_STATUS}" = "failure" ]; then
191+
echo "## ❌ Sync failed" >> "$GITHUB_STEP_SUMMARY"
192+
echo "Check the logs above for details." >> "$GITHUB_STEP_SUMMARY"
193+
elif [ "${REBASE_CLEAN}" = "true" ]; then
194+
echo "## ✅ PR opened — clean rebase" >> "$GITHUB_STEP_SUMMARY"
195+
echo "${COMMIT_COUNT} commits synced from main to alpha with no conflicts." >> "$GITHUB_STEP_SUMMARY"
196+
else
197+
echo "## ⚠️ PR opened — conflicts require resolution" >> "$GITHUB_STEP_SUMMARY"
198+
echo "${COMMIT_COUNT} commits from main; rebase had conflicts. PR opened for human resolution." >> "$GITHUB_STEP_SUMMARY"
199+
fi

scripts/rebase-main-to-alpha.sh

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/bin/bash
2+
# rebase-main-to-alpha.sh - Rebase all commits from main into the alpha branch.
3+
#
4+
# Creates a working branch off alpha, replays all commits from main that are
5+
# not yet in alpha, and opens a PR against alpha for human review and merge.
6+
# Merge conflicts are left in-place with conflict markers for the reviewer.
7+
#
8+
# Usage:
9+
# ./scripts/rebase-main-to-alpha.sh
10+
#
11+
# Requirements:
12+
# - git, gh (GitHub CLI)
13+
# - Authenticated to GitHub: gh auth status
14+
# - Remote named 'upstream' pointing to ambient-code/platform
15+
16+
set -euo pipefail
17+
18+
UPSTREAM="${UPSTREAM:-upstream}"
19+
MAIN_BRANCH="main"
20+
ALPHA_BRANCH="alpha"
21+
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
22+
WORK_BRANCH="chore/rebase-main-to-alpha-${TIMESTAMP}"
23+
24+
log() { echo "[rebase-main-to-alpha] $*"; }
25+
die() { echo "[rebase-main-to-alpha] ERROR: $*" >&2; exit 1; }
26+
27+
# Verify required tools
28+
command -v git >/dev/null || die "git not found"
29+
command -v gh >/dev/null || die "gh (GitHub CLI) not found — install from https://cli.github.com"
30+
31+
# Verify upstream remote exists
32+
git remote get-url "${UPSTREAM}" >/dev/null 2>&1 || \
33+
die "Remote '${UPSTREAM}' not found. Add it: git remote add upstream git@github.com:ambient-code/platform.git"
34+
35+
log "Fetching ${UPSTREAM}..."
36+
git fetch "${UPSTREAM}" "${MAIN_BRANCH}" "${ALPHA_BRANCH}"
37+
38+
MAIN_SHA="$(git rev-parse "${UPSTREAM}/${MAIN_BRANCH}")"
39+
ALPHA_SHA="$(git rev-parse "${UPSTREAM}/${ALPHA_BRANCH}")"
40+
MERGE_BASE="$(git merge-base "${ALPHA_SHA}" "${MAIN_SHA}")"
41+
42+
log "upstream/${MAIN_BRANCH}: ${MAIN_SHA}"
43+
log "upstream/${ALPHA_BRANCH}: ${ALPHA_SHA}"
44+
log "merge-base: ${MERGE_BASE}"
45+
46+
# Count commits in main not yet in alpha
47+
COMMIT_COUNT="$(git rev-list --count "${ALPHA_SHA}..${MAIN_SHA}")"
48+
if [ "${COMMIT_COUNT}" -eq 0 ]; then
49+
log "alpha is already up to date with main. Nothing to do."
50+
exit 0
51+
fi
52+
log "Commits in main not in alpha: ${COMMIT_COUNT}"
53+
54+
# Create work branch off alpha
55+
log "Creating work branch '${WORK_BRANCH}' from ${UPSTREAM}/${ALPHA_BRANCH}..."
56+
git checkout -b "${WORK_BRANCH}" "${UPSTREAM}/${ALPHA_BRANCH}"
57+
58+
# Attempt rebase of main onto the work branch (best-effort)
59+
log "Rebasing ${UPSTREAM}/${MAIN_BRANCH} onto ${WORK_BRANCH} (best-effort)..."
60+
REBASE_EXIT=0
61+
git rebase --onto "${WORK_BRANCH}" "${MERGE_BASE}" "${UPSTREAM}/${MAIN_BRANCH}" || REBASE_EXIT=$?
62+
63+
if [ "${REBASE_EXIT}" -ne 0 ]; then
64+
log "Rebase encountered conflicts. Collecting conflict state..."
65+
66+
# Stage all files — conflict markers will be preserved in working tree
67+
git add -A || true
68+
69+
CONFLICT_FILES="$(git diff --name-only --diff-filter=U HEAD 2>/dev/null || git status --short | grep '^UU' | awk '{print $2}' || echo "(see git status)")"
70+
71+
# Abort the rebase so we're on a clean branch, then merge instead as fallback
72+
git rebase --abort 2>/dev/null || true
73+
74+
log "Rebase aborted due to conflicts. Falling back to merge for best-effort commit..."
75+
git merge --no-ff --allow-unrelated-histories "${UPSTREAM}/${MAIN_BRANCH}" \
76+
-m "chore: merge main into alpha (best-effort — conflicts require human resolution)
77+
78+
Automated merge of upstream/main into upstream/alpha.
79+
Conflicts detected during rebase; falling back to merge.
80+
81+
Conflicting files:
82+
${CONFLICT_FILES}
83+
84+
Please resolve conflicts and merge this PR manually.
85+
Generated by scripts/rebase-main-to-alpha.sh on ${TIMESTAMP}." || {
86+
# Merge also has conflicts — stage everything and commit with markers
87+
git add -A
88+
git commit --no-verify -m "chore: best-effort merge main into alpha (conflicts present)
89+
90+
Automated merge of upstream/main into upstream/alpha.
91+
Both rebase and merge encountered conflicts. Conflict markers are
92+
present in the files listed below. A human must resolve these before
93+
merging this PR.
94+
95+
Generated by scripts/rebase-main-to-alpha.sh on ${TIMESTAMP}."
96+
}
97+
else
98+
log "Rebase completed cleanly."
99+
# Rebase leaves us in detached-ish state — need to update work branch
100+
git checkout -B "${WORK_BRANCH}"
101+
fi
102+
103+
# Push work branch
104+
log "Pushing ${WORK_BRANCH} to ${UPSTREAM}..."
105+
git push "${UPSTREAM}" "${WORK_BRANCH}" --force-with-lease
106+
107+
# Open PR against alpha
108+
log "Opening PR against ${ALPHA_BRANCH}..."
109+
PR_URL="$(gh pr create \
110+
--repo ambient-code/platform \
111+
--base "${ALPHA_BRANCH}" \
112+
--head "${WORK_BRANCH}" \
113+
--title "chore: rebase main into alpha (${TIMESTAMP})" \
114+
--body "$(cat <<EOF
115+
## Summary
116+
117+
Automated best-effort rebase of \`main\` into \`alpha\`.
118+
119+
- Commits from \`main\` not yet in \`alpha\`: **${COMMIT_COUNT}**
120+
- merge-base: \`${MERGE_BASE}\`
121+
- Generated: ${TIMESTAMP}
122+
123+
## Review Instructions
124+
125+
1. Check for conflict markers (\`<<<<<<<\`) in changed files.
126+
2. Resolve any conflicts and push to this branch.
127+
3. Verify the build passes.
128+
4. Merge into \`alpha\`.
129+
130+
If there are no conflicts, this PR can be merged directly.
131+
132+
Generated by \`scripts/rebase-main-to-alpha.sh\`.
133+
EOF
134+
)")"
135+
136+
log "PR opened: ${PR_URL}"
137+
log "Done. A human should review and merge: ${PR_URL}"

0 commit comments

Comments
 (0)