Skip to content

Commit 4795808

Browse files
authored
Merge pull request #25 from zdrapela/extract-ci-skills
feat: extract CI lifecycle and Prow job management skills from openshift/release
2 parents e58eecd + d4b252e commit 4795808

45 files changed

Lines changed: 3351 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-commit

100644100755
File mode changed.

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ Single version (`0.2.0`) kept in sync across three files:
7474

7575
Bump all three when releasing.
7676

77+
## Shared modules (lifecycle ↔ prow)
78+
79+
`skills/prow/scripts/rhdh_prow/repo.py` and `skills/prow/scripts/rhdh_prow/yaml.py` are copies of `skills/lifecycle/scripts/rhdh_lifecycle/repo.py` and `skills/lifecycle/scripts/rhdh_lifecycle/yaml.py`. The only difference is the internal import path (`rhdh_prow.repo` vs `rhdh_lifecycle.repo`). When modifying either copy, update both to keep them in sync.
80+
81+
`skills/prow/scripts/rhdh_prow/utils.py` is a subset of `skills/lifecycle/scripts/rhdh_lifecycle/utils.py`. When modifying either copy, update both to keep them in sync.
82+
7783
## Agent skills
7884

7985
### Issue tracker

skills/lifecycle/SKILL.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
name: lifecycle
3+
description: >-
4+
Check version lifecycle and support status for platforms and integrations
5+
used by RHDH. Covers OCP, AKS, EKS, GKE, RHDH releases, RHBK, Quay,
6+
PostgreSQL, and any Red Hat product via the Product Life Cycles API.
7+
Use when asking about version support, EOL dates, GA dates, support
8+
phases, or planning version upgrades. Also use for "is X still
9+
supported", "what versions should we test", or "when does X reach EOL".
10+
---
11+
# Version Lifecycle Checks
12+
13+
Check version lifecycle and support status for platforms and integrations used by RHDH.
14+
15+
## Prerequisites
16+
17+
- Python 3.9+
18+
- Internet connectivity for API access
19+
- For configured K8s version display (AKS/EKS): local `openshift/release` checkout or `gh` CLI
20+
21+
## Identify Platform
22+
23+
What platform or integration lifecycle do you need to check?
24+
25+
| Query matches | Workflow |
26+
|---|---|
27+
| "OCP", "OpenShift version", "OpenShift EOL", "OpenShift support" | `workflows/check-ocp.md` |
28+
| "RHDH version", "Developer Hub release", "is RHDH 1.x supported" | `workflows/check-rhdh.md` |
29+
| "AKS", "Azure Kubernetes" | `workflows/check-aks.md` |
30+
| "EKS", "Amazon EKS" | `workflows/check-eks.md` |
31+
| "GKE", "Google Kubernetes" | `workflows/check-gke.md` |
32+
| "RHBK", "Keycloak", "Red Hat Build of Keycloak", "Quay", any Red Hat product | `workflows/check-redhat.md` |
33+
| "PostgreSQL", "Postgres", "PG", "database versions" | `workflows/check-pg.md` |
34+
| "all platforms", "full lifecycle check" | Run all workflows in sequence |
35+
36+
After reading the workflow, follow it exactly.
37+
38+
## Available Scripts
39+
40+
All scripts support `--help` for usage details and `--json` for structured output.
41+
42+
| Script | Purpose |
43+
|--------|---------|
44+
| `scripts/check_ocp_lifecycle.py` | OCP version lifecycle with EUS phases |
45+
| `scripts/check_rhdh_lifecycle.py` | RHDH release lifecycle with OCP compatibility |
46+
| `scripts/check_lifecycle.py` | Generic Red Hat product (RHBK, Quay, etc.) |
47+
| `scripts/check_aks_lifecycle.py` | AKS K8s version lifecycle |
48+
| `scripts/check_eks_lifecycle.py` | EKS K8s version lifecycle |
49+
| `scripts/check_gke_lifecycle.py` | GKE K8s version lifecycle |
50+
| `scripts/check_pg_lifecycle.py` | PostgreSQL lifecycle across cloud providers |
51+
52+
## Library (`rhdh_lifecycle` package)
53+
54+
Shared utilities used by both lifecycle and prow skills:
55+
56+
| Module | Purpose |
57+
|--------|---------|
58+
| `rhdh_lifecycle.repo` | Resolve openshift/release repository root (local or remote) |
59+
| `rhdh_lifecycle.yaml` | Read and parse YAML files from openshift/release |
60+
| `rhdh_lifecycle.configured_versions` | Print configured K8s versions per branch |
61+
| `rhdh_lifecycle.redhat` | Red Hat Product Life Cycles API client |
62+
| `rhdh_lifecycle.ocp` | OCP version phase classification |
63+
| `rhdh_lifecycle.rhdh` | RHDH release lifecycle data |
64+
| `rhdh_lifecycle.pg` | PostgreSQL lifecycle across cloud providers |
65+
66+
## Related Skills
67+
68+
- **`prow`**: Manage Prow CI job configurations for RHDH in openshift/release
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.9"
4+
# dependencies = ["ruamel.yaml"]
5+
# ///
6+
"""Check AKS Kubernetes version lifecycle using the official AKS release status API.
7+
8+
Primary source: https://releases.aks.azure.com/parsed_data.json
9+
Cross-verify: https://endoflife.date/api/azure-kubernetes-service.json
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import json
16+
import sys
17+
from datetime import datetime, timezone
18+
19+
from rhdh_lifecycle.configured_versions import print_configured_versions
20+
from rhdh_lifecycle.repo import resolve_repo_root
21+
from rhdh_lifecycle.utils import fetch_json, filter_supported_eol_entries, ver_sort_key
22+
23+
AKS_API_URL = "https://releases.aks.azure.com/parsed_data.json"
24+
EOL_API_URL = "https://endoflife.date/api/azure-kubernetes-service.json"
25+
CONFIG_DIR = "ci-operator/config/redhat-developer/rhdh"
26+
27+
28+
def main(argv=None):
29+
parser = argparse.ArgumentParser(description="Check AKS K8s version lifecycle.")
30+
parser.add_argument("--mapt-ref", help="Path to MAPT ref YAML (repo-relative)")
31+
parser.add_argument("--test-pattern", help="Regex to match test names")
32+
parser.add_argument("--config-dir", default=CONFIG_DIR, help="CI config directory")
33+
parser.add_argument("--repo-dir", help="Path to openshift/release checkout")
34+
parser.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
35+
args = parser.parse_args(argv)
36+
37+
root, is_remote = resolve_repo_root(args.repo_dir)
38+
39+
# Print configured versions if test pattern provided
40+
if args.test_pattern and not args.json_output:
41+
print_configured_versions(
42+
args.config_dir, args.test_pattern, root, is_remote, args.mapt_ref
43+
)
44+
45+
# Fetch AKS release data (primary source)
46+
data = fetch_json(AKS_API_URL)
47+
if not data:
48+
sys.exit(1)
49+
50+
# Extract supported versions from the first region
51+
try:
52+
regional = data["Sections"]["KubernetesSupportedVersions"]["Components"][
53+
"KubernetesVersions"
54+
]["RegionalStatuses"]
55+
first_region = list(regional.values())[0][0]["Current"]["KubernetesVersionList"]
56+
except (KeyError, IndexError, TypeError):
57+
print("ERROR: Unexpected AKS API response structure", file=sys.stderr)
58+
sys.exit(1)
59+
60+
# Group by minor version
61+
minor_versions = {}
62+
for entry in first_region:
63+
parts = entry["VersionName"].split(".")
64+
minor = f"{parts[0]}.{parts[1]}"
65+
if minor not in minor_versions:
66+
minor_versions[minor] = {"is_lts": False, "is_preview": False}
67+
if entry.get("IsLTS"):
68+
minor_versions[minor]["is_lts"] = True
69+
if entry.get("IsPreview"):
70+
minor_versions[minor]["is_preview"] = True
71+
72+
sorted_versions = sorted(
73+
minor_versions.items(),
74+
key=lambda x: ver_sort_key(x[0]),
75+
reverse=True,
76+
)
77+
78+
# Deprecated version
79+
try:
80+
deprecated = regional[list(regional.keys())[0]][0]["Current"].get(
81+
"DeprecatedVersion", "N/A"
82+
)
83+
except (KeyError, IndexError):
84+
deprecated = "N/A"
85+
86+
# Cross-verify with endoflife.date
87+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
88+
eol_data = fetch_json(EOL_API_URL)
89+
eol_supported = filter_supported_eol_entries(eol_data, today) if eol_data else []
90+
91+
# JSON output
92+
if args.json_output:
93+
result = {
94+
"versions": [
95+
{
96+
"version": ver,
97+
"status": "LTS"
98+
if info["is_lts"]
99+
else ("Preview" if info["is_preview"] else "GA"),
100+
}
101+
for ver, info in sorted_versions
102+
],
103+
"deprecated": deprecated,
104+
"endoflife_date": [
105+
{"version": e["cycle"], "eol": str(e.get("eol", "N/A"))} for e in eol_supported
106+
],
107+
}
108+
json.dump(result, sys.stdout, indent=2)
109+
print()
110+
return
111+
112+
# Human-readable output
113+
print("=== AKS Release Status (releases.aks.azure.com) ===")
114+
print("Supported minor versions (newest first):")
115+
for ver, info in sorted_versions:
116+
if info["is_lts"]:
117+
status = "LTS"
118+
elif info["is_preview"]:
119+
status = "Preview"
120+
else:
121+
status = "GA"
122+
print(f" {ver:<8s} {status}")
123+
print(f"Recently deprecated: {deprecated}")
124+
125+
print()
126+
print("=== Cross-verify (endoflife.date) ===")
127+
if not eol_data:
128+
print("WARNING: Failed to fetch endoflife.date", file=sys.stderr)
129+
return
130+
for entry in eol_supported:
131+
print(f" {entry['cycle']}\tEOL: {entry.get('eol', 'N/A')}")
132+
133+
134+
if __name__ == "__main__":
135+
main()
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.9"
4+
# dependencies = ["ruamel.yaml"]
5+
# ///
6+
"""Check EKS Kubernetes version lifecycle using the official AWS EKS docs source.
7+
8+
Primary source: awsdocs/amazon-eks-user-guide raw AsciiDoc on GitHub
9+
Cross-verify: https://endoflife.date/api/amazon-eks.json
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import json
16+
import re
17+
import sys
18+
import urllib.error
19+
import urllib.request
20+
from datetime import datetime, timezone
21+
22+
from rhdh_lifecycle.configured_versions import print_configured_versions
23+
from rhdh_lifecycle.repo import resolve_repo_root
24+
from rhdh_lifecycle.utils import fetch_json, filter_supported_eol_entries
25+
26+
EKS_DOCS_URL = (
27+
"https://raw.githubusercontent.com/awsdocs/amazon-eks-user-guide"
28+
"/mainline/latest/ug/versioning/kubernetes-versions.adoc"
29+
)
30+
EOL_API_URL = "https://endoflife.date/api/amazon-eks.json"
31+
CONFIG_DIR = "ci-operator/config/redhat-developer/rhdh"
32+
33+
34+
def fetch_text(url):
35+
"""Fetch text content from a URL."""
36+
req = urllib.request.Request(url, headers={"User-Agent": "rhdh-skill"})
37+
try:
38+
with urllib.request.urlopen(req, timeout=30) as resp:
39+
return resp.read().decode("utf-8")
40+
except (urllib.error.URLError, OSError) as exc:
41+
print(f"ERROR: Failed to fetch {url}: {exc}", file=sys.stderr)
42+
return None
43+
44+
45+
def parse_supported_versions(docs):
46+
"""Extract supported versions and their tiers from the AsciiDoc."""
47+
section = ""
48+
versions = []
49+
for line in docs.splitlines():
50+
if "Available versions on standard support" in line:
51+
section = "Standard"
52+
elif "Available versions on extended support" in line:
53+
section = "Extended"
54+
elif "Amazon EKS Kubernetes release calendar" in line:
55+
section = ""
56+
elif section and re.match(r"^\* `\d+\.\d+`$", line):
57+
ver = line.strip("* `\n")
58+
versions.append((ver, section))
59+
return versions
60+
61+
62+
def parse_release_calendar(docs):
63+
"""Extract the release calendar table from the AsciiDoc."""
64+
lines = docs.splitlines()
65+
in_table = False
66+
entries = []
67+
i = 0
68+
while i < len(lines):
69+
line = lines[i]
70+
if line.strip() == "|===":
71+
in_table = not in_table
72+
i += 1
73+
continue
74+
if in_table and re.match(r"^\|`\d+\.\d+`", line):
75+
# Next 4 lines are: upstream, eks_release, end_std, end_ext
76+
version = line.lstrip("|").strip("`").strip()
77+
fields = []
78+
for j in range(1, 5):
79+
if i + j < len(lines):
80+
fields.append(lines[i + j].lstrip("|").strip())
81+
else:
82+
fields.append("N/A")
83+
entries.append((version, *fields))
84+
i += 5
85+
continue
86+
i += 1
87+
return entries
88+
89+
90+
def main(argv=None):
91+
parser = argparse.ArgumentParser(description="Check EKS K8s version lifecycle.")
92+
parser.add_argument("--mapt-ref", help="Path to MAPT ref YAML (repo-relative)")
93+
parser.add_argument("--test-pattern", help="Regex to match test names")
94+
parser.add_argument("--config-dir", default=CONFIG_DIR, help="CI config directory")
95+
parser.add_argument("--repo-dir", help="Path to openshift/release checkout")
96+
parser.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
97+
args = parser.parse_args(argv)
98+
99+
root, is_remote = resolve_repo_root(args.repo_dir)
100+
101+
# Print configured versions if test pattern provided
102+
if args.test_pattern and not args.json_output:
103+
print_configured_versions(
104+
args.config_dir, args.test_pattern, root, is_remote, args.mapt_ref
105+
)
106+
107+
# Fetch EKS docs source (primary)
108+
docs = fetch_text(EKS_DOCS_URL)
109+
if not docs:
110+
sys.exit(1)
111+
112+
versions = parse_supported_versions(docs)
113+
calendar = parse_release_calendar(docs)
114+
115+
# Cross-verify with endoflife.date
116+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
117+
eol_data = fetch_json(EOL_API_URL)
118+
eol_supported = filter_supported_eol_entries(eol_data, today) if eol_data else []
119+
120+
# JSON output
121+
if args.json_output:
122+
result = {
123+
"versions": [{"version": ver, "tier": tier} for ver, tier in versions],
124+
"calendar": [
125+
{
126+
"version": e[0],
127+
"upstream_release": e[1],
128+
"eks_release": e[2],
129+
"end_standard": e[3],
130+
"end_extended": e[4],
131+
}
132+
for e in calendar
133+
],
134+
"endoflife_date": [
135+
{
136+
"version": e["cycle"],
137+
"eol": str(e.get("eol", "N/A")),
138+
"extended_support": str(e.get("extendedSupport", "N/A")),
139+
}
140+
for e in eol_supported
141+
],
142+
}
143+
json.dump(result, sys.stdout, indent=2)
144+
print()
145+
return
146+
147+
# Human-readable output
148+
print("=== EKS Version Support (awsdocs/amazon-eks-user-guide) ===")
149+
print("Supported minor versions:")
150+
for ver, tier in versions:
151+
print(f" {ver:<8s} {tier}")
152+
153+
print()
154+
print("Release calendar:")
155+
print(
156+
f" {'VERSION':<8s} {'UPSTREAM RELEASE':<22s} {'EKS RELEASE':<22s} "
157+
f"{'END STANDARD':<22s} {'END EXTENDED':<22s}"
158+
)
159+
print(
160+
f" {'-------':<8s} {'----------------':<22s} {'-----------':<22s} "
161+
f"{'------------':<22s} {'------------':<22s}"
162+
)
163+
for entry in calendar:
164+
ver, upstream, eks_rel, end_std, end_ext = entry
165+
print(f" {ver:<8s} {upstream:<22s} {eks_rel:<22s} {end_std:<22s} {end_ext:<22s}")
166+
167+
print()
168+
print("=== Cross-verify (endoflife.date) ===")
169+
if not eol_data:
170+
print("WARNING: Failed to fetch endoflife.date", file=sys.stderr)
171+
return
172+
for entry in eol_supported:
173+
ext = entry.get("extendedSupport", "N/A")
174+
print(f" {entry['cycle']}\tEOL: {entry.get('eol', 'N/A')}\tExtended: {ext}")
175+
176+
177+
if __name__ == "__main__":
178+
main()

0 commit comments

Comments
 (0)