Changelog¶
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
0.2.1 - 2026-09-16¶
Fixed¶
self-updateno longer ends withinclude(): zlib: data error(orinternal corruption of phar) and exit 255 after successfully replacing the archive: nothing that runs after the swap needs code from the old PHAR any more. The file was replaced correctly before; only the process's exit was wrong.
0.2.0 - 2026-09-16¶
Added¶
- Every finding now carries a priority next to its verdict:
critical,high,medium,low, ornonefor a package the report does not flag. The verdict sets a base level —abandonedandsilentstart atcritical,pinnedandold-promiseathigh,staleatmedium— which drops one step for a transitive package and one more for a development-only one, never belowlow. A package nothing in the project reaches counts as transitive. --format=json: each finding gainspriority,directanddev, and the document gains aprioritiesobject with all five counts next tocounts. These are additions, so theschemanumber stays1.- The exit code,
--fail-onand the baseline are unchanged and still read the verdict alone. - Every remaining format now names the priority next to the verdict, always as
<verdict> (<priority>).--format=githubputs it in each annotation's title (lockrot: abandoned (critical));--format=gitlabopens each issue's description with it;--format=markdowngains aPriorityfirst column; and--format=sarifcarries each result's priority asrank— the 0.0–100.0 number SARIF 2.1.0 defines for it,criticalbeing100.0— alongsideproperties.priority,properties.directandproperties.dev. - Nothing that decides an outcome reads the priority: the annotation level, the GitLab severity and
fingerprint, and the SARIF
ruleIdandlevelall still follow the verdict alone, and the SARIF document still validates against the official 2.1.0 schema.
Changed¶
- The report is ordered by priority first, then verdict severity, then direct dependencies ahead of transitive ones, then package name. The install-time summary shows the same order, so its 10-line budget now goes to the packages that apply most directly to the project.
- The default
tableformat is now a width-aware list grouped by priority instead of a five-column box table that only read on a very wide terminal. Groups come highest priority first, headed by their level and count (not flagged (N)for the rows--alladds); each finding takes one line for its verdict, package, version and requirement chain and as many as it needs for its evidence, wrapped to the terminal with continuation lines indented past the verdict column. Critical and high rows are marked in red, medium in yellow. The summary block keeps its wording and order and gains apriority: …line right after the counts, printed whenever the run flagged anything; its lines wrap to the terminal too, at the full width and with no indent. The format id and the machine formats are unchanged. - The terminal width is taken from
COLUMNSwhen it is set, otherwise from the console itself, falling back to 120 columns and never going below 40. - The evidence for the
abandonedverdict now readsmarked abandoned by its repositoryormarked abandoned in composer.lock, and the SARIF rule description says the same, so flagged carries one meaning across lockrot's output: a finding lockrot itself raised. - Package metadata and community files for the first Packagist release: a
descriptionandkeywordswritten for search, ahomepage,support.docsandsupport.security, plusSECURITY.md,CONTRIBUTING.md, issue and pull-request templates, and a Dependabot configuration.
Fixed¶
--format=json,sarif,gitlab,githubandmarkdownare now written raw, so a<in a constraint or a package name reaches the parser on the other end exactly as lockrot produced it.
0.1.0 - 2026-09-15¶
composer lockrot(aliascomposer rot) command reporting dependency rot incomposer.lock.- Signals S1–S6: abandoned flag, no stable release, repository archived, no repository push, old release with an open-ended PHP constraint, and pinned branch/hash snapshots.
- Built-in finished-package allowlist (
resources/finished-packages.json) plus a project-levelextra.lockrot.ignorelist with mandatory reason and optional expiry. - Install-time summary on Composer's
PRE_OPERATIONS_EXEC:composer require/update/installprint a block of at most 10 lines for the packages the transaction is about to install or update, above Composer's own operations list. Bounded by a 5-second budget, and it never fails an install — an error lockrot cannot interpret becomes onelockrot: install-time check skipped: …line. The block is silent only when the transaction was both checked and clean: if nothing is flagged but a lookup failed (exhausted budget, unreachable repository), a shorterlockrot: N of M changed packages could not be checkedblock carries the reason instead. - Two new
extra.lockrotkeys:install-time(on/off, defaulton) andinstall-time-strict(defaultfalse), which appliesfail-onat install time and stops the transaction before any operation runs.LOCKROT_DISABLE=1still silences everything. - The install-time pass's time budget is configurable via
extra.lockrot.install-time-budget(integer seconds, 1–120, default 5); like the other install-time keys it has no CLI option or environment override. - The install-time summary's
viachain now resolves even when the lock on disk predates the transaction's own packages — a--dry-runupdate, or a project's very first lock — instead of showing no chain for a package new to the dependency graph. - The "Repository metadata unavailable" note now breaks out each distinct reason with its own
count when a run hits more than one, e.g.
Repository metadata unavailable for 4 packages: not checked: install-time budget exhausted (3); connection refused (1), instead of only naming the first reason it reached. table(default) and--format=jsonoutput.--format=githubprints GitHub Actions workflow commands, one per finding, so flagged packages appear as annotations on their own line ofcomposer.lockin a pull request; findings at or abovefail-onare annotated as errors, the rest of the flagged ones as warnings, and the rows only--allshows as notices.--format=sarifprints a SARIF 2.1.0 document forgithub/codeql-action/upload-sarif, with one rule per verdict,composer.lockline numbers, and stable per-package fingerprints so code scanning can track a finding across runs. Exit codes are unchanged by the chosen format.- Baseline file:
composer lockrot --generate-baselinewriteslockrot-baseline.jsonnext tocomposer.json(path configurable via--baselineorextra.lockrot.baseline), and every later run compares against it, so--fail-ononly trips on findings that are new or worse than the ones the project accepted. Matching is by package name, so a version bump that keeps the same verdict stays accepted;first_seensurvives regeneration; baseline entries whose package has left the lock are reported as stale, never failed on. The table gains abaseline: N known · M new · K worsened · S staleline and marks rows(baseline)/(was stale), the JSON report abaselineobject, and SARIF aproperties.baselineper result — with known findings reported at notice/note level so annotations keep matching the exit code.install-time-stricthonours the same comparison. The baseline is the only file lockrot writes, only on that explicit flag, and it is written atomically; an unreadable, schema-invalid or unwritable baseline is exit 2, never a silently ungated run. --format=gitlabprints a GitLab Code Quality JSON report (one issue per flagged finding, every finding with--all), soartifacts.reports.codequalityputs findings inline in a merge request's diff. Severity mirrors the GitHub/SARIF level (major/minor/info); each issue's fingerprint is a stable hash of the package name and verdict so it survives a version bump or a reformatted lock. Report notes are not representable in this format and are dropped; use--format=jsonfor them.--format=markdownprints a PR-comment-shaped report — heading, a findings table, notes as bullets, a<sub>summary footer — forcomposer lockrot --format=markdown > comment.mdpiped intogh pr comment --body-file. A clean run prints a one-line heading with no table; with a baseline, a second line carries the same known/new/worsened/stale counts as the table format, and a verdict is bold only when it is not already accepted by the baseline.- Exit codes 0/1/2 driven by
--fail-on, with network failures defaulting to exit 0 unless--strict-networkis set. - Package metadata is loaded through the repositories configured for the project
(
ComposerRepository::loadPackages()), in their configured order — Packagist by default, but Private Packagist, Satis instances and mirrors are honoured the same way, with Composer's own authentication and proxy settings. The first repository to answer for a package name wins, and a repository that could not answer hands the name on to the next one. Composer's own metadata cache is reused and revalidated on every run; GitHub repository-activity responses keep a fixed 24-hour cache in Composer's cache directory. - Repository metadata is loaded in two passes: tagged releases first, the
~devbranch file only for packages with no tagged release — roughly half the requests on a cold cache (wallabag: 403 → 206). --offlineserves both from cache and never opens a connection, including when lockrot runs as a Composer plugin. A package with no cached metadata is reported as unavailable rather than as absent from the repository, so an empty cache cannot read as a clean result.- Optional GitHub token (
GITHUB_TOKEN/LOCKROT_GITHUB_TOKEN/Composergithub-oauth) for repository activity signals; runs without one at a reduced candidate budget. - Standalone PHAR build (
build/lockrot.phar) for use without adding a Composer dependency. lockrot.phar self-updatereplaces the running PHAR with the latest GitHub release: it readsreleases/latest, compares the tag against the running version with Composer's own semver, then downloads the archive and thelockrot.phar.sha256published beside it, refuses any download whose hash does not match, checks that the PHP runtime can open it, and swaps it in with the old file's permissions.--checkreports without downloading and exits 1 when an update is available, so CI can notice a release;--forcereinstalls the current version. Every failure is exit 2 with the running PHAR untouched and the temporary file removed; there is no rollback in 0.1, since earlier releases stay downloadable from GitHub. The command exists only in the PHAR — a plugin install is updated withcomposer update somework/lockrot— and it needs write access to the PHAR's own directory and nothing else.- The PHAR now answers to command names (
lockrot, still the default, andself-update) instead of being a single-command application. Composer's own commands and the inspected project'scomposer.jsonscripts are deliberately not registered, solockrot.phar listshows lockrot's two commands and nothing in that project can be installed, updated or executed through the PHAR. - Configuration in
extra.lockrotis validated againstresources/lockrot-config.schema.json; threshold values must be integers. - Releases publish
lockrot.pharwith alockrot.phar.sha256checksum; GPG signing and a Docker image are deferred to a later release. - The standalone PHAR (and any lock-only run without an existing Composer instance) sets
COMPOSER_ROOT_VERSION=1.0.0when the variable is unset: lockrot never reads the root package's version, so Composer's VCS probing (git/hg/fossil/svn, about 5 of the 6 seconds a PHAR run took on a lock-only directory) and its "could not detect the root package version" warning on stderr are skipped. - The "GitHub token not set" note counts packages whose repository activity was checked, not distinct repositories, so packages sharing one repository are no longer under-reported.