Running lockrot in CI¶
Add one step to the pipeline and pick the threshold that should fail it:
With a committed baseline the same command fails only on new or worsened findings; no extra flag is needed, the file is picked up automatically. To run without installing the plugin, use the PHAR — phar.md has the CI snippet.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
No finding reached the fail-on threshold (or fail-on=none) |
1 |
A finding reached or exceeded the fail-on threshold |
2 |
Tool or configuration error (unparsable composer.json/composer.lock, invalid config value, unreadable or unwritable baseline) |
A network failure — a configured Composer repository or GitHub unreachable — never turns into a non-zero exit code on
its own. It is reported as a note, and the checks that could not run are treated as absent evidence.
--strict-network changes that to exit 1. This also covers --offline runs where a locked package has no cached
metadata: it is reported as a failure, not silently skipped. A composer.lock entry that Composer's own loader cannot
load (missing name/version, an unnormalizable version, a malformed entry) stops the report with exit 2 rather
than being skipped.
As a Composer plugin, a composer.json that Composer itself cannot parse never reaches lockrot at all: Composer parses
the project's manifest while collecting plugin commands, before any plugin class is loaded, so it stops with its own
exit 1 first. composer lockrot on an unparsable composer.json exits 1, not 2. The standalone PHAR reads and
validates composer.json itself, so the same failure there is exit 2. lockrot.phar self-update uses the same three
codes with its own meanings; see phar.md.
The install-time summary never sets an exit code unless install-time-strict is on. The exit code
is identical for every output format below; only the output changes.
--format=github¶
Workflow commands, one per finding, so every flagged package becomes an annotation on its own line of composer.lock
in the pull request's Files changed view:
- name: lockrot
run: composer lockrot --format=github --fail-on=silent --target-php=8.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
::error file=composer.lock,line=8010,title=lockrot%3A abandoned (critical)::sensio/framework-extra-bundle v6.2.10: marked abandoned by its repository, replacement: Symfony; …
Findings at or above --fail-on are annotated as errors, everything else flagged as warnings, and the rows that only
--all shows as notices — so the annotation colour matches the exit code. The report's own notes are printed as
notices too, so a run can emit notices without --all. The priority never changes any of that: a
critical finding below --fail-on is still a warning. With a baseline in place, findings it already
carries drop to notices for the same reason. GitHub renders only a limited number of annotations per step, so
on a large lock file the annotations are the headline and the step's own log holds every finding. The summary line at
the end of the output always states the full counts, and --format=sarif uploads the complete set.
--format=sarif¶
A SARIF 2.1.0 document for GitHub code scanning, which keeps the
findings in the repository's Security tab and tracks them across runs. The upload step needs the
security-events: write permission:
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- name: lockrot
run: composer lockrot --format=sarif --fail-on=silent --target-php=8.4 > lockrot.sarif
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: lockrot.sarif
if: always() keeps the upload running when --fail-on already failed the step. Each result carries the
priority as rank, the field SARIF 2.1.0 defines for it, and the same result's properties carry
priority, direct and dev by name. The rule a result points at and its level follow the verdict. Both github
and sarif point at composer.lock in the checkout root, so run them from the directory that holds the lock file.
--format=gitlab¶
A GitLab Code Quality report: a JSON array
with one issue per flagged finding (every finding with --all), so a merge request shows them inline in the diff of
composer.lock. Publish it as a codequality artifact:
lockrot:
script:
- composer lockrot --format=gitlab --fail-on=silent --target-php=8.4 > lockrot-codequality.json
artifacts:
reports:
codequality: lockrot-codequality.json
Code Quality has no title field of its own, so each issue's description opens with the package, the version and the
same <verdict> (<priority>) phrase the GitHub annotation title uses:
sensio/framework-extra-bundle v6.2.10 — abandoned (critical): marked abandoned by its repository, replacement: Symfony; …
Severity follows the same rule as the GitHub and SARIF level: a finding at or above --fail-on is major, any other
flagged verdict minor, and a row only --all shows (or one a baseline already knows) info. Each
issue's fingerprint is a stable hash of the package name and verdict, so a version bump that keeps the same verdict —
or a reformatted lock that moves the entry to a different line — keeps the same GitLab issue identity. The priority is
deliberately not part of it, so moving a package from require to require-dev does not open a second issue for a
finding GitLab already tracks. GitLab's Code Quality format has no field for a document-level note, so notes are
dropped here; use --format=json when you need them.
--format=markdown¶
A report shaped for a pull-request comment: a heading with the flagged/checked counts, a table of the findings led by
their priority, the report's notes as a bullet list, and a <sub> footer with the full summary.
composer lockrot --format=markdown --fail-on=silent --target-php=8.4 > comment.md
gh pr comment --body-file comment.md
| Priority | Package | Version | Verdict | Evidence | Via |
|---|---|---|---|---|---|
| critical | `sensio/framework-extra-bundle` | v6.2.10 | **abandoned** | marked abandoned by its repository, replacement: Symfony; … | direct |
| high | `friendsofsymfony/oauth-server-bundle` | dev-master | **pinned** | last release 2019-01-23 (7.6 years ago); pinned to branch snapshot dev-master | direct |
The evidence cells are abridged here; the real cells carry every signal. Rows keep the report's order, so a reviewer
reads down the first column and stops where the rows stop applying. A clean run prints
### lockrot: no dependency rot found in N packages and no table. With a baseline in place, a second
line under the heading carries the same known/new/worsened/stale counts as the table format, and a verdict is
bold only when the baseline has not already accepted it.
--format=json¶
The complete report, and the only format that carries every field: per-finding signals, chain, evidence and
data_date, plus the document's notes. example-run.md has a worked excerpt.