build: freeze operating system package inputs
refresh-image-locks / refresh (push) Failing after 24s
lint / lint (push) Successful in 1m2s

This commit is contained in:
2026-07-26 09:28:33 +00:00
parent 67fea5fd5e
commit 059fef1cac
9 changed files with 190 additions and 11 deletions
+3
View File
@@ -41,6 +41,9 @@ jobs:
npm install --package-lock-only --ignore-scripts --no-audit --no-fund
)
done
python3 scripts/complete_npm_lock_integrity.py \
bot_bottle/contrib/claude/package-lock.json \
bot_bottle/contrib/pi/package-lock.json
- name: Refresh pinned Codex installer checksum
run: |
+11 -1
View File
@@ -43,13 +43,23 @@
# is pip-installed to the same effect as the upstream image.
FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
# Freeze apt's package universe as well as the base filesystem. Without a
# snapshot, the same Dockerfile resolves different package versions over time.
ARG DEBIAN_SNAPSHOT=20260724T000000Z
RUN sed -i \
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
/etc/apt/sources.list.d/debian.sources
# Runtime system deps:
# git supplies the `git daemon` subcommand (no separate package)
# plus the core `git` binary the pre-receive hook invokes.
# openssh-client supplies the upstream SSH transport the
# pre-receive hook uses to forward accepted refs.
# ca-certificates is needed for mitmdump upstream TLS.
RUN apt-get update \
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
git openssh-client ca-certificates \
&& rm -rf /var/lib/apt/lists/*
+9 -1
View File
@@ -18,7 +18,15 @@
ARG ORCHESTRATOR_BASE_IMAGE
FROM ${ORCHESTRATOR_BASE_IMAGE}
RUN apt-get update \
ARG DEBIAN_SNAPSHOT=20260724T000000Z
RUN sed -i \
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
/etc/apt/sources.list.d/debian.sources
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
buildah crun netavark aardvark-dns \
&& rm -rf /var/lib/apt/lists/*
+10 -2
View File
@@ -11,6 +11,14 @@
# Version-qualified Node LTS, pinned to its multi-architecture manifest.
FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
ARG DEBIAN_SNAPSHOT=20260724T000000Z
RUN sed -i \
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
/etc/apt/sources.list.d/debian.sources
# Install runtime system deps. claude-code shells out to git for several
# features (status checks, commits, PR creation) — without git in the
# image, those features fail in surprising ways once the user does any
@@ -19,7 +27,7 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8
# HTTPS_PROXY-aware tool (curl itself, plus anything that shells out
# to it) works against egress's bumped TLS without the agent needing
# local DNS.
RUN apt-get update \
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
@@ -34,7 +42,7 @@ RUN apt-get update \
# (claude-code is a Node CLI), but is convenient for the agent to
# shell out to for ad-hoc scripts. Kept on its own layer so it can
# be moved to a downstream image if the base ever needs to shrink.
RUN apt-get update \
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/*
+10 -2
View File
@@ -10,7 +10,15 @@ FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e8
# installer source and checksum.
ARG CODEX_VERSION=0.145.0
RUN apt-get update \
ARG DEBIAN_SNAPSHOT=20260724T000000Z
RUN sed -i \
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
/etc/apt/sources.list.d/debian.sources
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
@@ -24,7 +32,7 @@ RUN apt-get update \
# (codex is a Node CLI), but is convenient for the agent to shell
# out to for ad-hoc scripts. Kept on its own layer so it can be
# moved to a downstream image if the base ever needs to shrink.
RUN apt-get update \
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/*
+10 -2
View File
@@ -4,7 +4,15 @@
FROM node:22.23.1-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
RUN apt-get update \
ARG DEBIAN_SNAPSHOT=20260724T000000Z
RUN sed -i \
-e "s|http://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian-security|https://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT}|g" \
-e "s|http://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
-e "s|https://deb.debian.org/debian|https://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT}|g" \
/etc/apt/sources.list.d/debian.sources
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
@@ -15,7 +23,7 @@ RUN apt-get update \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
RUN apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/*
+6 -3
View File
@@ -1056,7 +1056,8 @@
},
"engines": {
"node": ">=22.19.0"
}
},
"integrity": "sha512-yqbh68CyhqxMov/jUogFJfMqlu2Gd37GAki+tr59YCmAPHfomiCA5ESzusXtpGzABeiZFC/OrRdQ4GwCCOMIHA=="
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": {
"version": "0.81.1",
@@ -1080,7 +1081,8 @@
},
"engines": {
"node": ">=22.19.0"
}
},
"integrity": "sha512-hzHE7Z8l5mgJk+ke67Lge0rwS2+wbKJrFKl9o5M1R1rh33+cCT7D1AHz1OAtX5wFs90E1/BTGhyJRTUHaMxGvQ=="
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": {
"version": "0.81.1",
@@ -1092,7 +1094,8 @@
},
"engines": {
"node": ">=22.19.0"
}
},
"integrity": "sha512-OMEe+Zt8oQYi/rCq3upxsTlIScWL0FPhXwQus34TbQb3EmTx88S7Uzx32JxvQiEeWOw8eDCdJf2PBUBE9r6wIg=="
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": {
"version": "1.52.0",
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Complete duplicate npm lock entries from an identical verified artifact."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
def complete_lock(path: Path) -> int:
"""Fill missing SRI only from the same resolved URL; return change count."""
data = json.loads(path.read_text(encoding="utf-8"))
packages = data.get("packages", {})
integrity_by_url: dict[str, str] = {}
for package in packages.values():
resolved = package.get("resolved")
integrity = package.get("integrity")
if not isinstance(resolved, str) or not resolved.startswith(
("http://", "https://"),
):
continue
if not isinstance(integrity, str) or not integrity.startswith("sha512-"):
continue
prior = integrity_by_url.setdefault(resolved, integrity)
if prior != integrity:
raise ValueError(f"conflicting integrity values for {resolved}")
changed = 0
missing: list[str] = []
for package_path, package in packages.items():
resolved = package.get("resolved")
if not isinstance(resolved, str) or not resolved.startswith(
("http://", "https://"),
):
continue
integrity = package.get("integrity")
if isinstance(integrity, str) and integrity.startswith("sha512-"):
continue
known = integrity_by_url.get(resolved)
if known is None:
missing.append(package_path)
continue
package["integrity"] = known
changed += 1
if missing:
joined = ", ".join(missing)
raise ValueError(f"no verified duplicate artifact for: {joined}")
if changed:
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return changed
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("locks", nargs="+", type=Path)
args = parser.parse_args()
for path in args.locks:
changed = complete_lock(path)
print(f"{path}: completed {changed} duplicate integrity entries")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,66 @@
"""Unit tests for npm's duplicate lock-entry integrity completion."""
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from scripts.complete_npm_lock_integrity import complete_lock
class TestCompleteLock(unittest.TestCase):
def _lock(self, directory: str, packages: dict[str, dict[str, str]]) -> Path:
path = Path(directory) / "package-lock.json"
path.write_text(json.dumps({
"lockfileVersion": 3,
"packages": packages,
}))
return path
def test_copies_integrity_only_for_identical_resolved_url(self):
url = "https://registry.npmjs.org/example/-/example-1.2.3.tgz"
integrity = "sha512-trusted"
with tempfile.TemporaryDirectory() as directory:
path = self._lock(directory, {
"node_modules/example": {
"resolved": url,
"integrity": integrity,
},
"node_modules/parent/node_modules/example": {
"resolved": url,
},
})
self.assertEqual(1, complete_lock(path))
packages = json.loads(path.read_text())["packages"]
self.assertEqual(
integrity,
packages["node_modules/parent/node_modules/example"]["integrity"],
)
def test_rejects_missing_integrity_without_verified_duplicate(self):
with tempfile.TemporaryDirectory() as directory:
path = self._lock(directory, {
"node_modules/example": {
"resolved": (
"https://registry.npmjs.org/example/-/example-1.2.3.tgz"
),
},
})
with self.assertRaisesRegex(ValueError, "no verified duplicate"):
complete_lock(path)
def test_rejects_conflicting_integrities_for_same_url(self):
url = "https://registry.npmjs.org/example/-/example-1.2.3.tgz"
with tempfile.TemporaryDirectory() as directory:
path = self._lock(directory, {
"node_modules/a": {"resolved": url, "integrity": "sha512-a"},
"node_modules/b": {"resolved": url, "integrity": "sha512-b"},
})
with self.assertRaisesRegex(ValueError, "conflicting integrity"):
complete_lock(path)
if __name__ == "__main__":
unittest.main()