Skip to content

Commit 9d02e36

Browse files
Merge pull request #311 from espressif/ci/release_checker
ci: Add release checker
2 parents a0d4ed3 + 869bfab commit 9d02e36

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed

.github/ci/release_checker.py

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

0 commit comments

Comments
 (0)