webhubworks / package-updater
Bulk-update Composer packages (and Craft / Craft plugins) across all your local repos.
Package info
github.com/webhubworks/package-updater
Type:project
pkg:composer/webhubworks/package-updater
Requires
- php: ^8.2
- laravel-zero/framework: ^12.0.2
Requires (Dev)
- laravel/pint: ^1.25.1
- mockery/mockery: ^1.6.12
- pestphp/pest: ^3.8.4|^4.1.2
This package is auto-updated.
Last update: 2026-07-17 17:47:08 UTC
README
A Laravel Zero CLI for bulk-updating Composer packages — and Craft itself /
Craft plugins — across every repo on your machine. Runs git pull on the
appropriate long-lived branch (develop → dev → staging → stag →
stage → main → master → prod → live), then the package update,
optional composer prep, optional site-crawler. Skips dirty repos,
captures full per-repo transcripts, summarises everything, and offers to
open repos with uncommitted changes in GitKraken for review.
1. Installation
Global (recommended)
composer global require webhubworks/package-updater
Make sure Composer's global bin directory is on your PATH — typically
~/.composer/vendor/bin (macOS/Linux) or %APPDATA%\Composer\vendor\bin
(Windows). After that, both package-updater and the shorter alias pu
are available from anywhere.
Set REPOS_DIR in your shell (or via .env next to the tool) so it
points at the directory you want scanned (defaults to $HOME/reps). The
walker descends through grouping folders (e.g. ~/reps/my7steps/<repo>),
considers a directory a candidate only when it contains a
composer.lock, and filters out anything that's not a git repo or that
itself declares "type": "craft-plugin".
To upgrade later:
composer global update webhubworks/package-updater
Local clone (for development)
cd ~/reps git clone <repo-url> package-updater cd package-updater composer install ./package-updater list
2. Commands
Run pu <name> (or the longer package-updater <name>). Bare pu
prints the command list. Every command is fully interactive — it will
prompt for any answer it needs.
update
Single-repo helper. Run it from inside a repo: it verifies the working
tree is clean, runs composer update (auto-detecting ddev when
.ddev/config.yaml exists), parses Upgrading / Downgrading / Installing
/ Removing lines from composer's output, and commits the result with
title Package updates and a body listing each change. Optional package
arguments (pu update vendor/foo vendor/bar) restrict the update to
those packages. Use --no-ddev to force host composer, --commit /
--no-commit to skip the commit prompt.
update:all
Universal bulk update — one composer package across every local repo
that depends on it. Pick a vendor/name, pick a target version, pick
which of the matching repos to run on, pick parallelism, confirm. Each
repo runs git pull → ddev start → ddev composer update →
composer prep (if defined) → ddev stop (optional). After checking out
the lowest long-lived branch, the run fetches and aborts the repo if a
higher branch (e.g. main/master a teammate committed to directly) is
ahead of it — updating a stale branch would base the commit on the wrong
tree, so it's left for you to merge down first. Repos already at
the target version are pre-skipped; with a bare target version, repos
on a different major are pre-skipped too (prefix the version with !
to force across majors).
--filter-name=<substring> narrows the match set to repos whose
composer.json name contains the given substring — e.g.
pu update:all vendor/foo --filter-name=mvb only considers repos with
mvb in their composer name.
The package argument also accepts a wildcard — e.g.
pu update:all 'laravel-lang/*' matches every locked package under
that vendor prefix and runs composer update laravel-lang/* -W per
repo. Wildcards skip the target-version and transitive-parent prompts
and always pass -W (composer almost always needs it to bump siblings
together). Quote the pattern in your shell so it isn't glob-expanded.
remove
Bulk counterpart for removing a package. Same scan as update:all,
including wildcard support (pu remove 'laravel-lang/*'). Pre-skips
repos where the package is only a transitive dependency (composer
remove only works on declared deps) and pre-skips dirty repos.
For each remaining repo it detects whether the package is in require
or require-dev and groups the removals so composer is called with
--dev for the dev-only ones (plain for the rest). On success it
commits the change with title Remove <package> (or
Remove N packages for wildcards) and a body listing the removed
names.
pu remove vendor/foo # exact name pu remove 'laravel-lang/*' --dry-run # preview which packages would be removed
update:craft
Craft-aware variant. Same flow, but:
- Identifies repos by Craft plugin handle (or the literal
craftto match every site withcraftcms/cmsincomposer.json). - After
ddev start, always syncs the working copy before the update:ddev composer install→ddev php craft migrate/all→ddev php craft project-config/apply. - Runs
ddev php craft update <handle>(with sensible defaults you can edit at the prompt) instead ofcomposer update. - After
composer prep, optionally runssite-crawler crawl:ddevin a second multiselect-chosen subset of repos. Parses the crawler's "Failed requests" table and warns on any 5xx URLs even if the crawler itself exited cleanly.
--filter-name=<substring> works the same way as on update:all:
restricts the match set to repos whose composer.json name contains
the substring.
--maintenance is a one-flag preset for a dedicated update server. It
implies --yes and pre-seeds every prompt with the semi-automated
defaults: handle all (update every Craft package in every matched
repo), --parallel=3, --stop-ddev (stop each project after a
successful update), commit + push, crawl every repo, and a craft command
with --backup=0. When a Slack webhook is configured (see
Configuration) it also posts a run
summary at the end. Any explicit flag still wins, so
pu update:craft --maintenance --parallel=5 --no-push overrides just
those two defaults.
Because a dedicated update server never carries real local work,
--maintenance also hard-resets any repo with uncommitted changes
before updating it (git reset --hard HEAD + git clean -fd) instead of
skipping it, then lets the normal sync steps (ddev composer install,
migrate/all, project-config/apply --force) bring dependencies and
project config back in line. Gitignored paths (vendor/, .env,
storage/) are left untouched. This only happens under --maintenance —
a normal update:craft run still skips dirty repos.
--maintenance also retries the craft update once on a transient
composer download failure (a corrupt/0-byte dist zip, a truncated
download). Before retrying it restores a healthy state — git reset --hard + git clean -fd, ddev composer clear-cache, then ddev composer install from the committed lock — so a half-extracted vendor
tree (which can stop Craft from even booting) can't wedge the retry.
Genuine dependency conflicts are never retried.
retry
Re-runs the most recent update:all or update:craft non-interactively
using the answers persisted in logs/last-run.json. Useful for working
through a batch in chunks — already-up-to-date repos are skipped by the
target-version filter, so each retry picks up where the previous one left
off.
open
Opens repos from the most recent run in GitKraken (one tab per repo via
the gitkraken:// URL scheme). By default it surfaces repos that
warrant review: uncommitted working-tree changes, failed steps, failing
tests after composer prep, crawler failures, or 5xx URLs from the
crawler. You can narrow the pool with --filter=changed,
--filter=failed, or --filter=all. Both update:all and
update:craft also offer this prompt directly at the end of a run —
push the resulting commits via GitKraken without context-switching.
Logs and transcripts
logs/transcripts/<repo>-<timestamp>.log— full output of every step for one repo, fromgit statustoddev stop. Always written, success or fail.logs/<repo>-<step>-<timestamp>.log— narrow per-step log written when a specific command fails or its tests don't pass.logs/last-run.json— the resolved command + arguments + options of the last run (powersretry).logs/last-results.json— per-repo results of the last run (powersopen).
In sequential mode (--parallel=1) every step's output streams live to
the terminal. Parallel mode does not stream (output would interleave) —
the transcript and per-step logs are how you investigate.
3. Configuration
Repo directory
REPOS_DIR (env var, or .env next to the tool when running from a
local clone) — single source of truth for where to scan. Default:
$HOME/reps. Per-run override available on every command via
--reps-dir=.
Slack webhook (maintenance runs)
pu update:craft --maintenance posts a run summary to Slack when a
webhook is configured. To set it up:
- At https://api.slack.com/apps, create a Slack app (or reuse one), enable Incoming Webhooks, then Add New Webhook to Workspace and pick the channel the summary should post to.
- Copy the generated
https://hooks.slack.com/services/...URL. - Save it - interactively via
pu setup, or non-interactively:
pu setup --slack-webhook-url="https://hooks.slack.com/services/T.../B.../..." pu setup --no-slack # persist an empty webhook (notifications off)
The URL is stored in ~/.config/package-updater/config.json. Leave it
blank to disable notifications; once you have answered, pu setup won't
keep asking.
The summary groups the run into three buckets: repos updated, committed
and pushed; repos updated but not pushed (uncommitted, or committed with
the push held back by failing tests / PHPStan / a site-crawler issue);
and failed, skipped, or otherwise-flagged repos. The last bucket also
catches an otherwise up-to-date repo that still hit a problem during the
run — a 5xx during the crawl (with the offending URLs listed), a crawler
crash, or failing tests — so those never disappear into the "already up
to date" count. It's only sent on --maintenance runs, and only when a
webhook is configured. Any send error is logged as a warning and never
fails the run.
The message posts under the Slack app's own name and icon (the app the webhook belongs to). Slack ignores per-message sender overrides for app-based webhooks, so rename the Slack app itself if you want a different display name.
Git credentials (HTTPS remotes)
The tool shells out to git, which uses your CLI credentials — not
GitKraken's. For HTTPS GitHub remotes, install and configure the GitHub
CLI once:
brew install gh gh auth login # pick HTTPS, browser auth gh auth setup-git # register gh as git's credential helper
For Bitbucket / GitLab / Azure DevOps HTTPS remotes, use Git Credential Manager:
brew install --cask git-credential-manager git-credential-manager configure
After either, git pull runs without prompting on matching remotes.
Host SSH agent (SSH remotes)
Repos cloned over SSH (git@bitbucket.org:…, git@github.com:…) bypass
those credential helpers and need your host's SSH agent to have the right
key loaded. Add a per-host block to ~/.ssh/config so macOS loads it on
login:
Host bitbucket.org
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_rsa
Then once:
ssh-add --apple-use-keychain ~/.ssh/id_rsa
To find which local key matches the fingerprint shown in your git host's UI:
for f in ~/.ssh/*.pub; do ssh-keygen -lf "$f"; done
DDEV SSH for private composer dependencies
composer update runs inside ddev, which uses a global, shared SSH-agent
container. The tool runs ddev auth ssh for you once at the start of a
real run (after the confirm prompt, before any repo work) so private
GitHub composer sources resolve without per-repo setup. If your SSH keys
have passphrases, you'll be prompted at that point. The step can be
skipped if you've already loaded keys in this shell.
DDEV hostnames / sudo
The first time ddev starts a given project on this device it may need
sudo to add the project's .ddev.site hostname to /etc/hosts. In a
piped/parallel run that would hang forever on the password prompt — the
tool watches for the trigger lines (needs to run with administrative privileges / may need to enter your password for sudo) and kills ddev
immediately, then fails that repo with a hint telling you to run ddev start manually once and then retry.
GitKraken (for the open feature)
Uses the gitkraken://repo/path/<absolute-path> URL scheme via macOS
open. No extra setup beyond installing the GitKraken app.