-
-
Save johnnyshankman/758eb0aaa073bce596785d44356bb446 to your computer and use it in GitHub Desktop.
#!/bin/bash | |
# ==================================================================================================== | |
# UPGRADE-PKGS - Npm Package Upgrader for Multiple Repositories | |
# ==================================================================================================== | |
# | |
# DESCRIPTION: | |
# This script automates the process of updating npm packages across multiple repositories. | |
# It ensures all git operations are performed safely, by: | |
# - Working from the latest version of the main branch | |
# - Creating a new branch for changes | |
# - Never committing directly to main | |
# - Creating pull requests for each repository | |
# | |
# REQUIREMENTS: | |
# - Git | |
# - GitHub CLI (gh) - Must be authenticated before running | |
# - npm | |
# | |
# USAGE: | |
# chmod +x upgrade-pkgs.sh | |
# ./upgrade-pkgs.sh | |
# | |
# INTERACTIVE INPUTS: | |
# 1. Packages to update (space-separated) | |
# Example: @manifoldxyz/pkg1 @manifoldxyz/pkg2 | |
# | |
# 2. Version ranges (space-separated, matching package order) | |
# Example: ^1.0.0 ^2.2.1 | |
# | |
# 3. Repository paths (space-separated, relative to current directory) | |
# Example: studio-apps-r2 my-cool-other-repo | |
# | |
# BEHAVIOR: | |
# For each repository, the script will: | |
# 1. Check out the main branch and pull latest changes | |
# 2. Create a new branch for package updates | |
# 3. Update the specified packages to their target versions | |
# 4. Verify installations using: | |
# a. Remove node_modules directory for a clean environment | |
# b. First attempt with 'npm install --force' | |
# c. Remove node_modules again | |
# d. Second verification with regular 'npm install' | |
# 5. Commit changes if any updates were made | |
# 6. Push the branch to origin | |
# 7. Create a pull request with details of the updates | |
# | |
# SAFETY FEATURES: | |
# - Will not commit directly to main branch | |
# - Discards any uncommitted changes in repositories | |
# - Validates GitHub CLI authentication before proceeding | |
# - Creates descriptive pull requests for review before merging | |
# - Cleans up branches if no changes were made | |
# - Double verification of package installations | |
# - Clean node_modules removal before installations | |
# | |
# EXAMPLE WORKFLOW: | |
# $ ./upgrade-pkgs.sh | |
# Enter packages to update (space-separated): @manifoldxyz/pkg1 @manifoldxyz/pkg2 | |
# Enter version ranges (space-separated, matching the order of packages): ^1.0.0 ^2.2.1 | |
# Enter repository paths (space-separated): studio-apps-r2 my-cool-other-repo | |
# | |
# ==================================================================================================== | |
set -e | |
# Function to check if the gh CLI is logged in | |
check_gh_login() { | |
echo "Checking if you're logged into GitHub CLI..." | |
if ! gh auth status &>/dev/null; then | |
echo "Error: You are not logged into GitHub CLI. Please run 'gh auth login' first." | |
exit 1 | |
fi | |
echo "GitHub CLI authentication confirmed." | |
} | |
# Function to handle a specific repository | |
update_repo() { | |
local repo_path=$1 | |
local packages=("${!2}") | |
local versions=("${!3}") | |
local branch_name=$4 | |
local pr_title=$5 | |
local pr_body=$6 | |
echo "-------------------------------------" | |
echo "Processing repository: $repo_path" | |
# Navigate to the repository | |
cd "$repo_path" || { echo "Error: Could not navigate to $repo_path"; return 1; } | |
# Discard any uncommitted changes | |
git reset --hard HEAD | |
# Switch to main branch | |
echo "Switching to main branch..." | |
git checkout main || { echo "Error: Could not switch to main branch in $repo_path"; return 1; } | |
# Pull latest changes | |
echo "Pulling latest changes from origin/main..." | |
git pull origin main || { echo "Error: Could not pull latest changes in $repo_path"; return 1; } | |
# Create and switch to a new branch | |
echo "Creating and switching to new branch: $branch_name..." | |
git checkout -b "$branch_name" || { echo "Error: Could not create new branch in $repo_path"; return 1; } | |
# Update each package | |
echo "Updating packages:" | |
local update_success=false | |
# First, check if all packages exist in package.json | |
for i in "${!packages[@]}"; do | |
local pkg="${packages[$i]}" | |
if [ ! -f "package.json" ]; then | |
echo "Error: No package.json found in $repo_path" | |
return 1 | |
fi | |
if ! grep -q "\"$pkg\"" package.json; then | |
echo "Warning: Package $pkg not found in package.json" | |
fi | |
done | |
# If package.json exists, update it directly | |
if [ -f "package.json" ]; then | |
# Create a backup of package.json | |
cp package.json package.json.bak | |
# Update each package version directly in package.json | |
# This approach is more efficient than running npm install for each package | |
echo "Directly updating package versions in package.json..." | |
for i in "${!packages[@]}"; do | |
local pkg="${packages[$i]}" | |
local ver="${versions[$i]}" | |
echo " - $pkg to $ver" | |
# Update in dependencies section if it exists there | |
if grep -q "\"$pkg\"" package.json; then | |
# Using jq to determine which section contains the package and update it | |
if command -v jq >/dev/null 2>&1; then | |
# Try to update in each dependency section using jq | |
updated=false | |
for section in dependencies devDependencies peerDependencies; do | |
# Check if the package exists in this section | |
if jq -e ".$section.\"$pkg\" // empty" package.json >/dev/null 2>&1; then | |
echo "Updating $pkg in $section section" | |
# Update package version while preserving the file structure | |
jq --indent 2 "if .$section.\"$pkg\" != null then .$section.\"$pkg\" = \"$ver\" else . end" package.json > package.json.tmp && mv package.json.tmp package.json | |
updated=true | |
update_success=true | |
break | |
fi | |
done | |
if [ "$updated" = false ]; then | |
echo "Warning: Package $pkg found in package.json but not in any dependency section" | |
fi | |
else | |
# Fallback to sed if jq is not available | |
echo "jq not available, using sed for package.json updates (less reliable)" | |
# Try to update in each dependency section using sed | |
updated=false | |
# Create a backup copy | |
cp package.json package.json.original | |
for section in dependencies devDependencies peerDependencies; do | |
# Check if section exists and contains the package | |
if grep -q "\"$section\"" package.json && grep -q "\"$section\".*\"$pkg\"" package.json; then | |
# Use a more precise sed pattern to update only in the correct section | |
sed -i.tmp -E "s/(\"$section\"[^}]*\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\",?)/\1$ver\2/" package.json | |
if ! diff -q package.json package.json.tmp >/dev/null 2>&1; then | |
rm -f package.json.tmp | |
updated=true | |
update_success=true | |
break | |
fi | |
rm -f package.json.tmp | |
fi | |
done | |
# If not updated in any specific section, try a more general approach | |
if [ "$updated" = false ]; then | |
# General approach with sed (less reliable but catches edge cases) | |
sed -i.tmp -E "s/(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\",?)/\1$ver\2/" package.json | |
if ! diff -q package.json package.json.tmp >/dev/null 2>&1; then | |
rm -f package.json.tmp | |
update_success=true | |
echo "Updated $pkg using general approach" | |
else | |
# Restore original if no changes were made | |
mv package.json.original package.json | |
echo "Warning: Could not update $pkg in package.json" | |
fi | |
else | |
rm -f package.json.original | |
fi | |
fi | |
else | |
echo "Warning: Package $pkg not found in package.json" | |
fi | |
done | |
# Remove temporary files created by sed | |
rm -f package.json.tmp package.json.original | |
# If no updates were made, restore the backup | |
if [ "$update_success" = false ]; then | |
mv package.json.bak package.json | |
echo "No packages were updated in package.json" | |
else | |
rm -f package.json.bak | |
fi | |
fi | |
# Verify installation with --force followed by regular install | |
if [ "$update_success" = true ]; then | |
echo "Removing node_modules directory for clean installation..." | |
rm -rf node_modules/ | |
# First run with --force to update package-lock.json and dependencies | |
# This ensures all package versions are resolved correctly | |
echo "Updating package-lock.json and verifying installation with npm install --force..." | |
if ! npm install --force; then | |
echo "Error: Force installation failed in $repo_path" | |
return 1 | |
fi | |
echo "Removing node_modules directory again before verification..." | |
rm -rf node_modules/ | |
# Second run without --force for final verification | |
# This ensures the project can be built normally without force flags | |
echo "Verifying package installation with regular npm install..." | |
if ! npm install; then | |
echo "Error: Regular installation failed after forced install in $repo_path" | |
return 1 | |
fi | |
echo "Package installation verified successfully." | |
fi | |
# Check if there are changes to commit | |
if ! git diff --quiet package.json package-lock.json; then | |
echo "Changes detected. Committing and pushing..." | |
git add package.json package-lock.json | |
git commit -m "$pr_title" | |
# Push the changes | |
git push --set-upstream origin "$branch_name" || { echo "Error: Could not push changes for $repo_path"; return 1; } | |
# Create a PR | |
echo "Creating pull request..." | |
gh pr create --title "$pr_title" --body "$pr_body" --base main || { echo "Error: Could not create PR for $repo_path"; return 1; } | |
echo "Pull request created successfully for $repo_path" | |
else | |
echo "No changes detected in package.json or package-lock.json. Skipping PR creation." | |
# Clean up the branch since we didn't make any changes | |
git checkout main | |
git branch -D "$branch_name" | |
fi | |
# Return to the original directory | |
cd - >/dev/null | |
echo "Completed processing $repo_path" | |
} | |
# Main script execution | |
main() { | |
# Check that gh CLI is logged in | |
check_gh_login | |
# Define the packages to update | |
# Example: packages=("@manifoldxyz/pkg1" "@manifoldxyz/pkg2") | |
read -p "Enter packages to update (space-separated): " -a input_packages | |
# Define the version ranges | |
# Example: versions=("^1.0.0" "^2.2.1") | |
read -p "Enter version ranges (space-separated, matching the order of packages): " -a input_versions | |
# Validate input | |
if [ ${#input_packages[@]} -ne ${#input_versions[@]} ]; then | |
echo "Error: Number of packages and versions must match." | |
exit 1 | |
fi | |
if [ ${#input_packages[@]} -eq 0 ]; then | |
echo "Error: No packages specified." | |
exit 1 | |
fi | |
# Define the repositories to update | |
# Example: repos=("studio-apps-r2" "my-cool-other-repo") | |
read -p "Enter repository paths (space-separated): " -a input_repos | |
if [ ${#input_repos[@]} -eq 0 ]; then | |
echo "Error: No repositories specified." | |
exit 1 | |
fi | |
# Generate a timestamp for branch names | |
timestamp=$(date +"%Y%m%d%H%M%S") | |
# Create PR title and description | |
package_list="" | |
for i in "${!input_packages[@]}"; do | |
if [ $i -gt 0 ]; then | |
package_list+=", " | |
fi | |
package_list+="${input_packages[$i]}@${input_versions[$i]}" | |
done | |
pr_title="Update npm packages: $package_list" | |
# Create PR body with proper newlines | |
pr_body="This PR updates the following npm packages: | |
" | |
for i in "${!input_packages[@]}"; do | |
pr_body+="- ${input_packages[$i]} to ${input_versions[$i]} | |
" | |
done | |
pr_body+=" | |
Automatically generated by package upgrade script." | |
# Process each repository | |
for repo in "${input_repos[@]}"; do | |
# Create a unique branch name | |
branch_name="update-packages-$timestamp" | |
# Update the repository | |
update_repo "$repo" input_packages[@] input_versions[@] "$branch_name" "$pr_title" "$pr_body" | |
done | |
echo "-------------------------------------" | |
echo "Package update process completed." | |
} | |
# Execute the main function | |
main "$@" |
Manifold Example
./update-npm-pkgs.sh
Enter packages to update (space-separated): @manifoldxyz/studio-app-sdk-react @manifoldxyz/studio-app-sdk @manifoldxyz/studio-apps-client @manifoldxyz/studio-app-cli
Enter version ranges (space-separated, matching the order of packages): ^15.0.0 ^10.0.5 ^1.0.0 ^3.0.8
Enter repository paths (space-separated): studio-app-frame-edition studio-app-gachapon
- studio-app-rich-text-minting
- studio-app-claim-v2
- studio-app-burn-redeem-v2
- studio-app-cross-chain-burn-redeem
- studio-app-physicals-v2
- studio-app-gachapon
- studio-app-marketplace-v2
- studio-app-curate-page
- studio-app-thanks-page
- studio-app-frame-edition
- studio-app-batch-multi
- studio-app-batch-edition
- studio-app-edition
- studio-app-one-of-one
- studio-app-giveaways
Changed up the code here so it works for multiple packages and versions as well
#!/bin/bash
# Input parameters
folders=$1 # Comma-separated list of folders
package_names=$2 # Comma-separated list of npm package names
version_strings=$3 # Comma-separated list of Version strings, corresponding to the package names
if [ -z "$folders" ] || [ -z "$package_names" ] || [ -z "$version_strings" ]; then
echo "Usage: $0 <folders> <package_names> <version_strings>"
exit 1
fi
# Sanitize the packages name for branch name use
sanitized_package_name=$(echo "$package_names" | sed 's/[@\/]/-/g' | sed 's/,/-/g')
# Sanitize the versions string for branch name use by removing '^' and '~'
sanitized_version_string=$(echo "$version_strings" | sed 's/[\^~]//g' | sed 's/,/-/g')
# Convert comma-separated list into an array of packages
IFS=',' read -r -a folder_array <<< "$folders"
# Convert comma-separated list of packages name into an array of package names
IFS=',' read -r -a package_name_array <<< "$package_names"
# Convert comma-separated list of versions into an array of versions
IFS=',' read -r -a version_array <<< "$version_strings"
# Check if package_name_array and version_array have the same length
if [ ${#package_name_array[@]} -ne ${#version_array[@]} ]; then
echo "Error: The number of package names and version strings must be the same."
exit 1
fi
for folder in "${folder_array[@]}"; do
if [ -d "$folder" ]; then
cd "$folder" || { echo "Failed to change directory to $folder"; exit 1; }
if [ -f "package.json" ]; then
# Checkout main branch
git checkout main
# Pull latest changes
git pull
# Create a new git branch
branch_name="update-${sanitized_package_name}-${sanitized_version_string}"
git checkout -b "$branch_name"
for i in "${!package_name_array[@]}"; do
package_name="${package_name_array[i]}"
version_string="${version_array[i]}"
echo "Processing $folder/package.json"
# Update the package version in package.json
jq ".dependencies[\"$package_name\"]=\"$version_string\"" package.json > tmp.$$.json && mv tmp.$$.json package.json
# Install updated packages
npm install
# Add changes to git
git add package.json package-lock.json
# Commit changes
git commit -m "Update $package_name to $version_string"
done
# Push branch to remote
git push origin "$branch_name"
# Switch back to main branch
git checkout main
else
echo "package.json not found in $folder"
fi
cd - > /dev/null || { echo "Failed to change directory back"; exit 1; }
else
echo "Folder $folder does not exist"
fi
done
Example:
./update-npm-pkgs.sh \ studio-app-batch-mint,studio-app-burn-redeem-v2,studio-app-physicals-v2,studio-app-gachapon,studio-app-marketplace-v2,studio-app-curate-page,studio-app-thanks-page,studio-app-frame-edition,studio-app-batch-mint,studio-app-batch-edition,studio-app-single-mint,studio-app-claim-v2 @manifoldxyz/studio-app-sdk-react,@manifoldxyz/studio-app-sdk ^7.2.1,^6.2.0
oh niiiiiice so that you can update two packages at once in all repos? @donpdang smart. i like it. will update gist shortly.
updated so that it requires github cli and auto-opens the PR for you as I was wasting a shit ton of time doing that.
A much more sane way to do this is via my new npm package:
Requirements
Download the shell script and put it anywhere.
Ensure the script has the right perms
Now make sure you have brew installed
jq
Lastly make sure you have the Github CLI installed and that you are logged in
Caveats
It will blow out any unstaged/uncommitted changes.