#!/usr/bin/env bash # Audit installed extensions against config/extensions.lock.json. # Fails if a pinned extension is missing/at another version, if an expected transitive # dependency is at another version, or if anything is installed that the lock does not # describe at all (ad-hoc marketplace installs are contract violations here). # ./scripts/list-extension-versions.sh # baseline audit # ./scripts/list-extension-versions.sh --include-optional # also require optional entries set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" LOCK="$ROOT/config/extensions.lock.json" CODE_SERVER="${CODE_SERVER_BIN:-/usr/bin/code-server}" CONFIG="$ROOT/config/config.yaml" INCLUDE_OPTIONAL=0 for arg in "$@"; do case "$arg" in --include-optional) INCLUDE_OPTIONAL=1 ;; *) echo "unknown argument: $arg" >&2; exit 2 ;; esac done installed=$("$CODE_SERVER" --config "$CONFIG" --list-extensions --show-versions 2>/dev/null | sed '/^$/d' | sort) echo "installed:" printf '%s\n' "$installed" | sed 's/^/ /' echo INSTALLED="$installed" INCLUDE_OPTIONAL="$INCLUDE_OPTIONAL" python3 - "$LOCK" <<'PY' import json, os, sys lock = json.load(open(sys.argv[1])) want_optional = os.environ.get("INCLUDE_OPTIONAL") == "1" installed = {} for line in os.environ.get("INSTALLED", "").splitlines(): line = line.strip() if "@" in line: eid, _, ver = line.rpartition("@") installed[eid.lower()] = ver status = 0 known = set() # 1. forward check: every selected pin must be installed at the pinned version selected = [ e for e in lock["extensions"] if e["mode"] == "baseline" or (want_optional and e["mode"] == "optional") ] for e in selected: key = e["id"].lower() known.add(key) want = f'{e["id"]}@{e["version"]}' have = installed.get(key) if have == e["version"]: print(f"ok {want}") elif have is None: print(f"MISSING {want}", file=sys.stderr) status = 1 else: print(f'MISMATCH expected {want}, found {e["id"]}@{have}', file=sys.stderr) status = 1 # 2. expected transitive dependencies: pulled in by the pins above, so their presence is # legitimate; absence is not a lock violation, but a version drift is reported and is # labelled MISMATCH-DEP so it is never confused with a baseline pin mismatch. for e in lock.get("expected_transitive", []): key = e["id"].lower() known.add(key) want = f'{e["id"]}@{e["version"]}' have = installed.get(key) if have == e["version"]: print(f"ok-dep {want}") elif have is None: print(f"note {want} (expected transitive dependency not installed)") else: print( f'MISMATCH-DEP expected transitive {want}, found {e["id"]}@{have}', file=sys.stderr, ) status = 1 # 3. lock-described but not selected (e.g. optional entries in a baseline run): installed # copies are informational, never failures, because the lock does describe them. for e in lock["extensions"]: key = e["id"].lower() if key in known: continue known.add(key) have = installed.get(key) if have is None: continue suffix = "" if have == e["version"] else f' (lock pins {e["version"]})' print(f'note {e["id"]}@{have} installed, {e["mode"]} entry not selected{suffix}') # 4. reverse check: anything installed that the lock does not describe at all excluded = {e["id"].lower(): e.get("reason", "") for e in lock.get("excluded", [])} for eid, ver in sorted(installed.items()): if eid in known: continue why = excluded.get(eid) detail = f" (explicitly excluded by lock: {why})" if why else "" print(f"EXTRA {eid}@{ver}{detail}", file=sys.stderr) status = 1 sys.exit(status) PY