Skip to content
Merged
74 changes: 74 additions & 0 deletions .github/scripts/dep_upgrade/baseline_stem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Print the baseline_sbom stem a capture just recorded for an image:tag.

Reads the stem back out of the manifest rather than scraping capture logs.
container/context.yaml holds a single stem per framework and the licenses stage
appends -${TARGETARCH}, so per-arch stems that disagree are unrepresentable
downstream; this fails instead of shipping a half-right baseline.

Stdout: the bare stem, e.g. release@2366e4b4.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

DEFAULT_MANIFEST = Path("container/compliance/base_sboms/manifest.json")


def stem_for(manifest: dict, image: str, tag: str, arches: list[str]) -> str:
"""Return the shared baseline_sbom stem for image:tag across `arches`.

Raises SystemExit if any arch is missing, matches more than one row, has an
unexpected filename, or if the arches disagree on the stem.
"""
if not arches:
raise SystemExit("no architectures given; --arches must name at least one")
entries = manifest.get("entries", [])
stems: dict[str, str] = {}
for arch in arches:
rows = [
e
for e in entries
if e.get("from_image") == image
and e.get("from_tag") == tag
and e.get("platform") == f"linux/{arch}"
]
if len(rows) != 1:
raise SystemExit(
f"expected exactly 1 {arch} row for {image}:{tag}, found {len(rows)}"
)
suffix = f"-{arch}.cdx.json"
name = rows[0].get("baseline_sbom", "")
if not name.endswith(suffix):
raise SystemExit(f"{arch} baseline_sbom {name!r} does not end in {suffix}")
stems[arch] = name[: -len(suffix)]
if len(set(stems.values())) != 1:
raise SystemExit(f"per-arch baseline stems disagree: {stems}")
return next(iter(stems.values()))
Comment thread
nv-anants marked this conversation as resolved.


def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--image", required=True, help="from_image, e.g. nvcr.io/nvidia/x")
p.add_argument("--tag", required=True, help="from_tag, e.g. 1.3.0rc25")
p.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
p.add_argument(
"--arches",
default="amd64,arm64",
help="comma-separated arches that must agree (default: %(default)s)",
)
args = p.parse_args()

manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
arches = [a.strip() for a in args.arches.split(",") if a.strip()]
print(stem_for(manifest, args.image, args.tag, arches))
return 0


if __name__ == "__main__":
sys.exit(main())
51 changes: 50 additions & 1 deletion .github/scripts/dep_upgrade/bump_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# Match runtime_image_tag inside the trtllm: block (first sub-block,
# e.g. cuda13.1) without colliding with vllm/sglang earlier in the file.
re.compile(
r"(?m)(^trtllm:\s*?\n(?:[ \t]+[^\n]*\n)*?[ \t]+runtime_image_tag:\s+)\S+",
r"(?m)(^trtllm:\s*?\n(?:[ \t][^\n]*\n)*?[ \t]+runtime_image_tag:\s+)\S+",
),
r"\g<1>{ver}",
),
Expand All @@ -51,6 +51,41 @@
"trtllm": TRTLLM_TARGETS,
}

# Sibling of FRAMEWORK_TARGETS, kept separate because it is driven by a value
# only the compliance capture can produce (the new baseline's digest stem), not
# by the release version. Same block anchoring as runtime_image_tag above, so it
# cannot reach the vllm/sglang baseline_sbom lines earlier in the file.
FRAMEWORK_BASELINES: dict[str, tuple[str, Pattern[str], str]] = {
"trtllm": (
"container/context.yaml",
re.compile(
r"(?m)(^trtllm:\s*?\n(?:[ \t][^\n]*\n)*?[ \t]+baseline_sbom:\s+)\S+",
),
r"\g<1>{stem}",
),
}


def set_baseline_stem(framework: str, stem: str, repo_root: Path) -> bool:
"""Point the framework's baseline_sbom at a freshly captured stem.

The runtime image's licenses stage subtracts this baseline from NOTICES, so
leaving it behind a runtime_image_tag bump makes every moved package version
read as net-new and fails the license gate. Returns True if the file changed.
"""
rel, pat, tmpl = FRAMEWORK_BASELINES[framework]
path = repo_root / rel
text = path.read_text()
new_text, n = pat.subn(tmpl.replace("{stem}", stem), text)
if n != 1:
raise SystemExit(
f"{rel}: matched {n} {framework} baseline_sbom pins; expected 1"
)
if new_text == text:
return False
path.write_text(new_text)
return True


