Skip to content

fix(sync): use git merge-tree for false-positive-free conflict detection#16

Merged
gerchowl merged 1 commit intodevfrom
chore/sync-main-to-dev-merge-tree
Apr 15, 2026
Merged

fix(sync): use git merge-tree for false-positive-free conflict detection#16
gerchowl merged 1 commit intodevfrom
chore/sync-main-to-dev-merge-tree

Conversation

@gerchowl
Copy link
Copy Markdown
Contributor

Summary

Replaces the broken `Detect merge conflicts` step in `sync-main-to-dev.yml` with `git merge-tree --write-tree`, matching the current upstream vig-os/devcontainer implementation.

Not actually a new upstream bug: the current vig-os/devcontainer template already uses `git merge-tree`. Our scaffolded copy is stale and still runs the old `git merge --no-commit --no-ff` pattern. No upstream issue to file — this is a stale-template sync.

What was wrong

On PR #13 (the auto-created sync-back after the initial dev→main bootstrap), sync-main-to-dev opened a PR labeled `merge-conflict` and titled `(conflicts)`, but the PR had `mergeable: MERGEABLE`, 0 files changed, and all CI checks green. Locally, `git merge --no-commit --no-ff origin/main` on dev worked fine.

Root cause: `git merge --no-ff` has degenerate behavior when one side is already an ancestor of the other. After a dev→main merge PR, main's tip is a merge commit whose second parent is dev's old tip, so dev is an ancestor of main. In that topology `git merge --no-ff` can exit non-zero without producing a real conflict, which the workflow then mislabels. The `2>/dev/null` redirect compounds the problem by hiding all diagnostic output.

Fix

```yaml
if merge_out="$(git merge-tree --write-tree origin/dev origin/main 2>&1)"; then
echo "conflict=false" >> "$GITHUB_OUTPUT"
else
merge_rc=$?
if [ "${merge_rc}" -eq 1 ]; then
echo "conflict=true" >> "$GITHUB_OUTPUT"
...
else
echo "::error::git merge-tree failed with exit code ${merge_rc}"
exit "${merge_rc}"
fi
fi
```

`git merge-tree --write-tree` does an in-memory trial merge without touching the working tree or index. Exit 0 = clean (outputs merged tree SHA), exit 1 = conflicts, anything else = real error. stderr is now captured instead of dropped, so future misbehavior is visible.

Test plan

  • CI passes
  • On next dev→main merge, `sync-main-to-dev` opens the PR with a clean title (not `(conflicts)`) and no `merge-conflict` label

Out of scope

The upstream workflow also adds container-based execution, a `resolve-image` action, retry wrappers, and a `git push` flow for branch creation. Those require infrastructure (`resolve-image` action, the vigOS devcontainer image) that we don't have locally. This PR is the surgical fix; a broader template resync is a separate task.

🤖 Generated with Claude Code

The previous `Detect merge conflicts` step ran
`git merge --no-commit --no-ff origin/main 2>/dev/null` and
branched on exit code to set `conflict=true|false`. Two problems:

1. `2>/dev/null` hid git's stderr entirely, so whenever the step
   misfired we got no diagnostic — just a `(conflicts)` label on
   a PR that was actually clean.
2. `git merge --no-ff` has degenerate behavior when one side is
   an ancestor of the other. After a dev→main merge PR, `main`'s
   tip is a merge commit whose second parent is `dev`'s old tip,
   so `dev` is an ancestor of `main`. `git merge --no-ff` in this
   configuration can exit non-zero without an actual conflict,
   which the workflow then mislabels.

This was observed live on PR #13 (the sync-back from the initial
dev→main bootstrap): the PR had 0 files changed and was fully
mergeable, but arrived labeled `merge-conflict` and titled
"(conflicts)".

Replace with `git merge-tree --write-tree origin/dev origin/main`,
which does an in-memory trial merge without touching the working
tree or index. Exit semantics:
- 0 = clean merge, output is the merged tree SHA
- 1 = conflicts, output contains conflict markers
- anything else = real error (propagate)

Stderr is now captured into a variable rather than dropped, so any
future misbehavior is visible in the run log.

This pattern is lifted from the current
`vig-os/devcontainer/assets/workspace/.github/workflows/sync-main-to-dev.yml`
upstream — confirming this isn't a novel fix but an
already-corrected upstream implementation that our stale scaffolded
copy never picked up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gerchowl gerchowl merged commit 373809e into dev Apr 15, 2026
14 checks passed
@gerchowl gerchowl deleted the chore/sync-main-to-dev-merge-tree branch April 15, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant