diff --git a/.github/scripts/stable-sync.js b/.github/scripts/stable-sync.js new file mode 100644 index 00000000..d85405c6 --- /dev/null +++ b/.github/scripts/stable-sync.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +// USAGE: +// This will create/update a local stable-sync branch +// and get it in the state needed for a stable-sync PR +// Once the script successfully completes, you just +// need to push the branch to the remote repo. This will +// likely require a `git push --force` +// +// Usage: node stable-sync.js [branch-name] +// If no branch name is provided, defaults to 'stable-sync' +// +// Environment variables: +// CREATE_BRANCH - if set to 'true', will push the branch at the end + +const { promisify } = require('util'); +const exec = promisify(require('child_process').exec); + +async function runGitCommands() { + // Get branch name from command line arguments or use default + const branchName = process.argv[2] || 'stable-main'; + + // Check if CREATE_BRANCH environment variable exists and is set to true + const shouldPushBranch = (process.env.CREATE_BRANCH || 'false').toLowerCase() === 'true'; + + try { + try { + // Check if the branch already exists + const { stdout: branchExists } = await exec( + //`git rev-parse --quiet --verify ${branchName}`, + `git ls-remote origin ${branchName}`, + ); + if (branchExists.trim()) { + // Branch exists, so simply check it out + await exec(`git checkout ${branchName}`); + await exec(`git pull origin ${branchName}`); + console.log(`Checked out branch: ${branchName}`); + } else { + throw new Error( + 'git rev-parse --quiet --verify failed. Branch hash empty', + ); + } + } catch (error) { + if (error.stdout === '') { + console.warn( + `Branch does not exist, creating new ${branchName} branch.`, + ); + + // Branch does not exist, create and check it out + await exec(`git checkout -b ${branchName}`); + console.log(`Created and checked out branch: ${branchName}`); + } else { + console.error(`Error: ${error.message}`); + process.exit(1); + } + } + + await exec('git fetch'); + console.log('Executed: git fetch'); + + await exec('git reset --hard origin/stable'); + console.log('Executed: git reset --hard origin/stable'); + + try { + await exec('git merge origin/main'); + console.log('Executed: git merge origin/main'); + } catch (error) { + // Handle the error but continue script execution + if ( + error.stdout.includes( + 'Automatic merge failed; fix conflicts and then commit the result.', + ) + ) { + console.warn( + 'Merge conflict encountered. Continuing script execution.', + ); + } else { + console.error(`Error: ${error.message}`); + process.exit(1); + } + } + + await exec('git add .'); + await exec('git restore --source origin/main .'); + console.log('Executed: it restore --source origin/main .'); + + await exec('git checkout origin/main -- .'); + console.log('Executed: git checkout origin/main -- .'); + + await exec('git checkout origin/stable -- CHANGELOG.md'); + console.log('Executed: git checkout origin/stable -- CHANGELOG.md'); + + // Execute mobile-specific commands if REPO is 'mobile' + if (process.env.REPO === 'mobile') { + console.log('Executing mobile-specific commands...'); + + await exec('git checkout origin/stable -- bitrise.yml'); + console.log('Executed: git checkout origin/stable -- bitrise.yml'); + + await exec('git checkout origin/stable -- android/app/build.gradle'); + console.log('Executed: git checkout origin/stable -- android/app/build.gradle'); + + await exec('git checkout origin/stable -- ios/MetaMask.xcodeproj/project.pbxproj'); + console.log('Executed: git checkout origin/stable -- ios/MetaMask.xcodeproj/project.pbxproj'); + + await exec('git checkout origin/stable -- package.json'); + console.log('Executed: git checkout origin/stable -- package.json'); + } + // Execute extension-specific commands if REPO is 'extension' + else if (process.env.REPO === 'extension') { + console.log('Executing extension-specific commands...'); + + const { stdout: packageJsonContent } = await exec( + 'git show origin/master:package.json', + ); + const packageJson = JSON.parse(packageJsonContent); + const packageVersion = packageJson.version; + + await exec(`yarn version "${packageVersion}"`); + console.log('Executed: yarn version'); + } + // If REPO is not set or has an invalid value, skip both + else { + console.log('REPO environment variable not set or invalid. Skipping mobile/extension specific commands.'); + } + + await exec('git add .'); + console.log('Executed: git add .'); + + try { + // Check if there are any changes to commit + const { stdout: status } = await exec('git status --porcelain'); + if (!status.trim()) { + console.log('No changes to commit, skipping commit step'); + return; + } + + await exec(`git commit -m "Merge origin/main into ${branchName}" --no-verify`); + console.log('Executed: git commit'); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } + + console.log(`Your local ${branchName} branch is now ready to become a PR.`); + + // Push the branch if CREATE_BRANCH is true + if (shouldPushBranch) { + try { + console.log(`Checking if branch ${branchName} exists remotely...`); + const { stdout: remoteBranches } = await exec('git ls-remote --heads origin'); + const branchExists = remoteBranches.includes(`refs/heads/${branchName}`); + + if (branchExists) { + console.log(`Branch ${branchName} exists remotely, updating...`); + await exec(`git push origin ${branchName}`); + } else { + console.log(`Branch ${branchName} does not exist remotely, creating...`); + await exec(`git push --set-upstream origin ${branchName}`); + } + console.log(`Successfully pushed branch ${branchName} to remote`); + } catch (error) { + console.error(`Error pushing branch: ${error.message}`); + process.exit(1); + } + } else { + console.log('You likely now need to do `git push --force`'); + } + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } +} + +runGitCommands(); diff --git a/.github/workflows/stable-sync.yml b/.github/workflows/stable-sync.yml new file mode 100644 index 00000000..68fae196 --- /dev/null +++ b/.github/workflows/stable-sync.yml @@ -0,0 +1,114 @@ +name: Stable Sync + +on: + workflow_dispatch: + inputs: + semver-version: + required: true + type: string + description: 'The semantic version to use for the sync (e.g., x.x.x)' + repo-type: + required: false + type: choice + description: 'Type of repository (mobile or extension)' + options: + - mobile + - extension + default: 'mobile' + workflow_call: + inputs: + semver-version: + required: true + type: string + description: 'The semantic version to use for the sync (e.g., x.x.x)' + repo-type: + required: false + type: string + description: 'Type of repository (mobile or extension)' + default: 'mobile' + +jobs: + stable-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Check if PR exists + id: check-pr + uses: actions/github-script@v7 + with: + script: | + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:stable-main-${process.env.SEMVER_VERSION}`, + base: 'main' + }); + return prs.length > 0; + env: + SEMVER_VERSION: ${{ inputs.semver-version }} + + - name: Set Git user and email + run: | + git config --global user.name "metamaskbot" + git config --global user.email "metamaskbot@users.noreply.github.com" + + - name: Run stable sync + id: run-stable-sync + # if: steps.check-pr.outputs.result != 'true' + env: + CREATE_BRANCH: 'false' # let the script handle the branch creation + REPO: ${{ inputs.repo-type }} # Default to 'mobile' if not specified + run: | + node .github/scripts/stable-sync.js "stable-main-${{ inputs.semver-version }}" + # Check if branch exists remotely + BRANCH_NAME="stable-main-${{ inputs.semver-version }}" + if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then + git pull --rebase + echo "Branch $BRANCH_NAME exists remotely, pushing normally" + git push origin "$BRANCH_NAME" --force + else + echo "Branch $BRANCH_NAME doesn't exist remotely, pushing with --set-upstream" + git push --set-upstream origin "$BRANCH_NAME" + fi + + - name: Create Pull Request + if: steps.check-pr.outputs.result != 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: stable-main-${{ inputs.semver-version }} + VERSION: ${{ inputs.semver-version }} + run: | + # Create PR using GitHub CLI + gh pr create \ + --title "chore: sync stable to main for version $VERSION" \ + --body "This PR syncs the stable branch to main for version $VERSION. + + *Synchronization Process:* + + - Fetches the latest changes from the remote repository + - Resets the branch to match the stable branch + - Attempts to merge changes from main into the branch + - Handles merge conflicts if they occur + + *File Preservation:* + + Preserves specific files from the stable branch: + - CHANGELOG.md + - bitrise.yml + - android/app/build.gradle + - ios/MetaMask.xcodeproj/project.pbxproj + - package.json + + Indicates the next version candidate of main to $VERSION" \ + --base main \ + --head "$BRANCH_NAME" + #--label "sync" \ + #--label "stable"