Skip to content

Instantly share code, notes, and snippets.

@disinfeqt
Created January 29, 2026 03:14
Show Gist options
  • Select an option

  • Save disinfeqt/20b4ec77832fcd45399a841e18f11924 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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