Skip to content
Draft
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
369 changes: 369 additions & 0 deletions hack/release-automater/release_overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
import argparse
import sys
import tkinter as tk
from tkinter import messagebox
import requests
from datetime import datetime, date
from dateutil.parser import parse
import re
import colorsys

from typing import List, Dict

REPO_ID = "5fb813954070f53cd79231ff"
PYXIS_API = "https://catalog.redhat.com/api/containers/v1"

def to_date(value):
if not value or value == "N/A":
return None
try:
return parse(value).date()
except:
return None
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no bare except remains in this script
rg -nP '^\s*except\s*:\s*$' hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 137


🏁 Script executed:

cat -n hack/release-automater/release_overview.py | head -100

Repository: openshift/windows-machine-config-operator

Length of output: 3436


🏁 Script executed:

# Check imports and function signature around to_date()
rg -B5 -A5 'def to_date' hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 344


Replace bare except in to_date() with explicit exception handling.

Line 22 catches everything indiscriminately—including KeyboardInterrupt and SystemExit—making it impossible to distinguish parse failures from system-level errors. This violates proper error-handling standards for infrastructure automation.

Suggested fix
-from dateutil.parser import parse
+from dateutil.parser import ParserError, parse
...
-    except:
+    except (ParserError, TypeError, ValueError):
         return None
🧰 Tools
🪛 Ruff (0.15.10)

[error] 22-22: Do not use bare except

(E722)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` around lines 22 - 23, The bare
except in to_date() swallows system-level exceptions; replace it with explicit
exception handling by catching only parsing-related errors (e.g., except
(ValueError, TypeError) as e:) and return None (or log e) there, and let other
exceptions like KeyboardInterrupt and SystemExit propagate (do not catch them).
Locate the to_date() function and change the bare "except:" to an explicit tuple
of exceptions so only parse/type errors are handled.


def get_eol_date(version):
latest = None

for phase in version["phases"]:
end = to_date(phase.get("end_date"))
if end:
if not latest or end > latest:
latest = end

return latest

def get_current_phase(version, today=None):
today = today or date.today()

active = None

for phase in version["phases"]:
start_raw = phase.get("start_date")
end_raw = phase.get("end_date")

start = to_date(start_raw)
end = to_date(end_raw)

if not start and not end:
continue

if isinstance(start_raw, str) and start_raw.strip().startswith("GA of"):
if end and today <= end:
return "Full Support"
continue

if start and end:
if start <= today <= end:
return phase["name"]

if not start and end:
if today <= end:
active = phase["name"]

return active

def summarize_versions():
api_data = get_lifecycle_data()
today = date.today()
product = api_data["data"][0]
results = []
images_data = get_grade_data()
grade_data = extract_grade_data(images_data.get('data', images_data))

for version in product["versions"]:
current_phase = get_current_phase(version, today)
eol_date = get_eol_date(version)
wmco_z_stream_version = "unknown"
latest_grade = "unknown"
try:
minor_version = version["name"].split('.')[1]
except:
continue

try:
grade_info_list = grade_data[f"10.{minor_version}"]
most_recent = grade_info_list[0] # First element is most recent z-stream

wmco_z_stream_version = most_recent['version']

grades = most_recent.get('freshness_grades', [])
if grades:
latest_grade = grades[0].get('grade') if isinstance(grades[0], dict) else grades[0]

except:
continue
Comment on lines +79 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "release_overview.py" -type f

Repository: openshift/windows-machine-config-operator

Length of output: 129


🏁 Script executed:

cat -n hack/release-automater/release_overview.py | head -120 | tail -60

Repository: openshift/windows-machine-config-operator

Length of output: 2266


🏁 Script executed:

head -30 hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 723


🏁 Script executed:

cat -n hack/release-automater/release_overview.py | sed -n '50,120p'

Repository: openshift/windows-machine-config-operator

Length of output: 2662


Include versions with partial or missing data instead of silently dropping them.

Lines 82 and 95 use bare except: continue statements that omit entire versions from the report when parsing fails or grade data is unavailable. Since this script builds a release overview, every version should appear in the output—with defaults ("unknown") for missing fields. This preserves visibility into the complete release landscape and prevents gaps from going unnoticed.

Replace the try-except blocks with safer lookups using .get() and graceful fallbacks:

Suggested fix
-        try:
-            minor_version = version["name"].split('.')[1]
-        except:
-            continue
+        parts = version.get("name", "").split(".")
+        minor_version = parts[1] if len(parts) > 1 else None

-        try:
-            grade_info_list = grade_data[f"10.{minor_version}"]
+        grade_info_list = grade_data.get(f"10.{minor_version}", []) if minor_version else []
+        if grade_info_list:
             most_recent = grade_info_list[0]  # First element is most recent z-stream
             wmco_z_stream_version =  most_recent['version']
             grades = most_recent.get('freshness_grades', [])
             if grades:
                 latest_grade = grades[0].get('grade') if isinstance(grades[0], dict) else grades[0]
-
-        except:
-            continue
🧰 Tools
🪛 Ruff (0.15.10)

[error] 81-81: Do not use bare except

(E722)


[error] 81-82: try-except-continue detected, consider logging the exception

(S112)


[error] 94-94: Do not use bare except

(E722)


[error] 94-95: try-except-continue detected, consider logging the exception

(S112)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` around lines 79 - 95, The code
currently drops entire versions when parsing fails due to bare excepts around
computing minor_version and fetching grade info; update the logic in the loop
that reads version["name"] and accesses grade_data so it never continues on
exceptions but instead sets safe defaults (e.g., minor_version = "unknown",
wmco_z_stream_version = "unknown", latest_grade = "unknown"). Replace the first
try-except around minor_version with a guarded parse using version.get("name")
and split only if present, falling back to "unknown"; replace the second
try-except that indexes grade_data with a safe lookup using
grade_data.get(f"10.{minor_version}", []) and then handle an empty list by
leaving wmco_z_stream_version and latest_grade as "unknown". Ensure the code
still appends/records the version entry even when fields are unknown so no
versions are omitted.


