-
-
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 "$@" |
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:
Manifold Example