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:
parent
81534ddd69
commit
420f588205
4 changed files with 1318 additions and 0 deletions
20
.github/workflows/notebooks-ci.yml
vendored
20
.github/workflows/notebooks-ci.yml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
1142
scripts/data/colab_apt_list.gpu.txt
Normal file
1142
scripts/data/colab_apt_list.gpu.txt
Normal file
File diff suppressed because it is too large
Load diff
9
scripts/data/colab_os_info.gpu.txt
Normal file
9
scripts/data/colab_os_info.gpu.txt
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue