Created
January 29, 2026 03:14
-
-
Save disinfeqt/20b4ec77832fcd45399a841e18f11924 to your computer and use it in GitHub Desktop.
pnpm-update-all: Update deps across pnpm projects, test build, commit on success
This file contains hidden or 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
| #!/usr/bin/env zsh | |
| set -uo pipefail | |
| # pnpm-update-all: Update deps across pnpm projects, test build, commit on success | |
| # Usage: pnpm-update-all [--parallel N] [--dir DIR] [--dry-run] | |
| SITES_DIR="${SITES_DIR:-$HOME/Sites}" | |
| PARALLEL=1 | |
| DRY_RUN=false | |
| VERBOSE=false | |
| # Colors | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' | |
| usage() { | |
| cat << EOF | |
| Usage: pnpm-update-all [OPTIONS] | |
| Scan pnpm projects, update deps, test build, commit on success. | |
| Options: | |
| -d, --dir DIR Base directory to scan (default: ~/Sites) | |
| -p, --parallel N Run N updates in parallel (default: 1, sequential) | |
| -n, --dry-run Show what would be done without doing it | |
| -v, --verbose Verbose output | |
| -h, --help Show this help | |
| Examples: | |
| pnpm-update-all # Update all projects sequentially | |
| pnpm-update-all -p 4 # Update 4 projects in parallel | |
| pnpm-update-all -n # Dry run | |
| pnpm-update-all -d ~/work # Scan different directory | |
| EOF | |
| exit 0 | |
| } | |
| log() { print -P "%F{blue}[INFO]%f $1" } | |
| success() { print -P "%F{green}[OK]%f $1" } | |
| warn() { print -P "%F{yellow}[WARN]%f $1" } | |
| error() { print -P "%F{red}[ERROR]%f $1" } | |
| # Parse args | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -d|--dir) SITES_DIR="$2"; shift 2 ;; | |
| -p|--parallel) PARALLEL="$2"; shift 2 ;; | |
| -n|--dry-run) DRY_RUN=true; shift ;; | |
| -v|--verbose) VERBOSE=true; shift ;; | |
| -h|--help) usage ;; | |
| *) error "Unknown option: $1"; exit 1 ;; | |
| esac | |
| done | |
| # Find all pnpm projects | |
| find_pnpm_projects() { | |
| find "$SITES_DIR" -maxdepth 2 -name "pnpm-lock.yaml" -type f 2>/dev/null | while read -r lockfile; do | |
| dirname "$lockfile" | |
| done | |
| } | |
| # Update a single project, returns status via file | |
| update_project() { | |
| local dir="$1" | |
| local name="${dir:t}" | |
| local tmpdir="/tmp/pnpm-update-$$" | |
| mkdir -p "$tmpdir" | |
| local status_file="$tmpdir/$name.status" | |
| local update_log="$tmpdir/$name.update.log" | |
| local build_log="$tmpdir/$name.build.log" | |
| log "[$name] Starting..." | |
| cd "$dir" || { echo "error:cd" > "$status_file"; return 1; } | |
| # Check for uncommitted changes | |
| if [[ -n $(git status --porcelain 2>/dev/null) ]]; then | |
| warn "[$name] Has uncommitted changes, skipping" | |
| echo "skipped:uncommitted" > "$status_file" | |
| return 0 | |
| fi | |
| # Check if it's a git repo | |
| if [[ ! -d .git ]]; then | |
| warn "[$name] Not a git repo, skipping" | |
| echo "skipped:notgit" > "$status_file" | |
| return 0 | |
| fi | |
| if $DRY_RUN; then | |
| log "[$name] [DRY-RUN] Would run: pnpm update" | |
| log "[$name] [DRY-RUN] Would run: pnpm build" | |
| echo "dryrun" > "$status_file" | |
| return 0 | |
| fi | |
| # Run pnpm update | |
| log "[$name] Running pnpm update..." | |
| if ! pnpm update > "$update_log" 2>&1; then | |
| error "[$name] pnpm update failed" | |
| $VERBOSE && cat "$update_log" | |
| echo "error:update" > "$status_file" | |
| return 1 | |
| fi | |
| # Check if lockfile changed | |
| if [[ -z $(git diff --name-only pnpm-lock.yaml 2>/dev/null) ]]; then | |
| success "[$name] Already up to date" | |
| echo "uptodate" > "$status_file" | |
| return 0 | |
| fi | |
| # Check if build script exists | |
| if ! grep -q '"build"' package.json 2>/dev/null; then | |
| success "[$name] Updated (no build script)" | |
| echo "success:nobuild" > "$status_file" | |
| return 0 | |
| fi | |
| # Run build test | |
| log "[$name] Testing build..." | |
| if pnpm build > "$build_log" 2>&1; then | |
| success "[$name] Build passed ✓" | |
| echo "success" > "$status_file" | |
| else | |
| error "[$name] Build failed" | |
| $VERBOSE && tail -30 "$build_log" | |
| # Restore changes | |
| git checkout . 2>/dev/null || true | |
| echo "error:build" > "$status_file" | |
| return 1 | |
| fi | |
| } | |
| # Commit changes for a project | |
| commit_project() { | |
| local dir="$1" | |
| local name="${dir:t}" | |
| cd "$dir" || return 1 | |
| git add pnpm-lock.yaml package.json 2>/dev/null || true | |
| if [[ -n $(git diff --cached --name-only) ]]; then | |
| git commit -m "chore: update dependencies" | |
| success "[$name] Committed" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| main() { | |
| log "Scanning $SITES_DIR for pnpm projects..." | |
| local projects=() | |
| while IFS= read -r line; do | |
| [[ -n "$line" ]] && projects+=("$line") | |
| done < <(find_pnpm_projects) | |
| if [[ ${#projects[@]} -eq 0 ]]; then | |
| warn "No pnpm projects found" | |
| exit 0 | |
| fi | |
| log "Found ${#projects[@]} projects" | |
| echo "" | |
| for p in "${projects[@]}"; do echo " - ${p:t}"; done | |
| echo "" | |
| # Create temp dir | |
| local tmpdir="/tmp/pnpm-update-$$" | |
| mkdir -p "$tmpdir" | |
| # Update projects | |
| if [[ $PARALLEL -eq 1 ]]; then | |
| for dir in "${projects[@]}"; do | |
| update_project "$dir" | |
| done | |
| else | |
| log "Running $PARALLEL parallel updates..." | |
| local running=0 | |
| local pids=() | |
| for dir in "${projects[@]}"; do | |
| update_project "$dir" & | |
| pids+=($!) | |
| ((running++)) | |
| # Wait for a slot if at capacity | |
| if ((running >= PARALLEL)); then | |
| wait -n 2>/dev/null || wait $pids[1] | |
| pids=(${pids:1}) # remove first pid | |
| ((running--)) | |
| fi | |
| done | |
| # Wait for remaining jobs | |
| wait | |
| fi | |
| # Collect results | |
| echo "" | |
| print -P "%F{cyan}═══════════════════════════════════════%f" | |
| print -P "%F{cyan} SUMMARY%f" | |
| print -P "%F{cyan}═══════════════════════════════════════%f" | |
| local success_projects=() | |
| local failed_projects=() | |
| for dir in "${projects[@]}"; do | |
| local name="${dir:t}" | |
| local status_file="$tmpdir/$name.status" | |
| local result="unknown" | |
| [[ -f "$status_file" ]] && result=$(cat "$status_file") | |
| case "$result" in | |
| success|success:nobuild) | |
| success_projects+=("$dir") | |
| print -P "%F{green}✓%f $name - updated, build passed" | |
| ;; | |
| uptodate) | |
| print -P "%F{blue}○%f $name - already up to date" | |
| ;; | |
| error:*) | |
| failed_projects+=("$name") | |
| print -P "%F{red}✗%f $name - ${result#error:} failed" | |
| ;; | |
| skipped:*) | |
| print -P "%F{yellow}⊘%f $name - skipped (${result#skipped:})" | |
| ;; | |
| dryrun) | |
| print -P "%F{blue}◌%f $name - dry run" | |
| ;; | |
| *) | |
| print -P "%F{yellow}?%f $name - $result" | |
| ;; | |
| esac | |
| done | |
| print -P "%F{cyan}═══════════════════════════════════════%f" | |
| echo "" | |
| # Commit prompt | |
| if [[ ${#success_projects[@]} -gt 0 ]]; then | |
| print -P "%F{green}${#success_projects[@]} project(s) ready to commit:%f" | |
| for p in "${success_projects[@]}"; do echo " - ${p:t}"; done | |
| echo "" | |
| if $DRY_RUN; then | |
| log "[DRY-RUN] Would prompt to commit" | |
| else | |
| echo -n "Commit changes? [y/N] " | |
| read -k1 reply | |
| echo "" | |
| if [[ "$reply" =~ ^[Yy]$ ]]; then | |
| for dir in "${success_projects[@]}"; do | |
| commit_project "$dir" | |
| done | |
| echo "" | |
| success "Done! Push manually when ready:" | |
| for p in "${success_projects[@]}"; do | |
| echo " cd ${p} && git push" | |
| done | |
| else | |
| log "Changes staged but not committed." | |
| fi | |
| fi | |
| else | |
| log "No projects need committing" | |
| fi | |
| # Cleanup | |
| rm -rf "$tmpdir" 2>/dev/null || true | |
| [[ ${#failed_projects[@]} -gt 0 ]] && exit 1 | |
| exit 0 | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment