Skip to content
Open
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
73 changes: 17 additions & 56 deletions .github/scripts/merge-previous-releases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#
# Key behaviors:
# - Merges older active RCs into the new release branch
# - For merge conflicts, favors the destination branch (new release)
# - Always resets to destination (new RC) content after merge, preventing
# git from creating duplicate content when both branches have the same changes
# - Both branches remain open after merge
# - Fails fast on errors to prevent pushing partial merges
#
Expand Down Expand Up @@ -54,7 +55,8 @@ is_branch_merged() {
git merge-base --is-ancestor "origin/${source_branch}" HEAD 2>/dev/null
}

# Merge a source branch (older release branch) into the current branch (new release branch), favoring current branch on conflicts
# Merge a source branch (older release branch) into the current branch (new release branch),
# always keeping destination content.
merge_with_favor_destination() {
local source_branch="$1"
local dest_branch="$2"
Expand All @@ -70,74 +72,33 @@ merge_with_favor_destination() {
return 1 # Return 1 to indicate skipped
fi

# Try to merge with "ours" strategy for conflicts (favors current branch (new release))
if git_exec merge "origin/${source_branch}" -X ours --no-edit -m "Merge ${source_branch} into ${dest_branch}"; then
echo "✅ Successfully merged ${source_branch} into ${dest_branch}"
return 0 # Return 0 to indicate merged
fi
# Start merge without auto-commit
echo "Starting merge (no auto-commit)..."
git_exec merge "origin/${source_branch}" --no-commit --no-ff || true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why || true here? Is this command expected to fail?


# If merge still fails (shouldn't happen with -X ours, but just in case)
# First verify we're actually in a merge state (MERGE_HEAD exists)
# Verify we're in a merge state
if [[ ! -f .git/MERGE_HEAD ]]; then
echo "❌ Merge failed unexpectedly (no merge state). Aborting."
exit 1
fi

echo "⚠️ Merge conflict detected! Resolving by favoring destination branch (new release)..."

# Resolve any unmerged (conflicted) files by keeping destination version.
#
# Git merge terminology in this context:
Comment thread
cursor[bot] marked this conversation as resolved.
# - "ours" = destination branch (new release, e.g., release/2.1.2) - the branch we're ON
# - "theirs" = source branch (older release, e.g., release/2.1.1) - the branch being merged IN
#
# We favor "ours" (destination) because the new release branch should take precedence.
local conflict_files
local conflict_count=0
conflict_files=$(git diff --name-only --diff-filter=U 2>/dev/null || true)
if [[ -n "$conflict_files" ]]; then
while IFS= read -r file; do
if [[ -n "$file" ]]; then
echo " - Conflict in: ${file} → keeping destination version"
# Try to checkout destination version ("ours")
# If checkout fails, the file was deleted in destination - keep that deletion
if git checkout --ours "$file" 2>/dev/null; then
git add "$file"
else
# Modify/delete conflict scenario:
# - Destination branch (new release) ALREADY deleted this file intentionally
# - Source branch (older release) modified this file
# - Git doesn't know which action to keep
#
# We use "git rm" to confirm the deletion should stand (destination wins).
# This does NOT delete a file that exists - it tells Git "keep the file deleted".
# The --force flag is required because the file is in a conflicted/unmerged state.
echo " (file was deleted in destination, keeping deletion)"
git rm --force "$file" 2>/dev/null || true
fi
((conflict_count++)) || true
fi
done <<< "$conflict_files"
echo "✅ Resolved ${conflict_count} conflict(s) by keeping destination branch version"
fi
# Reset all files to destination (new RC) version
echo "Resetting all files to destination branch version..."
git checkout HEAD -- . 2>/dev/null || true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gudahtt
Two edge cases: a) if old RC has content not yet in main, it gets discarded; b) if destination deleted file that source has, it gets un-deleted. Not sure if this can happen in real life?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We expect all changes in the old RC to be on main in all cases, so I'm not worried about that case. If that happens, we're already in a mess that we'd have to clean up manually.

For the second case, could you elaborate? Why would the file get un-deleted? The goal here is to match the destination, so we don't want to restore files the destination has deleted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't recall how git checkout handles deleted files. If you're right about this, we may need special handling here of deleted files to ensure they remain deleted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran a quick test. After git merge --no-commit, if old RC has a file that destination doesn't have:

- git checkout HEAD -- . - file stays staged (overlay mode, doesn't remove files)
- git checkout --no-overlay HEAD -- . - file gets removed, matches destination exactly

From git-checkout docs:

  • default overlay mode: git checkout never removes files from the index or working tree
  • --no-overlay: files present locally but not in are removed

So git checkout HEAD -- . only restores paths that exist in HEAD, it doesn't delete extras brought in by the merge.

So looks like we need to git checkout --no-overlay HEAD -- . (or git merge -s ours)?


# Now add any remaining files (non-conflicted changes), excluding github-tools directory
# Stage all changes
git_exec add -- . ':!github-tools'

# Complete the merge - always commit when in merge state, even if no content changes
# Check if we're in a merge state (MERGE_HEAD exists)
if [[ -f .git/MERGE_HEAD ]]; then
if ! git_exec commit -m "Merge ${source_branch} into ${dest_branch}" --no-verify --allow-empty; then
echo "Failed to commit merge of ${source_branch}"
exit 1
fi
# Commit the merge
if ! git_exec commit -m "Merge ${source_branch} into ${dest_branch}" --no-verify --allow-empty; then
echo "❌ Failed to commit merge of ${source_branch}"
exit 1
fi

echo "✅ Successfully merged ${source_branch} into ${dest_branch} (${conflict_count} conflict(s) resolved)"
echo "✅ Successfully merged ${source_branch} into ${dest_branch}"
return 0 # Return 0 to indicate merged
}


# Find release branches that still have an open/draft release PR targeting stable.
# Returns: newline-separated list of branch names (e.g., release/7.36.0)
get_active_release_branches() {
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Restrict `merge-previous-releases` to older branches that still have an open/draft release PR targeting `stable`, instead of every older `release/*` branch ([#282](https://github.com/MetaMask/github-tools/pull/282))
- Fix `merge-previous-releases` creating duplicate content when git auto-merges identical changes from both branches ([#283](https://github.com/MetaMask/github-tools/pull/283))

## [1.18.0]

Expand Down