Baseline¶
Record the findings you have decided to live with, commit the file, and CI fails only on what is new or has got worse:
composer lockrot --target-php=8.4 --generate-baseline
git add lockrot-baseline.json
git commit -m "chore: accept current dependency rot"
Generate it with the options your CI step uses, or the step can fail on findings the baseline was meant to accept (below).
Every later run (plugin or PHAR) reads lockrot-baseline.json, or the file --baseline or the
baseline key names, and sorts each finding into one bucket:
| Bucket | The finding | Fails the build |
|---|---|---|
new |
is flagged and not in the baseline | When it reaches --fail-on, as without a baseline |
worsened |
is in the baseline at a less severe verdict than its current one | When it reaches --fail-on |
known |
is in the baseline at its current verdict or a more severe one | Never |
stale (not the stale verdict) |
is a baseline entry for a package that is not in composer.lock |
Never; reported as a note (in json: baseline.stale) |
What --generate-baseline does¶
- Writes
lockrot-baseline.jsonnext tocomposer.json, or the path in--baselineorextra.lockrot.baseline, relative tocomposer.jsonor absolute. UnderCOMPOSER=app/alt.jsonthat is next toalt.json, inapp/(environment overrides). - Records every flagged finding of the run.
ok,finishedandunknownfindings are not recorded. - Replaces the file: a package this run does not flag loses its entry, and a package that stays
keeps its
first_seen. - Never leaves a truncated baseline, even when interrupted.
- Prints nothing on stdout; on stderr it ends with
lockrot: baseline written to lockrot-baseline.json (<count> findings). - Exits
0whatever--fail-onsays. With--strict-network, it still exits1, after the file is written, when a run note counts as a network failure (exit codes). - With
--outputon the same run, writes the reports first and the baseline last. The reports carry no baseline comparison, and a report that cannot be written (exit2) leaves the baseline as it was.
{
"$schema": "https://lockrot.dev/schema/baseline-1.json",
"lockrot": {
"version": "x.y.z",
"schema": 1
},
"generated_at": "2026-09-14T00:00:00+00:00",
"findings": {
"behat/transliterator": {
"version": "v1.5.0",
"verdict": "abandoned",
"first_seen": "2026-09-14"
}
}
}
| Field | Meaning |
|---|---|
$schema |
The file's published JSON schema; lockrot validates every baseline it reads against it |
lockrot.version |
The lockrot release that wrote the file |
lockrot.schema |
The file's schema number |
generated_at |
The timestamp of the run that wrote it |
findings |
One entry per package, sorted by name so the diff stays reviewable |
findings.<package>.version |
The version installed when the entry was written; informational, not matched |
findings.<package>.verdict |
The verdict accepted |
findings.<package>.first_seen |
The date the package first entered the baseline, kept across regenerations |
Accept a new finding or drop a fixed one¶
Regenerating accepts every current finding at once, so review what it changes:
-
Rerun the generate command.
-
Review
git diff lockrot-baseline.json. An added entry is a finding you accept, a changedverdictis the verdict you accept. A removed entry is a package this run does not flag: fixed, allowlisted, gone fromcomposer.lock, orunknownbecause a lookup failed (the run's notes say so). -
Commit the file with the change that needs it.
On a merge conflict in the file, take either side and regenerate on the merged branch. Entries on
the side you took keep their first_seen dates.
Reading a baseline¶
A run with a baseline adds one line to the table and markdown output:
Matching rules:
- By package name only. Bumping
vendor/pkgfrom1.2.3to1.3.0while it staysabandonedkeeps itknown. - Compared by verdict, not priority. A finding that moves up the severity ladder (from the
staleverdict toabandoned) isworsenedand can fail the build again. A change of priority alone, such as a package moving fromrequire-devtorequire, keeps itknown. - Whichever threshold. A
knownfinding does not fail the run under a verdict, a priority oruncheckedthreshold. - Flagged findings only. A baseline never covers an
ok,finishedorunknownfinding, so one that carries S10 still fails--fail-on=uncheckedhowever often you regenerate. - Stale entries stay. A normal run never edits the file; regenerate it to drop them.
How each format shows the comparison:
| Format | known |
worsened |
|---|---|---|
table |
abandoned (baseline) |
abandoned (was stale), naming the accepted verdict |
github, sarif, gitlab, markdown |
Marked by row 1 of How each format marks a finding | Marked like a new finding |
json |
baseline.status is known; when the finding reaches --fail-on, gate.exempt_by is baseline (schema.md) |
baseline.status is worsened and baseline.previous_verdict names the accepted verdict; gate.exempt_by is null |
Generate it with the options your CI step uses¶
Any option that changes verdicts must match between the generate run and the CI step, or the step
reports accepted findings as new or worsened:
-
--dev. Generated without it and checked with it, the baseline holds no development findings, so the--devrun reports every one of them asnewand can fail. The reverse is safe:staleentries are measured against the wholecomposer.lock,packages-devincluded. -
The target PHP. Pass the same
--target-phporLOCKROT_TARGET_PHP, or set thetarget-phpkey so both runs read it. Without any of them, lockrot usesconfig.platform.php, else the PHP that runs it, which can differ between your machine and CI (target-php). -
Credentials and network. Generate with the token the CI step has (
GITHUB_TOKEN, …) and not--offline. Without it, checks a verdict rests on do not run, the baseline records weaker verdicts, and CI reports those packages asneworworsened(repository hosts). -
The manifest. Run with the same
COMPOSERvalue, so both runs read the same lock.
A change to a threshold key in extra.lockrot changes verdicts too: regenerate the baseline in the
same change.
When lockrot cannot read the file¶
A baseline lockrot cannot read is a configuration error, never an absent baseline: the run exits
2 instead of running ungated. A default lockrot-baseline.json that does not exist is not an
error; the run has no baseline.
| Symptom on stderr | Cause | Fix |
|---|---|---|
lockrot: <path> not found |
--baseline or extra.lockrot.baseline names a file that does not exist |
Fix the path, or create the file with --generate-baseline |
lockrot: <path> is not valid JSON: … or … must contain a JSON object |
The file is damaged, for example by a merge conflict | Restore it from version control, or delete it and generate a new one |
lockrot: baseline file is invalid: and one - <field>: <message> line per problem |
The file does not match the schema: a field of the wrong type or a missing key | Fix the named field, or delete the file and generate a new one |
lockrot: Cannot read <path>: … |
The file exists but cannot be read | Fix its permissions |
--generate-baseline reads an existing file to keep its first_seen dates, so it stops on a
damaged file too. Delete the file first.
Install time¶
install-time-strict uses the same comparison: a known finding (in the baseline at its current
verdict or a more severe one) does not stop a composer require, and the compact block still
lists it (install-time.md).
At install time an unreadable baseline, or an extra.lockrot.baseline naming a missing file, does
not fail the install. The check is skipped with one lockrot: install-time check skipped: … line,
and the install-time-strict gate does not run for that install. composer lockrot reports the
same problem as exit 2.
Related¶
- configuration.md — the
baselinekey and--baseline=<path> - ci.md — the exit codes a gated run returns
- ci.md — how each format marks a
knownfinding - schema.md — the published baseline schema and the report's
baselinefields - install-time.md — the compact block and
install-time-strict