ion-bazan/composer-diff
Composer Diff compares composer.lock files across Git branches, commits, or tags and reports added, removed, and updated Composer packages. Use it to review dependency changes in pull requests and see exactly what a merge will alter in PHP projects.
ion-bazan/composer-diff is a Composer plugin that adds a composer diff command. It is a narrow, read-only reporting tool: it compares two composer.lock files and emits a Markdown or JSON summary of added, removed, and changed packages. For day-to-day work it is most valuable as a PR/release-notes aid, not as a dependency policy engine.
Install it as a dev dependency:
composer require --dev ion-bazan/composer-diff
Composer 2.2+ will ask whether to allow the plugin. In CI or non-interactive environments, allow it explicitly:
composer config allow-plugins.ion-bazan/composer-diff true
The first command to run is:
composer diff
By default this compares the current composer.lock against the one in HEAD, which is exactly what you want after a local composer update but before committing. To compare two refs:
composer diff --from=origin/main --to=HEAD
The output is a Markdown report, usually split into production and dev dependency changes. The first practical use case is pasting that report into a pull request description:
composer diff --from=origin/main --to=HEAD --format=markdown > composer-diff.md
Look first at composer diff --help, then the repository README and the docs/formatters.md and docs/url-generators.md files. Those document the two features most teams actually care about: machine-readable output and links to upstream version-compare pages. The package requires Composer 2 (composer-plugin-api ^2.0), so it will not help on legacy Composer 1 projects.
Local review after dependency changes. Run composer diff after composer update, composer require, or composer remove. It gives a much clearer summary than reading a raw lock-file diff, especially when a single top-level bump pulls in several transitive updates. Use --no-dev when you only care about production dependencies.
PR body or comment generation in CI. The common workflow is: check out the repo with enough history to resolve the base ref, install dependencies including dev dependencies, run the diff, and publish the Markdown. A GitHub Actions step can append directly to the job summary:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-interaction
- run: composer diff --from=origin/${{ github.base_ref }} --to=HEAD --format=markdown >> "$GITHUB_STEP_SUMMARY"
If you want an actual PR comment instead of a step summary, write the output to a file and pass it to gh pr comment or a comment action. Keep the generation step non-blocking; the report is informational.
Release notes. For a release branch or tag comparison, use explicit refs:
composer diff --from=v1.4.0 --to=v1.5.0 --format=markdown
This produces a concise dependency-changelog section that can be pasted into release notes. It is especially useful when the release contains grouped Renovate or Dependabot updates.
Machine-readable reporting. Use --format=json when you need to post to Slack, Discord, a custom dashboard, or a bot. Parse the JSON with jq, extract package names and version transitions, and render your own message. This is usually better than trying to postprocess the Markdown table.
Dependency-bot integration. For Renovate or Dependabot PRs, run the diff against the target branch and include the generated table in the PR body. Reviewers can see whether a "minor" bump also changed transitive dependencies, which is often the real risk. If the bot already writes a PR body, append the diff output rather than replacing the existing text.
Upstream compare links. The Markdown formatter can link changed packages to compare pages on GitHub, GitLab, or Bitbucket when the lock file contains a recognized source URL. This is the feature that makes the report genuinely useful during review: a reviewer can jump straight to the upstream diff between the old and new version instead of searching release notes manually. The URL generators are documented in docs/url-generators.md; treat them as best-effort for public, recognized hosts.
Formatter versus post-processing. For most teams, stick to the built-in markdown and json formatters. Custom formatters are an extension point, but unless you need a very specific output format, transforming the JSON outside the package is simpler and keeps you on the supported path.
actions/checkout defaults to fetch-depth: 1, so origin/main may not exist. Use fetch-depth: 0 or explicitly fetch the base ref before running composer diff.composer install silently omits the command or prompts in CI, add ion-bazan/composer-diff to config.allow-plugins in composer.json.--no-dev install is a trap. If the CI job runs composer install --no-dev, the plugin will not be installed. Run the diff job with dev dependencies, or install the plugin separately.HEAD is convenient locally but surprising in CI. Prefer explicit SHAs or fetched branch refs, not moving branch names, for reproducible reports.composer.lock will fail or produce an empty/odd result. This matters for old tags or branches that did not commit lock files.composer diff reports changes; it is not designed to fail when dependencies change. If you need a gate, wrap it and inspect the output yourself.--no-dev and filter noisy transitive changes.composer diff --format=json. It exposes the structured data behind the table.composer audit, composer why, or security scanning. Pair it with those tools rather than treating the diff as a risk assessment.composer.lock and update it alongside your other dev tooling.How can I help you explore Laravel packages today?