def apply(framework: str, version: str, repo_root: Path) -> int:
"""Apply all bumps for the framework. Returns count of files written."""
Expand Down Expand Up @@ -81,10 +116,24 @@ def main() -> int:
required=True,
help="version without 'v' prefix, e.g. 1.3.0rc12",
)
p.add_argument(
"--baseline-stem",
help=(
"Compliance baseline stem from capture_baseline_sbom.py, e.g. "
"release@2366e4b4. When given, also repoints the framework's "
"baseline_sbom in container/context.yaml. Omit to bump versions only."
),
)
p.add_argument("--repo-root", type=Path, default=Path("."))
args = p.parse_args()
n = apply(args.framework, args.version, args.repo_root)
print(f"changed {n} files")
if args.baseline_stem:
changed = set_baseline_stem(args.framework, args.baseline_stem, args.repo_root)
print(
f"baseline_sbom -> {args.baseline_stem}"
f"{'' if changed else ' (already set)'}"
)
return 0


Expand Down
83 changes: 81 additions & 2 deletions .github/scripts/dep_upgrade/check_bump_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
version and asserts it rewrites exactly MAIN_TOT.trtllm in
docs/fern/components/releases.data.ts — one line, inside the MAIN_TOT block,
no RELEASES pin touched, and the result still parses through
gen_llms_tables.py. Run from anywhere; exit 0 == safe to bump.
gen_llms_tables.py.

Then runs set_baseline_stem with a synthetic stem and asserts it rewrites
exactly the baseline_sbom inside the trtllm: block of container/context.yaml,
leaving runtime_image_tag and the vllm/sglang blocks alone. Run from anywhere;
exit 0 == safe to bump.
"""
from __future__ import annotations

import importlib.util
import shutil
import sys
import tempfile
import traceback
from pathlib import Path

REPO = Path(__file__).resolve().parents[3]
Expand All @@ -25,6 +31,8 @@
SEED_FILES = ["container/context.yaml", "pyproject.toml", DATA_REL]

SYNTH = "9.9.9rc99" # synthetic upstream TRT-LLM version, absent from real data
SYNTH_STEM = "release@0bad0bad" # synthetic baseline stem, absent from real data
CTX_REL = "container/context.yaml"


def load(name: str, path: Path):
Expand All @@ -35,10 +43,21 @@ def load(name: str, path: Path):


def fail(msg: str) -> None:
"""Report a failed assertion and exit non-zero (exit 0 == safe to bump)."""
print(f"FAIL: {msg}")
sys.exit(1)


def fail_exc(what: str, exc: BaseException) -> None:
"""Same, for an unexpected crash: keep the traceback for debugging.

The traceback is printed rather than propagated so the script keeps its
single contract -- exit 0 means safe to bump, anything else means stop.
"""
traceback.print_exc()
fail(f"{what} raised {type(exc).__name__}: {exc}")


def main() -> int:
bump = load("bump_dependency", BUMP)
gen = load("gen_llms_tables", GEN)
Expand All @@ -62,7 +81,7 @@ def main() -> int:
except SystemExit as exc:
fail(f"bump apply() raised SystemExit: {exc}")
except Exception as exc: # noqa: BLE001 - any crash is a red result
fail(f"bump apply() raised {type(exc).__name__}: {exc}")
fail_exc("bump apply()", exc)

after = data_path.read_text()
if after == before:
Expand Down Expand Up @@ -116,9 +135,69 @@ def main() -> int:
if not rel_pins:
fail("no RELEASES trtllm pins parsed; cross-check is meaningless")

# --- baseline stem rewrite ------------------------------------------
# Runs against the already-bumped tree, matching the workflow's order.
ctx_path = root / CTX_REL
ctx_before = ctx_path.read_text()

try:
if not bump.set_baseline_stem("trtllm", SYNTH_STEM, root):
fail("set_baseline_stem reported no change")
except SystemExit as exc:
fail(f"set_baseline_stem raised SystemExit: {exc}")
except Exception as exc: # noqa: BLE001 - any crash is a red result
fail_exc("set_baseline_stem", exc)

ctx_after = ctx_path.read_text()
cb, ca = ctx_before.splitlines(), ctx_after.splitlines()
if len(cb) != len(ca):
fail("context.yaml line count changed; expected an in-place stem edit")
ctx_changed = [i for i, (x, y) in enumerate(zip(cb, ca)) if x != y]
if len(ctx_changed) != 1:
fail(
f"expected exactly 1 changed context.yaml line, "
f"got {len(ctx_changed)}: {ctx_changed}"
)
stem_idx = ctx_changed[0]
if ca[stem_idx].strip() != f"baseline_sbom: {SYNTH_STEM}":
fail(f"changed line is not the baseline_sbom pin: {ca[stem_idx]!r}")

# It must sit inside the trtllm: block, not vllm/sglang earlier in the file.
fw_start = next(
(i for i, ln in enumerate(ca) if ln.rstrip() == "trtllm:"), None
)
if fw_start is None:
fail("no top-level 'trtllm:' block found in context.yaml")
fw_end = next(
(
i
for i in range(fw_start + 1, len(ca))
if ca[i][:1].strip() and not ca[i].startswith("#")
),
len(ca),
)
if not (fw_start < stem_idx < fw_end):
fail(
f"baseline_sbom line {stem_idx} is outside the trtllm: block "
f"({fw_start}..{fw_end})"
)

# The version bump and the stem must not clobber each other.
if f"runtime_image_tag: {SYNTH}" not in ctx_after:
fail("runtime_image_tag lost its bumped value after the stem rewrite")
if ctx_after.count(SYNTH_STEM) != 1:
fail(
f"{SYNTH_STEM} appears {ctx_after.count(SYNTH_STEM)} times; expected 1"
)

# Idempotent: re-applying the same stem is a no-op, not a second edit.
if bump.set_baseline_stem("trtllm", SYNTH_STEM, root):
fail("set_baseline_stem is not idempotent")

print("PASS: bump rewrote only MAIN_TOT.trtllm; RELEASES pins intact; file parses")
print(f" MAIN_TOT.trtllm -> {SYNTH}")
print(f" {len(rel_pins)} RELEASES trtllm pins left untouched")
print(f"PASS: baseline_sbom -> {SYNTH_STEM} (trtllm block only, idempotent)")
return 0


Expand Down
37 changes: 32 additions & 5 deletions .github/scripts/dep_upgrade/detect_latest_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
# SPDX-License-Identifier: Apache-2.0
"""Detect latest upstream version for a Dynamo framework dependency.

