CtrlK
BlogDocsLog inGet started
Tessl Logo

rockcrafters/mason

Agent kit for working on canonical/chisel-releases. Cross-agent skills + scripts for authoring and reviewing chisel slice definition files.

73

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

check-diff.pyskills/chisel-releases/scripts/

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["pyyaml"]
# ///
"""check-diff: catch append-only regressions between two versions of an SDF.

Published slices are append-only: removing an SDF or a slice fails the
removed-slices CI gate; dropping a content path from a kept slice has no CI
gate but is an append-only regression reviewers reject. Those are diff-shaped
checks -- single-file linting can't see them. This does:

  - an SDF that existed is now gone (or renamed) -> removed-slices CI fails
  - a slice key that existed is now gone         -> removed-slices CI fails
  - a content path dropped from a kept slice     -> append-only regression

Each is reported unless the package or path genuinely left the archive (which
only the archive can confirm -- a human/CI decides that), so these are warnings
carrying the CI consequence, not hard blocks.

Usage:
  check-diff.py --base <ref> [<pathspec> ...]   # git: compare <ref> vs worktree
  check-diff.py <old.yaml> <new.yaml>           # compare two files directly

--base mode enumerates changed slices/*.yaml via git and compares each against
<ref> (e.g. the release branch a PR targets). Default pathspec: slices/*.yaml.

Output: one finding per line, `SEVERITY  where: message`. Exit code always 0
(advisory); grep for `warn` to gate. Stdlib + pyyaml + git.
"""
from __future__ import annotations

import subprocess
import sys
from pathlib import Path
from typing import Any

import yaml


def parse(text: str) -> Any:
    try:
        return yaml.safe_load(text)
    except yaml.YAMLError:
        return None


def slice_paths(doc: Any) -> dict[str, set[str]]:
    """slice name -> set of content paths. empty if not an SDF."""
    out: dict[str, set[str]] = {}
    slices = doc.get("slices") if isinstance(doc, dict) else None
    if not isinstance(slices, dict):
        return out
    for name, body in slices.items():
        contents = body.get("contents") if isinstance(body, dict) else None
        out[name] = set(contents) if isinstance(contents, dict) else set()
    return out


def compare(old_text: str, new_text: str, label: str) -> list[tuple[str, str, str]]:
    rows: list[tuple[str, str, str]] = []
    old, new = slice_paths(parse(old_text)), slice_paths(parse(new_text))
    if not old:
        return rows  # nothing published before, or old wasn't an SDF
    if parse(new_text) is None or not new:
        # don't report every old slice as removed when the real defect is a
        # broken new file -- that misdirects the fix.
        return [("warn", label, "new file is not a parseable SDF -- fix that first")]
    for sname in old:
        if sname not in new:
            rows.append(("warn", f"{label}: {sname}",
                         "slice removed -- removed-slices CI fails unless the package left the archive"))
            continue
        gone = old[sname] - new[sname]
        for path in sorted(gone):
            rows.append(("warn", f"{label}: {sname}",
                         f"path removed from published slice: {path} (append-only regression)"))
    return rows


def git(args: list[str]) -> str | None:
    try:
        r = subprocess.run(["git", *args], capture_output=True, text=True)
    except OSError:
        return None
    return r.stdout if r.returncode == 0 else None


def run_base(base: str, pathspecs: list[str]) -> list[tuple[str, str, str]]:
    specs = pathspecs or ["slices/*.yaml"]
    status = git(["diff", "--name-status", base, "--", *specs])
    if status is None:
        return [("warn", base, "git diff failed -- is this a chisel-releases checkout, and is the ref present?")]
    rows: list[tuple[str, str, str]] = []
    for line in status.splitlines():
        parts = line.split("\t")
        if len(parts) < 2:
            continue
        code, path = parts[0], parts[-1]
        if code.startswith("D"):
            rows.append(("warn", path, "SDF removed -- removed-slices CI fails unless the package left the archive"))
        elif code.startswith("R") and len(parts) >= 3:
            # rename: old path is parts[1], new is parts[2]. the old filename is
            # gone (removed-slices CI fails on it, e.g. a soname bump), and the
            # rename may also drop slices/paths -- compare old vs new content.
            old_path, new_path = parts[1], parts[2]
            rows.append(("warn", old_path, "SDF renamed away -- removed-slices CI fails unless the package left the archive"))
            old_text = git(["show", f"{base}:{old_path}"])
            new = Path(new_path)
            if old_text is not None and new.exists():
                rows += compare(old_text, new.read_text(encoding="utf-8"), new_path)
        elif code.startswith("M"):
            old_text = git(["show", f"{base}:{path}"])
            new = Path(path)
            if old_text is None or not new.exists():
                continue
            rows += compare(old_text, new.read_text(encoding="utf-8"), path)
    return rows


def main(argv: list[str]) -> int:
    if argv and argv[0] in ("-h", "--help"):
        print(__doc__)
        return 0
    rows: list[tuple[str, str, str]]
    if argv and argv[0] == "--base":
        if len(argv) < 2:
            print("usage: check-diff.py --base <ref> [<pathspec> ...]", file=sys.stderr)
            return 2
        rows = run_base(argv[1], argv[2:])
    elif len(argv) == 2:
        old, new = Path(argv[0]), Path(argv[1])
        rows = compare(old.read_text(encoding="utf-8"), new.read_text(encoding="utf-8"), str(new))
    else:
        print("usage: check-diff.py --base <ref> [<pathspec> ...] | <old.yaml> <new.yaml>", file=sys.stderr)
        return 2

    for sev, where, msg in rows:
        print(f"{sev:5}  {where}: {msg}")
    if not rows:
        print("ok: no append-only regressions")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

tile.json