results.append({
"version": version["name"],
"current_phase": current_phase,
"end_of_life": eol_date,
"wmco_z_stream_version": wmco_z_stream_version,
"latest_grade": latest_grade
})

return results

def get_lifecycle_data():
url = "https://access.redhat.com/product-life-cycles/api/v1/products"
params = {
"name": "Openshift Container Platform"
}
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "application/json"
}

res = requests.get(url, headers=headers, params=params)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all requests/session GET call sites for timeout coverage
rg -nP '(requests\.get|session\.get)\(' hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 219


Add explicit timeouts to all outbound HTTP calls to prevent automation hangs.

Lines 117, 128, and 141 make HTTP requests without timeout parameters. Network calls that hang indefinitely will stall the release automation pipeline—this is a critical error handling gap.

All three call sites need explicit timeout configuration. A 15-second timeout is reasonable for catalog API interactions.

Suggested fix
 REPO_ID = "5fb813954070f53cd79231ff"
 PYXIS_API = "https://catalog.redhat.com/api/containers/v1"
+HTTP_TIMEOUT_SECONDS = 15
...
-    res = requests.get(url, headers=headers, params=params)
+    res = requests.get(url, headers=headers, params=params, timeout=HTTP_TIMEOUT_SECONDS)
...
-    resp = session.get(url)
+    resp = session.get(url, timeout=HTTP_TIMEOUT_SECONDS)
...
-    resp = session.get(images_url)
+    resp = session.get(images_url, timeout=HTTP_TIMEOUT_SECONDS)
🧰 Tools
🪛 Ruff (0.15.10)

