Skip to content

Releases: somework/lockrot

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 16:45

lockrot 0.13.0 writes several reports from one run, keeps self-update within its major version, and writes the decisions behind each verdict, priority and exit code into the report as typed fields. Read Before you upgrade if you: run lockrot in CI, set COMPOSER or LOCKROT_*, publish reports, validate them with Ajv strict or a vendored schema, run self-update in CI, or use lockrot's PHP classes.

No verdict or priority rule changed. Documents written by earlier releases still validate.

Before you upgrade

If you… What changes What to do
allow exit 1 in CI (e.g. GitLab allow_failure: exit_codes: [1]) A command line lockrot cannot read (unknown option, missing value) exits 2, not 1. An unknown command name is still 1 Fix the command line (exit codes)
set LOCKROT_FAIL_ON or LOCKROT_TARGET_PHP An invalid value is a configuration error (exit 2), even when the matching option overrides it Fix or unset the variable
set COMPOSER=alt.json lockrot reads extra.lockrot from alt.json and analyses alt.lock, as Composer does, so verdicts can differ; annotations name alt.lock Keep extra.lockrot and the baseline with that manifest (environment overrides)
publish reports json and html carry run.root_package, composer.json's name, whatever extra.lockrot.project says Remove the name before publishing if it must stay private
published reports from earlier releases Earlier releases could print logins, URL tokens and machine paths. 0.13.0 quotes none (SARIF's %SRCROOT% aside) Check old published reports; rotate any token you find (what a report reveals)
validate reports with Ajv strict, or keep a copy of the schema Sets that grow (signal ids, reasons, note codes, formats…) are open strings with x-known-values. The schema URLs serve the newest release's files, so this applies even if you stay on 0.12 Declare x-known-values or set strict: false; refresh a vendored copy (open sets)
run self-update in CI A 0.13 archive installs only within its major (--allow-major moves one), skips releases needing a newer PHP or a key it lacks, and reads lockrot.phar.meta.json from github.com. --check exits 1 only when an update would install; --force can exit 2 Allow github.com where only api.github.com was allowed; let a --force job accept exit 2 (self-update exit codes)
reviewed lockrot or set an egress allowlist from 0.12.0's SECURITY.md That page left out GitLab, Bitbucket, GitHub's release downloads and the GitLab token variables, which earlier releases used too Recheck against what lockrot does and does not do
use lockrot's PHP classes Everything under src/ is @internal; Lockrot\Extension\ is reserved Depend on the CLI and the json report
cut the JSON out of an html page That recipe wrote an empty file when the page did not match Write both from one run: --output=json:lockrot.json (the JSON beside the page)

What's new

  • Several reports from one run. Repeat --output=<format>:<path>; --format still decides stdout, and every file shares one analysis:

    php lockrot.phar --format=github \
      --output=sarif:lockrot.sarif --output=html:lockrot.html --output=json:lockrot.json
  • The report states what lockrot decided. Instead of rebuilding it from prose or lockrot's rules, read it from the json report:

    • gate: whether the run fails and why, and per finding whether it reaches --fail-on or is exempt by the baseline;
    • priority_basis and no_fix_expected: how each priority was reached, and which advisories expect no fix;
    • origin, from_composer_repository and replacement_url: where each lock entry came from, and links lockrot can vouch for;
    • note_details: every run note typed, with a link to its section;
    • libyears_unmeasured, exposure_rule with unattributed, S6's release facts, and which release branches the project's PHP can take.

    All are listed in schema.md. The html page (lockrot-report 0.13.0) reads them.

  • A warning for a mistyped key. An extra.lockrot key lockrot does not read gets one line on stderr, with the likely key: lockrot: unknown key extra.lockrot.install-tme ignored (did you mean install-time?).

  • A safer self-update. It stays within its major version, checks the PHP floor and signing key a release declares, and moves to a new key through a transition release.

  • A draft of the 1.0 promise. Compatibility says what 1.0 freezes, how verdicts may change between releases, and the deprecation policy.

Fixed

  • Text from a package or an error message can no longer restyle the console, open a terminal link or end the run with exit 2.
  • A key starting with a NUL byte no longer switches extra.lockrot or baseline validation off.
  • LOCKROT_DISABLE=1 skips the run before the configuration is read.
  • Report and baseline files are written through an exclusive temporary file and never follow a planted symlink.
  • Several docs statements that gave wrong results when followed; each is in the changelog.

The full list, with a link from every entry to the page that explains it, is the 0.13.0 changelog.

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 10:13
lockrot 0.12.0

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 09:29

Added

  • S10: what the run could not check. ok meant two things — every check ran and found nothing,
    or a check never ran — and the difference was invisible on the finding. Anonymously the activity
    round asks only about packages already stale on release age, so a repository archived a week
    after its last release cannot be seen, and the only trace was a note counting how many packages
    were skipped without naming one. A package whose newest releases are dated by a commit their tags
    share has the same shape: S2 has nothing to measure, and the finding read as ok. S10 carries
    the missing check on the finding, with the reason and the signals it blocked (repository_activity
    → S3, S4; release_dates → S2, S8). It is informational, like S7 and S9: it never decides a
    verdict, and it is raised only where the missing check could have changed one — never on a package
    the repository already marks abandoned, nor on an allowlisted one. Credentials for every host take
    away the repository_activity reasons and only those — release_dates asks no forge — and a
    branch snapshot, which is on no release branch, is told about S2 alone.
    --fail-on=unchecked fails a run that could not check everything,
    which is how a pipeline catches the workflow that never passed GITHUB_TOKEN through. See
    What was not checked.

  • One number for how far behind the lock is: libyears. For each package, the years between
    the release installed and the package's newest stable release, summed over the analysed lock —
    171.3 on the wallabag fixture. Every finding carries its libyears in --format=json (null
    when not measured: a branch snapshot, no dated stable release, not from a Composer repository,
    metadata unavailable), and the document a libyears block that is the arithmetic over them —
    total, direct_requirements (the same sum over the direct requirements, the nearest number
    to php-libyear's, which reads composer.json), measured, unmeasured by reason and
    furthest_behind. total and direct_requirements are null where nothing could be measured, so
    that a reader adding the field up over several projects never counts an unmeasurable lock as a
    lock with nothing to fix; a measured lock with nothing behind reports 0. The table and markdown footers print one line (libyears: 171.3 behind across 195 of 200 packages · 111.7 from direct requirements · furthest behind smalot/pdfparser v1.1.0 at 4.7); the HTML page shows the total in its ledger, a sortable column, and the counts
    by reason on the Run tab; --explain prints the package's own value under its verdict, or the
    reason it was not measured. It is laid over the verdicts, not one of them: it counts every drift,
    healthy patches included, and enters no priority, --fail-on or baseline. The schemas gain the
    fields under the same number, optional so that older documents still validate. No new request
    is made: both dates were already in the data. A split package's installed version is dated by
    its monorepo parent's tag of the same version, not by the lock: the lock copies the date
    Packagist gave the split's tag, the commit its tags share, and illuminate/contracts v8.83.27
    would read as 4.65 libyears behind for 3.75. --explain prints that date as installed release
    and names the monorepo it came from. That holds for an installed tag on a shared commit
    under a branch whose newest tag has a commit of its own, as after a change to the split's
    directory: the parent is asked for the installed version, not only for the branch. --explain
    says whose date it is under
    installed_release and installed_release_dated_by, and so does the package card. Where no
    parent dates it, such a tag is not measured at all rather than measured from the commit's date:
    the installed tag's own entry in the repository decides that, not what dated the package's
    newest release.

  • left-behind suggests a branch the project can actually move to. S8 named the newest
    releasing branch and wrote the constraint that follows it, whatever PHP that branch requires:
    Matomo supports php >=7.2.5, locks monolog 1.27.1, and was told require ^3.12 — monolog
    3.x needs PHP 8.1. On the weekly watch, 99 of the 138 constraints suggested to projects that
    declare a require.php contradicted it. Every release branch now carries the php requirement of
    the release it names (read from the same repository data, no new request), and S8 holds the
    higher branches to two floors — the project's own require.php and the target PHP, the version
    Composer resolves against. The newest branch still proves the upstream moved on; the branch the
    evidence tells the project to follow is the newest releasing one within both floors, and when
    that is not the newest the line says what holds the newest back: 3.x released 3.12.0 (2026-09-09), needs php >=8.1 above the project's php >=7.2.5; 2.x released 2.11.1 (2026-09-02); require ^2.11 to follow. With no releasing branch within reach it says so and suggests nothing:
    the way forward is a PHP upgrade, not a composer.json line. --format=json gains newest_php,
    newest_within_reach, floor_php, floor_source and reachable_branch/_version/_release
    on the signal; --explain shows each branch's php requirement in the branch table and in the
    JSON branches rows. See Within reach.

  • abandoned says whether there is somewhere to go. Packagist's marker comes with a free-text
    replacement, and on the weekly watch 19 of 72 abandoned packages carried one — 17 naming a
    package, two naming Symfony and a sentence. The verdict stays one (it is what --fail-on, the
    baseline, the SARIF rule and every count key on); what changes is the reading. Each finding
    carries replacement in --format=json — the named package when it is a Composer package name,
    null otherwise, the free text staying in the evidence as text — the document carries
    "abandoned": {"total", "with_replacement"} next to counts, the summary line reads
    abandoned 7 (6 with a replacement) where that is not zero, and the HTML page tags the row with
    the replacement and links it on the card. Both fields are optional in the schemas. See
    Abandoned, and where to.

Changed

  • One reading of the installed version's date. The lock's time is a release date only
    sometimes — a branch snapshot carries its commit's, a subtree split's tag the date of a commit
    its tags share, a monorepo parent can date the version instead — and each surface re-derived
    that for itself, which is how one explanation came to call a date a release four lines above
    saying it was not one. InstalledRelease answers it once. No output changes.

  • old-promise reads the date against the target's major, not its minor. S5 held a release
    against the GA of the target PHP minor: anything older than 2024-11-21 with a >=7.x constraint
    was an old promise about PHP 8.4. That caught the wrong thing. A >=7.2 cut in 2022 was written
    with PHP 8.1 on every CI matrix and differs from a ^7.2 || ^8.0 of the same day in spelling
    alone, yet only the first was flagged — and the evidence blamed the style (has no upper bound),
    which is Symfony's own convention. The line is now the GA of the target's major (8.0,
    2020-11-26, for any 8.x target): the release predates the major it admits, and its constraint was
    written for an older one. On the weekly watch that is 22 of the 178 old-promise verdicts the
    minor line produced; the other 156 were releases of the PHP 8 era. The evidence reads released 2020-01-11 for PHP 5 (php ">=5.3.2"), before PHP 8 existed (8.0 GA 2020-11-26); admits 8.4 untested. In --format=json the signal's ga_date is now the major's, next to a new
    target_major, and written_for_php names the constraint's lower major. The verdict, its
    priority and --fail-on are unchanged; a project's count of old-promise will drop.

Fixed

  • --explain says what the lock's date is a date of. The composer.lock block called that
    date a release whenever the entry carried one, so a branch snapshot read released 2026-09-21 · branch snapshot and a subtree split read released 2023-06-05 four lines above the block saying
    that same date is a commit its tags share, not a release. It now reads dated … by its commit
    for a snapshot and dated … by a commit its tags share for a split, and released … only where
    the repository dated the version by a release. The JSON keeps its lock.released field, whose
    schema description now says which of the three it is carrying.
  • The report schema accepts the empty chain a run can really produce. chain was declared with
    minItems: 1, and two ordinary runs break that: a composer.lock with no composer.json beside
    it has no direct requirements at all, so nothing reaches any package, and a lock can hold a
    package only the skipped require-dev asks for. Every finding of such a run was valid JSON that
    its own published schema rejected. Documents themselves are unchanged.
  • migrate to names a package, or says nothing. The clause read the abandoned marker as the
    repository wrote it, so free text became an instruction — no fix expected; migrate to Symfony,
    migrate to EnglishInflector from the String component — and a repository that names the package
    itself sent the reader back to what they were leaving, while the new
    abandoned.with_replacement counted it. The clause, the JSON replacement and that count now
    all read the same validated successor: a Composer package name that is not this package's own.
    The repository's text is still shown in the evidence, as all free text is.

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 16:43

Security

  • A repository URL no longer carries its credentials into a report. A private Composer source is routinely configured with a token in the URL — https://gitlab-ci-token:$CI_JOB_TOKEN@… is how GitLab CI hands a job access to one, and Bitbucket app passwords take the same shape — and Composer keeps it in the lock because it has to fetch with it. --explain printed that value verbatim: in the text output as the source line, and in --format=json as lock.repository. A pasted terminal buffer, an uploaded CI artifact or a report attached to a ticket therefore carried a working token to everyone who could read it. Every repository URL lockrot prints now has its userinfo removed and its host kept (Lockrot\Data\Repository\RepositoryUrl).

    Affects 0.8.0 and 0.9.0, where --explain was the only path to it — a run that never asked for an explanation was never affected. If you have published an --explain output from either release for a project with a token in a repository URL, treat that token as disclosed and rotate it.

Added

  • --format=html: the whole run as one self-contained page. It carries the report, the release branches behind every finding, the advisories and the baseline comparison inside a single file, so it opens from file://, uploads as one CI artifact and attaches to a ticket — no server, no network, no fonts or scripts fetched from anywhere.

    What a stream cannot show: one line per signal instead of one sentence with four semicolons in it, every release branch on a time axis with the installed one marked, advisories grouped by whether the fix is a patch on your own branch or a move to another, and what is new or worsened since the baseline. Filters and the open package live in the URL hash, the query understands verdict:, priority:, signal:, severity:, cve:, direct: and dev:, and ? opens a glossary of every verdict and signal.

    A report is usually read by someone who did not run it, so the page closes with the two commands that produce the same page for their own lock. The payload's report key is what --format=json writes, envelope included, so it validates against the published report schema. --all puts every package in the page at roughly 4 KB each; without it a 100-package lock lands around 250 KB. The page carries a description and an Open Graph card so a shared link says what was found, and no robots directive: whether a published report may be indexed is the publisher's call, made in their robots.txt, not this file's.

    See CI and reports.

  • The report says what it was decided against. Every verdict depends on settings the document did not record: the thresholds separate stale from silent, the target PHP decides whether a release predates it. Until now --format=json named the target in exactly one place — inside the data of an S5 signal — so a run where S5 never fired left no trace of what it aimed at, and the thresholds left none at all. Two people comparing two reports could not tell whether they differ because the locks do or because the settings do.

    The report now carries a run block: the project's own name from composer.json — until now nothing in a report said which project it was about, every lock being called composer.lock — the target PHP, the thresholds, the fail-on, the name of the lock (never its path, which carries the account and often the client's directory) and flagged_verdicts, the verdicts the run counted as findings. extra.lockrot.project overrides the name where the manifest has none, or where its name is not the one to publish: a package inside a monorepo names itself after the package, and a private project names itself after the client.

    Each finding also carries baseline, where it stands against the baseline file — known, new or worsened, with the verdict the baseline accepted — beside the totals the baseline block already gave.

    Both are optional in the published schema, so documents written by 0.9.0 still validate, and lockrot.schema stays 1.

Verifying this release

sha256sum -c lockrot.phar.sha256
gpg --verify lockrot.phar.asc lockrot.phar          # key 39EC C3F6 4AE8 D06A 9A63  FD99 AB6F 7F52 AE51 3141
gh attestation verify lockrot.phar --repo somework/lockrot

sha256sum is GNU; on macOS use shasum -a 256 -c. What each check proves, and how to fetch
the key, is on the PHAR page.

See the full changelog.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 14:02

Added

  • Published JSON schemas for every document lockrot writes for a machine, and the one it reads. The --format=json report (report-1.json), the --explain document (explain-1.json), the baseline file (baseline-1.json) and extra.lockrot (config-1.json). The report, the explanation and the baseline file now open with a $schema key naming theirs, so an editor completes a baseline file as you type it and a CI step can validate a report with any draft-04 validator — no copy of lockrot required. The files ship under resources/ in the repository and the PHAR. Objects are open: under one number a document only ever gains fields, so a report from a newer lockrot still validates against the copy you vendored earlier, and the number moves only when a field is removed or renamed — lockrot.schema stays 1. The test suite validates what the formatters write, and every JSON sample in the docs, against these files, with a strict copy that rejects any undeclared field, so the published schema, the code and the docs cannot drift apart. See JSON schemas.

  • A split package's release branches are dated by the monorepo they are cut from. Since 0.8.0 a tag sharing its commit with two others is read as undated, which killed the false left-behind on illuminate/macroable but also stopped measuring a branch that really did end. The monorepo's own tag for the same version carries the release date, and replace: {illuminate/contracts: self.version} says the two are one release — so where a branch of the split package has no date, the branch of the same name in its parent supplies one. A lock on illuminate/contracts v5.8.36 reported nothing in 0.8.0 and now reads:

      left-behind  illuminate/contracts v5.8.36  direct
                   branch 5.x last released 2020-08-18 (6.1 years ago, dated by laravel/framework);
                   12.x released v12.69.2 (2026-09-08); require ^12.69 to follow
    

    Every other Laravel component on a branch of its own is measured again rather than skipped. Laravel's late security tags on 6.x, 7.x and 8.x are recent enough that those branches read as current under the default thresholds: being measured is the difference, not the verdict.

    The parent is taken from the lock when it is already there; otherwise it is loaded from the configured repositories, one request, and only when resources/monorepo-parents.json lists that monorepo as carrying a package this lock needs dates for — laravel/framework, symfony/symfony and cakephp/cakephp, with the components each replaces, refreshed by bin/refresh-monorepo-parents. A lock without such a package fetches nothing, and neither does one whose undated packages belong to no listed monorepo, which is the common case: symfony/polyfill-* is cut by a repository no Packagist package replaces. An install-time run that has used up its budget skips the request and the branch stays as it was. What a parent dates is its live replace list, never the snapshot.

    --format=json carries the parent as dated_by on S8 and S2, and --explain marks the branch rows it supplied.

Verify this release

curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.9.0/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.9.0/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gpg --keyserver hkps://keys.openpgp.org --recv-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.9.0/lockrot.phar.asc
gpg --verify lockrot.phar.asc lockrot.phar
gh attestation verify lockrot.phar --repo somework/lockrot

Or rebuild it: check out v0.9.0, run build/build-phar.sh with Composer 2.10.3, and compare the sha256 — see the PHAR page.

Full changelog: CHANGELOG.md.

lockrot 0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 17:56

Added

  • --explain=vendor/package: one package, everything it was decided on, one call. The verdict and priority with how the package is reached; every signal with its summary and raw data (the dates the years were computed from, one line per advisory with what fixes it and where); the composer.lock entry; the repository metadata with the table S8 reads — every release branch, its highest tag and that tag's date, the installed branch marked, and a line saying so when that branch's highest tag is undated and S8 therefore does not measure it; the repository activity; the thresholds and target PHP; the run's notes. Text, or the same as JSON with --format=json. Exit 0 — it answers a question, it does not gate; a package not in the lock, or in packages-dev without --dev, is a configuration error (exit 2). See Explaining one package.
  • left-behind says what to require. S8 carries suggested_constraint — the constraint that follows the upstream onto the branch fixes land on, written as composer require writes it (^8.2 from 8.2.0, ^0.4.3 below 1.0) — and for a package the project requires itself the evidence ends …; 8.x released 8.2.0 (2026-09-06); require ^8.2 to follow. A transitive package's parent owns that line, so there the clause stays off the row and the constraint stays on the signal's data.
  • no fix expected says where to go when there is somewhere. On an abandoned package whose repository names a replacement, an advisory nothing fixes reads no fix expected; migrate to symfony/mailer — the fix is not coming here, and the package that took over is where it lands.

Fixed

  • A false left-behind on packages split out of a monorepo. A subtree split (illuminate/*, symfony/*) cuts a tag on every release whether or not the directory changed, so tags pile up on one commit and Packagist dates each of them by that commit: illuminate/macroable has 83 stable tags on the commit behind v10.49.0, all dated 2023-06-05, and a lock on 10.x read as branch 10.x last released 2023-06-05 (3.3 years ago) while Laravel 10 kept releasing. A tag that shares its commit with two or more other stable tags is now read as undated — the date is the directory's, not the release's — so S8 does not measure the branch and S2 does not measure the package. Two tags on one commit keep their date: a re-tag, or a branch's last two releases cut with nothing changed between them (symfony/* 3.4.46 and 3.4.47), where the date is one release interval off at most — so a Symfony 3.4 lock still reads left-behind on every component. The cost is the other way: a split branch that really did stop and piled up more tags (illuminate/contracts 8.x, 31 on one commit) is no longer reported left-behind, since its last tag is dated the same way. Dev branches and pre-releases on a tag's commit do not count.

Changed

  • The footer's advisory line says why composer audit counts more. Without --dev it now reads … the report does not flag; see composer audit (it counts packages-dev too, which this run skipped; pass --dev to include them): plain composer audit totals packages-dev and a plain lockrot run does not, and the difference should read as the scope it is. --format=json records the scope as include_dev.
  • The JSON schema number stays 1: include_dev at the top level and suggested_constraint in S8's data are additions, nothing removed or renamed.

Verify this release

curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.8.0/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.8.0/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gpg --keyserver hkps://keys.openpgp.org --recv-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.8.0/lockrot.phar.asc
gpg --verify lockrot.phar.asc lockrot.phar
gh attestation verify lockrot.phar --repo somework/lockrot

Or rebuild it: check out v0.8.0, run build/build-phar.sh with Composer 2.10.3, and compare the sha256 — see the PHAR page.

Full changelog: CHANGELOG.md.

lockrot 0.7.0

Choose a tag to compare

@github-actions github-actions released this 18 Sep 20:05

Added

  • left-behind: a verdict for the branch you are on, not the package. Signal S8 takes the newest stable release on the installed version's release branch (1.x; 0.3.x below 1.0; the patch alone below 0.1 — what a caret constraint stays inside; pre-releases do not count), measures its age against release-warn-years / release-high-years, and fires only when a higher branch has released since and within release-warn-years of today — a package dead on every branch stays S2's. composer outdated --major-only says a newer major exists; S2 sees the package's newest release and stays quiet; this says the branch installed here gets no fixes. The verdict is left-behind at either threshold — between pinned and old-promise in severity, base priority high. The evidence reads branch 1.x last released 2021-08-03 (5.1 years ago); 2.x released v2.12.5 (2026-04-17) — the higher branch whose release is newest, named as the branch fixes land on. A branch whose highest tag the repository leaves undated is not measured: Packagist dates a tag by its commit, and a subtree split (illuminate/*, symfony/*) has undated tags and tags dated years before the release.
  • Security advisories on the finding, with whether a fix is coming. Signal S9 carries the advisories that affect the installed version — the same ones composer audit reports, fetched through Composer's own advisory API from the configured repositories, honouring Composer's ignore lists (config.policy.advisories on 2.10+, config.audit.ignore before) — under data.advisories in --format=json. S9 never decides a verdict. Each advisory is held against the highest stable tag on the installed version's branch and the package's highest stable tag, each only when above the installed version; one out of both ranges is already fixed, and the line says by what (fixed by 6.3.0; 1 fixed by v3.4.47, 3 fixed by v8.1.7 when they differ), with affected_versions, fixed_by and fixed_on_branch on each advisory. On an abandoned, silent or left-behind package the advisories nothing listed fixes — on a left-behind branch, nothing listed on the branch — earn no fix expected (no fix expected on 3.x next to a fix in a higher branch) and the priority goes up one step, critical at most. Advisories on packages the report does not flag stay off the rows and are totalled in the footer: 53 security advisories on 17 packages the report does not flag; see composer audit. On Composer 2.2, under --offline and once the install-time budget is spent the report carries one note instead.

Changed

  • A package on a quiet branch of a living upstream is now left-behind where it was ok, stale or old-promise; --fail-on=left-behind sees it. A baseline holding such a package at stale or old-promise reports it worsened.
  • A priority threshold can trip on a verdict that did not move: an advisory on an abandoned, silent or left-behind package lifts high to critical, so --fail-on=critical now fails on it. left-behind itself starts at high, so a run that passed --fail-on=high can fail on a package that was ok before. The baseline, keyed on the verdict, still calls the finding known.
  • S2 stays quiet when the package's highest non-dev tag carries no release date: "last release" would otherwise date the newest tag the repository dated and say nothing about the undated ones above it.
  • --strict-network covers the advisory request too: every repository that publishes advisories is asked, as composer audit asks them. A private repository that is down fails a strict run where it used to pass unnoticed.
  • The counts, priority and pulled in by: lines fold between their ·-separated items, never between a label and its number, and never wider than the terminal. The install-time block shows up to three notes, one per source that could not answer.
  • The evidence line opens with the signal that decided the verdict, then the rest in signal order, then what the package pulls in. --format=json keeps the signals in signal order.
  • The counts line gained left-behind; the verdict enums in the config and baseline schemas, the SARIF rule list and --fail-on accept it. The JSON schema number stays 1: an added enum value and two new signal ids, nothing removed or renamed.

Verify this release

curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.7.0/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.7.0/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gpg --keyserver hkps://keys.openpgp.org --recv-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.7.0/lockrot.phar.asc
gpg --verify lockrot.phar.asc lockrot.phar
gh attestation verify lockrot.phar --repo somework/lockrot

Or rebuild it: check out v0.7.0, run build/build-phar.sh with Composer 2.10.3, and compare the sha256 — see the PHAR page.

Full changelog: CHANGELOG.md.

lockrot 0.6.1

Choose a tag to compare

@github-actions github-actions released this 17 Sep 20:15

Fixed

  • phive install somework/lockrot works again. PHIVE takes any release asset ending in .asc or .sig for the GPG signature of the PHAR (last one wins), and 0.6.0's self-update signature was published as lockrot.phar.sig — so PHIVE tried to verify the archive with a JSON document and failed. The asset is lockrot.phar.sig.json from this release on, and was renamed on the 0.6.0 release as well.
  • A 0.6.0 archive cannot self-update. It looks for the old asset name, which no later release carries: it reports release v0.6.1 has no lockrot.phar.sig asset and leaves itself in place. Download this release by hand or run phive update. Every other build, 0.5.0 included, updates as before — and from 0.6.1 on, self-update verifies the release signature with the key built into the archive.

Verify this release

curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.1/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.1/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gpg --keyserver hkps://keys.openpgp.org --recv-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.1/lockrot.phar.asc
gpg --verify lockrot.phar.asc lockrot.phar
gh attestation verify lockrot.phar --repo somework/lockrot

Or rebuild it: check out v0.6.1, run build/build-phar.sh with Composer 2.10.3, and compare the sha256 — see the PHAR page.

Full changelog: CHANGELOG.md.

lockrot 0.6.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 19:44

Added

  • self-update verifies the release signature. Every release from 0.6.0 on publishes lockrot.phar.sig next to the archive — an RSA signature (PKCS#1 v1.5 over SHA-384) by the new lockrot self-update key, in the {"sha384": "<base64>"} file format Composer uses for its own self-update — and the archive checks it with openssl_verify() against the public key built into itself before anything is written, after the sha256 check it already made. A release signed with a key the archive does not know, a signature over other bytes, or a signature file that is not one is reported and not installed. The key is RSA 4096, separate from the GPG release key (which still signs lockrot.phar.asc for people and PHIVE); its public half is lockrot-selfupdate-key.pub and SECURITY.md says how it is rotated. The archive running 0.5.0 still checks the checksum only when it updates to 0.6.0; releases before 0.6.0 carry no .sig, so a 0.6.0 archive cannot --force its way back to one.
  • The PHAR is built reproducibly. build/build-phar.sh on the tagged commit — with Box 4.7.0, which the script downloads and checks, and the Composer version the release workflow pins at that tag — produces the archive byte for byte, whatever the PHP version, so a release can be verified against its own source without trusting the builder. CI rebuilds every commit on a second machine, on another PHP version, and compares the bytes. The recipe is on the PHAR page.

Changed

  • The mutation-testing gate now covers the whole source tree instead of the verdict engine, the signals, the analyzer and the output formats alone. No behaviour changes: the escapes it surfaced were closed with sharper tests, and a handful of statements no test could observe were removed as redundant.
  • The PHAR's alias is lockrot.phar (Box used to generate a random one per build), and its installed.php names lockrot as dev-main with no commit reference. lockrot reads neither.

Verify this release

curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.0/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.0/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gpg --keyserver hkps://keys.openpgp.org --recv-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141
curl -fsSL -O https://github.com/somework/lockrot/releases/download/v0.6.0/lockrot.phar.asc
gpg --verify lockrot.phar.asc lockrot.phar
gh attestation verify lockrot.phar --repo somework/lockrot

Or rebuild it: check out v0.6.0, run build/build-phar.sh with Composer 2.10.3, and compare the sha256.

Full changelog: CHANGELOG.md.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 06:53

Added

  • Signed releases. From this release on, lockrot.phar.asc ships next to the PHAR: a detached OpenPGP signature by the lockrot release key (39EC C3F6 4AE8 D06A 9A63 FD99 AB6F 7F52 AE51 3141, public half in lockrot-release-key.asc and on keys.openpgp.org), together with a GitHub build-provenance attestation. The release workflow verifies its own signature against the committed public key before it publishes anything. Verify with gpg --verify lockrot.phar.asc lockrot.phar or gh attestation verify lockrot.phar --repo somework/lockrot; phive install somework/lockrot --trust-gpg-keys 39ECC3F64AE8D06A9A63FD99AB6F7F52AE513141 now works. The sha256 checksum and self-update are unchanged: self-update still verifies the checksum only. See The standalone PHAR.

Fixed

  • phpstan/phpstan is no longer abandoned. A package is no longer flagged because an older release points at an archived repository. The repository asked about activity is the one the highest stable release names — its source, else its support.source — then the lock entry's, and when none of those names one the package is judged without a repository-activity check (Packagist's own abandoned flag still counts). phpstan/phpstan was reported abandoned on every project that runs lockrot with a GitHub token: its recent releases carry no source, support.source names the live phpstan/phpstan-src, and three old releases point at a one-off build repository that has since been archived. A support.source in the shape Packagist fills in by default, <repository>/tree/<version> (or /src/<ref> on Bitbucket), is reduced to the repository first.

Full changelog: v0.4.0...v0.5.0