davidrjonas/composer-lock-diff
A CLI tool that compares two composer.lock files and reports which packages were added, removed, or updated, including version changes. Useful for changelogs, release notes, and reviewing dependency upgrades in pull requests. Outputs plain text or Markdown tables.
Add the tool as a dev dependency so it never ships to production:
composer require --dev davidrjonas/composer-lock-diff
It exposes an executable at vendor/bin/composer-lock-diff (and, when the plugin is enabled, a composer lock-diff subcommand). The mental model is simple: point it at two composer.lock files — either on disk or as git refs — and it prints a table of every package that was added, removed, upgraded, or downgraded.
First use case, and the one you will reach for constantly: reviewing a composer update before you commit it.
cp composer.lock /tmp/composer.lock.before
composer update
vendor/bin/composer-lock-diff --from=/tmp/composer.lock.before --to=composer.lock
Look at the output first if you are just exploring: it defaults to a plain table, and --pretty gives you color and unicode borders for readability. The --from/--to pair is the only interface you need to learn — everything else is plumbing around those two inputs.
For reviewing someone else's branch (or a Dependabot/Renovate PR) without checking it out:
git fetch origin
vendor/bin/composer-lock-diff --from=origin/main --to=origin/dependabot/composer/laravel/framework-10.x
That is the "where to look first" — refs work just as well as paths, and git resolves them for you.
PR review summaries. The highest-leverage integration is a GitHub Actions job that posts (or edits) a PR comment with the package delta whenever composer.lock changes. Run the diff against the merge base, capture stdout, and use gh pr comment or actions/github-script to upsert it. This turns a 2,000-line lock file diff into ten readable rows, which is what reviewers actually need to see.
- name: Composer lock diff
if: contains(github.event.pull_request.changed_files, 'composer.lock')
run: |
git fetch origin ${{ github.base_ref }} --depth=1
vendor/bin/composer-lock-diff \
--from=origin/${{ github.base_ref }} --to=HEAD \
--no-dev --pretty > lock-diff.txt
CI gating on unexpected changes. If your policy is "no transitive upgrades outside Dependabot PRs", run the diff on every PR and fail the build when it emits rows (a small wc -l or exit-code check around it). Pair this with --no-dev in production-focused pipelines so phpunit, pest, and larastan bumps do not trip the gate.
Local pre-push / audit workflow. Drop it in a Composer script so it becomes muscle memory:
"scripts": {
"lock:diff": "composer-lock-diff --from=HEAD --to=composer.lock --pretty",
"update:safe": [
"cp composer.lock /tmp/composer.lock.bak",
"composer update --prefer-dist",
"composer lock:diff --from=/tmp/composer.lock.bak --to=composer.lock --pretty"
]
}
Run composer update:safe and you immediately see the blast radius before you stage anything.
Wrapping in an Artisan command. If your team has an internal php artisan deps:diff command, wrap the binary with Symfony\Component\Process\Process rather than re-implementing lock parsing:
$process = new Process([
base_path('vendor/bin/composer-lock-diff'),
'--from=HEAD~1',
'--to=composer.lock',
'--no-dev',
]);
$process->mustRun();
$this->line($process->getOutput());
Keep --no-dev and formatting flags as command options so the same wrapper serves both the audit and CI use cases.
Release notes. Before tagging a minor release, diff the previous release tag against HEAD (--from=v1.4.0 --to=HEAD) and paste the output into the changelog's "Dependencies" section. It is far more accurate than eyeballing composer outdated.
Scoping noisy output. Use --only-name when you only need to know what moved, and --only-version when you are building a compact changelog table. Combine with --no-dev in production-audit contexts and omit it when you are chasing a test-only regression.
composer.json. Both --from and --to must resolve to an actual composer.lock. Comparing composer.json or a constraint string will not do what you want. If you need to compare "what a fresh update would do", copy the current lock to a temp path first (as above) rather than trying to diff a manifest.origin/main may not be fetched and the tool will error out unhelpfully. Always git fetch the base ref with sufficient depth before invoking it — this is the single most common CI failure with this tool.--no-dev in any automated output that a reviewer will read, and only drop the flag when auditing vulnerabilities or test breakage.--dev dependency. If your deploy composer install --no-dev step runs in the same image, the binary simply will not be there — guard any Artisan command that shells out to it with a file_exists(base_path('vendor/bin/composer-lock-diff')) check.|-split or awk is usually enough) or check whether your version exposes a JSON flag — but for a PR comment the pretty table is exactly right, so resist over-engineering this."config": { "allow-plugins": false } or a strict allow-plugins allowlist in Composer 2.2+), you must explicitly allow davidrjonas/composer-lock-diff. Otherwise the composer lock-diff subcommand silently will not register, even though the vendor/bin script works. Prefer the vendor/bin binary in scripts for exactly this reason.How can I help you explore Laravel packages today?