Stdout: one-line JSON: {"framework", "current", "latest", "upgrade_needed"}.
Stdout: one-line JSON:
{"framework", "current", "latest", "upgrade_needed", "runtime_image"}.
runtime_image is the registry repo the tag belongs to, so callers can build the
full ref (<runtime_image>:<latest>) without re-parsing context.yaml.
Exit 0 on success regardless of upgrade_needed; non-zero only on real errors.
"""
from __future__ import annotations
Expand All @@ -24,7 +27,11 @@
# Anchored inside the trtllm: block so we don't pick up
# vllm/sglang runtime_image_tag lines earlier in the file.
"current_regex": re.compile(
r"(?m)^trtllm:\s*?\n(?:[ \t]+[^\n]*\n)*?[ \t]+runtime_image_tag:\s*(\S+)\s*$",
r"(?m)^trtllm:\s*?\n(?:[ \t][^\n]*\n)*?[ \t]+runtime_image_tag:\s*(\S+)\s*$",
),
# Same anchoring for the sibling runtime_image the tag hangs off.
"runtime_image_regex": re.compile(
r"(?m)^trtllm:\s*?\n(?:[ \t][^\n]*\n)*?[ \t]+runtime_image:\s*(\S+)\s*$",
),
},
}
Expand Down Expand Up @@ -75,15 +82,34 @@ def latest_release(repo: str, include_prereleases: bool) -> str:
return max(candidates, key=parse_version)


def current_pin(framework: str, repo_root: Path) -> str:
def _context_field(framework: str, key: str, repo_root: Path) -> str:
"""Return the single capture of FRAMEWORK_SOURCES[framework][key] in context.yaml.

Raises SystemExit if the pattern does not match, so a drifted pin format
fails loudly rather than silently reporting a stale value.
"""
src = FRAMEWORK_SOURCES[framework]
text = (repo_root / "container" / "context.yaml").read_text()
m = src["current_regex"].search(text) # type: ignore[union-attr]
m = src[key].search(text) # type: ignore[union-attr]
if not m:
raise SystemExit(f"no current pin found for {framework}")
raise SystemExit(f"no {key} match for {framework} in container/context.yaml")
return m.group(1)


def current_pin(framework: str, repo_root: Path) -> str:
"""Return the framework's currently pinned version, e.g. 1.3.0rc25."""
return _context_field(framework, "current_regex", repo_root)


def runtime_image(framework: str, repo_root: Path) -> str:
"""Return the registry repo the pin belongs to, without a tag.

e.g. nvcr.io/nvidia/tensorrt-llm/release. Callers join this with the
detected version to form the ref to capture a baseline against.
"""
return _context_field(framework, "runtime_image_regex", repo_root)


def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--framework", required=True, choices=sorted(FRAMEWORK_SOURCES))
Expand All @@ -105,6 +131,7 @@ def main() -> int:
"current": cur,
"latest": latest,
"upgrade_needed": parse_version(latest) > parse_version(cur),
"runtime_image": runtime_image(args.framework, args.repo_root),
}
)
)
Expand Down
Loading
Loading