Skip to content

Instantly share code, notes, and snippets.

@kolber
Last active November 20, 2025 04:16
Show Gist options
  • Select an option

  • Save kolber/197d68272fd41ac3e5c81abce3afd890 to your computer and use it in GitHub Desktop.

Select an option

Save kolber/197d68272fd41ac3e5c81abce3afd890 to your computer and use it in GitHub Desktop.
Zora Monorepo PR Migration Tool - Interactive script to migrate PRs from old repos to monorepo
#!/bin/bash
set -euo pipefail
# PR Migration Script for Zora Monorepo
# Interactively migrate PRs from old repos to the monorepo
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Repository configurations
BACKEND_REPO="ourzora/zora-backend-v2"
FRONTEND_REPO="ourzora/zora-co"
PROTOCOL_REPO="ourzora/zora-protocol-private"
BACKEND_WORKSPACE="backend"
FRONTEND_WORKSPACE="frontend"
PROTOCOL_WORKSPACE="protocol"
# Temporary files
TEMP_DIR=$(mktemp -d)
PR_LIST_FILE="$TEMP_DIR/pr_list.txt"
trap "rm -rf $TEMP_DIR" EXIT
#############################################################################
# Dependency Checks
#############################################################################
check_dependencies() {
echo -e "${BLUE}Checking dependencies...${NC}"
if ! command -v gh &> /dev/null; then
echo -e "${RED}βœ— GitHub CLI (gh) not found${NC}"
echo "Install with: brew install gh"
echo "Then authenticate: gh auth login"
exit 1
fi
if ! gh auth status &> /dev/null; then
echo -e "${RED}βœ— Not authenticated with GitHub CLI${NC}"
echo "Run: gh auth login"
exit 1
fi
if ! command -v fzf &> /dev/null; then
echo -e "${YELLOW}⚠ fzf not found - using simple selection${NC}"
echo "For better experience, install fzf: brew install fzf"
USE_FZF=false
else
USE_FZF=true
fi
if ! command -v jq &> /dev/null; then
echo -e "${RED}βœ— jq not found${NC}"
echo "Install with: brew install jq"
exit 1
fi
if [ ! -d ".git" ]; then
echo -e "${RED}βœ— Not in a git repository${NC}"
echo "Run this script from the monorepo root directory"
exit 1
fi
echo -e "${GREEN}βœ“ All dependencies satisfied${NC}\n"
}
#############################################################################
# Fetch PRs from all repos
#############################################################################
fetch_prs() {
local repo=$1
local workspace=$2
local color=$3
echo -e "${CYAN}Fetching open PRs from ${repo}...${NC}"
gh pr list \
--repo "$repo" \
--state open \
--json number,title,author,createdAt,headRefName,body,reviewDecision,labels \
--limit 100 | \
jq -r --arg workspace "$workspace" '.[] |
"[\($workspace)] #\(.number) β”‚ @\(.author.login) β”‚ ⏰ \(.createdAt | fromdateiso8601 | (now - .) / 86400 | floor)d ago β”‚ \(if .reviewDecision == "APPROVED" then "βœ“" elif .reviewDecision == "CHANGES_REQUESTED" then "βœ—" else "β—‹" end) β”‚ \(.labels | length) labels β”‚ \(.title)β•‘\(.body // "" | gsub("\n"; " "))"' | \
while IFS= read -r line; do
printf "${color}%s${NC}\n" "$line"
done
}
build_pr_list() {
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} Fetching Open PRs from All Repositories${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
> "$PR_LIST_FILE"
# Fetch PRs from each repo
fetch_prs "$BACKEND_REPO" "$BACKEND_WORKSPACE" "$RED" >> "$PR_LIST_FILE" || true
fetch_prs "$FRONTEND_REPO" "$FRONTEND_WORKSPACE" "$BLUE" >> "$PR_LIST_FILE" || true
fetch_prs "$PROTOCOL_REPO" "$PROTOCOL_WORKSPACE" "$YELLOW" >> "$PR_LIST_FILE" || true
if [ ! -s "$PR_LIST_FILE" ]; then
echo -e "${YELLOW}No open PRs found in any repository${NC}"
exit 0
fi
echo -e "\n${GREEN}βœ“ Found $(wc -l < "$PR_LIST_FILE") open PRs${NC}\n"
}
#############################################################################
# Interactive Selection
#############################################################################
select_pr() {
if [ "$USE_FZF" = true ]; then
select_pr_with_fzf
else
select_pr_simple
fi
}
select_pr_with_fzf() {
echo -e "${BLUE}Use arrow keys to navigate, type to filter, Enter to select${NC}"
echo -e "${BLUE}Tip: Prefix search with ' for exact match (e.g., 'frontend)${NC}\n"
local selected=$(cat "$PR_LIST_FILE" | \
fzf --ansi \
--height=80% \
--border \
--prompt="Select PR to migrate > " \
--header="Search: workspace, author, title, PR# | Use 'term for exact match" \
--delimiter='β•‘' \
--nth=1 \
--scheme=path \
--tiebreak=begin,length,index \
--preview='
line={}
workspace=$(echo "$line" | grep -o "\[.*\]" | head -1)
pr_num=$(echo "$line" | grep -o "#[0-9]\+" | head -1)
author=$(echo "$line" | grep -o "@[^ β”‚]*" | head -1)
age=$(echo "$line" | grep -o "[0-9]\+d ago" | head -1)
pr_status=$(echo "$line" | grep -o "[βœ“βœ—β—‹]" | head -1)
labels=$(echo "$line" | grep -o "[0-9]\+ labels" | head -1)
title=$(echo "$line" | cut -d"β•‘" -f1 | sed "s/.*labels β”‚ //" | sed "s/[[:space:]]*$//")
desc=$(echo "$line" | cut -d"β•‘" -f2)
review_text="Pending"
[ "$pr_status" = "βœ“" ] && review_text="Approved"
[ "$pr_status" = "βœ—" ] && review_text="Changes requested"
echo -e "\033[1;36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m"
echo -e "\033[1m$workspace $pr_num\033[0m"
echo ""
echo -e " Author: $author"
echo -e " Created: $age"
echo -e " Review: $pr_status $review_text"
echo -e " Labels: $labels"
echo ""
echo -e "\033[1mTitle:\033[0m"
echo -e " $title"
echo ""
echo -e "\033[1mDescription:\033[0m"
if [ -n "$desc" ]; then
echo "$desc" | fold -w 68 -s | sed "s/^/ /"
else
echo " (No description)"
fi
echo ""
echo -e "\033[1;36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m"
' \
--preview-window=up:20:wrap)
if [ -z "$selected" ]; then
echo -e "${YELLOW}No PR selected, exiting${NC}"
exit 0
fi
parse_selected_pr "$selected"
}
select_pr_simple() {
cat "$PR_LIST_FILE" | nl
echo ""
read -p "Enter PR number to select (1-$(wc -l < "$PR_LIST_FILE")): " selection
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $(wc -l < "$PR_LIST_FILE") ]; then
echo -e "${RED}Invalid selection${NC}"
exit 1
fi
local selected=$(sed -n "${selection}p" "$PR_LIST_FILE")
parse_selected_pr "$selected"
}
parse_selected_pr() {
local line="$1"
# Extract workspace and PR number from the formatted line (only from start, before β•‘)
local metadata=$(echo "$line" | cut -d'β•‘' -f1)
SELECTED_WORKSPACE=$(echo "$metadata" | grep -o '^\[.*\]' | tr -d '[]')
SELECTED_PR=$(echo "$metadata" | grep -o '#[0-9]\+' | tr -d '#' | head -1)
case "$SELECTED_WORKSPACE" in
"$BACKEND_WORKSPACE")
SELECTED_REPO="$BACKEND_REPO"
;;
"$FRONTEND_WORKSPACE")
SELECTED_REPO="$FRONTEND_REPO"
;;
"$PROTOCOL_WORKSPACE")
SELECTED_REPO="$PROTOCOL_REPO"
;;
*)
echo -e "${RED}Unknown workspace: $SELECTED_WORKSPACE${NC}"
exit 1
;;
esac
echo -e "\n${GREEN}Selected: ${SELECTED_REPO}#${SELECTED_PR} β†’ monorepo/${SELECTED_WORKSPACE}/${NC}\n"
}
#############################################################################
# PR Migration Logic
#############################################################################
migrate_pr() {
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} Starting Automated PR Migration${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
# Fetch PR details
echo -e "${CYAN}Fetching PR details...${NC}"
PR_DATA=$(gh pr view "$SELECTED_PR" --repo "$SELECTED_REPO" --json title,body,author,number,headRefName,baseRefName)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_BODY=$(echo "$PR_DATA" | jq -r '.body // ""')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
PR_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName')
PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName')
NEW_BRANCH="migrate-${SELECTED_WORKSPACE}-pr-${SELECTED_PR}"
echo -e "${GREEN}βœ“ PR Details:${NC}"
echo -e " Title: ${PR_TITLE}"
echo -e " Author: @${PR_AUTHOR}"
echo -e " Branch: ${PR_BRANCH}"
echo -e " Base: ${PR_BASE}\n"
# Confirm migration
read -p "Continue with migration? (Y/n): " confirm
if [[ "$confirm" =~ ^[Nn]$ ]]; then
echo -e "${YELLOW}Migration cancelled${NC}"
exit 0
fi
# Create new branch
echo -e "\n${CYAN}Creating migration branch: ${NEW_BRANCH}${NC}"
git checkout -b "$NEW_BRANCH" main 2>/dev/null || {
echo -e "${YELLOW}Branch already exists, checking out${NC}"
git checkout "$NEW_BRANCH"
}
# Fetch PR commits
echo -e "${CYAN}Fetching PR commits from ${SELECTED_REPO}...${NC}"
# Add remote if not exists
if ! git remote | grep -q "^migrate-temp$"; then
git remote add migrate-temp "https://github.com/${SELECTED_REPO}.git"
fi
# Fetch both the PR head and base branches
echo -e "${PURPLE}β†’ Fetching PR head branch...${NC}"
git fetch migrate-temp "pull/${SELECTED_PR}/head:temp-pr-${SELECTED_PR}" || {
echo -e "${RED}Failed to fetch PR branch${NC}"
cleanup_migration
exit 1
}
echo -e "${PURPLE}β†’ Fetching base branch (${PR_BASE})...${NC}"
git fetch migrate-temp "${PR_BASE}:temp-base-${SELECTED_PR}" || {
echo -e "${RED}Failed to fetch base branch${NC}"
cleanup_migration
exit 1
}
# Get commits in PR (compare PR head against PR base, not monorepo main)
COMMITS=$(git log --reverse --format=%H temp-base-${SELECTED_PR}..temp-pr-${SELECTED_PR} 2>/dev/null || echo "")
if [ -z "$COMMITS" ]; then
echo -e "${RED}No commits found in PR${NC}"
cleanup_migration
exit 1
fi
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
echo -e "${GREEN}βœ“ Found ${COMMIT_COUNT} commits to migrate${NC}\n"
# Export commits as patches and apply with path prefix
echo -e "${CYAN}Applying commits with path prefix (${SELECTED_WORKSPACE}/)...${NC}"
PATCH_DIR="$TEMP_DIR/patches"
mkdir -p "$PATCH_DIR"
# Generate patches
echo -e "${PURPLE}Generating patches...${NC}"
git format-patch temp-base-${SELECTED_PR}..temp-pr-${SELECTED_PR} -o "$PATCH_DIR" --quiet
PATCH_COUNT=$(ls -1 "$PATCH_DIR"/*.patch 2>/dev/null | wc -l | tr -d ' ')
if [ "$PATCH_COUNT" -eq 0 ]; then
echo -e "${RED}No patches generated${NC}"
cleanup_migration
exit 1
fi
echo -e "${GREEN}βœ“ Generated ${PATCH_COUNT} patches${NC}\n"
# Apply patches with directory prefix
local patch_num=0
for patch in "$PATCH_DIR"/*.patch; do
patch_num=$((patch_num + 1))
patch_name=$(basename "$patch")
subject=$(grep '^Subject:' "$patch" | sed 's/Subject: \[PATCH[^]]*\] //' | head -c 60)
echo -e "${PURPLE}[$patch_num/$PATCH_COUNT]${NC} Applying: $subject..."
# Apply patch with directory prefix and three-way merge
if ! git am --directory="${SELECTED_WORKSPACE}/" --3way "$patch" 2>&1 | tee /tmp/am_output.txt; then
# Check if it's a conflict that needs resolution
if grep -q "Patch failed" /tmp/am_output.txt; then
echo -e "${YELLOW} ⚠ Patch has conflicts, resolving...${NC}"
# Check for conflict markers
conflicted=$(git diff --name-only --diff-filter=U 2>/dev/null)
if [ -n "$conflicted" ]; then
echo -e "${YELLOW} Conflicted files: ${NC}"
echo "$conflicted" | sed 's/^/ /'
echo ""
echo -e "${RED} βœ— Cannot auto-resolve conflicts${NC}"
echo -e "${YELLOW} Please resolve manually:${NC}"
echo -e " 1. Fix conflicts in the files above"
echo -e " 2. Run: git add <files>"
echo -e " 3. Run: git am --continue"
echo -e " 4. Re-run this script to continue migration"
exit 1
fi
# Try to continue if conflicts were auto-resolved
git am --continue 2>/dev/null || {
echo -e "${RED} βœ— Failed to apply patch${NC}"
git am --abort 2>/dev/null
cleanup_migration
exit 1
}
else
echo -e "${RED} βœ— Failed to apply patch${NC}"
echo -e "${YELLOW} Patch: $patch_name${NC}"
git am --abort 2>/dev/null
cleanup_migration
exit 1
fi
fi
done
echo -e "\n${GREEN}βœ“ Successfully applied all ${PATCH_COUNT} patches${NC}"
# Push branch (force in case it exists from previous migration attempt)
echo -e "\n${CYAN}Pushing migration branch...${NC}"
git push origin "$NEW_BRANCH" --force
# Create new PR
echo -e "${CYAN}Creating new PR in monorepo...${NC}"
NEW_PR_TITLE="[${SELECTED_WORKSPACE}] ${PR_TITLE}"
NEW_PR_BODY="Migrated from ${SELECTED_REPO}#${SELECTED_PR}
**Original PR:** https://github.com/${SELECTED_REPO}/pull/${SELECTED_PR}
**Original Author:** @${PR_AUTHOR}
---
${PR_BODY}
---
πŸ€– *This PR was automatically migrated using the monorepo migration script*"
gh pr create \
--repo ourzora/zora-monorepo-poc \
--title "$NEW_PR_TITLE" \
--body "$NEW_PR_BODY" \
--base main \
--head "$NEW_BRANCH" || {
echo -e "${RED}Failed to create PR${NC}"
echo -e "${YELLOW}Branch pushed successfully, but PR creation failed.${NC}"
echo -e "${YELLOW}Create PR manually at: https://github.com/ourzora/zora-monorepo-poc/compare/${NEW_BRANCH}${NC}"
exit 1
}
# Cleanup
cleanup_migration
echo -e "\n${GREEN}═══════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Migration Complete! πŸŽ‰${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}\n"
echo -e "${GREEN}βœ“ Migrated ${COMMIT_COUNT} commits${NC}"
echo -e "${GREEN}βœ“ Created PR: ${NEW_PR_TITLE}${NC}"
echo -e "${GREEN}βœ“ Branch: ${NEW_BRANCH}${NC}\n"
if [ "$PR_AUTHOR" != "$(git config user.name)" ]; then
echo -e "${YELLOW}Note: Don't forget to mention @${PR_AUTHOR} in the PR!${NC}"
fi
}
cleanup_migration() {
# Cleanup temp remote and branches
git remote remove migrate-temp 2>/dev/null || true
git branch -D "temp-pr-${SELECTED_PR}" 2>/dev/null || true
git branch -D "temp-base-${SELECTED_PR}" 2>/dev/null || true
}
#############################################################################
# Main Script
#############################################################################
main() {
echo -e "${PURPLE}"
echo "╔════════════════════════════════════════════════════════════╗"
echo "β•‘ β•‘"
echo "β•‘ Zora Monorepo PR Migration Tool β•‘"
echo "β•‘ β•‘"
echo "β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
echo -e "${NC}\n"
check_dependencies
build_pr_list
select_pr
migrate_pr
}
main

PR Migration Script

Automated tool to migrate open PRs from the old repositories to the monorepo during the migration period.

Features

  • πŸ” Interactive Selection: Browse all open PRs from backend, frontend, and protocol repos with fuzzy search
  • πŸ“Š Full Details: View PR number, title, author, creation date, review status, and description preview
  • πŸ€– Fully Automated: Cherry-picks commits, fixes paths, creates new PR automatically
  • πŸ”§ Auto-fix Conflicts: Automatically resolves path-related conflicts by moving files to correct workspace
  • 🎨 Beautiful UI: Color-coded output with progress indicators

Prerequisites

Install required tools:

# GitHub CLI (required)
brew install gh
gh auth login

# fzf for interactive selection (highly recommended)
brew install fzf

# jq for JSON parsing (required)
brew install jq

Usage

  1. Navigate to the monorepo root directory
  2. Run the script:
./scripts/migrate-pr.sh
  1. The script will:
    • Fetch all open PRs from all 3 repositories
    • Display them in an interactive searchable list
    • Let you select the PR you want to migrate
    • Automatically migrate the PR with all commits
    • Create a new PR in the monorepo with proper context

What the Script Does

1. Fetches Open PRs

Shows PRs from all three repos:

  • ourzora/zora-backend-v2 β†’ backend/
  • ourzora/zora-co β†’ frontend/
  • ourzora/zora-protocol-private β†’ protocol/

2. Interactive Selection

Display format:

[frontend] #123 β”‚ @username β”‚ ⏰ 2d ago β”‚ βœ“ β”‚ 3 labels β”‚ Add user bio field β”‚ This PR adds...
[backend] #456 β”‚ @username β”‚ ⏰ 5d ago β”‚ β—‹ β”‚ 1 labels β”‚ Fix GraphQL schema β”‚ Fixes issue...
[protocol] #789 β”‚ @username β”‚ ⏰ 1d ago β”‚ βœ— β”‚ 0 labels β”‚ Update contracts β”‚ Updates the...

Legend:

  • βœ“ = Approved
  • β—‹ = No review decision yet
  • βœ— = Changes requested

3. Automated Migration

For each commit in the PR:

  1. Cherry-picks the commit
  2. Detects path conflicts (files not in workspace directory)
  3. Automatically moves files to correct workspace (e.g., src/index.ts β†’ frontend/src/index.ts)
  4. Continues with next commit

4. Creates New PR

The new PR includes:

  • Title prefixed with workspace: [frontend] Original Title
  • Link to original PR
  • Credit to original author
  • Full original description
  • Note about automatic migration

Example Migration

$ ./scripts/migrate-pr.sh

╔═══════════════════════════════════════════════════════════╗
β•‘        Zora Monorepo PR Migration Tool                   β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Checking dependencies...
βœ“ All dependencies satisfied

Fetching open PRs from all repositories...
βœ“ Found 12 open PRs

Use arrow keys to navigate, type to filter, Enter to select

> [frontend] #234 β”‚ @alice β”‚ ⏰ 1d ago β”‚ βœ“ β”‚ 2 labels β”‚ Add bio field...

Selected: ourzora/zora-co#234 β†’ monorepo/frontend/

Starting Automated PR Migration
───────────────────────────────────────────────────────

βœ“ PR Details:
  Title:  Add user bio field to profile
  Author: @alice
  Branch: feature/user-bio

Continue with migration? (y/N): y

Creating migration branch: migrate-frontend-pr-234
βœ“ Found 3 commits to migrate

Cherry-picking commits and fixing paths...
[1/3] Cherry-picking Add bio field to schema...
[2/3] Cherry-picking Add bio UI component...
  ⚠ Conflicts detected, attempting auto-fix...
    βœ“ Moved src/Profile.tsx β†’ frontend/src/Profile.tsx
[3/3] Cherry-picking Add tests for bio field...

βœ“ Successfully cherry-picked all commits
βœ“ Auto-fixed 1 path conflicts

Pushing migration branch...
Creating new PR in monorepo...

═══════════════════════════════════════════════════════════
     Migration Complete! πŸŽ‰
═══════════════════════════════════════════════════════════

βœ“ Migrated 3 commits
βœ“ Created PR: [frontend] Add user bio field to profile
βœ“ Branch: migrate-frontend-pr-234

When to Use This Script

Good candidates for migration:

  • βœ… Simple feature PRs with few conflicts
  • βœ… Bug fixes
  • βœ… PRs that only touch one workspace
  • βœ… PRs where quick migration is valuable

Not recommended for migration:

  • ❌ Very large PRs with many files (finish in old repo first)
  • ❌ PRs that touch multiple repos (would need manual coordination)
  • ❌ PRs with complex merge conflicts
  • ❌ PRs very close to being merged (just finish them in old repo)

Troubleshooting

"No commits found in PR"

  • The PR may have no unique commits compared to main
  • Try rebasing the PR in the old repo first

"Failed to resolve conflicts automatically"

  • Some conflicts require manual resolution
  • The script will stop and show you what needs fixing
  • Resolve conflicts manually, then run: git cherry-pick --continue

"Failed to create PR"

  • The branch was pushed successfully but PR creation failed
  • Create the PR manually at the URL shown in the output

PR appears empty in monorepo

  • Check that the original PR branch exists in the old repo
  • Ensure you have access to fetch from the old repo

Manual Cleanup

If something goes wrong and you need to start over:

# Delete migration branch
git branch -D migrate-frontend-pr-234

# Delete remote branch if pushed
git push origin --delete migrate-frontend-pr-234

# Remove temp remote (if exists)
git remote remove migrate-temp

Tips

  1. Use fzf: The fuzzy search makes finding your PR much easier
  2. Filter by workspace: Type the workspace name to filter (e.g., "frontend")
  3. Search by author: Type your GitHub username to see only your PRs
  4. Check original PR: After migration, review both PRs to ensure everything migrated correctly
  5. Close old PR: After successful migration, close the original PR with a comment linking to the new one

Support

If you encounter issues:

  1. Check the error message - they're designed to be helpful
  2. Ask in #monorepo-migration Slack channel
  3. Ping the migration team

Script Internals

The script uses:

  • gh pr list to fetch PRs with full metadata
  • gh pr view to get detailed PR information
  • git cherry-pick to migrate commits
  • git fetch with PR refs to get commit history
  • Conflict detection via git diff --name-only --diff-filter=U
  • Automatic path fixing by moving files into workspace directories

Note: This script is designed for the parallel running phase of the monorepo migration. Once all teams have fully migrated, this script will no longer be needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment