-
-
Save fcsonline/6f04319b1d7b99a09eb842f7e14aa063 to your computer and use it in GitHub Desktop.
Migration script to merge multiple repositories into a single monorepo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# NOTE: To be able to run this migration you need git >= 2.22.0 | |
# | |
# If you don't have it, install it with: | |
# sudo add-apt-repository ppa:git-core/ppa | |
# sudo apt update | |
# sudo apt install git | |
set -e | |
echo 'Installing git-filter-repo' | |
python3 -m pip install --user git-filter-repo | |
if [ -z "$GITHUB_TOKEN" ]; then | |
echo "\$GITHUB_TOKEN is empty" | |
exit | |
fi | |
# NOTE: Add here all repositories. | |
# Syntax: <name> <subfolder_name> <master_branch> | |
ITEMS=( | |
"mobile mobile main" | |
"frontend frontend master" | |
"backend backend master" | |
) | |
# NOTE: Change here the monorepo repository name. | |
MONOREPO="monorepo" | |
for ITEM in "${ITEMS[@]}"; do | |
REPOSITORY=$(echo "$ITEM" | cut -d' ' -f 1) | |
FOLDER=$(echo "$ITEM" | cut -d' ' -f 2) | |
MASTER_BRANCH=$(echo "$ITEM" | cut -d' ' -f 3) | |
URL=$(echo "git@github.com:factorialco/${REPOSITORY}.git") | |
TMP="$(mktemp -d -t "${FOLDER##*/}.XXXX")" | |
echo "🟢 Cloning ${URL} content..." | |
git clone "$URL" "$TMP" | |
echo "🟢 Initial cleanup..." | |
git remote remove "$FOLDER" || true | |
echo "🟢 Migrating all branches from $URL content into $FOLDER folder..." | |
pushd "$TMP" | |
for BRANCH in $(git branch -a | grep "^ *remotes/" | grep -v HEAD | grep -v $MASTER_BRANCH); do | |
git branch --track ${BRANCH#remotes/origin/} $BRANCH; | |
done | |
git filter-repo --refs $(git branch | grep -v '\*' | xargs -0) --force --to-subdirectory $FOLDER/ | |
git filter-repo --refs $MASTER_BRANCH --force --to-subdirectory $FOLDER/ | |
popd | |
echo "🟢 Linking monorepo with temporal ${FOLDER} folder..." | |
git remote add "$FOLDER" "$TMP" | |
git fetch "$FOLDER" | |
echo "🟢 Bringing back the old branches..." | |
git branch | grep -v "*" | xargs -r git branch -D | |
for BRANCH in $(git branch -a | grep "^ *remotes/$FOLDER" | grep -v HEAD | grep -v $MASTER_BRANCH); do | |
git checkout -b "${BRANCH/remotes\/}" $BRANCH | |
done | |
echo "🟢 Going back the main..." | |
git checkout main | |
echo "🟢 Merging temporal ${FOLDER} folder into monorepo..." | |
git merge "$FOLDER/$MASTER_BRANCH" --allow-unrelated-histories --no-ff -m "[Monorepo] Add ${FOLDER}" | |
git push -f origin main | |
echo "🟢 Cleaning up..." | |
rm -rf "$TMP" | |
git remote rm "$FOLDER" | |
git gc | |
git prune | |
for BRANCH in $(git branch -a | grep "^ *$FOLDER" | grep -v HEAD | grep -v $MASTER_BRANCH); do | |
echo "🟢 Pushing $BRANCH branch..." | |
git push -f origin $BRANCH | |
git branch -D $BRANCH | |
done | |
echo "🟢 Creating pull requests for $FOLDER..." | |
npm ci | |
REPOSITORY=$REPOSITORY FOLDER=$FOLDER MASTER_BRANCH=$MASTER_BRANCH MONOREPO=$MONOREPO node ./pullrequest.js | |
echo "🟢 Process completed for $FOLDER repository" | |
done | |
echo "💯 Monorepo process completed!" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { graphql } from '@octokit/graphql' | |
if (!process.env.GITHUB_TOKEN) { | |
throw new Error('You have to pass a GITHUB_TOKEN') | |
} | |
if (!process.env.REPOSITORY) { | |
throw new Error('You have to pass a REPOSITORY') | |
} | |
if (!process.env.FOLDER) { | |
throw new Error('You have to pass a FOLDER') | |
} | |
if (!process.env.MASTER_BRANCH) { | |
throw new Error('You have to pass a MASTER_BRANCH') | |
} | |
if (!process.env.MONOREPO) { | |
throw new Error('You have to pass a MONOREPO') | |
} | |
const graphqlWithAuth = graphql.defaults({ | |
headers: { | |
authorization: `token ${process.env.GITHUB_TOKEN}` | |
} | |
}) | |
const fetchMonoRepository = async (name) => { | |
const { repository } = await graphqlWithAuth(` | |
{ | |
repository(owner: "factorialco", name: "${name}") { | |
id | |
} | |
} | |
`) | |
return repository.id | |
} | |
const fetchPullRequest = async (repositoryId, after) => { | |
const { repository } = await graphqlWithAuth(` | |
query fetchPullRequest ($after: String, $limit: Int!){ | |
repository(owner: "factorialco", name: "${repositoryId}") { | |
pullRequests(after: $after, first: $limit, states: [OPEN]) { | |
pageInfo { | |
hasNextPage | |
endCursor | |
} | |
edges { | |
node { | |
id | |
isDraft | |
title | |
body | |
url | |
assignees(last: 10) { | |
edges { | |
node { | |
id | |
} | |
} | |
} | |
author { | |
login | |
} | |
baseRefName | |
headRefName | |
} | |
} | |
} | |
} | |
} | |
`, { | |
after, | |
limit: 5 | |
}) | |
return repository | |
} | |
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms)) | |
const migrateRepository = async (repositoryId, folder) => { | |
const createPullRequest = async (element) => { | |
const { id, title, body, url, isDraft, baseRefName, headRefName, author, assignees } = element.node | |
const newBaseRefName = baseRefName === process.env.MASTER_BRANCH ? 'main' : `${folder}/${baseRefName}` | |
const newHeadRefName = `${folder}/${headRefName}` | |
const newBody = `${body}\n\n### :monkey: Monorepo\nOriginal Pull Request: ${url}` | |
console.log(`Waiting 1 minute before creating pull request with title '${title}'`) | |
await timeout(60_000) | |
console.log(`Creating pull request by ${author.login} with title '${title}'`) | |
try { | |
const { createPullRequest } = await graphqlWithAuth(` | |
mutation createPullRequest($title: String!, $body: String){ | |
createPullRequest ( | |
input: { | |
repositoryId: "${monorepoId}", | |
title: $title, | |
body: $body, | |
baseRefName: "${newBaseRefName}", | |
headRefName: "${newHeadRefName}", | |
draft: ${isDraft}, | |
} | |
) { | |
pullRequest { | |
id | |
url | |
} | |
clientMutationId | |
} | |
} | |
`, { | |
title, | |
body: newBody | |
}) | |
console.log(`Done! New pull request: ${createPullRequest.pullRequest.url} (Mutation: ${createPullRequest.clientMutationId})`) | |
if (author.login === 'dependabot') { | |
console.log(`Depenedabot pull request. Skipping asssinees step`) | |
return | |
} | |
const { user } = await graphqlWithAuth(` | |
{ | |
user(login: "${author.login}") { | |
id | |
} | |
} | |
`) | |
const newAssignees = [ | |
...assignees.edges.map(assignee => assignee.node.id), | |
user.id | |
] | |
console.log(`Assigning pull request to ${newAssignees}`) | |
const { addAssigneesToAssignable } = await graphqlWithAuth(` | |
mutation addAssignee($newAssignees: [ID!]!) { | |
addAssigneesToAssignable(input: { | |
assignableId: "${createPullRequest.pullRequest.id}", | |
assigneeIds: $newAssignees | |
}) { | |
clientMutationId | |
} | |
} | |
`, { | |
newAssignees: [...new Set(newAssignees)] | |
}) | |
console.log(`Pull request assigned to ${author.login} (Mutation: ${addAssigneesToAssignable.clientMutationId})`) | |
} catch (error) { | |
console.log('ERROR', error) | |
} | |
} | |
const monorepoId = await fetchMonoRepository(process.env.MONOREPO) | |
let hasNextPage = true | |
let after = null | |
do { | |
console.log(`Fetching a new page of pullRequests (cursor: ${after})`) | |
const repository = await fetchPullRequest(repositoryId, after) | |
repository.pullRequests.edges.forEach(createPullRequest) | |
hasNextPage = repository.pullRequests.pageInfo.hasNextPage | |
after = repository.pullRequests.pageInfo.endCursor | |
} while (hasNextPage) | |
} | |
migrateRepository(process.env.REPOSITORY, process.env.FOLDER) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment