Skip to content

Commit 6465e3f

Browse files
authored
ci: fix sync-upstream auto-resolve for modify/delete conflicts (#68)
* ci: fix sync-upstream auto-resolve for modify/delete conflicts The previous logic used 'xargs git checkout --theirs' over all conflicting workflow files in one batch. When upstream deletes a file we still keep (e.g. publish-regular-docker-image-on-demand.yml in v11.0.3) the checkout fails with 'does not have their version' and exit 123 kills the whole step before the PR fallback can run. Move the resolution logic into .github/scripts/auto-resolve-merge-conflicts.sh and iterate per file, dispatching on git status code: - DU (we deleted, they modified) -> git rm (keep our deletion) - UD (we modified, they deleted) -> keep ours if in KEEP_WORKFLOWS, else rm - UU (content conflict) -> KEEP_WORKFLOWS keep ours, others take theirs - DD / unhandled -> warn, fall through to manual review The script exits 1 when conflicts remain so the caller can decide whether to abort (merge step) or commit a WIP state (PR-on-conflict step). * ci: warn on unhandled conflict statuses in auto-resolve script Address review nits on PR #68: - Add fallthrough warning for non-VERSION, non-workflow conflicts so they show up in CI logs instead of being silently left for the exit-1 path. - Add same fallthrough for VERSION_FILES with unexpected status (e.g. DU/UD). - Make 'git checkout --ours' for KEEP_WORKFLOWS symmetric with --theirs: guard with 2>/dev/null and emit warning on failure.
1 parent b50e2a1 commit 6465e3f

2 files changed

Lines changed: 155 additions & 35 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env bash
2+
# Auto-resolve common merge conflicts after `git merge <upstream-tag>`.
3+
# Called by .github/workflows/sync-upstream.yml.
4+
#
5+
# Resolution rules:
6+
# - VERSION_FILES (mix.exs, CHANGELOG.md, etc.): always take upstream content
7+
# - .github/workflows/* in KEEP_WORKFLOWS: keep DOS version
8+
# (these files are DOS-authored and must survive when upstream renames/deletes them)
9+
# - .github/workflows/* not in KEEP_WORKFLOWS: prefer upstream content,
10+
# accept either side's deletion (upstream-only files get pruned in the next step anyway)
11+
#
12+
# After this script runs, `git status` may still show unmerged paths for content
13+
# conflicts in non-workflow files. Exits 0 when fully resolved, 1 when conflicts remain.
14+
#
15+
# NOTE: deliberately does NOT use `set -e` so a single file failure does not
16+
# abort the whole batch (previous bug: a single `xargs git checkout --theirs`
17+
# failure on a modify/delete conflict killed the entire step with exit 123).
18+
19+
set -uo pipefail
20+
21+
VERSION_FILES=(
22+
mix.exs
23+
rel/config.exs
24+
docker/Makefile
25+
CHANGELOG.md
26+
apps/block_scout_web/mix.exs
27+
apps/ethereum_jsonrpc/mix.exs
28+
apps/explorer/mix.exs
29+
apps/indexer/mix.exs
30+
apps/nft_media_handler/mix.exs
31+
apps/utils/mix.exs
32+
)
33+
34+
KEEP_WORKFLOWS=(
35+
.github/workflows/deploy-config.yml
36+
.github/workflows/publish-regular-docker-image-on-demand.yml
37+
.github/workflows/sync-upstream.yml
38+
)
39+
40+
is_in_array() {
41+
local needle="$1"; shift
42+
local hay
43+
for hay in "$@"; do [ "$hay" = "$needle" ] && return 0; done
44+
return 1
45+
}
46+
47+
# `git status --porcelain` unmerged status codes (XY columns, see git-status(1)):
48+
# DD both deleted
49+
# AU added by us
50+
# UD deleted by them (we modified, they deleted)
51+
# UA added by them
52+
# DU deleted by us (we deleted, they modified)
53+
# AA both added
54+
# UU both modified
55+
while IFS= read -r line; do
56+
[ -z "$line" ] && continue
57+
status="${line:0:2}"
58+
file="${line:3}"
59+
60+
# VERSION_FILES: always take upstream (content conflict only)
61+
if is_in_array "$file" "${VERSION_FILES[@]}"; then
62+
case "$status" in
63+
UU|AU|UA)
64+
echo " Resolve VERSION (theirs): $file"
65+
if git checkout --theirs -- "$file" 2>/dev/null; then
66+
git add -- "$file"
67+
else
68+
echo "::warning::Cannot checkout --theirs for $file ($status)"
69+
fi
70+
continue
71+
;;
72+
*)
73+
echo "::warning::VERSION file $file has unhandled conflict status $status - left for manual review"
74+
continue
75+
;;
76+
esac
77+
fi
78+
79+
# .github/workflows/*
80+
if [[ "$file" == .github/workflows/* ]]; then
81+
case "$status" in
82+
DU)
83+
# We deleted, upstream modified - keep our deletion.
84+
echo " Resolve workflow (keep our deletion): $file"
85+
git rm -f -- "$file" >/dev/null 2>&1 || true
86+
;;
87+
UD)
88+
# We modified, upstream deleted.
89+
if is_in_array "$file" "${KEEP_WORKFLOWS[@]}"; then
90+
echo " Resolve workflow (keep ours; upstream deleted): $file"
91+
git add -- "$file"
92+
else
93+
echo " Resolve workflow (drop; upstream deleted): $file"
94+
git rm -f -- "$file" >/dev/null 2>&1 || true
95+
fi
96+
;;
97+
UU|AU|UA)
98+
if is_in_array "$file" "${KEEP_WORKFLOWS[@]}"; then
99+
echo " Resolve workflow (keep ours; content conflict): $file"
100+
if git checkout --ours -- "$file" 2>/dev/null; then
101+
git add -- "$file"
102+
else
103+
echo "::warning::Cannot checkout --ours for $file ($status)"
104+
fi
105+
else
106+
echo " Resolve workflow (theirs; content conflict): $file"
107+
if git checkout --theirs -- "$file" 2>/dev/null; then
108+
git add -- "$file"
109+
else
110+
echo "::warning::Cannot checkout --theirs for $file ($status)"
111+
fi
112+
fi
113+
;;
114+
DD)
115+
echo " Resolve workflow (both deleted): $file"
116+
git rm -f -- "$file" >/dev/null 2>&1 || true
117+
;;
118+
*)
119+
echo "::warning::Unhandled workflow conflict status $status for $file"
120+
;;
121+
esac
122+
continue
123+
fi
124+
125+
# Non-VERSION, non-workflow path with a conflict - left for manual review.
126+
# Logged so it's easy to spot in CI output before the final summary.
127+
echo "::warning::Conflict in $file ($status) - left for manual review"
128+
done < <(git status --porcelain | grep -E '^(UU|DU|UD|AA|DD|AU|UA) ' || true)
129+
130+
# Report remaining conflicts; caller decides whether to abort or proceed (e.g. for PR fallback).
131+
REMAINING=$(git diff --name-only --diff-filter=U)
132+
if [ -n "$REMAINING" ]; then
133+
echo "::warning::Remaining unresolved conflicts:"
134+
echo "$REMAINING" | sed 's/^/ - /'
135+
exit 1
136+
fi
137+
138+
exit 0

.github/workflows/sync-upstream.yml

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ jobs:
6363
git config user.name "github-actions[bot]"
6464
git config user.email "github-actions[bot]@users.noreply.github.com"
6565
66-
# Files that MUST match upstream exactly (versions, deps, build config)
66+
# Files that MUST match upstream exactly (versions, deps, build config).
67+
# Kept in sync with .github/scripts/auto-resolve-merge-conflicts.sh.
6768
VERSION_FILES=(
6869
mix.exs
6970
rel/config.exs
@@ -81,39 +82,24 @@ jobs:
8182
if git merge "$TAG" --no-edit -m "Merge upstream $TAG"; then
8283
echo "Clean merge succeeded"
8384
else
84-
# Merge failed auto-resolve conflicts
85+
# Merge failed - auto-resolve conflicts
8586
echo "::notice::Merge conflict detected, attempting auto-resolve..."
8687
87-
for f in "${VERSION_FILES[@]}"; do
88-
if git diff --name-only --diff-filter=U | grep -qx "$f"; then
89-
echo " Auto-resolve (theirs): $f"
90-
git checkout --theirs "$f"
91-
git add "$f"
92-
fi
93-
done
94-
95-
# Accept upstream for ALL upstream workflow files (version bumps)
96-
# Our custom workflows (docker-publish, deploy-config, sync-upstream) won't conflict
97-
CONFLICT_WORKFLOWS=$(git diff --name-only --diff-filter=U | grep '^\.github/workflows/' || true)
98-
if [ -n "$CONFLICT_WORKFLOWS" ]; then
99-
echo " Auto-resolve (theirs): workflow files"
100-
echo "$CONFLICT_WORKFLOWS" | xargs git checkout --theirs
101-
echo "$CONFLICT_WORKFLOWS" | xargs git add
102-
fi
103-
104-
# Check if any conflicts remain
105-
REMAINING=$(git diff --name-only --diff-filter=U || true)
106-
if [ -n "$REMAINING" ]; then
88+
if bash .github/scripts/auto-resolve-merge-conflicts.sh; then
89+
git commit --no-edit -m "Merge upstream $TAG (auto-resolved conflicts)"
90+
else
91+
# Conflicts remain - capture list, abort merge, hand off to PR step.
92+
REMAINING=$(git diff --name-only --diff-filter=U || true)
10793
echo "::warning::Unresolvable conflicts in: $REMAINING"
10894
git merge --abort
10995
echo "result=conflict" >> $GITHUB_OUTPUT
110-
echo "conflict_files<<EOF" >> $GITHUB_OUTPUT
111-
echo "$REMAINING" >> $GITHUB_OUTPUT
112-
echo "EOF" >> $GITHUB_OUTPUT
96+
{
97+
echo "conflict_files<<EOF"
98+
echo "$REMAINING"
99+
echo "EOF"
100+
} >> $GITHUB_OUTPUT
113101
exit 0
114102
fi
115-
116-
git commit --no-edit -m "Merge upstream $TAG (auto-resolved conflicts)"
117103
fi
118104
119105
# Post-merge: ensure version files match upstream EXACTLY
@@ -217,14 +203,10 @@ jobs:
217203
git checkout -b "$BRANCH"
218204
git merge "$TAG" --no-edit || true
219205
220-
# Auto-resolve what we can (same strategy as above)
221-
for f in mix.exs rel/config.exs docker/Makefile CHANGELOG.md apps/*/mix.exs; do
222-
if git diff --name-only --diff-filter=U | grep -qx "$f" 2>/dev/null; then
223-
git checkout --theirs "$f" && git add "$f"
224-
fi
225-
done
226-
CONFLICT_WF=$(git diff --name-only --diff-filter=U | grep '^\.github/workflows/' || true)
227-
[ -n "$CONFLICT_WF" ] && echo "$CONFLICT_WF" | xargs git checkout --theirs && echo "$CONFLICT_WF" | xargs git add
206+
# Auto-resolve what we can. Script exits 1 when conflicts remain;
207+
# that is expected here - we still commit and push the WIP state
208+
# so reviewers see only the unresolvable conflicts.
209+
bash .github/scripts/auto-resolve-merge-conflicts.sh || true
228210
229211
git add -A
230212
git commit -m "WIP: Merge upstream $TAG (has conflicts)" --no-verify || true

0 commit comments

Comments
 (0)