[error] 117-117: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` at line 117, The requests.get
calls in release_overview.py (the statement res = requests.get(url,
headers=headers, params=params) and the two other outbound HTTP calls in this
file) are missing timeouts; update each call site to pass an explicit timeout=15
parameter (e.g., requests.get(..., timeout=15)) and ensure any surrounding
try/except logic will handle requests.exceptions.Timeout/RequestException
consistently (log and fail fast) so the release automation cannot hang
indefinitely.

res.raise_for_status()

data = res.json()
return data


def get_grade_data():
session = requests.Session()

url = f"{PYXIS_API}/repositories/id/{REPO_ID}"
resp = session.get(url)
resp.raise_for_status()
repo_data = resp.json()

if '_links' in repo_data and 'images' in repo_data['_links']:
images_path = repo_data['_links']['images']['href']
images_url = f"https://catalog.redhat.com/api/containers{images_path}"
else:
# Fallback to constructing from registry/repository
registry = repo_data.get('registry', 'registry.access.redhat.com')
repository = repo_data.get('repository', '')
images_url = f"{PYXIS_API}/repositories/registry/{registry}/repository/{repository}/images"

resp = session.get(images_url)
resp.raise_for_status()

return resp.json()

def extract_grade_data(images_data: List[Dict]) -> Dict[str, List[Dict]]:
grade_data = {}
version_pattern = re.compile(r'^v(\d+\.\d+\.\d+)$')

for image in images_data:
tags = []
repositories = image.get('repositories', [])
for repo in repositories:
tags.extend(repo.get('tags', []))

full_version = None
for tag in tags:
tag_name = tag.get('name') if isinstance(tag, dict) else tag
match = version_pattern.match(tag_name)
if match:
full_version = match.group(1)
break

if not full_version:
continue

parts = full_version.split('.')
minor_version = f"{parts[0]}.{parts[1]}"

grade_info = {
'_id': image.get('_id'),
'docker_image_digest': image.get('docker_image_digest'),
'version': full_version,
'tags': [t.get('name') if isinstance(t, dict) else t for t in tags],
'freshness_grades': image.get('freshness_grades', []),
'repositories': image.get('repositories', []),
'parsed_data': {
'architecture': image.get('parsed_data', {}).get('architecture'),
'layers': len(image.get('parsed_data', {}).get('layers', [])),
},
'sum_layer_size_bytes': image.get('sum_layer_size_bytes'),
'vulnerabilities': image.get('vulnerabilities', {}),
}

if minor_version not in grade_data:
grade_data[minor_version] = []
grade_data[minor_version].append(grade_info)

for minor_version in grade_data:
grade_data[minor_version].sort(
key=lambda x: tuple(map(int, x['version'].split('.'))),
reverse=True
)

return grade_data

def release_version(version):
print(version)

def main():
parser = argparse.ArgumentParser(description="WMCO Release Status Dashboard")
parser.add_argument(
"-g", "--gui",
action="store_true",
help="Run using GUI"
)

args = parser.parse_args()

if args.gui:
run_gui()
else:
return run_cli()
return 0
Comment on lines +210 to +214
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
fd -t f release_overview.py hack/

Repository: openshift/windows-machine-config-operator

Length of output: 127


🏁 Script executed:

# Once found, examine the relevant section and surrounding context
cat -n hack/release-automater/release_overview.py | head -250 | tail -100

Repository: openshift/windows-machine-config-operator

Length of output: 3572


🏁 Script executed:

find . -name "release_overview.py" -type f 2>/dev/null

Repository: openshift/windows-machine-config-operator

Length of output: 129


🏁 Script executed:

# Check imports and see what modules are used
head -50 hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 1165


🏁 Script executed:

# Check the summarize_versions function and other functions that might make API calls
sed -n '1,150p' hack/release-automater/release_overview.py | head -100

Repository: openshift/windows-machine-config-operator

Length of output: 2736


🏁 Script executed:

# Look for any requests or API calls in the file
rg "requests\.|http\.|api|API" hack/release-automater/release_overview.py -n -A 2

Repository: openshift/windows-machine-config-operator

Length of output: 1071


🏁 Script executed:

# Look at get_lifecycle_data and get_grade_data functions to see if they handle exceptions
sed -n '100,145p' hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 1432


🏁 Script executed:

# Check if there's any try-catch in run_gui and run_cli
sed -n '200,280p' hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 1895


🏁 Script executed:

# Check the complete main function and surrounding context to see if script is called with any error handling
tail -20 hack/release-automater/release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 506


🏁 Script executed:

# Check if there's any try-catch around summarize_versions calls in run_cli or run_gui
rg "try|except|raise" hack/release-automater/release_overview.py -n -B 2 -A 2

Repository: openshift/windows-machine-config-operator

Length of output: 1728


Add error handling for API failures in main().

The script makes HTTP requests to external APIs (get_lifecycle_data() and get_grade_data()) without catching failures. Network timeouts, connection errors, and HTTP error responses propagate as unhandled exceptions, producing noisy tracebacks unsuitable for automation pipelines. This violates proper error handling practices for build/release tooling.

Suggested fix
 def main():
     parser = argparse.ArgumentParser(description="WMCO Release Status Dashboard")
     parser.add_argument(
         "-g", "--gui",
         action="store_true",
         help="Run using GUI"
     )
 
     args = parser.parse_args()
    
-    if args.gui:
-        run_gui()
-    else: 
-        return run_cli()
-    return 0
+    try:
+        if args.gui:
+            run_gui()
+        else:
+            run_cli()
+        return 0
+    except requests.RequestException as exc:
+        print(f"request failed: {exc}", file=sys.stderr)
+        return 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` around lines 210 - 214, main()
currently calls get_lifecycle_data() and get_grade_data() (indirectly via
run_cli()/run_gui()) without handling network/API failures; wrap the API call
sequence in a try/except that catches requests.RequestException (or a broad
Exception as a fallback), log a clear error message including exception details,
and exit with a non‑zero status (e.g. return/raise SystemExit with code != 0) so
automation pipelines see failure; update the code paths around
run_cli()/run_gui() and the top-level return logic to ensure exceptions from
get_lifecycle_data()/get_grade_data() are caught and translated to a logged
error + nonzero exit instead of an unhandled traceback.


