ci(notebooks): diff Colab oracle against committed snapshots

Extend notebook_validator.py with a colab-diff subcommand that
fetches three files from googlecolab/backend-info:

  pip-freeze.gpu.txt   -> snapshot at scripts/data/colab_pip_freeze.gpu.txt
  apt-list-gpu.txt     -> snapshot at scripts/data/colab_apt_list.gpu.txt
  os-info-gpu.txt      -> snapshot at scripts/data/colab_os_info.gpu.txt

Each file is parsed with a format-specific parser (pip ==, apt
listing, free-form os-info) and compared against the committed
snapshot. The diff reports NEW / REMOVED / CHANGED keys per file.

Wired into Notebooks CI two ways:
- PR-time static job: advisory step (continue-on-error: true) so
  upstream Colab rotations surface in the PR check UI without
  blocking authors.
- Daily static-with-pypi cron: --strict step so backend-info drift
  fails the cron within ~24h and the maintainer can refresh the
  snapshots intentionally.

Catches the same bug classes the existing R-INST-002/003/004/005
rules catch, but earlier: when Colab bumps libcudnn / Python /
torch wheels, we hear about it before a notebook breaks.

Add baseline snapshots from current backend-info HEAD: 1136 apt
packages, 4 os-info entries, 720 pip-freeze entries.
This commit is contained in:
Daniel Han 2026-05-08 11:39:38 +00:00
commit 420f588205
4 changed files with 1318 additions and 0 deletions

View file

@ -101,6 +101,18 @@ jobs:
--out unsloth/scripts/data/colab_pip_freeze.gpu.txt \
|| echo "::warning::refresh-colab failed; using committed snapshot"
- name: Diff Colab oracle vs committed snapshots (advisory)
# Pulls pip-freeze.gpu.txt + apt-list-gpu.txt + os-info-gpu.txt
# from googlecolab/backend-info and prints NEW / REMOVED /
# CHANGED entries against scripts/data/colab_*.txt. Non-blocking
# on PRs; the daily cron job below runs the same step with
# --strict so upstream rotations surface within ~24h.
continue-on-error: true
working-directory: ${{ github.workspace }}
run: |
python unsloth/scripts/notebook_validator.py colab-diff \
--snapshot-dir unsloth/scripts/data
- name: Drift check (re-run update_all_notebooks.py + git diff)
working-directory: ${{ github.workspace }}
# Reported as non-blocking until the upstream `unslothai/notebooks`
@ -169,6 +181,14 @@ jobs:
run: |
python unsloth/scripts/notebook_validator.py refresh-colab \
--out unsloth/scripts/data/colab_pip_freeze.gpu.txt
- name: Diff Colab oracle vs committed snapshots (--strict on cron)
# Cron-only escalation of the advisory PR-time check. Fails if
# any of pip-freeze.gpu.txt / apt-list-gpu.txt / os-info-gpu.txt
# has drifted from scripts/data/colab_*.txt; refresh the
# snapshots in this repo to acknowledge.
run: |
python unsloth/scripts/notebook_validator.py colab-diff \
--snapshot-dir unsloth/scripts/data --strict
- name: Lint with live PyPI metadata
run: |
python unsloth/scripts/notebook_validator.py lint \

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
# Do not modify this file directly; it is generated by extract_colabx_testing_tarballs.sh via
# $ (lsb_release -ds;python --version;) > os-info-gpu.txt
# Be aware that this list does not necessarily reflect the current state of the
# staging or production container, but rather the state as of the most recent
# submitted CL where extract_colabx_testing_tarballs.sh was run.
Ubuntu 22.04.5 LTS
Python 3.12.13
R version 4.5.3 (2026-03-11) -- "Reassured Reassurer"
julia version 1.12.6

View file

