Skip to content

Commit 2afc850

Browse files
t ci: Add release checker
1 parent 4c7f3ef commit 2afc850

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed

.github/ci/release_checker.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# flake8: noqa
5+
6+
import os
7+
import yaml
8+
import subprocess
9+
from tabulate import tabulate
10+
import re
11+
from datetime import datetime
12+
import wcwidth
13+
from pathlib import Path
14+
import pytz
15+
16+
repo_path = Path(".")
17+
18+
target_dirs = [
19+
os.path.join(repo_path, "device"),
20+
os.path.join(repo_path, "host"),
21+
]
22+
23+
deprecated = []
24+
25+
priority_order = {
26+
"⛔ Yes": 0,
27+
"⚠️ MD ": 1,
28+
"✔️ No ": 2
29+
}
30+
31+
results = []
32+
33+
release_commits = {}
34+
component_paths = {}
35+
36+
37+
def run_git_command(args, cwd):
38+
result = subprocess.run(["git"] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
39+
return result.stdout.strip()
40+
41+
42+
for base_dir in target_dirs:
43+
if os.path.exists(base_dir):
44+
for root, dirs, files in os.walk(base_dir):
45+
if "idf_component.yml" in files:
46+
yml_path = os.path.join(root, "idf_component.yml")
47+
component_name = os.path.basename(root)
48+
version = "N/A"
49+
release_date = "?"
50+
changes_since_version = "N/A"
51+
commit_count = "?"
52+
53+
if component_name in deprecated:
54+
continue
55+
56+
try:
57+
with open(yml_path, "r") as f:
58+
yml_data = yaml.safe_load(f)
59+
version = yml_data.get("version", "N/A")
60+
except Exception as e:
61+
print(f"Chyba: {e}")
62+
63+
if version != "N/A":
64+
try:
65+
rel_yml_path = os.path.relpath(yml_path, repo_path).replace("\\", "/")
66+
log_output = run_git_command(["log", "-p", "--", rel_yml_path], cwd=repo_path)
67+
68+
current_commit = None
69+
current_date = None
70+
old_version = None
71+
new_version = None
72+
commit_hash = None
73+
74+
lines = log_output.splitlines()
75+
for i, line in enumerate(lines):
76+
if line.startswith("commit "):
77+
current_commit = line.split()[1]
78+
old_version = None
79+
new_version = None
80+
elif line.startswith("Date:"):
81+
raw_date = line.replace("Date:", "").strip()
82+
try:
83+
dt = datetime.strptime(raw_date, "%a %b %d %H:%M:%S %Y %z")
84+
current_date = dt.strftime("%d.%m.%Y")
85+
except Exception as e:
86+
print(f"Chyba: {e}")
87+
current_date = raw_date
88+
elif line.startswith("-version:") and not line.startswith(" "):
89+
match = re.match(r"-version:\s*['\"]?([\w\.\-~]+)['\"]?", line)
90+
if match:
91+
old_version = match.group(1)
92+
elif line.startswith("+version:") and not line.startswith(" "):
93+
match = re.match(r"\+version:\s*['\"]?([\w\.\-~]+)['\"]?", line)
94+
if match:
95+
new_version = match.group(1)
96+
97+
if old_version and new_version and old_version != new_version:
98+
commit_hash = current_commit
99+
release_date = current_date
100+
break
101+
102+
if not commit_hash:
103+
first_commit = run_git_command(["log", "--diff-filter=A", "--format=%H %aD", "--", rel_yml_path], cwd=repo_path)
104+
if first_commit:
105+
parts = first_commit.split()
106+
commit_hash = parts[0]
107+
try:
108+
dt = datetime.strptime(" ".join(parts[1:]), "%a, %d %b %Y %H:%M:%S %z")
109+
release_date = dt.strftime("%d.%m.%Y")
110+
except Exception as e:
111+
print(f"Chyba: {e}")
112+
release_date = "?"
113+
114+
if commit_hash:
115+
rel_component_path = os.path.relpath(root, repo_path).replace("\\", "/")
116+
117+
# Save
118+
release_commits[component_name] = commit_hash
119+
component_paths[component_name] = rel_component_path
120+
121+
diff_output = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_component_path], cwd=repo_path)
122+
123+
extensions = {}
124+
changed_paths = diff_output.splitlines()
125+
if diff_output:
126+
extensions = {os.path.splitext(path)[1] for path in changed_paths}
127+
128+
if extensions == {'.md'}:
129+
changes_since_version = "⚠️ MarkDown "
130+
elif changed_paths and all("test_apps/" in path for path in changed_paths):
131+
changes_since_version = "🧪 Test only"
132+
elif extensions:
133+
changes_since_version = "⛔ Yes"
134+
else:
135+
changes_since_version = "✔️ No "
136+
137+
# count_output = run_git_command(["rev-list", f"{commit_hash}..HEAD", "--count", rel_component_path], cwd=repo_path)
138+
commit_count = len(changed_paths) if changed_paths else "?"
139+
140+
except Exception as e:
141+
print(f"Chyba: {e}")
142+
143+
if release_date != "?":
144+
extension_str = ", ".join(sorted(extensions)) if extensions else " "
145+
results.append([component_name, version, release_date, changes_since_version + f" ({commit_count})", extension_str])
146+
147+
148+
def show_diff_for_component(component_name):
149+
commit_hash = release_commits.get(component_name)
150+
rel_path = component_paths.get(component_name)
151+
152+
if not commit_hash or not rel_path:
153+
print("Commit path not found.")
154+
return
155+
156+
# List of changed files
157+
changed_files = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_path], cwd=repo_path)
158+
changed_files = [f for f in changed_files.splitlines() if not f.endswith(".md")]
159+
160+
if not changed_files:
161+
print("No changes except *.md files.")
162+
return
163+
164+
print(f"Changes for component '{component_name}' from last release (except *.md files):\n")
165+
subprocess.run(["git", "diff", "--color=always", f"{commit_hash}..HEAD", "--"] + changed_files, cwd=repo_path)
166+
167+
168+
# Calculate width
169+
def visual_width(text):
170+
return sum(wcwidth.wcwidth(c) for c in text)
171+
172+
173+
# Text align
174+
def pad_visual(text, target_width):
175+
current_width = visual_width(text)
176+
padding = max(0, target_width - current_width)
177+
return text + " " * padding
178+
179+
180+
def get_change_key(row):
181+
change = row[3].strip()
182+
extensions = row[4].split(", ")
183+
has_code_change = any(ext in ['.c', '.h'] for ext in extensions)
184+
185+
if change.startswith("⛔") and has_code_change:
186+
return 0
187+
elif change.startswith("⛔"):
188+
return 1
189+
elif change.startswith("🧪"):
190+
return 2
191+
elif change.startswith("⚠️"):
192+
return 3
193+
elif change.startswith("✔️"):
194+
return 4
195+
return 99
196+
197+
198+
# Sort by priority
199+
results.sort(key=get_change_key)
200+
201+
# Column align
202+
for row in results:
203+
row[3] = pad_visual(row[3], 8)
204+
205+
# Table header
206+
headers = ["Component", "Version", "Released", "Changed", "File Types"]
207+
208+
tz = pytz.timezone("Europe/Prague")
209+
last_updated = datetime.now(tz).strftime("%d.%m.%Y %H:%M:%S %Z")
210+
if os.getenv("CI") != "true":
211+
markdown_table = tabulate(results, headers=headers, tablefmt="github")
212+
print("# Component/BSP release version checker")
213+
print("This page shows all components in the BSP repository with their latest versions and indicates whether any changes have not yet been released.")
214+
else:
215+
markdown_table = tabulate(results, headers=headers, tablefmt="html")
216+
deprecated_str = ", ".join(deprecated)
217+
print("<html><head>")
218+
print("<title>Component/BSP release version checker</title>")
219+
print(f"""<style>
220+
body {{ font-family: sans-serif; padding: 2em; }}
221+
table {{ border-collapse: collapse; width: 100%; }}
222+
th, td {{ border: 1px solid #ccc; padding: 0.5em; text-align: left; }}
223+
th {{ background-color: #f0f0f0; }}
224+
td:nth-child(4) {{ text-align: center; }}
225+
td:nth-child(5) {{ font-style: italic; color: #888; }}
226+
</style>""")
227+
print("</head><body>")
228+
print("<h1>Component/BSP release version checker</h1>")
229+
print(f"<p>Last updated: {last_updated}</p>")
230+
print("<p>This page shows all components in the BSP repository with their latest versions and indicates whether any changes have not yet been released.</p>")
231+
print(f"<p>Deprecated components: {deprecated_str}</p>")
232+
print("</body></html>")
233+
234+
print(markdown_table)
235+
236+
if os.getenv("CI") != "true":
237+
while True:
238+
component_name = input("Input the component name for diff (or type 'exit' to quit): ")
239+
if component_name.lower() == 'exit':
240+
break
241+
show_diff_for_component(component_name)

