From 1a6652256d81be6daacdf0ed64f103ff75040166 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:01:12 +0000 Subject: [PATCH 1/2] Initial plan From c48da3a75ec53b47aa79b2ae1a3acb62dfa72f14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:08:33 +0000 Subject: [PATCH 2/2] Fast-forward autoloop branches when ahead=0 instead of merging Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/fd8b9575-c039-4e3a-ad47-adf60b149480 Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- .github/workflows/autoloop.md | 32 ++++++++++-- .github/workflows/sync-branches.lock.yml | 2 +- .github/workflows/sync-branches.md | 63 +++++++++++++++++++++++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/autoloop.md b/.github/workflows/autoloop.md index 222c9d2f..04857054 100644 --- a/.github/workflows/autoloop.md +++ b/.github/workflows/autoloop.md @@ -378,11 +378,35 @@ Each run executes **one iteration for the single selected program**: - Program `build-tsb-pandas-typescript-migration` → branch `autoloop/build-tsb-pandas-typescript-migration` ```bash - git fetch origin + git fetch origin main if git ls-remote --exit-code origin autoloop/{program-name}; then - # Branch exists — check it out and merge the default branch - git checkout -b autoloop/{program-name} origin/autoloop/{program-name} - git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}" + # Branch exists — fetch it too so the ahead/behind counts below are + # computed against up-to-date local copies of the remote tips. + git fetch origin autoloop/{program-name} + + ahead=$(git rev-list --count origin/main..origin/autoloop/{program-name}) + behind=$(git rev-list --count origin/autoloop/{program-name}..origin/main) + + if [ "$ahead" = "0" ] && [ "$behind" != "0" ]; then + # All of the branch's commits are already in main (typical case after a + # successful merge of the previous iteration's PR). A merge here 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 to main instead. This is lossless because ahead=0 + # proves every commit on the branch is already reachable from main. + git checkout -B autoloop/{program-name} origin/main + git push --force-with-lease origin autoloop/{program-name} + elif [ "$ahead" != "0" ] && [ "$behind" != "0" ]; then + # True divergence: branch has unique commits AND main has moved on. + # Merge main into the branch so the iteration builds on the latest code. + git checkout -B autoloop/{program-name} origin/autoloop/{program-name} + git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}" + else + # Already at main (ahead=0, behind=0) or only ahead of main (ahead>0, + # behind=0). Nothing to merge — just check out the branch. + git checkout -B autoloop/{program-name} origin/autoloop/{program-name} + fi else # Branch does not exist — create it from the default branch git checkout -b autoloop/{program-name} origin/main diff --git a/.github/workflows/sync-branches.lock.yml b/.github/workflows/sync-branches.lock.yml index 82909525..efa596f1 100644 --- a/.github/workflows/sync-branches.lock.yml +++ b/.github/workflows/sync-branches.lock.yml @@ -321,7 +321,7 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GITHUB_REPOSITORY: ${{ github.repository }} name: Merge default branch into all autoloop program branches - run: "python3 - << 'PYEOF'\nimport os, re, subprocess, sys\n\ntoken = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\ndefault_branch = os.environ.get(\"DEFAULT_BRANCH\", \"main\")\n\n# List all remote branches matching the autoloop/* pattern\nresult = subprocess.run(\n [\"git\", \"branch\", \"-r\", \"--list\", \"origin/autoloop/*\"],\n capture_output=True, text=True\n)\nif result.returncode != 0:\n print(f\"Failed to list remote branches: {result.stderr}\")\n sys.exit(0)\n\nall_branches = [b.strip().replace(\"origin/\", \"\") for b in result.stdout.strip().split(\"\\n\") if b.strip()]\n\n# Filter to canonical branches only: autoloop/{name} without hash suffixes.\n# Stale branches created by the framework (e.g. autoloop/name-a1b2c3d4e5f6g7h8)\n# are skipped — they are not the long-running program branches.\n_hash_suffix = re.compile(r'-[0-9a-f]{16}$')\nbranches = [b for b in all_branches if not _hash_suffix.search(b)]\nskipped_branches = [b for b in all_branches if _hash_suffix.search(b)]\n\nif skipped_branches:\n print(f\"Skipping {len(skipped_branches)} stale branch(es) with hash suffixes: {skipped_branches}\")\n\nif not branches:\n print(\"No canonical autoloop/* branches found. Nothing to sync.\")\n sys.exit(0)\n\nprint(f\"Found {len(branches)} canonical autoloop branch(es) to sync: {branches}\")\n\nfailed = []\nfor branch in branches:\n print(f\"\\n--- Syncing {branch} with {default_branch} ---\")\n\n # Fetch both branches\n subprocess.run([\"git\", \"fetch\", \"origin\", branch], capture_output=True)\n subprocess.run([\"git\", \"fetch\", \"origin\", default_branch], capture_output=True)\n\n # Check out the program branch\n checkout = subprocess.run(\n [\"git\", \"checkout\", branch],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n # Try creating a local tracking branch\n checkout = subprocess.run(\n [\"git\", \"checkout\", \"-b\", branch, f\"origin/{branch}\"],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n print(f\" Failed to checkout {branch}: {checkout.stderr}\")\n failed.append(branch)\n continue\n\n # Merge the default branch into the program branch\n merge = subprocess.run(\n [\"git\", \"merge\", f\"origin/{default_branch}\", \"--no-edit\",\n \"-m\", f\"Merge {default_branch} into {branch}\"],\n capture_output=True, text=True\n )\n if merge.returncode != 0:\n print(f\" Merge conflict or failure for {branch}: {merge.stderr}\")\n # Abort the merge to leave a clean state\n subprocess.run([\"git\", \"merge\", \"--abort\"], capture_output=True)\n failed.append(branch)\n continue\n\n # Push the updated branch\n push = subprocess.run(\n [\"git\", \"push\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n\n print(f\" Successfully synced {branch}\")\n\n# Return to default branch\nsubprocess.run([\"git\", \"checkout\", default_branch], capture_output=True)\n\nif failed:\n print(f\"\\n⚠️ Failed to sync {len(failed)} branch(es): {failed}\")\n print(\"These branches may need manual conflict resolution.\")\n # Don't fail the workflow — log the issue but continue\nelse:\n print(f\"\\n✅ All {len(branches)} branch(es) synced successfully.\")\nPYEOF\n" + run: "python3 - << 'PYEOF'\nimport os, re, subprocess, sys\n\ntoken = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\ndefault_branch = os.environ.get(\"DEFAULT_BRANCH\", \"main\")\n\n# List all remote branches matching the autoloop/* pattern\nresult = subprocess.run(\n [\"git\", \"branch\", \"-r\", \"--list\", \"origin/autoloop/*\"],\n capture_output=True, text=True\n)\nif result.returncode != 0:\n print(f\"Failed to list remote branches: {result.stderr}\")\n sys.exit(0)\n\nall_branches = [b.strip().replace(\"origin/\", \"\") for b in result.stdout.strip().split(\"\\n\") if b.strip()]\n\n# Filter to canonical branches only: autoloop/{name} without hash suffixes.\n# Stale branches created by the framework (e.g. autoloop/name-a1b2c3d4e5f6g7h8)\n# are skipped — they are not the long-running program branches.\n_hash_suffix = re.compile(r'-[0-9a-f]{16}$')\nbranches = [b for b in all_branches if not _hash_suffix.search(b)]\nskipped_branches = [b for b in all_branches if _hash_suffix.search(b)]\n\nif skipped_branches:\n print(f\"Skipping {len(skipped_branches)} stale branch(es) with hash suffixes: {skipped_branches}\")\n\nif not branches:\n print(\"No canonical autoloop/* branches found. Nothing to sync.\")\n sys.exit(0)\n\nprint(f\"Found {len(branches)} canonical autoloop branch(es) to sync: {branches}\")\n\nfailed = []\nfor branch in branches:\n print(f\"\\n--- Syncing {branch} with {default_branch} ---\")\n\n # Fetch both branches\n subprocess.run([\"git\", \"fetch\", \"origin\", branch], capture_output=True)\n subprocess.run([\"git\", \"fetch\", \"origin\", default_branch], capture_output=True)\n\n # Check out the program branch\n checkout = subprocess.run(\n [\"git\", \"checkout\", branch],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n # Try creating a local tracking branch\n checkout = subprocess.run(\n [\"git\", \"checkout\", \"-b\", branch, f\"origin/{branch}\"],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n print(f\" Failed to checkout {branch}: {checkout.stderr}\")\n failed.append(branch)\n continue\n\n # Determine whether the branch can be fast-forwarded to the default\n # branch (ahead=0) or has truly diverged and needs a merge.\n ahead_proc = subprocess.run(\n [\"git\", \"rev-list\", \"--count\", f\"origin/{default_branch}..origin/{branch}\"],\n capture_output=True, text=True,\n )\n behind_proc = subprocess.run(\n [\"git\", \"rev-list\", \"--count\", f\"origin/{branch}..origin/{default_branch}\"],\n capture_output=True, text=True,\n )\n if ahead_proc.returncode != 0 or behind_proc.returncode != 0:\n # Don't guess — a failed rev-list with empty stdout would\n # otherwise be parsed as 0 below and could trigger an\n # incorrect fast-forward that loses commits.\n print(f\" Failed to compute ahead/behind for {branch}: \"\n f\"ahead.rc={ahead_proc.returncode} stderr={ahead_proc.stderr.strip()!r} \"\n f\"behind.rc={behind_proc.returncode} stderr={behind_proc.stderr.strip()!r}\")\n failed.append(branch)\n continue\n try:\n ahead = int(ahead_proc.stdout.strip())\n behind = int(behind_proc.stdout.strip())\n except ValueError:\n print(f\" Failed to parse ahead/behind counts for {branch}: \"\n f\"ahead={ahead_proc.stdout!r} behind={behind_proc.stdout!r}\")\n failed.append(branch)\n continue\n\n if behind == 0:\n # Branch already contains every commit on the default branch.\n print(f\" {branch} is already up to date with {default_branch} (ahead={ahead}, behind=0)\")\n continue\n\n if ahead == 0:\n # Lossless fast-forward: every commit on the branch is already\n # reachable from the default branch (typical case once the\n # previous iteration's PR has been merged). A real merge here\n # would create a \"Merge default into branch\" commit that re-\n # exposes every historical file as a patch touch — the noise\n # that trips gh-aw's MAX_FILES limit when the next iteration\n # opens a new PR. Reset the branch to the default branch's HEAD\n # and force-push (with lease) instead.\n reset = subprocess.run(\n [\"git\", \"reset\", \"--hard\", f\"origin/{default_branch}\"],\n capture_output=True, text=True,\n )\n if reset.returncode != 0:\n print(f\" Failed to fast-forward {branch}: {reset.stderr}\")\n failed.append(branch)\n continue\n push = subprocess.run(\n [\"git\", \"push\", \"--force-with-lease\", \"origin\", branch],\n capture_output=True, text=True,\n )\n if push.returncode != 0:\n print(f\" Failed to push fast-forward of {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n print(f\" Fast-forwarded {branch} to {default_branch} (was behind by {behind})\")\n continue\n\n # True divergence (ahead>0 and behind>0): merge the default branch in.\n merge = subprocess.run(\n [\"git\", \"merge\", f\"origin/{default_branch}\", \"--no-edit\",\n \"-m\", f\"Merge {default_branch} into {branch}\"],\n capture_output=True, text=True\n )\n if merge.returncode != 0:\n print(f\" Merge conflict or failure for {branch}: {merge.stderr}\")\n # Abort the merge to leave a clean state\n subprocess.run([\"git\", \"merge\", \"--abort\"], capture_output=True)\n failed.append(branch)\n continue\n\n # Push the updated branch\n push = subprocess.run(\n [\"git\", \"push\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n\n print(f\" Successfully synced {branch}\")\n\n# Return to default branch\nsubprocess.run([\"git\", \"checkout\", default_branch], capture_output=True)\n\nif failed:\n print(f\"\\n⚠️ Failed to sync {len(failed)} branch(es): {failed}\")\n print(\"These branches may need manual conflict resolution.\")\n # Don't fail the workflow — log the issue but continue\nelse:\n print(f\"\\n✅ All {len(branches)} branch(es) synced successfully.\")\nPYEOF\n" - name: Configure Git credentials env: diff --git a/.github/workflows/sync-branches.md b/.github/workflows/sync-branches.md index 772e2438..a32905ac 100644 --- a/.github/workflows/sync-branches.md +++ b/.github/workflows/sync-branches.md @@ -82,7 +82,68 @@ steps: failed.append(branch) continue - # Merge the default branch into the program branch + # Determine whether the branch can be fast-forwarded to the default + # branch (ahead=0) or has truly diverged and needs a merge. + ahead_proc = subprocess.run( + ["git", "rev-list", "--count", f"origin/{default_branch}..origin/{branch}"], + capture_output=True, text=True, + ) + behind_proc = subprocess.run( + ["git", "rev-list", "--count", f"origin/{branch}..origin/{default_branch}"], + capture_output=True, text=True, + ) + if ahead_proc.returncode != 0 or behind_proc.returncode != 0: + # Don't guess — a failed rev-list with empty stdout would + # otherwise be parsed as 0 below and could trigger an + # incorrect fast-forward that loses commits. + print(f" Failed to compute ahead/behind for {branch}: " + f"ahead.rc={ahead_proc.returncode} stderr={ahead_proc.stderr.strip()!r} " + f"behind.rc={behind_proc.returncode} stderr={behind_proc.stderr.strip()!r}") + failed.append(branch) + continue + try: + ahead = int(ahead_proc.stdout.strip()) + behind = int(behind_proc.stdout.strip()) + except ValueError: + print(f" Failed to parse ahead/behind counts for {branch}: " + f"ahead={ahead_proc.stdout!r} behind={behind_proc.stdout!r}") + failed.append(branch) + continue + + if behind == 0: + # Branch already contains every commit on the default branch. + print(f" {branch} is already up to date with {default_branch} (ahead={ahead}, behind=0)") + continue + + if ahead == 0: + # Lossless fast-forward: every commit on the branch is already + # reachable from the default branch (typical case once the + # previous iteration's PR has been merged). A real merge here + # would create a "Merge default into branch" commit that re- + # exposes every historical file as a patch touch — the noise + # that trips gh-aw's MAX_FILES limit when the next iteration + # opens a new PR. Reset the branch to the default branch's HEAD + # and force-push (with lease) instead. + reset = subprocess.run( + ["git", "reset", "--hard", f"origin/{default_branch}"], + capture_output=True, text=True, + ) + if reset.returncode != 0: + print(f" Failed to fast-forward {branch}: {reset.stderr}") + failed.append(branch) + continue + push = subprocess.run( + ["git", "push", "--force-with-lease", "origin", branch], + capture_output=True, text=True, + ) + if push.returncode != 0: + print(f" Failed to push fast-forward of {branch}: {push.stderr}") + failed.append(branch) + continue + print(f" Fast-forwarded {branch} to {default_branch} (was behind by {behind})") + continue + + # True divergence (ahead>0 and behind>0): merge the default branch in. merge = subprocess.run( ["git", "merge", f"origin/{default_branch}", "--no-edit", "-m", f"Merge {default_branch} into {branch}"],