From 2b36e979a87c090879d03851c8151644b4742b40 Mon Sep 17 00:00:00 2001 From: Tyler Paxton Date: Mon, 22 Jun 2026 17:17:54 -0700 Subject: [PATCH] feat(cg-actions): source-aware version check (registry vs GitHub) check_version.py now detects how the skill was installed and checks the matching update channel: - Installed via Chainguard Agent Skills (chainctl skills install): files resolve under an `.agents/skills/` dir (often a symlink), so it queries the registry with `chainctl skills versions chainguard/chainguard-dev/cg-actions` and, if behind, suggests re-running the chainctl install. - Installed from GitHub (clone/download): compares against VERSION in chainguard-dev/cg-skills and suggests downloading the latest. If the detected source is unreachable it falls back to the other, and CG_ACTIONS_UPDATE_SOURCE=registry|github forces a choice. Version comparison is now semver-aware (1.10.1 > 1.2.0). Non-blocking as before. Bump VERSION to 1.3.0. Co-Authored-By: Claude Opus 4.8 --- skills/cg-actions/VERSION | 2 +- skills/cg-actions/scripts/check_version.py | 103 ++++++++++++++++++--- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/skills/cg-actions/VERSION b/skills/cg-actions/VERSION index 26aaba0..f0bb29e 100644 --- a/skills/cg-actions/VERSION +++ b/skills/cg-actions/VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/skills/cg-actions/scripts/check_version.py b/skills/cg-actions/scripts/check_version.py index 68f3fa8..1486fca 100644 --- a/skills/cg-actions/scripts/check_version.py +++ b/skills/cg-actions/scripts/check_version.py @@ -2,10 +2,17 @@ """ check_version.py — is this cg-actions skill the latest published version? -Compares the local VERSION file against the VERSION published on the skill's -GitHub repo (default branch). Prints a one-line status; if a newer version -exists, prints an update suggestion. Never fails the run — a network/auth issue -just yields "could not check for updates". +Detects how the skill was installed and checks the matching source: + + * Installed via Chainguard Agent Skills (`chainctl skills install ...`): + files live under an `.agents/skills/` directory (usually via a symlink), so + we ask the registry: `chainctl skills versions `. + * Installed from GitHub (clone or downloaded .skill): we compare against the + `VERSION` file in the chainguard-dev/cg-skills repo. + +Override detection with CG_ACTIONS_UPDATE_SOURCE=registry|github. If the detected +source can't be reached, the other is tried as a fallback. The check is +non-blocking: any network/auth issue just yields "could not check for updates". Usage: python3 scripts/check_version.py @@ -14,29 +21,80 @@ import argparse import base64 import os +import re import subprocess import sys import urllib.request -REPO = "chainguard-dev/cg-skills" # canonical source of the skill -VERSION_PATH = "skills/cg-actions/VERSION" # location of VERSION within REPO +# GitHub source (manual / cloned / downloaded installs) +GH_REPO = "chainguard-dev/cg-skills" +GH_VERSION_PATH = "skills/cg-actions/VERSION" + +# Registry source (Chainguard Agent Skills installs) +SKILL_REF = "skills.cgr.dev/chainguard/chainguard-dev/cg-actions" +SKILL_NAME = "chainguard/chainguard-dev/cg-actions" # org/name for `chainctl skills versions` + HERE = os.path.dirname(os.path.abspath(__file__)) +SKILL_ROOT = os.path.dirname(HERE) + +_VER_RE = re.compile(r"\bv?(\d+(?:\.\d+){1,2})\b") def local_version(): try: - with open(os.path.join(HERE, "..", "VERSION"), encoding="utf-8") as f: + with open(os.path.join(SKILL_ROOT, "VERSION"), encoding="utf-8") as f: return f.read().strip() except OSError: return None -def remote_version(): - """Latest published VERSION. Prefer gh (already a skill prerequisite); fall - back to the raw URL so the check still works without gh.""" +def detect_source(): + """Return 'registry' or 'github' based on where the skill was installed.""" + env = os.environ.get("CG_ACTIONS_UPDATE_SOURCE", "").strip().lower() + if env in ("registry", "github"): + return env + # chainctl writes a canonical copy under `.agents/skills/` and symlinks each + # agent's skills dir to it. So if our resolved path runs through + # `.agents/skills/` (or the skill dir is itself a symlink), the registry is + # the source of truth. Otherwise assume a GitHub-style manual install. + real = os.path.realpath(SKILL_ROOT).replace("\\", "/") + if "/.agents/skills/" in real or os.path.islink(SKILL_ROOT.rstrip("/")): + return "registry" + return "github" + + +def _max_version(text): + """Largest vX.Y.Z-style token found in text, or None.""" + best, best_key = None, () + for m in _VER_RE.finditer(text or ""): + key = tuple(int(x) for x in m.group(1).split(".")) + if key > best_key: + best_key, best = key, m.group(1) + return best + + +def registry_latest(): + """Latest published tag from the skills registry via chainctl.""" + for extra in (["-o", "json"], []): + try: + r = subprocess.run( + ["chainctl", "skills", "versions", SKILL_NAME, *extra], + capture_output=True, text=True, + ) + except Exception: + return None + if r.returncode == 0 and r.stdout.strip(): + v = _max_version(r.stdout) + if v: + return v + return None + + +def github_latest(): + """Latest published VERSION from the GitHub repo (gh, then raw URL).""" try: r = subprocess.run( - ["gh", "api", f"repos/{REPO}/contents/{VERSION_PATH}", "--jq", ".content"], + ["gh", "api", f"repos/{GH_REPO}/contents/{GH_VERSION_PATH}", "--jq", ".content"], capture_output=True, text=True, ) if r.returncode == 0 and r.stdout.strip(): @@ -44,7 +102,7 @@ def remote_version(): except Exception: pass try: - url = f"https://raw.githubusercontent.com/{REPO}/main/{VERSION_PATH}" + url = f"https://raw.githubusercontent.com/{GH_REPO}/main/{GH_VERSION_PATH}" with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310 (fixed host) return resp.read().decode("utf-8").strip() except Exception: @@ -60,6 +118,22 @@ def _parse(v): return tuple(out) +UPDATE_HINT = { + "registry": f"Update: re-run chainctl skills install {SKILL_REF}:latest", + "github": f"Update: download the latest from https://github.com/{GH_REPO}", +} + + +def latest_version(source): + """Latest version from the detected source, falling back to the other one.""" + order = [source, "github" if source == "registry" else "registry"] + for src in order: + rem = registry_latest() if src == "registry" else github_latest() + if rem: + return rem, src + return None, source + + def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) @@ -68,7 +142,8 @@ def main(argv=None): args = ap.parse_args(argv) loc = local_version() - rem = remote_version() + source = detect_source() + rem, used = latest_version(source) if not loc: if not args.quiet: @@ -81,7 +156,7 @@ def main(argv=None): if _parse(rem) > _parse(loc): print(f"⚠️ cg-actions is out of date: you have v{loc}, latest is v{rem}.") - print(f" Update by downloading the latest from https://github.com/{REPO}") + print(" " + UPDATE_HINT[used]) elif not args.quiet: print(f"cg-actions v{loc} (up to date).") return 0