Skip to content

Commit 0ca20aa

Browse files
committed
ci(release_checker): Script for check changes after last release for all components and BSPs
1 parent 836697f commit 0ca20aa

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

.github/ci/release_checker.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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+
15+
repo_path = Path(".")
16+
17+
target_dirs = [
18+
os.path.join(repo_path, "bsp"),
19+
os.path.join(repo_path, "components"),
20+
]
21+
22+
deprecated = [
23+
"esp-box",
24+
"esp-box-lite",
25+
"esp32_azure_iot_kit",
26+
"esp32_s2_kaluga_kit",
27+
"hts221",
28+
29+
]
30+
31+
priority_order = {
32+
"⛔ Yes": 0,
33+
"⚠️ MD ": 1,
34+
"✔️ No ": 2
35+
}
36+
37+
results = []
38+
39+
release_commits = {}
40+
component_paths = {}
41+
42+
43+
def run_git_command(args, cwd):
44+
result = subprocess.run(["git"] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
45+
return result.stdout.strip()
46+
47+
48+
for base_dir in target_dirs:
49+
if os.path.exists(base_dir):
50+
for root, dirs, files in os.walk(base_dir):
51+
if "idf_component.yml" in files:
52+
yml_path = os.path.join(root, "idf_component.yml")
53+
component_name = os.path.basename(root)
54+
version = "N/A"
55+
release_date = "?"
56+
changes_since_version = "N/A"
57+
commit_count = "?"
58+
59+
if component_name in deprecated:
60+
continue
61+
62+
try:
63+
with open(yml_path, "r") as f:
64+
yml_data = yaml.safe_load(f)
65+
version = yml_data.get("version", "N/A")
66+
except Exception as e:
67+
print(f"Chyba: {e}")
68+
69+
if version != "N/A":
70+
try:
71+
rel_yml_path = os.path.relpath(yml_path, repo_path).replace("\\", "/")
72+
log_output = run_git_command(["log", "-p", "--", rel_yml_path], cwd=repo_path)
73+
74+
current_commit = None
75+
current_date = None
76+
old_version = None
77+
new_version = None
78+
commit_hash = None
79+
80+
lines = log_output.splitlines()
81+
for i, line in enumerate(lines):
82+
if line.startswith("commit "):
83+
current_commit = line.split()[1]
84+
old_version = None
85+
new_version = None
86+
elif line.startswith("Date:"):
87+
raw_date = line.replace("Date:", "").strip()
88+
try:
89+
dt = datetime.strptime(raw_date, "%a %b %d %H:%M:%S %Y %z")
90+
current_date = dt.strftime("%d.%m.%Y")
91+
except Exception as e:
92+
print(f"Chyba: {e}")
93+
current_date = raw_date
94+
elif line.startswith("-version:") and not line.startswith(" "):
95+
match = re.match(r"-version:\s*['\"]?([\w\.\-~]+)['\"]?", line)
96+
if match:
97+
old_version = match.group(1)
98+
elif line.startswith("+version:") and not line.startswith(" "):
99+
match = re.match(r"\+version:\s*['\"]?([\w\.\-~]+)['\"]?", line)
100+
if match:
101+
new_version = match.group(1)
102+
103+
if old_version and new_version and old_version != new_version:
104+
commit_hash = current_commit
105+
release_date = current_date
106+
break
107+
108+
if not commit_hash:
109+
first_commit = run_git_command(["log", "--diff-filter=A", "--format=%H %aD", "--", rel_yml_path], cwd=repo_path)
110+
if first_commit:
111+
parts = first_commit.split()
112+
commit_hash = parts[0]
113+
try:
114+
dt = datetime.strptime(" ".join(parts[1:]), "%a, %d %b %Y %H:%M:%S %z")
115+
release_date = dt.strftime("%d.%m.%Y")
116+
except Exception as e:
117+
print(f"Chyba: {e}")
118+
release_date = "?"
119+
120+
if commit_hash:
121+
rel_component_path = os.path.relpath(root, repo_path).replace("\\", "/")
122+
123+
# Save
124+
release_commits[component_name] = commit_hash
125+
component_paths[component_name] = rel_component_path
126+
127+
diff_output = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_component_path], cwd=repo_path)
128+
129+
extensions = {}
130+
if diff_output:
131+
extensions = {os.path.splitext(path)[1] for path in diff_output.splitlines()}
132+
133+
if extensions == {'.md'}:
134+
changes_since_version = "⚠️ MD "
135+
elif extensions:
136+
changes_since_version = "⛔ Yes"
137+
else:
138+
changes_since_version = "✔️ No "
139+
140+
count_output = run_git_command(["rev-list", f"{commit_hash}..HEAD", "--count", rel_component_path], cwd=repo_path)
141+
commit_count = count_output if count_output.isdigit() else "?"
142+
143+
except Exception as e:
144+
print(f"Chyba: {e}")
145+
146+
if release_date != "?":
147+
extension_str = ", ".join(sorted(extensions)) if extensions else " "
148+
results.append([component_name, version, release_date, changes_since_version + f" ({commit_count})", extension_str])
149+
150+
151+
def show_diff_for_component(component_name):
152+
commit_hash = release_commits.get(component_name)
153+
rel_path = component_paths.get(component_name)
154+
155+
if not commit_hash or not rel_path:
156+
print("Commit path not found.")
157+
return
158+
159+
# Získání seznamu změněných souborů
160+
changed_files = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_path], cwd=repo_path)
161+
changed_files = [f for f in changed_files.splitlines() if not f.endswith(".md")]
162+
163+
if not changed_files:
164+
print("No changes except *.md files.")
165+
return
166+
167+
print(f"Changes for component '{component_name}' from last release:\n")
168+
subprocess.run(["git", "diff", "--color=always", f"{commit_hash}..HEAD", "--"] + changed_files, cwd=repo_path)
169+
170+
171+
# Funkce pro výpočet vizuální šířky textu
172+
def visual_width(text):
173+
return sum(wcwidth.wcwidth(c) for c in text)
174+
175+
176+
# Funkce pro zarovnání textu na požadovanou vizuální šířku
177+
def pad_visual(text, target_width):
178+
current_width = visual_width(text)
179+
padding = max(0, target_width - current_width)
180+
return text + " " * padding
181+
182+
183+
def get_change_key(row):
184+
change = row[3].strip()
185+
extensions = row[4].split(", ")
186+
has_code_change = any(ext in ['.c', '.h'] for ext in extensions)
187+
188+
if change.startswith("⛔") and has_code_change:
189+
return 0
190+
elif change.startswith("⛔"):
191+
return 1
192+
elif change.startswith("⚠️"):
193+
return 2
194+
elif change.startswith("✔️"):
195+
return 3
196+
return 99
197+
198+
199+
# Seřaď results podle priority
200+
results.sort(key=get_change_key)
201+
202+
# Zarovnáme sloupec „Změny“ na vizuální šířku 8 znaků
203+
for row in results:
204+
row[3] = pad_visual(row[3], 8)
205+
206+
# Výpis jako Markdown tabulka
207+
headers = ["Component", "Version", "Released", "Changed", "Files"]
208+
209+
last_updated = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
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 show all components in BSP repository with its latest versions and how many changes was not released.")
214+
else:
215+
markdown_table = tabulate(results, headers=headers, tablefmt="html")
216+
print("<html><head>")
217+
print("<title>Component/BSP release version checker</title>")
218+
print(f"""<style>
219+
body {{ font-family: sans-serif; padding: 2em; }}
220+
table {{ border-collapse: collapse; width: 100%; }}
221+
th, td {{ border: 1px solid #ccc; padding: 0.5em; text-align: left; }}
222+
th {{ background-color: #f0f0f0; }}
223+
td:nth-child(4) {{ text-align: center; }}
224+
td:nth-child(5) {{ font-style: italic; color: #888; }}
225+
</style>""")
226+
print("</head><body>")
227+
print("<h1>Component/BSP release version checker</h1>")
228+
print(f"<p>Last updated: {last_updated}</p>")
229+
print("<p>This page show all components in BSP repository with its latest versions and how many changes was not released.</p>")
230+
print("</body></html>")
231+
232+
print(markdown_table)
233+
234+
if os.getenv("CI") != "true":
235+
while True:
236+
component_name = input("Input the component name for diff (or type 'exit' to quit): ")
237+
if component_name.lower() == 'exit':
238+
break
239+
show_diff_for_component(component_name)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
name: Daily Release Check
3+
4+
on:
5+
pull_request:
6+
types: [opened, reopened, synchronize, labeled]
7+
schedule:
8+
- cron: '0 0 * * *' # every day at midnight
9+
workflow_dispatch: # can be run manually
10+
11+
12+
jobs:
13+
build-and-deploy:
14+
runs-on: ubuntu-latest
15+
16+
permissions:
17+
pages: write
18+
id-token: write
19+
20+
environment:
21+
name: github-pages
22+
url: ${{ steps.deployment.outputs.page_url }}
23+
24+
steps:
25+
- name: Checkout repo
26+
uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0 # all git history
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@v5
32+
with:
33+
python-version: '3.11'
34+
35+
- name: Run release_checker.py
36+
run: |
37+
pip install requests pyyaml tabulate wcwidth
38+
mkdir site
39+
python .github/ci/release_checker.py > site/release_checker.html
40+
41+
- name: Upload file to gh pages
42+
uses: actions/upload-pages-artifact@v3
43+
with:
44+
path: site
45+
46+
- name: Deploy to GitHub Pages
47+
id: deployment
48+
uses: actions/deploy-pages@v4

0 commit comments

Comments
 (0)