search-with-proof: reproducible npm builds for old lockless projects (--before commit date)

A lockless npm project (e.g. glance, 2015) resolved an unpinned transitive range to the
latest major ([email protected] -> mime@4, which dropped .lookup()), so the built app crashed
serving files -- a build-reproducibility defect, not an app or agent fault. The executor
now, for the npm-install (no-lock) case only and only when .git is present, resolves
dependencies as of the checkout's HEAD commit date via `npm install --before <date>`,
giving era-appropriate, compatible versions. npm ci (lockfile present) is untouched, and
any git/date failure falls back to plain install. demo.py also recognizes
npm-shrinkwrap.json as a lock for the ci-vs-install choice.

Tests: executor + smoke 75 passed, demo 107 passed; ruff clean; mypy clean on executor.py.
Executor-side change -> image rebuild required.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-07 13:37:23 +02:00
co-authored by Claude Opus 4.8
parent 8de0c2732b
commit c30dd9126a
3 changed files with 78 additions and 1 deletions
+2 -1
View File
@@ -270,7 +270,8 @@ def preparation_command(recipe: str, path: Path, checkout: Path, products: Path)
*(["-r", str(target)] if requirements else [str(target)]),
], project
if recipe == "npm":
return ["npm", "ci" if (project / "package-lock.json").is_file() else "install", "--ignore-scripts"], project
locked = any((project / name).is_file() for name in ("package-lock.json", "npm-shrinkwrap.json"))
return ["npm", "ci" if locked else "install", "--ignore-scripts"], project
if recipe == "composer":
return ["composer", "install", "--no-interaction", "--no-plugins", "--no-scripts"], project
if recipe == "maven":
+14
View File
@@ -412,6 +412,20 @@ class InProcessExecutor:
if recipe["recipe"] != "none":
path = self._path(recipe["arguments"][0])
argv, project = preparation_command(recipe["recipe"], path, self.repo, products)
if argv[:2] == ["npm", "install"] and (self.repo / ".git").exists():
# Lockless old projects should resolve dependencies from their era.
# Metadata is optional; unavailable git must not prevent a build.
with suppress(OSError, subprocess.SubprocessError, UnicodeError):
commit_date = subprocess.run(
["git", "-C", str(project), "show", "-s", "--format=%cI", "HEAD"],
check=True,
capture_output=True,
text=True,
env=clean_environment(),
timeout=min(5, self._remaining()),
).stdout.strip()
if commit_date:
argv.extend(["--before", commit_date])
self.phase = "build"
built = self._task_run(argv, self._remaining(), {"disk_bytes": task_storage.DEFAULT_SCRATCH_BYTES, **limits}, cwd=project)
phases["build"] = time.monotonic() - before