mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +01:00
"Are we on the newest version?" was not mechanically answerable across these repos: constraint styles had drifted per-ecosystem and a caret range is "newest-compatible" by definition, which gives a checker nothing to compare. This gate makes it answerable and then enforces the answer. Checks two things at once -- that every dependency carries an exact pin, and that the pin equals its registry's latest STABLE release -- across pub.dev, PyPI, npm, crates.io, the Go proxy, kuhyx/utils git tags, and the Flutter / Node / Python toolchains. Three deliberate design points: * Pre-releases are rejected per-ecosystem, because every ecosystem leaks them differently and each leak fails GREEN. npm lets a maintainer point `dist-tags.latest` at an rc; crates.io's `max_version` includes them; PyPI's legacy `info.version` includes yanked releases. Unit-tested with fixtures, since an end-to-end run cannot surface a wrong "up to date". * Offline, the gate serves an expired cache entry and PASSES with a DEGRADED banner (exit 3 only under --strict, which is CI). `--no-verify` is banned here, so a hook that hard-failed offline would leave no way to commit at all. * The allowlist has two entry classes. `blocked_by: transitive:<pkg>@<ver>` is a predicate that clears itself when upstream moves and must NOT carry a date; everything else is discretionary and requires `expires`, capped at 90 days. Exact pins plus latest-everywhere is provably unsatisfiable when a dependency internally constrains a shared transitive, and expiring the predicate class on a calendar would turn the gate red for a reason no commit could fix. An entry with nothing left to excuse is itself an error. One hook id, one shim, one shared implementation -- always delegating, so it cannot fork the way the 250-line cap forked into four ids, two of which were independent reimplementations. 256 tests, 100% branch coverage on every module of the gate. Also adds the workflow that runs them, which incidentally gives md_naming's 71 tests their first CI run. Not yet installed in any repo: a repo that gets the gate before its deps are bumped is instantly and permanently red, so the gate and the upgrade land together, per repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""`pubspec.yaml` (+ `pubspec.lock`) -> Dart/Flutter dependencies.
|
|
|
|
Three shapes live in the same map: a plain constraint (`http: ^1.6.0`), an SDK
|
|
package (`flutter: {sdk: flutter}`) and a git-tag dependency on `kuhyx/utils`,
|
|
whose "latest" is a tag rather than a registry version. `dependency_overrides`
|
|
is parsed too and always reported: an override is an unpinned dependency
|
|
wearing a disguise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from dep_freshness._tables import (
|
|
GITTAG,
|
|
PUB,
|
|
PUB_CARET_ALLOWED,
|
|
PUB_OVERRIDE_KEY,
|
|
PUB_SDK_PACKAGES,
|
|
TOOLCHAIN,
|
|
)
|
|
from dep_freshness.models import Dep
|
|
from dep_freshness.parsers._lines import index
|
|
from dep_freshness.versions import exact_pin
|
|
|
|
_SECTIONS = (("dependencies", False), ("dev_dependencies", True))
|
|
|
|
|
|
def _load(path: Path) -> dict:
|
|
try:
|
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
except (OSError, yaml.YAMLError):
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def locked_versions(lock: Path) -> dict[str, str]:
|
|
"""`package -> resolved version` from a committed `pubspec.lock`.
|
|
|
|
A missing lock means "library, check the manifest alone" (four of the nine
|
|
shared libs gitignore theirs) — never a violation.
|
|
"""
|
|
data = _load(lock)
|
|
out: dict[str, str] = {}
|
|
for name, entry in (data.get("packages") or {}).items():
|
|
if isinstance(entry, dict) and entry.get("version"):
|
|
out[str(name)] = str(entry["version"])
|
|
return out
|
|
|
|
|
|
def _git_ref(spec: dict) -> str | None:
|
|
git = spec.get("git")
|
|
if isinstance(git, dict):
|
|
return str(git.get("ref") or "") or None
|
|
return None
|
|
|
|
|
|
def parse(path: Path) -> list[Dep]:
|
|
data = _load(path)
|
|
if not data:
|
|
return []
|
|
lines = index(path)
|
|
locks = locked_versions(path.with_name("pubspec.lock"))
|
|
deps: list[Dep] = []
|
|
|
|
sdk = ((data.get("environment") or {}) or {}).get("sdk")
|
|
if isinstance(sdk, str):
|
|
deps.append(Dep(
|
|
ecosystem=TOOLCHAIN, name="dart", constraint=sdk, path=path,
|
|
line=lines.get("sdk", 0), pinned=exact_pin(sdk), caret_ok=True,
|
|
))
|
|
|
|
sections = [*_SECTIONS, (PUB_OVERRIDE_KEY, False)]
|
|
for section, is_dev in sections:
|
|
for name, spec in (data.get(section) or {}).items():
|
|
name = str(name)
|
|
override = section == PUB_OVERRIDE_KEY
|
|
if isinstance(spec, dict) and (ref := _git_ref(spec)):
|
|
package, _, version = ref.rpartition("-v")
|
|
deps.append(Dep(
|
|
ecosystem=GITTAG, name=package or name, constraint=ref,
|
|
path=path, line=lines.get(name, 0), pinned=version or None,
|
|
dev=is_dev, override=override,
|
|
))
|
|
continue
|
|
if name in PUB_SDK_PACKAGES or (
|
|
isinstance(spec, dict) and "sdk" in spec
|
|
):
|
|
continue # ships with the SDK; no registry version exists
|
|
if isinstance(spec, dict):
|
|
continue # path/hosted dep: nothing comparable to a registry
|
|
constraint = "" if spec is None else str(spec)
|
|
deps.append(Dep(
|
|
ecosystem=PUB, name=name, constraint=constraint, path=path,
|
|
line=lines.get(name, 0), pinned=exact_pin(constraint),
|
|
locked=locks.get(name), dev=is_dev,
|
|
caret_ok=name in PUB_CARET_ALLOWED, override=override,
|
|
))
|
|
return deps
|