.github/workflows/gh-pages.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Deploy Release Checker results to GitHub Pages
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize]
6+
# Allows you to run this workflow manually from the Actions tab
7+
workflow_dispatch:
8+
9+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
16+
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
17+
concurrency:
18+
group: "pages"
19+
cancel-in-progress: false
20+
21+
jobs:
22+
release-check:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout repo
26+
uses: actions/checkout@v5
27+
with:
28+
fetch-depth: 0 # all git history
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@v6
32+
with:
33+
python-version: '3.11'
34+
35+
- name: Run release_checker.py
36+
run: |
37+
pip install requests pyyaml tabulate wcwidth pytz
38+
mkdir pages
39+
python .github/ci/release_checker.py > pages/release_checker.html
40+
41+
- name: Copy USB certification results
42+
run: |
43+
cp -r docs/device/usbcv_results/* pages/
44+
45+
- name: Upload to GitHub Pages
46+
uses: actions/upload-pages-artifact@v4
47+
with:
48+
path: pages/
49+
50+
# Deployment job
51+
deploy:
52+
environment:
53+
name: github-pages
54+
url: ${{ steps.deployment.outputs.page_url }}
55+
runs-on: ubuntu-latest
56+
needs: release-check
57+
steps:
58+
- name: Deploy to GitHub Pages
59+
id: deployment
60+
uses: actions/deploy-pages@v4

0 commit comments

Comments
 (0)