def run_cli():
data = summarize_versions()

headers = [
"Version",
"Phase",
"End of Life",
"WMCO Version",
"Grade"
]

rows = []
for v in data:
rows.append([
v["version"],
v["current_phase"] or "EOL",
v["end_of_life"].strftime("%Y-%m-%d"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd hack/release-automater && wc -l release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 108


🏁 Script executed:

cd hack/release-automater && sed -n '220,245p' release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 703


🏁 Script executed:

cd hack/release-automater && sed -n '295,315p' release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 662


🏁 Script executed:

cd hack/release-automater && grep -n "def get_eol_date" release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 114


🏁 Script executed:

cd hack/release-automater && sed -n '25,80p' release_overview.py

Repository: openshift/windows-machine-config-operator

Length of output: 1608


🏁 Script executed:

cd hack/release-automater && grep -n "end_of_life" release_overview.py | head -20

Repository: openshift/windows-machine-config-operator

Length of output: 236


Guard end_of_life before calling strftime().

The get_eol_date() function (line 25) can legitimately return None for incomplete lifecycle data, and this None value propagates directly into the data dictionary at line 100. Lines 232 and 306 will throw AttributeError when attempting to call .strftime() on None.

Suggested fix
-            v["end_of_life"].strftime("%Y-%m-%d"),
+            v["end_of_life"].strftime("%Y-%m-%d") if v["end_of_life"] else "N/A",
...
-            v["end_of_life"].strftime("%B %d, %Y"),
+            v["end_of_life"].strftime("%B %d, %Y") if v["end_of_life"] else "N/A",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` at line 232, The call to
v["end_of_life"].strftime(...) can raise AttributeError when get_eol_date()
returns None; update the formatting at the places that consume the data (where
v["end_of_life"] is used, e.g., the strftime calls at the spots flagged) to
guard against None (for example, check if v.get("end_of_life") is truthy before
calling strftime and emit a default like "" or "N/A" when missing). Locate the
data producer get_eol_date and the consumer code that formats v["end_of_life"]
(the two strftime usages) and add the conditional/ternary-style handling so None
values are safely serialized instead of calling strftime on them.

v["wmco_z_stream_version"],
v["latest_grade"],
])

col_widths = [
max(len(str(row[i])) for row in [headers] + rows)
for i in range(len(headers))
]

def format_row(row):
return " ".join(
str(cell).ljust(col_widths[i])
for i, cell in enumerate(row)
)

print(format_row(headers))
print(format_row(["-" * w for w in col_widths]))

for row in rows:
print(format_row(row))


def run_gui():
window = tk.Tk()
window.title("WMCO Release Status Dashboard")
window.geometry("900x400")

title = tk.Label(
window,
text="WMCO Release Status Dashboard",
font=("Arial", 16, "bold")
)
title.pack(pady=10)

status_frame = tk.Frame(window)
status_frame.pack(fill="both", expand=True)

for i in range(5):
status_frame.grid_columnconfigure(i, weight=1)
Comment on lines +270 to +271
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Configure all grid columns used by the table.

You render 6 columns (including the action column) but configure only 5. Include column 5 so the RELEASE column resizes consistently.

Suggested fix
-    for i in range(5):
+    for i in range(6):
         status_frame.grid_columnconfigure(i, weight=1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for i in range(5):
status_frame.grid_columnconfigure(i, weight=1)
for i in range(6):
status_frame.grid_columnconfigure(i, weight=1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` around lines 270 - 271, The grid
column configuration loop currently configures only five columns (for i in
range(5)) while the table renders six columns including the action/RELEASE
column; update the code that calls status_frame.grid_columnconfigure (the loop)
to include column index 5 (e.g., change range(5) to range(6) or add a call
status_frame.grid_columnconfigure(5, weight=1)) so the RELEASE/action column is
given weight and resizes consistently.


data = summarize_versions()

headers = ["Version", "Phase", "End of Life", "Current WMCO Version", "Grade", ""]

# HEADER
for col, text in enumerate(headers):
tk.Label(
status_frame,
text=text,
font=("Arial", 10, "bold"),
bg="#2b2b2b",
fg="white",
bd=0,
highlightthickness=0,
).grid(
row=0,
column=col,
sticky="nsew",
padx=0,
pady=0
)

for row, v in enumerate(data, start=1):

bg_hsl = get_grade_color(v["latest_grade"])
if v["current_phase"] is None:
bg_hsl = desaturate_hsl(bg_hsl)

bg = hsl_to_hex(bg_hsl)

values = [
v["version"],
str(v["current_phase"]),
v["end_of_life"].strftime("%B %d, %Y"),
v["wmco_z_stream_version"],
v["latest_grade"],
]

for col, value in enumerate(values):
tk.Label(
status_frame,
text=value,
bg=bg,
fg="black",
anchor="w",
bd=0,
highlightthickness=0,
padx=8,
pady=6,
).grid(
row=row,
column=col,
sticky="nsew",
padx=0,
pady=0
)
tk.Button(
status_frame,
text="RELEASE",
command=lambda vname=v["version"]: release_version(vname),
bg="#444",
fg="white",
activebackground="#666",
activeforeground="white",
bd=0,
highlightthickness=0,
cursor="hand2",
).grid(row=row, column=5, sticky="nsew", padx=0, pady=0)
Comment on lines +311 to +340
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n hack/release-automater/release_overview.py | sed -n '300,350p'

Repository: openshift/windows-machine-config-operator

Length of output: 1873


🏁 Script executed:

cat -n hack/release-automater/release_overview.py | sed -n '280,345p'

Repository: openshift/windows-machine-config-operator

Length of output: 2330


Move RELEASE button outside the inner loop—it's being created redundantly on each iteration.

The button instantiation at lines 329–340 sits inside the for col, value in enumerate(values) loop, which runs 5 times per row. This creates 5 button widgets at the same grid position (row=row, column=5), with only the final one visible. The grid manager keeps overwriting earlier instances.

Unindent the button block to place it after the label loop completes—it belongs at the outer for row, v in enumerate(data, start=1) indentation level. This gives you one button per row, properly positioned at column 5.

Suggested fix
         for col, value in enumerate(values):
             tk.Label(
                 status_frame,
@@
             ).grid(
                 row=row,
                 column=col,
                 sticky="nsew",
                 padx=0,   
                 pady=0
             )
-            tk.Button(
-                status_frame,
-                text="RELEASE",
-                command=lambda vname=v["version"]: release_version(vname),
-                bg="#444",
-                fg="white",
-                activebackground="#666",
-                activeforeground="white",
-                bd=0,
-                highlightthickness=0,
-                cursor="hand2",
-            ).grid(row=row, column=5, sticky="nsew", padx=0, pady=0)
+        tk.Button(
+            status_frame,
+            text="RELEASE",
+            command=lambda vname=v["version"]: release_version(vname),
+            bg="#444",
+            fg="white",
+            activebackground="#666",
+            activeforeground="white",
+            bd=0,
+            highlightthickness=0,
+            cursor="hand2",
+        ).grid(row=row, column=5, sticky="nsew", padx=0, pady=0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for col, value in enumerate(values):
tk.Label(
status_frame,
text=value,
bg=bg,
fg="black",
anchor="w",
bd=0,
highlightthickness=0,
padx=8,
pady=6,
).grid(
row=row,
column=col,
sticky="nsew",
padx=0,
pady=0
)
tk.Button(
status_frame,
text="RELEASE",
command=lambda vname=v["version"]: release_version(vname),
bg="#444",
fg="white",
activebackground="#666",
activeforeground="white",
bd=0,
highlightthickness=0,
cursor="hand2",
).grid(row=row, column=5, sticky="nsew", padx=0, pady=0)
for col, value in enumerate(values):
tk.Label(
status_frame,
text=value,
bg=bg,
fg="black",
anchor="w",
bd=0,
highlightthickness=0,
padx=8,
pady=6,
).grid(
row=row,
column=col,
sticky="nsew",
padx=0,
pady=0
)
tk.Button(
status_frame,
text="RELEASE",
command=lambda vname=v["version"]: release_version(vname),
bg="#444",
fg="white",
activebackground="#666",
activeforeground="white",
bd=0,
highlightthickness=0,
cursor="hand2",
).grid(row=row, column=5, sticky="nsew", padx=0, pady=0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/release-automater/release_overview.py` around lines 311 - 340, The
RELEASE tk.Button is being created inside the inner loop that iterates over
values (for col, value in enumerate(values)), causing five redundant buttons per
row; move the tk.Button creation (the block that calls tk.Button(...,
command=lambda vname=v["version"]: release_version(vname), ...).grid(...)) out
of that inner loop so it executes once after the label loop completes for each
row (i.e., unindent it to the same level as the for col, value in
enumerate(values) loop body), keeping status_frame, release_version, and
v["version"] usage unchanged.


window.mainloop()

def get_grade_color(grade: str):
return {
"A": (95, 0.35, 0.55),
"B": (45, 0.55, 0.65),
"C": (25, 0.75, 0.60),
"D": (10, 0.70, 0.55),
"F": (10, 0.70, 0.50),
}.get(grade, (0, 0, 1))

def desaturate_hsl(hsl, factor=0.5, darken=0.85):
h, s, l = hsl
return (h, s * factor, l * darken)

def hsl_to_hex(hsl):
h, s, l = hsl
r, g, b = colorsys.hls_to_rgb(h / 360.0, l, s)
return "#{:02x}{:02x}{:02x}".format(
int(r * 255),
int(g * 255),
int(b * 255),
)



if __name__ == "__main__":
sys.exit(main())