Repository navigation
Add analyze-payload command and improve CI skills #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
5f0867b
3649afe
9821ca0
4bb8761
8f4e2c0
7aaa3e0
3250439
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -503,7 +503,7 @@ | |
| "name": "Triage Regression" | ||
| } | ||
| ], | ||
| "version": "0.0.11" | ||
| "version": "0.0.12" | ||
| }, | ||
| { | ||
| "commands": [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,69 +1,150 @@ | ||
| #!/usr/bin/env python3 | ||
| """Fetch new pull requests included in an OpenShift payload that were not in the previous one. | ||
|
|
||
| Uses the Sippy payload diff API to retrieve PRs that are new in a given payload tag. | ||
| Uses the Sippy payload diff API to retrieve PRs that are new in a given payload tag, | ||
| falling back to the release controller API when Sippy has not yet ingested the payload. | ||
| """ | ||
|
|
||
| import argparse | ||
| import json | ||
| import re | ||
| import sys | ||
| import urllib.error | ||
| import urllib.parse | ||
| import urllib.request | ||
| from typing import Dict, List | ||
| from typing import Dict, List, Optional | ||
|
|
||
| SIPPY_API_BASE = "https://sippy.dptools.openshift.org/api" | ||
| RELEASE_CONTROLLER_BASE = "https://amd64.ocp.releases.ci.openshift.org/api/v1" | ||
|
|
||
|
|
||
| def fetch_new_prs(payload_tag: str) -> list: | ||
| """Fetch new PRs in the given payload tag compared to its predecessor.""" | ||
| url = f"{SIPPY_API_BASE}/payloads/diff?toPayload={urllib.parse.quote(payload_tag)}" | ||
| def _http_get_json(url: str, timeout: int = 30) -> dict: | ||
| """Fetch JSON from a URL. Raises on HTTP or connection errors.""" | ||
| req = urllib.request.Request(url, headers={"Accept": "application/json"}) | ||
| with urllib.request.urlopen(req, timeout=timeout) as resp: | ||
| return json.loads(resp.read().decode("utf-8")) | ||
|
|
||
|
|
||
| def _parse_stream(payload_tag: str) -> Optional[str]: | ||
| """Extract the release stream from a payload tag. | ||
|
|
||
| Examples: | ||
| 4.21.0-0.ci-2026-02-27-125225 -> 4.21.0-0.ci | ||
| 4.22.0-0.nightly-2026-01-15-114134 -> 4.22.0-0.nightly | ||
| """ | ||
| m = re.match(r"^(.+)-\d{4}-\d{2}-\d{2}-\d{6}$", payload_tag) | ||
| return m.group(1) if m else None | ||
|
|
||
|
|
||
| def _find_previous_tag(stream: str, payload_tag: str) -> Optional[str]: | ||
| """Find the payload immediately before payload_tag in the stream.""" | ||
| url = f"{RELEASE_CONTROLLER_BASE}/releasestream/{urllib.parse.quote(stream)}/tags" | ||
| data = _http_get_json(url) | ||
| tags = [t["name"] for t in data.get("tags", [])] | ||
| try: | ||
| req = urllib.request.Request(url) | ||
| with urllib.request.urlopen(req, timeout=30) as resp: | ||
| data = json.loads(resp.read().decode("utf-8")) | ||
| except urllib.error.HTTPError as e: | ||
| if e.code == 404: | ||
| print( | ||
| f"Error: Payload '{payload_tag}' not found.", | ||
| file=sys.stderr, | ||
| ) | ||
| print( | ||
| "Verify the payload tag exists (e.g., 4.22.0-0.ci-2026-02-06-195709).", | ||
| file=sys.stderr, | ||
| ) | ||
| sys.exit(1) | ||
| else: | ||
| print(f"Error: HTTP {e.code} from Sippy API: {e.reason}", file=sys.stderr) | ||
| sys.exit(1) | ||
| except urllib.error.URLError as e: | ||
| print(f"Error: Failed to connect to Sippy API: {e.reason}", file=sys.stderr) | ||
| print("Check network connectivity.", file=sys.stderr) | ||
| sys.exit(1) | ||
| except Exception as e: | ||
| print(f"Error: {e}", file=sys.stderr) | ||
| sys.exit(1) | ||
| idx = tags.index(payload_tag) | ||
| except ValueError: | ||
| return None | ||
| if idx + 1 >= len(tags): | ||
| return None | ||
| return tags[idx + 1] | ||
|
|
||
|
|
||
| def fetch_from_sippy(payload_tag: str) -> Optional[list]: | ||
| """Try fetching PR diff from Sippy. Returns None if the payload is not available.""" | ||
| url = f"{SIPPY_API_BASE}/payloads/diff?toPayload={urllib.parse.quote(payload_tag)}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct? I think it needs
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like https://sippy.dptools.openshift.org/api/payloads/diff?toPayload=4.22.0-0.nightly-2026-01-06-110407 returns the same results
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I guess so, that's existing code here. I only added fallback to the release controller, the diff is indentation changes |
||
| try: | ||
| data = _http_get_json(url) | ||
| except urllib.error.HTTPError: | ||
| return None | ||
| except urllib.error.URLError: | ||
| return None | ||
|
|
||
| if not isinstance(data, list): | ||
| print("Error: Unexpected API response format (expected a JSON array).", file=sys.stderr) | ||
| sys.exit(1) | ||
| return None | ||
|
|
||
| # Transform the response into a cleaner structure | ||
| prs = [] | ||
| for entry in data: | ||
| pr = { | ||
| prs.append({ | ||
| "url": entry.get("url", ""), | ||
| "pull_request_id": entry.get("pull_request_id", ""), | ||
| "component": entry.get("name", ""), | ||
| "description": entry.get("description", ""), | ||
| "bug_url": entry.get("bug_url", ""), | ||
| } | ||
| prs.append(pr) | ||
| }) | ||
| return prs | ||
|
|
||
|
|
||
| def fetch_from_release_controller(payload_tag: str) -> list: | ||
| """Fetch PR diff from the release controller API.""" | ||
| stream = _parse_stream(payload_tag) | ||
| if not stream: | ||
| print( | ||
| f"Error: Cannot parse release stream from tag '{payload_tag}'.", | ||
| file=sys.stderr, | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| prev_tag = _find_previous_tag(stream, payload_tag) | ||
| if not prev_tag: | ||
| print( | ||
| f"Error: Cannot find previous payload for '{payload_tag}' in stream '{stream}'.", | ||
| file=sys.stderr, | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| url = ( | ||
| f"{RELEASE_CONTROLLER_BASE}/releasestream/{urllib.parse.quote(stream)}" | ||
| f"/release/{urllib.parse.quote(payload_tag)}" | ||
| f"?from={urllib.parse.quote(prev_tag)}" | ||
| ) | ||
|
|
||
| try: | ||
| data = _http_get_json(url, timeout=60) | ||
| except urllib.error.HTTPError as e: | ||
| print(f"Error: HTTP {e.code} from release controller: {e.reason}", file=sys.stderr) | ||
| sys.exit(1) | ||
| except urllib.error.URLError as e: | ||
| print(f"Error: Failed to connect to release controller: {e.reason}", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| changelog = data.get("changeLogJson", {}) | ||
| updated_images = changelog.get("updatedImages", []) or [] | ||
|
|
||
| prs = [] | ||
| for image in updated_images: | ||
| component = image.get("name", "") | ||
| for commit in image.get("commits", []): | ||
| pull_url = commit.get("pullURL", "") | ||
| pull_id = str(commit.get("pullID", "")) | ||
| description = commit.get("subject", "") | ||
| issues = commit.get("issues", {}) or {} | ||
| bug_url = next(iter(issues.values()), "") | ||
| prs.append({ | ||
|
stbenjam marked this conversation as resolved.
Resolved
|
||
| "url": pull_url, | ||
| "pull_request_id": pull_id, | ||
| "component": component, | ||
| "description": description, | ||
| "bug_url": bug_url, | ||
| }) | ||
|
|
||
| return prs | ||
|
|
||
|
|
||
| def fetch_new_prs(payload_tag: str) -> list: | ||
| """Fetch new PRs, trying Sippy first then falling back to the release controller.""" | ||
| prs = fetch_from_sippy(payload_tag) | ||
| if prs is not None: | ||
| return prs | ||
|
|
||
| print( | ||
| f"Sippy does not have payload '{payload_tag}', " | ||
| "falling back to release controller.", | ||
| file=sys.stderr, | ||
|
stbenjam marked this conversation as resolved.
Resolved
|
||
| ) | ||
| return fetch_from_release_controller(payload_tag) | ||
|
|
||
|
|
||
| def format_summary(prs: list, payload_tag: str) -> str: | ||
| """Format PRs as a human-readable summary.""" | ||
| lines = [] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.