Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ jobs:
else
npm install --no-audit --no-fund
fi
- name: Lockfile version matches package.json
# `npm ci` above already fails when the lockfile's DEPENDENCY entries
# disagree with package.json; it does not check the lockfile's copy of
# this project's own version. A release that bumps package.json without
# running an install leaves that field behind, and nothing caught it β€”
# measured in bareguard, stale across three consecutive releases.
# No-ops via --if-present in a repo that has not added the script.
run: npm run check:lockfile --if-present
- name: Typecheck
run: npm run typecheck --if-present
- name: Test
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

### Fixed

- **The publish workflow now fails when `package-lock.json`'s version drifts from `package.json`.** npm writes that field on install, so a release that bumps `package.json` without running one leaves it behind β€” and nothing caught it: `npm ci` fails when the lockfile's *dependency* entries disagree, but never checks the lockfile's copy of the project's own version. `scripts/check-lockfile.mjs` (`npm run check:lockfile`) compares both places npm writes it and runs in the publish workflow. No lockfile is not a failure.

## 0.11.2 β€” 2026-08-31

### Added
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"prepublishOnly": "npm run build:types",
"test": "node --test test/unit/*.test.js test/integration/*.test.js",
"test:ios": "node --test test/ios/*.test.js",
"ios:check": "node test/ios/check-prerequisites.js"
"ios:check": "node test/ios/check-prerequisites.js",
"check:lockfile": "node scripts/check-lockfile.mjs"
},
"keywords": [
"android",
Expand Down
53 changes: 53 additions & 0 deletions scripts/check-lockfile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Fail when package-lock.json's copy of this project's own version disagrees
// with package.json.
//
// npm writes that copy on install. A release that bumps package.json without
// running an install leaves it behind, and nothing else catches it: `npm ci`
// fails when the lockfile's DEPENDENCY entries disagree with package.json, but
// it does not check the lockfile's copy of the project's own version. Measured
// in bareguard, where it sat at 0.13.0 while 0.14.0, 0.15.0 and 0.16.0 all
// shipped.
//
// Harmless to consumers β€” npm does not include a library's lockfile in the
// published tarball β€” so this is repo hygiene, not a user-visible bug. It is a
// hard failure anyway because it is trivially fixable and can never be a false
// positive: it compares two numbers this repo owns.
//
// No lockfile is not a failure. Some repos deliberately ship without one, and
// this check runs from a workflow shared across all of them.
import { readFileSync } from "node:fs";
import path from "node:path";

const root = path.resolve(import.meta.dirname, "..");
const read = (f) => JSON.parse(readFileSync(path.join(root, f), "utf8"));

let lock;
try {
lock = read("package-lock.json");
} catch (err) {
if (err.code === "ENOENT") {
console.log("βœ“ no package-lock.json β€” nothing to check");
process.exit(0);
}
throw err;
}

const pkg = read("package.json");

// Two places hold it: the lockfile root, and the root package's own entry in
// `packages[""]`. npm writes both, so both are checked β€” drift in either is the
// same drift, and checking only one lets the other move unseen.
const found = {
"package-lock.json version": lock.version,
'package-lock.json packages[""].version': lock.packages?.[""]?.version,
};
const wrong = Object.entries(found).filter(([, v]) => v !== pkg.version);

if (wrong.length) {
console.error(`βœ— lockfile version drift β€” package.json is ${pkg.version}:`);
for (const [where, v] of wrong) console.error(` ${where} = ${v ?? "(missing)"}`);
console.error(" Fix: npm install --package-lock-only (then commit package-lock.json)");
process.exit(1);
}

console.log(`βœ“ package-lock.json version matches package.json β€” ${pkg.version}`);
Loading