Some checks failed
CI / skinny-install (aco) (push) Successful in 46s
CI / skinny-install (api) (push) Successful in 28s
CI / skinny-install (bcda) (push) Successful in 26s
CI / skinny-install (bib) (push) Successful in 24s
CI / skinny-install (bls) (push) Successful in 28s
CI / skinny-install (ccw) (push) Successful in 31s
CI / skinny-install (cli) (push) Successful in 26s
CI / skinny-install (cms) (push) Successful in 26s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (pfs) (push) Successful in 26s
CI / skinny-install (rex) (push) Successful in 24s
CI / lint-test (push) Successful in 5m50s
Infra CI / notebooks (push) Successful in 1m14s
Infra CI / zotero (push) Failing after 5s
Infra CI / docs (push) Successful in 6s
Infra CI / api (push) Successful in 13s
Infra CI / mc (push) Successful in 7s
Deploy / build-scan-report (push) Successful in 5m6s
Cherry-picked from feat/pkg-supply-chain, adapted for infra/ tree layout: - dev/scripts/pkg_inventory.py — scans Dockerfiles, pyproject.toml, CI workflows, and shell scripts to build a unified package manifest - dev/scripts/pkg_mirror_sync.py — syncs PyPI/APK/npm packages to Gitea package registry (replaces devpi/apt-cacher-ng) - dev/scripts/pkg_drift.py — compares mirror contents against manifest, flags missing or stale packages - dev/scripts/pkg_issues.py — auto-creates Gitea issues for drift and CVE findings - dev/scripts/test_network_isolation.sh — verifies containers can't reach the internet except through mirrors - dev/pipelines/pkg-supply-chain.yml — CI-agnostic pipeline spec - .gitea/workflows/pkg-supply-chain.yml — daily + push-triggered CI job - PYPI_INDEX_URL build arg added to api and notebooks Dockerfiles
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
"""Detect drift between the package manifest and what is actually used.
|
|
|
|
Compares data/pkg-manifest.json against the repo source to detect:
|
|
- packages in manifest but missing from source (phantom)
|
|
- packages in source but missing from manifest (untracked)
|
|
|
|
Usage:
|
|
uv run python dev/scripts/pkg_drift.py # report + exit code
|
|
uv run python dev/scripts/pkg_drift.py --json # JSON output
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
MANIFEST_PATH = ROOT / "data" / "pkg-manifest.json"
|
|
|
|
|
|
def load_manifest() -> dict:
|
|
if not MANIFEST_PATH.exists():
|
|
print(f"ERROR: {MANIFEST_PATH} not found. Run pkg_inventory.py first.")
|
|
raise SystemExit(2)
|
|
return json.loads(MANIFEST_PATH.read_text())
|
|
|
|
|
|
def scan_fresh() -> dict:
|
|
"""Run a fresh inventory scan and return the result."""
|
|
from pkg_inventory import scan
|
|
|
|
return scan()
|
|
|
|
|
|
def diff_lists(manifest_items: list, fresh_items: list, key: str = "name") -> dict:
|
|
"""Compare two lists of dicts by a key field."""
|
|
if manifest_items and isinstance(manifest_items[0], dict):
|
|
m_set = {item[key] for item in manifest_items}
|
|
f_set = {item[key] for item in fresh_items}
|
|
else:
|
|
m_set = set(manifest_items)
|
|
f_set = set(fresh_items)
|
|
|
|
return {
|
|
"phantom": sorted(m_set - f_set),
|
|
"untracked": sorted(f_set - m_set),
|
|
}
|
|
|
|
|
|
def check_drift() -> dict:
|
|
manifest = load_manifest()
|
|
fresh = scan_fresh()
|
|
drift: dict = {"apt": {}, "apk": {}, "pypi": {}, "npm": {}, "has_drift": False}
|
|
|
|
# apt
|
|
all_apt_manifest = set()
|
|
for pkgs in manifest.get("apt", {}).values():
|
|
all_apt_manifest.update(pkgs)
|
|
all_apt_fresh = set()
|
|
for pkgs in fresh.get("apt", {}).values():
|
|
all_apt_fresh.update(pkgs)
|
|
apt_diff = {
|
|
"phantom": sorted(all_apt_manifest - all_apt_fresh),
|
|
"untracked": sorted(all_apt_fresh - all_apt_manifest),
|
|
}
|
|
if apt_diff["phantom"] or apt_diff["untracked"]:
|
|
drift["apt"] = apt_diff
|
|
drift["has_drift"] = True
|
|
|
|
# apk
|
|
all_apk_manifest = set()
|
|
for pkgs in manifest.get("apk", {}).values():
|
|
all_apk_manifest.update(pkgs)
|
|
all_apk_fresh = set()
|
|
for pkgs in fresh.get("apk", {}).values():
|
|
all_apk_fresh.update(pkgs)
|
|
apk_diff = {
|
|
"phantom": sorted(all_apk_manifest - all_apk_fresh),
|
|
"untracked": sorted(all_apk_fresh - all_apk_manifest),
|
|
}
|
|
if apk_diff["phantom"] or apk_diff["untracked"]:
|
|
drift["apk"] = apk_diff
|
|
drift["has_drift"] = True
|
|
|
|
# pypi (compare all sections)
|
|
for section in ("project_prod", "project_dev", "notebook", "dockerfile_adhoc"):
|
|
d = diff_lists(
|
|
manifest.get("pypi", {}).get(section, []),
|
|
fresh.get("pypi", {}).get(section, []),
|
|
)
|
|
if d["phantom"] or d["untracked"]:
|
|
drift["pypi"][section] = d
|
|
drift["has_drift"] = True
|
|
|
|
# npm
|
|
d = diff_lists(
|
|
manifest.get("npm", []),
|
|
fresh.get("npm", []),
|
|
)
|
|
if d["phantom"] or d["untracked"]:
|
|
drift["npm"] = d
|
|
drift["has_drift"] = True
|
|
|
|
return drift
|
|
|
|
|
|
def main() -> None:
|
|
drift = check_drift()
|
|
|
|
if "--json" in sys.argv:
|
|
print(json.dumps(drift, indent=2))
|
|
else:
|
|
if not drift["has_drift"]:
|
|
print("No drift detected — manifest matches repo source.")
|
|
else:
|
|
print("DRIFT DETECTED:")
|
|
for pkg_type in ("apt", "apk", "pypi", "npm"):
|
|
section = drift[pkg_type]
|
|
if not section:
|
|
continue
|
|
if pkg_type == "pypi":
|
|
for sub, d in section.items():
|
|
if d.get("phantom"):
|
|
print(
|
|
f" pypi/{sub} phantom (in manifest, not in source): {d['phantom']}"
|
|
)
|
|
if d.get("untracked"):
|
|
print(
|
|
f" pypi/{sub} untracked (in source, not in manifest): {d['untracked']}"
|
|
)
|
|
else:
|
|
if section.get("phantom"):
|
|
print(f" {pkg_type} phantom: {section['phantom']}")
|
|
if section.get("untracked"):
|
|
print(f" {pkg_type} untracked: {section['untracked']}")
|
|
|
|
raise SystemExit(1 if drift["has_drift"] else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|