Skip to content

Instantly share code, notes, and snippets.

@johnnyshankman
Last active May 19, 2025 19:34
Show Gist options
  • Save johnnyshankman/758eb0aaa073bce596785d44356bb446 to your computer and use it in GitHub Desktop.
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 "$@"
@donpdang
Copy link

donpdang commented Aug 1, 2024

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

@donpdang
Copy link

donpdang commented Aug 1, 2024

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

@johnnyshankman
Copy link
Author

oh niiiiiice so that you can update two packages at once in all repos? @donpdang smart. i like it. will update gist shortly.

@johnnyshankman
Copy link
Author

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.

@johnnyshankman
Copy link
Author

A much more sane way to do this is via my new npm package:

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