From ed5be1765f847c36626f4965841dcbfa9ab2ee13 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Mon, 8 Dec 2025 13:00:17 -0500 Subject: [PATCH 01/14] INFRA-3187: Automate Merging Release Branch workflow Signed-off-by: Pavel Dvorkin --- .../merge-previous-releases/action.yml | 50 ++++ .github/scripts/merge-previous-releases.js | 252 ++++++++++++++++++ .github/scripts/merge-previous-releases.sh | 224 ++++++++++++++++ .github/workflows/merge-previous-releases.yml | 36 +++ 4 files changed, 562 insertions(+) create mode 100644 .github/actions/merge-previous-releases/action.yml create mode 100644 .github/scripts/merge-previous-releases.js create mode 100644 .github/scripts/merge-previous-releases.sh create mode 100644 .github/workflows/merge-previous-releases.yml diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml new file mode 100644 index 00000000..badda71b --- /dev/null +++ b/.github/actions/merge-previous-releases/action.yml @@ -0,0 +1,50 @@ +name: Merge Previous Releases +description: 'An action to merge previous release branches into a newly created release branch.' + +inputs: + new-release-branch: + required: true + description: 'The newly created release branch (e.g., release/2.1.2)' + github-token: + description: 'GitHub token used for authentication.' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repository, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - name: Checkout GitHub tools repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: ./github-tools + + - name: Set Git user and email + shell: bash + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run merge previous releases script + id: merge-releases + env: + NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} + GITHUB_TOKEN: ${{ inputs.github-token }} + shell: bash + run: | + # Ensure github-tools is in .gitignore to prevent it from being committed + if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then + echo "github-tools/" >> .gitignore + echo "Added github-tools/ to .gitignore" + fi + + # Execute the script from github-tools + bash ./github-tools/.github/scripts/merge-previous-releases.sh diff --git a/.github/scripts/merge-previous-releases.js b/.github/scripts/merge-previous-releases.js new file mode 100644 index 00000000..ccbdec46 --- /dev/null +++ b/.github/scripts/merge-previous-releases.js @@ -0,0 +1,252 @@ +#!/usr/bin/env node + +/** + * Merge Previous Release Branches Script + * + * This script is triggered when a new release branch is created (e.g., release/2.1.2). + * It finds all previous release branches and merges them into the new release branch. + * + * Key behaviors: + * - Merges ALL older release branches into the new one + * - For merge conflicts, favors the destination branch (new release) + * - Both branches remain open after merge + * - Fails fast on errors to prevent pushing partial merges + * + * Environment variables: + * - NEW_RELEASE_BRANCH: The newly created release branch (e.g., release/2.1.2) + */ + +const { promisify } = require('util'); +const exec = promisify(require('child_process').exec); + +/** + * Parse a release branch name to extract version components + * @param {string} branchName - Branch name like "release/2.1.2" + * @returns {object|null} - { major, minor, patch } or null if not a valid release branch + */ +function parseReleaseVersion(branchName) { + // Match release/X.Y.Z format (does not match release candidates like release/2.1.2-rc.1) + const match = branchName.match(/^release\/(\d+)\.(\d+)\.(\d+)$/); + if (!match) { + return null; + } + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + }; +} + +/** + * Compare two version objects + * @returns {number} - negative if a < b, positive if a > b, 0 if equal + */ +function compareVersions(a, b) { + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + return a.patch - b.patch; +} + +/** + * Execute a git command and log it + */ +async function gitExec(command, options = {}) { + const { ignoreError = false } = options; + console.log(`Executing: git ${command}`); + try { + const { stdout, stderr } = await exec(`git ${command}`); + if (stdout.trim()) console.log(stdout.trim()); + if (stderr.trim()) console.log(stderr.trim()); + return { stdout, stderr, success: true }; + } catch (error) { + if (ignoreError) { + console.warn(`Warning: ${error.message}`); + return { stdout: error.stdout, stderr: error.stderr, success: false, error }; + } + throw error; + } +} + +/** + * Get all remote release branches + */ +async function getReleaseBranches() { + await gitExec('fetch origin'); + const { stdout } = await exec('git branch -r --list "origin/release/*"'); + return stdout + .split('\n') + .map((branch) => branch.trim().replace('origin/', '')) + .filter((branch) => branch && parseReleaseVersion(branch)); +} + +/** + * Check if a branch has already been merged into the current branch. If yes, skip the merge. + * @param {string} sourceBranch - The branch to check if it has already been merged into the current branch + * @returns {Promise} - True if the branch has already been merged into the current branch, false otherwise + */ +async function isBranchMerged(sourceBranch) { + try { + // Check if the source branch's HEAD is an ancestor of current HEAD + const { stdout } = await exec( + `git merge-base --is-ancestor origin/${sourceBranch} HEAD && echo "merged" || echo "not-merged"`, + ); + return stdout.trim() === 'merged'; + } catch { + // If the command fails, assume not merged + return false; + } +} + +/** + * Merge a source branch into the current branch, favoring current branch on conflicts + * Uses approach similar to stable-sync.js + */ +async function mergeWithFavorDestination(sourceBranch, destBranch) { + console.log(`\n${'='.repeat(60)}`); + console.log(`Merging ${sourceBranch} into ${destBranch}`); + console.log('='.repeat(60)); + + // Check if already merged + const alreadyMerged = await isBranchMerged(sourceBranch); + if (alreadyMerged) { + console.log(`Branch ${sourceBranch} is already merged into ${destBranch}. Skipping.`); + return { skipped: true }; + } + + // Try to merge with "ours" strategy for conflicts (favors current branch) + const mergeResult = await gitExec( + `merge origin/${sourceBranch} -X ours --no-edit -m "Merge ${sourceBranch} into ${destBranch}"`, + { ignoreError: true }, + ); + + if (!mergeResult.success) { + // If merge still fails (shouldn't happen with -X ours, but just in case) + console.log('Merge had conflicts, resolving by favoring destination branch...'); + + // Add all files and resolve conflicts by keeping destination version + await gitExec('add .'); + + // For any remaining conflicts, checkout our version + try { + const { stdout: conflictFiles } = await exec('git diff --name-only --diff-filter=U'); + if (conflictFiles.trim()) { + for (const file of conflictFiles.trim().split('\n')) { + if (file) { + console.log(`Resolving conflict in ${file} by keeping destination version`); + await gitExec(`checkout --ours "${file}"`); + await gitExec(`add "${file}"`); + } + } + } + } catch (e) { + // No conflicts to resolve + } + + // Complete the merge + const { stdout: status } = await exec('git status --porcelain'); + if (status.trim()) { + const commitResult = await gitExec( + `commit -m "Merge ${sourceBranch} into ${destBranch}" --no-verify`, + { ignoreError: true }, + ); + if (!commitResult.success) { + throw new Error(`Failed to commit merge of ${sourceBranch}: ${commitResult.error?.message}`); + } + } + } + + console.log(`Successfully merged ${sourceBranch} into ${destBranch}`); + return { skipped: false }; +} + +async function main() { + const newReleaseBranch = process.env.NEW_RELEASE_BRANCH; + + if (!newReleaseBranch) { + console.error('Error: NEW_RELEASE_BRANCH environment variable is not set'); + process.exit(1); + } + + console.log(`New release branch: ${newReleaseBranch}`); + + const newVersion = parseReleaseVersion(newReleaseBranch); + if (!newVersion) { + console.error( + `Error: ${newReleaseBranch} is not a valid release branch (expected format: release/X.Y.Z)`, + ); + process.exit(1); + } + + console.log(`Parsed version: ${newVersion.major}.${newVersion.minor}.${newVersion.patch}`); + + // Get all release branches + const allReleaseBranches = await getReleaseBranches(); + console.log(`\nFound ${allReleaseBranches.length} release branches:`); + allReleaseBranches.forEach((b) => console.log(` - ${b}`)); + + // Filter to only branches older than the new one, sorted from oldest to newest + const olderBranches = allReleaseBranches + .filter((branch) => { + const version = parseReleaseVersion(branch); + return version && compareVersions(version, newVersion) < 0; + }) + .sort((a, b) => { + const versionA = parseReleaseVersion(a); + const versionB = parseReleaseVersion(b); + return compareVersions(versionA, versionB); + }); + + if (olderBranches.length === 0) { + console.log('\nNo older release branches found. Nothing to merge.'); + return; + } + + console.log(`\nOlder release branches found (oldest to newest):`); + olderBranches.forEach((b) => console.log(` - ${b}`)); + + // Merge all older branches + const branchesToMerge = olderBranches; + console.log(`\nWill merge all ${branchesToMerge.length} older branches.`); + + // We should already be on the new release branch (checkout was done in the workflow) + // But let's verify and ensure we're on the right branch + const { stdout: currentBranch } = await exec('git branch --show-current'); + if (currentBranch.trim() !== newReleaseBranch) { + console.log(`Switching to ${newReleaseBranch}...`); + await gitExec(`checkout ${newReleaseBranch}`); + } + + // Merge each branch (fail fast on errors) + let mergedCount = 0; + let skippedCount = 0; + + for (const olderBranch of branchesToMerge) { + const result = await mergeWithFavorDestination(olderBranch, newReleaseBranch); + if (result.skipped) { + skippedCount++; + } else { + mergedCount++; + } + } + + // Only push if we actually merged something + if (mergedCount > 0) { + console.log('\nPushing merged changes...'); + await gitExec(`push origin ${newReleaseBranch}`); + } else { + console.log('\nNo new merges were made (all branches were already merged).'); + } + + console.log('\n' + '='.repeat(60)); + console.log('Merge complete!'); + console.log(` Branches merged: ${mergedCount}`); + console.log(` Branches skipped (already merged): ${skippedCount}`); + console.log(`All source branches remain open as requested.`); + console.log('='.repeat(60)); +} + +main().catch((error) => { + console.error(`\nFatal error: ${error.message}`); + console.error('Aborting to prevent pushing partial merges.'); + process.exit(1); +}); diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh new file mode 100644 index 00000000..dba94ae1 --- /dev/null +++ b/.github/scripts/merge-previous-releases.sh @@ -0,0 +1,224 @@ +#!/bin/bash + +# Merge Previous Release Branches Script +# +# This script is triggered when a new release branch is created (e.g., release/2.1.2). +# It finds all previous release branches and merges them into the new release branch. +# +# Key behaviors: +# - Merges ALL older release branches into the new one +# - For merge conflicts, favors the destination branch (new release) +# - Both branches remain open after merge +# - Fails fast on errors to prevent pushing partial merges +# +# Environment variables: +# - NEW_RELEASE_BRANCH: The newly created release branch (e.g., release/2.1.2) + +set -e + +# Parse a release branch name to extract version components +# Returns: "major minor patch" or empty string if not valid +parse_release_version() { + local branch_name="$1" + if [[ "$branch_name" =~ ^release/([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}" + fi +} + +# Check if version A is older than version B +# Returns: exit code 0 if a < b, 1 otherwise +is_version_older() { + local a_major="$1" a_minor="$2" a_patch="$3" + local b_major="$4" b_minor="$5" b_patch="$6" + + if [[ "$a_major" -lt "$b_major" ]]; then return 0; fi + if [[ "$a_major" -gt "$b_major" ]]; then return 1; fi + if [[ "$a_minor" -lt "$b_minor" ]]; then return 0; fi + if [[ "$a_minor" -gt "$b_minor" ]]; then return 1; fi + if [[ "$a_patch" -lt "$b_patch" ]]; then return 0; fi + return 1 +} + +# Execute a git command and log it +git_exec() { + echo "Executing: git $*" + git "$@" +} + +# Check if a branch has already been merged into the current branch. If yes, we skip merging it again. +# Returns: exit code 0 if merged, 1 if not merged +is_branch_merged() { + local source_branch="$1" + 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_with_favor_destination() { + local source_branch="$1" + local dest_branch="$2" + + echo "" + echo "============================================================" + echo "Merging ${source_branch} into ${dest_branch}" + echo "============================================================" + + # Check if already merged + if is_branch_merged "$source_branch"; then + echo "Branch ${source_branch} is already merged into ${dest_branch}. Skipping." + 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 + + # If merge still fails (shouldn't happen with -X ours, but just in case) + echo "Merge had conflicts, resolving by favoring destination branch (new release)..." + + # Add all files and resolve conflicts by keeping destination version + git_exec add . + + # For any remaining conflicts, checkout our version + local conflict_files + 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 "Resolving conflict in ${file} by keeping destination version" + git_exec checkout --ours "$file" + git_exec add "$file" + fi + done <<< "$conflict_files" + fi + + # Complete the merge + local status + status=$(git status --porcelain) + if [[ -n "$status" ]]; then + if ! git_exec commit -m "Merge ${source_branch} into ${dest_branch}" --no-verify; then + echo "Failed to commit merge of ${source_branch}" + exit 1 + fi + fi + + echo "Successfully merged ${source_branch} into ${dest_branch}" + return 0 # Return 0 to indicate merged +} + +main() { + if [[ -z "$NEW_RELEASE_BRANCH" ]]; then + echo "Error: NEW_RELEASE_BRANCH environment variable is not set" + exit 1 + fi + + echo "New release branch: ${NEW_RELEASE_BRANCH}" + + # Parse the new release version + local new_version + new_version=$(parse_release_version "$NEW_RELEASE_BRANCH") + if [[ -z "$new_version" ]]; then + echo "Error: ${NEW_RELEASE_BRANCH} is not a valid release branch (expected format: release/X.Y.Z)" + exit 1 + fi + + read -r new_major new_minor new_patch <<< "$new_version" + echo "Parsed version: ${new_major}.${new_minor}.${new_patch}" + + # Fetch all remote branches + git_exec fetch origin + + # Get all release branches + local all_release_branches=() + while IFS= read -r branch; do + # Remove "origin/" prefix and whitespace + branch="${branch#*origin/}" + branch="${branch// /}" + if [[ -n "$branch" ]] && [[ -n "$(parse_release_version "$branch")" ]]; then + all_release_branches+=("$branch") + fi + done < <(git branch -r --list "origin/release/*") + + echo "" + echo "Found ${#all_release_branches[@]} release branches:" + for b in "${all_release_branches[@]}"; do + echo " - $b" + done + + # Filter to only branches older than the new one + local older_branches=() + for branch in "${all_release_branches[@]}"; do + local version + version=$(parse_release_version "$branch") + if [[ -n "$version" ]]; then + read -r major minor patch <<< "$version" + if is_version_older "$major" "$minor" "$patch" "$new_major" "$new_minor" "$new_patch"; then + older_branches+=("$branch") + fi + fi + done + + # Sort older branches from oldest to newest using version sort + local sorted_branches=() + while IFS= read -r branch; do + [[ -n "$branch" ]] && sorted_branches+=("$branch") + done < <(printf '%s\n' "${older_branches[@]}" | sort -V) + older_branches=("${sorted_branches[@]}") + + if [[ ${#older_branches[@]} -eq 0 ]]; then + echo "" + echo "No older release branches found. Nothing to merge." + exit 0 + fi + + echo "" + echo "Older release branches found (oldest to newest):" + for b in "${older_branches[@]}"; do + echo " - $b" + done + + echo "" + echo "Will merge all ${#older_branches[@]} older branches." + + # Verify we're on the right branch + local current_branch + current_branch=$(git branch --show-current) + if [[ "$current_branch" != "$NEW_RELEASE_BRANCH" ]]; then + echo "Switching to ${NEW_RELEASE_BRANCH}..." + git_exec checkout "$NEW_RELEASE_BRANCH" + fi + + # Merge each branch (fail fast on errors) + local merged_count=0 + local skipped_count=0 + + for older_branch in "${older_branches[@]}"; do + if merge_with_favor_destination "$older_branch" "$NEW_RELEASE_BRANCH"; then + ((merged_count++)) + else + ((skipped_count++)) + fi + done + + # Only push if we actually merged something + if [[ "$merged_count" -gt 0 ]]; then + echo "" + echo "Pushing merged changes..." + git_exec push origin "$NEW_RELEASE_BRANCH" + else + echo "" + echo "No new merges were made (all branches were already merged)." + fi + + echo "" + echo "============================================================" + echo "Merge complete!" + echo " Branches merged: ${merged_count}" + echo " Branches skipped (already merged): ${skipped_count}" + echo "All source branches remain open as requested." + echo "============================================================" +} + +# Run main and handle errors +main "$@" diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml new file mode 100644 index 00000000..80b2ad65 --- /dev/null +++ b/.github/workflows/merge-previous-releases.yml @@ -0,0 +1,36 @@ +name: Merge Previous Releases + +on: + workflow_call: + inputs: + new-release-branch: + required: true + type: string + description: 'The newly created release branch (e.g., release/2.1.2)' + secrets: + github-token: + required: true + description: 'GitHub token for authentication' + + workflow_dispatch: + inputs: + new-release-branch: + required: true + type: string + description: 'The newly created release branch (e.g., release/2.1.2)' + +jobs: + merge-previous-releases: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + ref: ${{ inputs.new-release-branch }} + fetch-depth: 0 + + - name: Merge previous releases + uses: ./.github/actions/merge-previous-releases + with: + new-release-branch: ${{ inputs.new-release-branch }} + github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} From c87ac05d99e2611ffe55c2e70932d5453fe66139 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 12:14:54 -0500 Subject: [PATCH 02/14] INFRA-3187: Removed extra action, fixed workflow --- .../merge-previous-releases/action.yml | 50 ---- .github/scripts/merge-previous-releases.js | 252 ------------------ .github/workflows/merge-previous-releases.yml | 27 +- 3 files changed, 23 insertions(+), 306 deletions(-) delete mode 100644 .github/actions/merge-previous-releases/action.yml delete mode 100644 .github/scripts/merge-previous-releases.js diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml deleted file mode 100644 index badda71b..00000000 --- a/.github/actions/merge-previous-releases/action.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Merge Previous Releases -description: 'An action to merge previous release branches into a newly created release branch.' - -inputs: - new-release-branch: - required: true - description: 'The newly created release branch (e.g., release/2.1.2)' - github-token: - description: 'GitHub token used for authentication.' - required: true - github-tools-repository: - description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repository, and usually does not need to be changed.' - required: false - default: ${{ github.action_repository }} - github-tools-ref: - description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' - required: false - default: ${{ github.action_ref }} - -runs: - using: composite - steps: - - name: Checkout GitHub tools repository - uses: actions/checkout@v6 - with: - repository: ${{ inputs.github-tools-repository }} - ref: ${{ inputs.github-tools-ref }} - path: ./github-tools - - - name: Set Git user and email - shell: bash - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Run merge previous releases script - id: merge-releases - env: - NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} - GITHUB_TOKEN: ${{ inputs.github-token }} - shell: bash - run: | - # Ensure github-tools is in .gitignore to prevent it from being committed - if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then - echo "github-tools/" >> .gitignore - echo "Added github-tools/ to .gitignore" - fi - - # Execute the script from github-tools - bash ./github-tools/.github/scripts/merge-previous-releases.sh diff --git a/.github/scripts/merge-previous-releases.js b/.github/scripts/merge-previous-releases.js deleted file mode 100644 index ccbdec46..00000000 --- a/.github/scripts/merge-previous-releases.js +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env node - -/** - * Merge Previous Release Branches Script - * - * This script is triggered when a new release branch is created (e.g., release/2.1.2). - * It finds all previous release branches and merges them into the new release branch. - * - * Key behaviors: - * - Merges ALL older release branches into the new one - * - For merge conflicts, favors the destination branch (new release) - * - Both branches remain open after merge - * - Fails fast on errors to prevent pushing partial merges - * - * Environment variables: - * - NEW_RELEASE_BRANCH: The newly created release branch (e.g., release/2.1.2) - */ - -const { promisify } = require('util'); -const exec = promisify(require('child_process').exec); - -/** - * Parse a release branch name to extract version components - * @param {string} branchName - Branch name like "release/2.1.2" - * @returns {object|null} - { major, minor, patch } or null if not a valid release branch - */ -function parseReleaseVersion(branchName) { - // Match release/X.Y.Z format (does not match release candidates like release/2.1.2-rc.1) - const match = branchName.match(/^release\/(\d+)\.(\d+)\.(\d+)$/); - if (!match) { - return null; - } - return { - major: parseInt(match[1], 10), - minor: parseInt(match[2], 10), - patch: parseInt(match[3], 10), - }; -} - -/** - * Compare two version objects - * @returns {number} - negative if a < b, positive if a > b, 0 if equal - */ -function compareVersions(a, b) { - if (a.major !== b.major) return a.major - b.major; - if (a.minor !== b.minor) return a.minor - b.minor; - return a.patch - b.patch; -} - -/** - * Execute a git command and log it - */ -async function gitExec(command, options = {}) { - const { ignoreError = false } = options; - console.log(`Executing: git ${command}`); - try { - const { stdout, stderr } = await exec(`git ${command}`); - if (stdout.trim()) console.log(stdout.trim()); - if (stderr.trim()) console.log(stderr.trim()); - return { stdout, stderr, success: true }; - } catch (error) { - if (ignoreError) { - console.warn(`Warning: ${error.message}`); - return { stdout: error.stdout, stderr: error.stderr, success: false, error }; - } - throw error; - } -} - -/** - * Get all remote release branches - */ -async function getReleaseBranches() { - await gitExec('fetch origin'); - const { stdout } = await exec('git branch -r --list "origin/release/*"'); - return stdout - .split('\n') - .map((branch) => branch.trim().replace('origin/', '')) - .filter((branch) => branch && parseReleaseVersion(branch)); -} - -/** - * Check if a branch has already been merged into the current branch. If yes, skip the merge. - * @param {string} sourceBranch - The branch to check if it has already been merged into the current branch - * @returns {Promise} - True if the branch has already been merged into the current branch, false otherwise - */ -async function isBranchMerged(sourceBranch) { - try { - // Check if the source branch's HEAD is an ancestor of current HEAD - const { stdout } = await exec( - `git merge-base --is-ancestor origin/${sourceBranch} HEAD && echo "merged" || echo "not-merged"`, - ); - return stdout.trim() === 'merged'; - } catch { - // If the command fails, assume not merged - return false; - } -} - -/** - * Merge a source branch into the current branch, favoring current branch on conflicts - * Uses approach similar to stable-sync.js - */ -async function mergeWithFavorDestination(sourceBranch, destBranch) { - console.log(`\n${'='.repeat(60)}`); - console.log(`Merging ${sourceBranch} into ${destBranch}`); - console.log('='.repeat(60)); - - // Check if already merged - const alreadyMerged = await isBranchMerged(sourceBranch); - if (alreadyMerged) { - console.log(`Branch ${sourceBranch} is already merged into ${destBranch}. Skipping.`); - return { skipped: true }; - } - - // Try to merge with "ours" strategy for conflicts (favors current branch) - const mergeResult = await gitExec( - `merge origin/${sourceBranch} -X ours --no-edit -m "Merge ${sourceBranch} into ${destBranch}"`, - { ignoreError: true }, - ); - - if (!mergeResult.success) { - // If merge still fails (shouldn't happen with -X ours, but just in case) - console.log('Merge had conflicts, resolving by favoring destination branch...'); - - // Add all files and resolve conflicts by keeping destination version - await gitExec('add .'); - - // For any remaining conflicts, checkout our version - try { - const { stdout: conflictFiles } = await exec('git diff --name-only --diff-filter=U'); - if (conflictFiles.trim()) { - for (const file of conflictFiles.trim().split('\n')) { - if (file) { - console.log(`Resolving conflict in ${file} by keeping destination version`); - await gitExec(`checkout --ours "${file}"`); - await gitExec(`add "${file}"`); - } - } - } - } catch (e) { - // No conflicts to resolve - } - - // Complete the merge - const { stdout: status } = await exec('git status --porcelain'); - if (status.trim()) { - const commitResult = await gitExec( - `commit -m "Merge ${sourceBranch} into ${destBranch}" --no-verify`, - { ignoreError: true }, - ); - if (!commitResult.success) { - throw new Error(`Failed to commit merge of ${sourceBranch}: ${commitResult.error?.message}`); - } - } - } - - console.log(`Successfully merged ${sourceBranch} into ${destBranch}`); - return { skipped: false }; -} - -async function main() { - const newReleaseBranch = process.env.NEW_RELEASE_BRANCH; - - if (!newReleaseBranch) { - console.error('Error: NEW_RELEASE_BRANCH environment variable is not set'); - process.exit(1); - } - - console.log(`New release branch: ${newReleaseBranch}`); - - const newVersion = parseReleaseVersion(newReleaseBranch); - if (!newVersion) { - console.error( - `Error: ${newReleaseBranch} is not a valid release branch (expected format: release/X.Y.Z)`, - ); - process.exit(1); - } - - console.log(`Parsed version: ${newVersion.major}.${newVersion.minor}.${newVersion.patch}`); - - // Get all release branches - const allReleaseBranches = await getReleaseBranches(); - console.log(`\nFound ${allReleaseBranches.length} release branches:`); - allReleaseBranches.forEach((b) => console.log(` - ${b}`)); - - // Filter to only branches older than the new one, sorted from oldest to newest - const olderBranches = allReleaseBranches - .filter((branch) => { - const version = parseReleaseVersion(branch); - return version && compareVersions(version, newVersion) < 0; - }) - .sort((a, b) => { - const versionA = parseReleaseVersion(a); - const versionB = parseReleaseVersion(b); - return compareVersions(versionA, versionB); - }); - - if (olderBranches.length === 0) { - console.log('\nNo older release branches found. Nothing to merge.'); - return; - } - - console.log(`\nOlder release branches found (oldest to newest):`); - olderBranches.forEach((b) => console.log(` - ${b}`)); - - // Merge all older branches - const branchesToMerge = olderBranches; - console.log(`\nWill merge all ${branchesToMerge.length} older branches.`); - - // We should already be on the new release branch (checkout was done in the workflow) - // But let's verify and ensure we're on the right branch - const { stdout: currentBranch } = await exec('git branch --show-current'); - if (currentBranch.trim() !== newReleaseBranch) { - console.log(`Switching to ${newReleaseBranch}...`); - await gitExec(`checkout ${newReleaseBranch}`); - } - - // Merge each branch (fail fast on errors) - let mergedCount = 0; - let skippedCount = 0; - - for (const olderBranch of branchesToMerge) { - const result = await mergeWithFavorDestination(olderBranch, newReleaseBranch); - if (result.skipped) { - skippedCount++; - } else { - mergedCount++; - } - } - - // Only push if we actually merged something - if (mergedCount > 0) { - console.log('\nPushing merged changes...'); - await gitExec(`push origin ${newReleaseBranch}`); - } else { - console.log('\nNo new merges were made (all branches were already merged).'); - } - - console.log('\n' + '='.repeat(60)); - console.log('Merge complete!'); - console.log(` Branches merged: ${mergedCount}`); - console.log(` Branches skipped (already merged): ${skippedCount}`); - console.log(`All source branches remain open as requested.`); - console.log('='.repeat(60)); -} - -main().catch((error) => { - console.error(`\nFatal error: ${error.message}`); - console.error('Aborting to prevent pushing partial merges.'); - process.exit(1); -}); diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index 80b2ad65..b7842676 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -29,8 +29,27 @@ jobs: ref: ${{ inputs.new-release-branch }} fetch-depth: 0 - - name: Merge previous releases - uses: ./.github/actions/merge-previous-releases + - name: Checkout GitHub tools repository + uses: actions/checkout@v5 with: - new-release-branch: ${{ inputs.new-release-branch }} - github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} + repository: metamask/github-tools + path: ./github-tools + + - name: Set Git user and email + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run merge previous releases script + env: + NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} + GITHUB_TOKEN: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} + run: | + # Ensure github-tools is in .gitignore to prevent it from being committed + if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then + echo "github-tools/" >> .gitignore + echo "Added github-tools/ to .gitignore" + fi + + # Execute the script + bash ./github-tools/.github/scripts/merge-previous-releases.sh From 8f8a22e647366fa39320eac1f68ceb479fea514f Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 12:21:33 -0500 Subject: [PATCH 03/14] INFRA-3187: Update ref for testing --- .github/workflows/merge-previous-releases.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index b7842676..7a51b9a6 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -33,6 +33,7 @@ jobs: uses: actions/checkout@v5 with: repository: metamask/github-tools + ref: INFRA-3187-AutomateMergingReleaseBranches path: ./github-tools - name: Set Git user and email From 860652aec45a5fe1794cc90a6e10b0df480e4bc9 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 12:31:03 -0500 Subject: [PATCH 04/14] INFRA-2187:Fix already merged branch issue --- .github/scripts/merge-previous-releases.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index dba94ae1..e6dc67fb 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -195,9 +195,9 @@ main() { for older_branch in "${older_branches[@]}"; do if merge_with_favor_destination "$older_branch" "$NEW_RELEASE_BRANCH"; then - ((merged_count++)) + ((merged_count++)) || true else - ((skipped_count++)) + ((skipped_count++)) || true fi done From 999d4b3d318322fe33a7c18cbbe27e7f20873933 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 13:03:27 -0500 Subject: [PATCH 05/14] INFRA-3187: Update comments for merge conflicts --- .github/scripts/merge-previous-releases.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index e6dc67fb..733e123e 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -70,27 +70,30 @@ merge_with_favor_destination() { # 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}" + echo "✅ Successfully merged ${source_branch} into ${dest_branch}" return 0 # Return 0 to indicate merged fi # If merge still fails (shouldn't happen with -X ours, but just in case) - echo "Merge had conflicts, resolving by favoring destination branch (new release)..." + echo "⚠️ Merge conflict detected! Resolving by favoring destination branch (new release)..." # Add all files and resolve conflicts by keeping destination version git_exec add . # For any remaining conflicts, checkout our version 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 "Resolving conflict in ${file} by keeping destination version" + echo " - Conflict in: ${file} → keeping destination version" git_exec checkout --ours "$file" git_exec add "$file" + ((conflict_count++)) || true fi done <<< "$conflict_files" + echo "✅ Resolved ${conflict_count} conflict(s) by keeping destination branch version" fi # Complete the merge @@ -103,7 +106,7 @@ merge_with_favor_destination() { fi fi - echo "Successfully merged ${source_branch} into ${dest_branch}" + echo "✅ Successfully merged ${source_branch} into ${dest_branch} (${conflict_count} conflict(s) resolved)" return 0 # Return 0 to indicate merged } From 2b07d72f5e5dd908404885ba0015d853d55ca33b Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 13:23:20 -0500 Subject: [PATCH 06/14] INFRA-3187: Code review fixes --- .github/scripts/merge-previous-releases.sh | 11 +++++++---- .github/workflows/merge-previous-releases.yml | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index 733e123e..624e6eca 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -77,10 +77,7 @@ merge_with_favor_destination() { # If merge still fails (shouldn't happen with -X ours, but just in case) echo "⚠️ Merge conflict detected! Resolving by favoring destination branch (new release)..." - # Add all files and resolve conflicts by keeping destination version - git_exec add . - - # For any remaining conflicts, checkout our version + # First, resolve any unmerged (conflicted) files by keeping our version local conflict_files local conflict_count=0 conflict_files=$(git diff --name-only --diff-filter=U 2>/dev/null || true) @@ -96,6 +93,12 @@ merge_with_favor_destination() { echo "✅ Resolved ${conflict_count} conflict(s) by keeping destination branch version" fi + # Now add any remaining files (non-conflicted changes) + git_exec add . + + # Reset .gitignore to avoid committing workflow-specific changes (github-tools/ entry) + git checkout HEAD -- .gitignore 2>/dev/null || true + # Complete the merge local status status=$(git status --porcelain) diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index 7a51b9a6..b7842676 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -33,7 +33,6 @@ jobs: uses: actions/checkout@v5 with: repository: metamask/github-tools - ref: INFRA-3187-AutomateMergingReleaseBranches path: ./github-tools - name: Set Git user and email From 879a51dd33727eb1a2c90e1c7c407f4ff0455a85 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 13:35:11 -0500 Subject: [PATCH 07/14] INFRA-3187: Resolve more code review bugs --- .github/scripts/merge-previous-releases.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index 624e6eca..c3b5c6c1 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -93,17 +93,17 @@ merge_with_favor_destination() { echo "✅ Resolved ${conflict_count} conflict(s) by keeping destination branch version" fi - # Now add any remaining files (non-conflicted changes) - git_exec add . + # Now add any remaining files (non-conflicted changes), excluding github-tools directory + git_exec add -- . ':!github-tools' - # Reset .gitignore to avoid committing workflow-specific changes (github-tools/ entry) - git checkout HEAD -- .gitignore 2>/dev/null || true + # Unstage .gitignore to avoid committing workflow-specific changes (github-tools/ entry) + # Using reset instead of checkout because .gitignore may not exist in HEAD + git reset HEAD -- .gitignore 2>/dev/null || true - # Complete the merge - local status - status=$(git status --porcelain) - if [[ -n "$status" ]]; then - if ! git_exec commit -m "Merge ${source_branch} into ${dest_branch}" --no-verify; then + # 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 From f967d6edd55bf20af9b8d8c6adc3bdd973f56b41 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 13:47:39 -0500 Subject: [PATCH 08/14] INFRA-3187: More code review fixes --- .github/scripts/merge-previous-releases.sh | 4 ---- .github/workflows/merge-previous-releases.yml | 10 +--------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index c3b5c6c1..06184d6a 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -96,10 +96,6 @@ merge_with_favor_destination() { # Now add any remaining files (non-conflicted changes), excluding github-tools directory git_exec add -- . ':!github-tools' - # Unstage .gitignore to avoid committing workflow-specific changes (github-tools/ entry) - # Using reset instead of checkout because .gitignore may not exist in HEAD - git reset HEAD -- .gitignore 2>/dev/null || true - # 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 diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index b7842676..2064c901 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -44,12 +44,4 @@ jobs: env: NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} GITHUB_TOKEN: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} - run: | - # Ensure github-tools is in .gitignore to prevent it from being committed - if ! grep -q "^github-tools/" .gitignore 2>/dev/null; then - echo "github-tools/" >> .gitignore - echo "Added github-tools/ to .gitignore" - fi - - # Execute the script - bash ./github-tools/.github/scripts/merge-previous-releases.sh + run: bash ./github-tools/.github/scripts/merge-previous-releases.sh From 1d21176114487e1d845c892d707823b04e14893e Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Tue, 9 Dec 2025 14:04:47 -0500 Subject: [PATCH 09/14] INFRA-3187: More code review fixes --- .github/scripts/merge-previous-releases.sh | 6 ++++++ .github/workflows/merge-previous-releases.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index 06184d6a..f8af05d0 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -75,6 +75,12 @@ merge_with_favor_destination() { fi # 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) + 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)..." # First, resolve any unmerged (conflicted) files by keeping our version diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index 2064c901..9ee5b89e 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -28,6 +28,7 @@ jobs: with: ref: ${{ inputs.new-release-branch }} fetch-depth: 0 + token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} - name: Checkout GitHub tools repository uses: actions/checkout@v5 From c12d2b94894e025365c0f178c8bcdd68bd1b2cc4 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Wed, 10 Dec 2025 10:44:07 -0500 Subject: [PATCH 10/14] INFRA-3187: Code review fix --- .../merge-previous-releases/action.yml | 42 +++++++++++++++++++ .github/workflows/merge-previous-releases.yml | 19 ++------- 2 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 .github/actions/merge-previous-releases/action.yml diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml new file mode 100644 index 00000000..09cee609 --- /dev/null +++ b/.github/actions/merge-previous-releases/action.yml @@ -0,0 +1,42 @@ +name: Merge Previous Releases +description: 'An action to merge previous release branches into a newly created release branch.' + +inputs: + new-release-branch: + required: true + description: 'The newly created release branch (e.g., release/2.1.2)' + github-token: + description: 'GitHub token used for authentication.' + required: true + github-tools-repository: + description: 'The GitHub repository containing the GitHub tools. Defaults to the GitHub tools action repository, and usually does not need to be changed.' + required: false + default: ${{ github.action_repository }} + github-tools-ref: + description: 'The SHA of the action to use. Defaults to the current action ref, and usually does not need to be changed.' + required: false + default: ${{ github.action_ref }} + +runs: + using: composite + steps: + - name: Checkout GitHub tools repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.github-tools-repository }} + ref: ${{ inputs.github-tools-ref }} + path: ./github-tools + + - name: Set Git user and email + shell: bash + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run merge previous releases script + env: + NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} + GITHUB_TOKEN: ${{ inputs.github-token }} + shell: bash + run: bash ./github-tools/.github/scripts/merge-previous-releases.sh + diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml index 9ee5b89e..489140c4 100644 --- a/.github/workflows/merge-previous-releases.yml +++ b/.github/workflows/merge-previous-releases.yml @@ -30,19 +30,8 @@ jobs: fetch-depth: 0 token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} - - name: Checkout GitHub tools repository - uses: actions/checkout@v5 + - name: Merge previous releases + uses: metamask/github-tools/.github/actions/merge-previous-releases@INFRA-3187-AutomateMergingReleaseBranches with: - repository: metamask/github-tools - path: ./github-tools - - - name: Set Git user and email - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Run merge previous releases script - env: - NEW_RELEASE_BRANCH: ${{ inputs.new-release-branch }} - GITHUB_TOKEN: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} - run: bash ./github-tools/.github/scripts/merge-previous-releases.sh + new-release-branch: ${{ inputs.new-release-branch }} + github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} From 22f3e417bbe49368f544b766398f5c27d9c84149 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Wed, 10 Dec 2025 11:13:57 -0500 Subject: [PATCH 11/14] INFRA-3187: Removed workflow in favor of action being called directly --- .../merge-previous-releases/action.yml | 7 ++++ .github/workflows/merge-previous-releases.yml | 37 ------------------- 2 files changed, 7 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/merge-previous-releases.yml diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml index 09cee609..57ab249d 100644 --- a/.github/actions/merge-previous-releases/action.yml +++ b/.github/actions/merge-previous-releases/action.yml @@ -20,6 +20,13 @@ inputs: runs: using: composite steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ inputs.new-release-branch }} + fetch-depth: 0 + token: ${{ inputs.github-token }} + - name: Checkout GitHub tools repository uses: actions/checkout@v6 with: diff --git a/.github/workflows/merge-previous-releases.yml b/.github/workflows/merge-previous-releases.yml deleted file mode 100644 index 489140c4..00000000 --- a/.github/workflows/merge-previous-releases.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Merge Previous Releases - -on: - workflow_call: - inputs: - new-release-branch: - required: true - type: string - description: 'The newly created release branch (e.g., release/2.1.2)' - secrets: - github-token: - required: true - description: 'GitHub token for authentication' - - workflow_dispatch: - inputs: - new-release-branch: - required: true - type: string - description: 'The newly created release branch (e.g., release/2.1.2)' - -jobs: - merge-previous-releases: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - ref: ${{ inputs.new-release-branch }} - fetch-depth: 0 - token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} - - - name: Merge previous releases - uses: metamask/github-tools/.github/actions/merge-previous-releases@INFRA-3187-AutomateMergingReleaseBranches - with: - new-release-branch: ${{ inputs.new-release-branch }} - github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} From 7f9ddf206edc247020120394b6a36c1c63e7539d Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Mon, 15 Dec 2025 13:30:52 -0500 Subject: [PATCH 12/14] INFRA-3187: Updated commit metadata to use metamaskbot --- .github/actions/merge-previous-releases/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml index 57ab249d..bf0e7c27 100644 --- a/.github/actions/merge-previous-releases/action.yml +++ b/.github/actions/merge-previous-releases/action.yml @@ -37,8 +37,8 @@ runs: - name: Set Git user and email shell: bash run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "metamaskbot" + git config --global user.email "metamaskbot@users.noreply.github.com" - name: Run merge previous releases script env: From e20158bb44b857e1b62e22b595ff682e7938ea75 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Mon, 15 Dec 2025 13:56:55 -0500 Subject: [PATCH 13/14] INFRA-3187: Code review fix --- .github/scripts/merge-previous-releases.sh | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/scripts/merge-previous-releases.sh b/.github/scripts/merge-previous-releases.sh index f8af05d0..75ff04d0 100644 --- a/.github/scripts/merge-previous-releases.sh +++ b/.github/scripts/merge-previous-releases.sh @@ -83,7 +83,13 @@ merge_with_favor_destination() { echo "⚠️ Merge conflict detected! Resolving by favoring destination branch (new release)..." - # First, resolve any unmerged (conflicted) files by keeping our version + # Resolve any unmerged (conflicted) files by keeping destination version. + # + # Git merge terminology in this context: + # - "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) @@ -91,8 +97,22 @@ merge_with_favor_destination() { while IFS= read -r file; do if [[ -n "$file" ]]; then echo " - Conflict in: ${file} → keeping destination version" - git_exec checkout --ours "$file" - git_exec add "$file" + # 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" From 535b887681165f77ef85b2b9780eab9184f884b1 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Mon, 15 Dec 2025 15:02:54 -0500 Subject: [PATCH 14/14] INFRA-2817: Resolved linting error --- .github/actions/merge-previous-releases/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/merge-previous-releases/action.yml b/.github/actions/merge-previous-releases/action.yml index bf0e7c27..c87eca2b 100644 --- a/.github/actions/merge-previous-releases/action.yml +++ b/.github/actions/merge-previous-releases/action.yml @@ -46,4 +46,3 @@ runs: GITHUB_TOKEN: ${{ inputs.github-token }} shell: bash run: bash ./github-tools/.github/scripts/merge-previous-releases.sh -