@ -55,6 +55,21 @@ COLAB_PIP_FREEZE_URL = (
)
COLAB_FALLBACK_FILE = DATA_DIR / "colab_pip_freeze.gpu.txt"
# Oracle files we snapshot from googlecolab/backend-info. The diff
# subcommand fetches each, compares against the committed snapshot,
# and surfaces NEW / REMOVED / CHANGED entries so upstream Colab base
# image rotations land in CI within ~24h instead of when a notebook
# breaks. Every rule in this validator that resolves against the
# Colab preinstall (R-INST-002/003/004/005) gets earlier signal.
COLAB_ORACLE_FILES: dict[str, str] = {
"pip-freeze.gpu.txt": "colab_pip_freeze.gpu.txt",
"apt-list-gpu.txt": "colab_apt_list.gpu.txt",
"os-info-gpu.txt": "colab_os_info.gpu.txt",
}
COLAB_ORACLE_BASE_URL = (
"https://raw.githubusercontent.com/googlecolab/backend-info/main/"
)
# ----- Compat tables. PRs add rows as new releases land. ----- #
# torch.minor -> set of compatible torchcodec.minor strings.
@ -1086,6 +1101,130 @@ def cmd_refresh_colab(args: argparse.Namespace) -> int:
return 0
def _parse_pip_lines(text: str) -> dict[str, str]:
out: dict[str, str] = {}
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
m = re.match(r"^([A-Za-z0-9._-]+)\s*==\s*(.+?)\s*(;.*)?$", line)
if m:
out[m.group(1).lower()] = m.group(2)
return out
def _parse_apt_lines(text: str) -> dict[str, str]:
"""`pkg/release,now ver arch [installed[,automatic]]` -> {pkg: ver}."""
out: dict[str, str] = {}
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or line == "Listing...":
continue
m = re.match(r"^([^/\s]+)/\S+\s+(\S+)\s+\S+\s+\[installed", line)
if m:
out[m.group(1).lower()] = m.group(2)
return out
def _parse_os_lines(text: str) -> dict[str, str]:
"""Free-form `<tool> <version>` lines. Skip comments. The key is the
first token lower-cased; the value is the rest of the line."""
out: dict[str, str] = {}
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 1)
if len(parts) == 2:
out[parts[0].lower()] = parts[1]
else:
out[parts[0].lower()] = ""
return out
_COLAB_ORACLE_PARSERS = {
"pip-freeze.gpu.txt": _parse_pip_lines,
"apt-list-gpu.txt": _parse_apt_lines,
"os-info-gpu.txt": _parse_os_lines,
}
def _diff_oracle(
upstream: dict[str, str], snapshot: dict[str, str]
) -> tuple[list[tuple[str, str]], list[tuple[str, str]], list[tuple[str, str, str]]]:
"""Return (new, removed, changed). new/removed are (key, value);
changed is (key, old, new)."""
new = sorted((k, upstream[k]) for k in upstream.keys() - snapshot.keys())
removed = sorted((k, snapshot[k]) for k in snapshot.keys() - upstream.keys())
changed = sorted(
(k, snapshot[k], upstream[k])
for k in upstream.keys() & snapshot.keys()
if upstream[k] != snapshot[k]
)
return new, removed, changed
def cmd_colab_diff(args: argparse.Namespace) -> int:
"""Fetch every Colab oracle file in COLAB_ORACLE_FILES, diff against
the committed snapshot, and print NEW / REMOVED / CHANGED. Advisory
by default (rc=0); --strict promotes any diff to rc=1 so the daily
cron can fail loudly when upstream rotates."""
snapshot_dir = pathlib.Path(args.snapshot_dir).resolve()
any_diff = False
for upstream_name, snapshot_name in COLAB_ORACLE_FILES.items():
url = COLAB_ORACLE_BASE_URL + upstream_name
snap_path = snapshot_dir / snapshot_name
try:
with urllib.request.urlopen(url, timeout = 15) as r:
upstream_text = r.read().decode("utf-8", errors = "replace")
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e:
print(f"::warning::colab-diff: could not fetch {url}: {e}")
continue
if not snap_path.exists():
print(f"::warning::colab-diff: no committed snapshot at {snap_path}; skipping")
continue
snapshot_text = snap_path.read_text(encoding = "utf-8", errors = "replace")
parser = _COLAB_ORACLE_PARSERS[upstream_name]
upstream = parser(upstream_text)
snapshot = parser(snapshot_text)
new, removed, changed = _diff_oracle(upstream, snapshot)
n = len(new) + len(removed) + len(changed)
print(
f"\n=== {upstream_name}: "
f"upstream={len(upstream)} snapshot={len(snapshot)} "
f"diff={n} (new={len(new)} removed={len(removed)} changed={len(changed)}) ==="
)
if not n:
print(" no drift")
continue
any_diff = True
for k, v in new[:50]:
print(f" NEW {k}=={v}")
if len(new) > 50:
print(f" ...and {len(new) - 50} more new entries")
for k, v in removed[:50]:
print(f" REMOVED {k} (was {v})")
if len(removed) > 50:
print(f" ...and {len(removed) - 50} more removed entries")
for k, old, ver in changed[:80]:
print(f" CHANGED {k}: {old} -> {ver}")
if len(changed) > 80:
print(f" ...and {len(changed) - 80} more changed entries")
if any_diff and args.strict:
print(
"\n::error::Colab oracle drifted from committed snapshot; "
"refresh scripts/data/colab_*.txt to acknowledge.",
file = sys.stderr,
)
return 1
if any_diff:
print(
"\n::notice::Colab oracle drifted; "
"refresh scripts/data/colab_*.txt at your convenience."
)
return 0
# ----- Helpers ----- #
@ -1132,6 +1271,13 @@ def main(argv: list[str] | None = None) -> int:
pa = sub.add_parser("refresh-colab")
pa.add_argument("--out", default = str(COLAB_FALLBACK_FILE))
pa = sub.add_parser("colab-diff")
pa.add_argument("--snapshot-dir", default = str(DATA_DIR))
pa.add_argument(
"--strict", action = "store_true",
help = "exit 1 on any drift (default: advisory; exit 0)",
)
args = p.parse_args(argv)
return {
"drift": cmd_drift,
@ -1141,6 +1287,7 @@ def main(argv: list[str] | None = None) -> int:
"api": cmd_api,
"all": cmd_all,
"refresh-colab": cmd_refresh_colab,
"colab-diff": cmd_colab_diff,
}[args.cmd](args)