Skip to content

feat(quality): ask npm once whether the lockfile still matches the manifest - #767

Merged
rubenvdlinde merged 2 commits into
mainfrom
feat/lockfile-sync-check
Sep 14, 2026
Merged

feat(quality): ask npm once whether the lockfile still matches the manifest#767
rubenvdlinde merged 2 commits into
mainfrom
feat/lockfile-sync-check

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes half of #766.

The defect

The main to beta to development sync merge takes main's package.json
across and keeps development's package-lock.json. Neither file is
individually wrong. The merge has no conflict to report, because the two are
separate paths. Git has no reason to complain and npm has every reason to.

Measured 2026-09-12 in #766: one broken install read as sixteen failing jobs
on decidiq and twenty-three on shillinq
. Eslint, stylelint, build, l10n, unit
tests, npm audit, licence, gates: every one of them was npm ci dying at step
one. It also blocked every open PR in both repos, because Actions checks out
the merge of head into base for pull_request events, so each PR inherited the
broken base through no fault of its own.

What lands

A Lockfile Sync job that runs npm ci --dry-run --ignore-scripts and nothing
else. It answers in seconds the question those thirty-nine jobs were each
discovering separately.

npm install succeeding proves nothing here: it REWRITES the lockfile to
match, which is the repair rather than the test. npm ls reads an installed
tree. Only npm ci compares the two files as committed.

It warns, it does not block

All 21 core apps resolve this workflow at @main, so a gate merged as blocking
fails every affected repo the same minute. That has happened before on this
fleet and it is why the default is off.

Measured today, with npm ci itself rather than a proxy, on the toolchain
pins (Node 24.11.1, npm 11.19.1) and with each repo's own .npmrc:

branch result
development, all 21 core apps 21 of 21 pass
beta, all 21 core apps 21 of 21 pass

So the would-fail count is zero. decidiq and shillinq, the two apps #766
found broken this morning, were repaired by decidiq#1300 and shillinq#1591 and
are clean at their current tips. lockfile-sync-blocking: true flips it per
repo, the same shape as reuse-blocking and check-code-blocking. Re-take the
measurement before flipping it anywhere, rather than trusting this table later.

Only one npm answer counts as a finding

npm reports EUSAGE for the desync AND for a missing lockfile, with the same
code and a different sentence. A code on its own is not a diagnosis, so the
check requires both the code and the message that names the two files. Every
other non-zero exit is a registry hiccup, a proxy or a missing optional
platform binary, and is recorded as no verdict rather than as a fact about
the repo. A gate that reds on those is a gate that gets switched off.

Verification

The step logic was run against three real trees on the CI pins:

tree classified as
decidiq at 69971ec, the commit before the repair desync, finding
decidiq development today the two files agree
a tree with a package.json and no lockfile no verdict, not a finding

The first one is the positive control: this check demonstrably fails on the
real defect, so its green elsewhere means something.

Repo assertions, all passing on the patched workflow:
assert-quality-report-gates-every-leg.py (the new job is in Quality Report's needs:, so it can gate once a repo turns it on),
assert-no-producer-deletes-a-verdict.py, assert-run-steps-resolvable.py,
and yaml.safe_load.

What this PR does not do

#766 asks for two things. This is the first. The second, making the sync
workflow regenerate the lock when it carries a package.json across (or refuse
and open an issue), is a change to sync-to-beta.yml and fleet-back-merge.yml
and is not in this diff.

🤖 Generated with Claude Code

…nifest

The main to beta to development sync merge can carry main's package.json
across while keeping development's package-lock.json. Neither file is
individually wrong, the two are separate paths so git reports no conflict,
and npm then refuses to install anything.

Measured on 2026-09-12 (#766): one broken install read as sixteen failing
jobs on decidiq and twenty-three on shillinq, every one of them npm ci dying
at step one, and it blocked every open PR in both repos because Actions
builds pull_request runs on the merge of head into base.

This job asks that one question with npm ci --dry-run, in seconds, before
anything is installed or built.

It WARNS, it does not block. All 21 core apps resolve this workflow at @main,
so a gate merged as blocking fails every affected repo the same minute.
Measured today on the toolchain pins (Node 24.11.1, npm 11.19.1) with each
repo's own .npmrc: 21 of 21 pass on development and 21 of 21 on beta, so it
would fail nothing. lockfile-sync-blocking flips it per repo, the same shape
as reuse-blocking.

Only npm's EUSAGE desync answer counts as a finding, and only when the
message names the two files: npm answers EUSAGE for a missing lockfile as
well. Anything else is reported as no verdict rather than as a fact about
the repo.

Verified against three real trees: decidiq at 69971ec (pre-repair) classifies
as a desync, decidiq development classifies as agreeing, and a tree with no
lockfile classifies as no verdict rather than as a finding.
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

One red check on this PR, and it is inherited rather than this diff's.

Package invariants fails on test_gate_mainline_push_audits_everything.sh with 5 assertions. That suite exercises hydra-gates/scripts/run-hydra-gates.sh and its empty-tree fallback. This PR touches .github/workflows/quality.yml and CONVENTIONS.md and nothing under hydra-gates/.

Evidence it is not mine, rather than an assertion that it is not:

  • Every pull_request run of hydra-gates-package.yml since 11:49Z today has failed the same way: runs 34692041586 (feat/verification-speed), 34693351405 (feat/block-polling-hook), 34696756937 (feat/gate19-flag-cited-exclusions) and this one. Two of those three have already been merged.
  • Running the suite locally at 172c397, this PR's base, with the change absent, reproduces the identical 5 failures.
  • The same suite passes on a push to main at that commit (run 34706740018), which is what makes this a pull_request-event problem in the suite rather than a fact about any of these branches.

Reported and not fixed, per the inherited-debt rule. It belongs to whoever owns the runner's push-event scoping, not to a workflow PR.

Everything else on this PR is green: 22 successes, zero pending.

@rubenvdlinde
rubenvdlinde merged commit ac9437e into main Sep 14, 2026
42 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant