Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions rh-basic/skills/red-hat-product-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
134 changes: 134 additions & 0 deletions rh-basic/skills/red-hat-product-lifecycle/scripts/rh_lifecycle.py
Original file line number Diff line number Diff line change
@@ -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()