diff --git a/rh-basic/skills/red-hat-product-lifecycle/SKILL.md b/rh-basic/skills/red-hat-product-lifecycle/SKILL.md index dc927be6..e9237dd3 100644 --- a/rh-basic/skills/red-hat-product-lifecycle/SKILL.md +++ b/rh-basic/skills/red-hat-product-lifecycle/SKILL.md @@ -5,16 +5,16 @@ license: Apache-2.0 user_invocable: true model: inherit color: cyan -allowed-tools: +allowed-tools: Bash --- # Red Hat Product Lifecycle Advisor -Identify product and version from user message. If unclear, ask. Look up lifecycle data and respond using the output format below. If MCP tools unavailable, fall back to WebFetch -- never decline because a tool is missing. +Identify product and version from user message. If unclear, ask. Look up lifecycle data using the script below and respond using the output format. ## Prerequisites -None — all data is available via WebFetch against public Red Hat documentation. +Python 3 — the script uses only the stdlib. ## When to Use This Skill @@ -23,24 +23,32 @@ When the user asks about lifecycle status, support phases, or EOL dates for any ## Workflow 1. Identify product and version from the user message. -2. Fetch lifecycle data via WebFetch. -3. Return current phase, key dates, and action recommendation. +2. Run the lifecycle script via Bash: + ``` + python scripts/rh_lifecycle.py "Product Name Version" + ``` + The script queries `https://access.redhat.com/product-life-cycles/api/v1/products` and returns JSON to stdout. + Errors are on stderr (JSON with `"error"` key); exit code 1 on failure. +3. Parse the JSON output and respond using the output format below. + +### Output schema +```json +{ + "product": "Red Hat Enterprise Linux", + "version": "9", + "current_phase": "Full Support", + "phases": { + "General availability": { "start": "2022-05-18", "end": "2022-05-18" }, + "Full support": { "start": "2022-05-18", "end": "2027-05-31" }, + "Maintenance support": { "start": "2027-06-01", "end": "2032-05-31" } + } +} +``` +Dates are `YYYY-MM-DD`; `"N/A"` means no date; `"Ongoing"` means open-ended. ## Dependencies -None. - -## Data Sources (stop when you have dates) - -RHEL major/minor: -1. `WebFetch` -> `https://access.redhat.com/product-life-cycles/?product=Red%20Hat%20Enterprise%20Linux` - -App Streams (Node.js, PostgreSQL, .NET, etc.): -1. `WebFetch` -> `https://access.redhat.com/support/policy/updates/errata/` - -OpenShift, Ansible, JBoss, Satellite, all others: -1. `WebFetch` -> `https://access.redhat.com/product-life-cycles/update_policies` -- find product link, fetch that page -2. Common direct URLs: `/support/policy/updates/openshift` | `/support/policy/updates/ansible-tower` | `/support/policy/updates/jboss_notes` | `/support/policy/updates/satellite` +Script: `scripts/rh_lifecycle.py` ## Lifecycle Phase Reference diff --git a/rh-basic/skills/red-hat-product-lifecycle/scripts/rh_lifecycle.py b/rh-basic/skills/red-hat-product-lifecycle/scripts/rh_lifecycle.py new file mode 100644 index 00000000..1a49adc3 --- /dev/null +++ b/rh-basic/skills/red-hat-product-lifecycle/scripts/rh_lifecycle.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +Red Hat product lifecycle CLI. +Returns lifecycle phase dates for a given Red Hat product as JSON. +""" +import argparse +import json +import re +import sys +import urllib.request +from difflib import get_close_matches + +API_URL = "https://access.redhat.com/product-life-cycles/api/v1/products" + + +def fetch_products(): + req = urllib.request.Request( + API_URL, + headers={"User-Agent": "rh-lifecycle-cli/1.0"}, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read().decode())["data"] + + +def parse_input(product_input): + """Split 'Product Name X.Y' into (name, version) where version is trailing digits/dots.""" + parts = product_input.rsplit(" ", 1) + if len(parts) == 2 and re.fullmatch(r"\d[\d.]*", parts[1]): + return parts[0].strip(), parts[1] + return product_input.strip(), None + + +def find_product(products, name): + names_lower = {p["name"].lower(): p for p in products} + needle = name.lower() + matches = get_close_matches(needle, names_lower.keys(), n=5, cutoff=0.5) + if not matches: + return None, [] + best = names_lower[matches[0]] + suggestions = [names_lower[m]["name"] for m in matches[1:]] + return best, suggestions + + +def find_version(product, version): + versions = product.get("versions", []) + if version is None: + return versions[0] if versions else None + for v in versions: + if v["name"] == version: + return v + # prefix fallback: "4.2" matches "4.20" is wrong; only allow exact or major match + for v in versions: + if v["name"].startswith(version + ".") or version.startswith(v["name"] + "."): + return v + return None + + +def format_date(value, fmt): + if value is None or value in ("N/A", "Ongoing", ""): + return value + if fmt == "date" and isinstance(value, str) and "T" in value: + return value[:10] + return value + + +def build_output(product, version_data): + phases = {} + for phase in version_data.get("phases", []): + phases[phase["name"]] = { + "start": format_date(phase.get("start_date"), phase.get("start_date_format")), + "end": format_date(phase.get("end_date"), phase.get("end_date_format")), + } + return { + "product": product["name"], + "version": version_data["name"], + "current_phase": version_data.get("type"), + "phases": phases, + } + + +def error_exit(msg, **extra): + payload = {"error": msg, **extra} + print(json.dumps(payload, indent=2), file=sys.stderr) + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + description="Get lifecycle dates for a Red Hat product.", + epilog=( + "Examples:\n" + " %(prog)s 'Red Hat Enterprise Linux 9'\n" + " %(prog)s 'Red Hat OpenShift Container Platform 4.20'\n" + " %(prog)s 'Kubernetes NMState Operator 4.20'" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "product", + nargs="+", + help='Product name with version, e.g. "Red Hat Enterprise Linux 9"', + ) + args = parser.parse_args() + + product_input = " ".join(args.product) + name, version = parse_input(product_input) + + try: + products = fetch_products() + except Exception as exc: + error_exit(f"Failed to fetch lifecycle data: {exc}") + + product, suggestions = find_product(products, name) + if product is None: + error_exit(f"Product not found: {name!r}") + + version_data = find_version(product, version) + if version_data is None: + available = [v["name"] for v in product.get("versions", [])] + error_exit( + f"Version {version!r} not found for {product['name']!r}", + available_versions=available, + ) + + result = build_output(product, version_data) + # Only hint at alternatives when the match wasn't exact + if suggestions and product["name"].lower() != name.lower(): + result["_note"] = f"Matched {product['name']!r}; other close matches: {suggestions}" + + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main()