Skip to content
Merged
Show file tree
Hide file tree
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
20 changes: 10 additions & 10 deletions .github/workflows/sync-branches.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 66 additions & 9 deletions .github/workflows/sync-branches.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,82 @@ steps:

print(f"Found {len(branches)} autoloop branch(es) to sync: {branches}")

def rev_count(range_spec):
r = subprocess.run(
["git", "rev-list", "--count", range_spec],
capture_output=True, text=True
)
if r.returncode != 0:
return None
try:
return int(r.stdout.strip())
except ValueError:
return None

failed = []
for branch in branches:
print(f"\n--- Syncing {branch} with {default_branch} ---")

# Fetch both branches
# Fetch both branches so the ahead/behind counts below are computed
# against up-to-date local copies of the remote tips.
subprocess.run(["git", "fetch", "origin", branch], capture_output=True)
subprocess.run(["git", "fetch", "origin", default_branch], capture_output=True)

# Check out the program branch
# Compute ahead/behind counts using the remote-tracking refs so we
# make a decision based on commit delta (not content delta).
ahead = rev_count(f"origin/{default_branch}..origin/{branch}")
behind = rev_count(f"origin/{branch}..origin/{default_branch}")
if ahead is None or behind is None:
print(f" Failed to compute ahead/behind for {branch}")
failed.append(branch)
continue
print(f" ahead={ahead} behind={behind}")

if ahead == 0 and behind > 0:
# All of the branch's commits are already in the default branch.
# Merging would produce a noisy "Merge main into branch" commit
# that re-exposes every historical file as a patch touch — the
# failure mode that triggers gh-aw's E003 (>100 files) when a
# new PR is opened. Fast-forward the canonical branch instead.
# This is lossless because ahead=0 proves every commit on the
# branch is already reachable from the default branch.
ff = subprocess.run(
["git", "checkout", "-B", branch, f"origin/{default_branch}"],
capture_output=True, text=True
)
if ff.returncode != 0:
print(f" Failed to fast-forward {branch}: {ff.stderr}")
failed.append(branch)
continue
# Use --force-with-lease so that if anyone else is simultaneously
# pushing to the branch, the update is rejected rather than
# overwriting their commits.
push = subprocess.run(
["git", "push", "--force-with-lease", "origin", branch],
capture_output=True, text=True
)
if push.returncode != 0:
print(f" Failed to force-push {branch}: {push.stderr}")
failed.append(branch)
continue
print(f" Fast-forwarded {branch} to origin/{default_branch}")
continue

if ahead == 0 and behind == 0:
# Already at default branch — nothing to do.
print(f" {branch} is already up to date with origin/{default_branch}")
continue

if ahead > 0 and behind == 0:
# Unique work preserved; no upstream drift to merge.
print(f" {branch} is ahead of origin/{default_branch} with no upstream drift; nothing to merge.")
continue

# True divergence (ahead > 0 and behind > 0): check out and merge.
checkout = subprocess.run(
["git", "checkout", branch],
["git", "checkout", "-B", branch, f"origin/{branch}"],
capture_output=True, text=True
)
if checkout.returncode != 0:
# Try creating a local tracking branch
checkout = subprocess.run(
["git", "checkout", "-b", branch, f"origin/{branch}"],
capture_output=True, text=True
)
if checkout.returncode != 0:
print(f" Failed to checkout {branch}: {checkout.stderr}")
failed.append(branch)
Expand Down
25 changes: 19 additions & 6 deletions tests/test_scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,18 +786,31 @@ def _load_steps(self):
return step_names

def _load_lock_steps(self):
"""Return the list of step names from .github/workflows/sync-branches.lock.yml."""
"""Return the list of step names from the agent job in
.github/workflows/sync-branches.lock.yml.

Parsed with a regex (rather than PyYAML) so the test has no
external dependencies beyond pytest.
"""
import os
import yaml

lock_path = os.path.join(
os.path.dirname(__file__), "..", ".github", "workflows", "sync-branches.lock.yml"
)
with open(lock_path) as f:
data = yaml.safe_load(f)
# Collect step names from the 'agent' job
steps = data.get("jobs", {}).get("agent", {}).get("steps", [])
return [s.get("name", "") for s in steps if s.get("name")]
content = f.read()
# Restrict to the 'agent:' job body so we don't pick up step names
# from other jobs (e.g. 'activation').
agent_match = re.search(r"^ agent:\n((?: .*\n|\n)+)", content, re.MULTILINE)
if not agent_match:
return []
agent_body = agent_match.group(1)
# Step names appear as either ' - name: <Name>' or
# ' name: <Name>' (when the step starts with '- env:').
step_names = []
for m in re.finditer(r'^\s{6,8}(?:- )?name:\s*(.+)$', agent_body, re.MULTILINE):
step_names.append(m.group(1).strip())
return step_names

def test_cred_step_exists(self):
"""A step that configures Git identity/auth must exist in the source."""
Expand Down
Loading
Loading