Skip to content

Commit 6274601

Browse files
committed
fix: hacs release zip file & improved release workflow
1 parent 035f0bf commit 6274601

25 files changed

Lines changed: 1390 additions & 617 deletions

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
name: Report an issue
2-
description: Report an issue with the unofficial Foodsharing Integration
2+
description: Report an issue with the Foodsharing Integration
3+
title: "[Bug]: "
34
labels: ["bug"]
5+
assignees:
6+
- FaserF
47
body:
58
- type: markdown
69
attributes:
@@ -31,15 +34,6 @@ body:
3134
Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/).
3235
3336
[![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/)
34-
- type: input
35-
id: version_integration
36-
validations:
37-
required: true
38-
attributes:
39-
label: What version of Foodsharing Integration has the issue?
40-
placeholder:
41-
description: >
42-
The version from the release page you are using. Latest version can be found here: https://github.com/FaserF/ha-foodsharing/releases/latest
4337
- type: dropdown
4438
validations:
4539
required: true
@@ -54,6 +48,15 @@ body:
5448
- Home Assistant Container
5549
- Home Assistant Supervised
5650
- Home Assistant Core
51+
- type: input
52+
id: version_integration
53+
validations:
54+
required: true
55+
attributes:
56+
label: What version of this Integration has the issue?
57+
placeholder:
58+
description: >
59+
The version from the release page you are using. Latest version can be found here: https://github.com/FaserF/ha-foodsharing/releases/latest
5760
- type: markdown
5861
attributes:
5962
value: |
@@ -74,4 +77,4 @@ body:
7477
attributes:
7578
label: Additional information
7679
description: >
77-
If you have any additional information for us, use the field below.
80+
If you have any additional information for us, use the field below.

.github/ISSUE_TEMPLATE/feature_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Feature request
2-
description: Suggest an idea for this project for the unoficial Foodsharing integration
2+
description: Suggest an idea for this integration
33
title: "[Feature Request]: "
44
labels: ["enhancement"]
55
assignees:
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import re
4+
import subprocess
5+
import json
6+
import glob
7+
from datetime import datetime
8+
9+
10+
def run_git(args):
11+
try:
12+
return (
13+
subprocess.check_output(["git"] + args, stderr=subprocess.DEVNULL)
14+
.decode("utf-8")
15+
.strip()
16+
)
17+
except subprocess.CalledProcessError:
18+
return ""
19+
20+
21+
def main():
22+
rtype = os.environ.get("RELEASE_TYPE", "beta")
23+
bump_level = os.environ.get("BUMP_LEVEL", "patch")
24+
version_override = os.environ.get("VERSION_OVERRIDE", "")
25+
repo = os.environ.get("REPO", "").lower()
26+
27+
# Determine owner and repo_name dynamically
28+
owner = "faserf"
29+
repo_name = os.path.basename(os.getcwd())
30+
if "/" in repo:
31+
owner, repo_name = repo.split("/", 1)
32+
33+
# Dynamic manifest location
34+
manifest_files = glob.glob("custom_components/*/manifest.json")
35+
if not manifest_files:
36+
print("Error: manifest.json not found!")
37+
return
38+
manifest_path = manifest_files[0]
39+
domain = os.path.basename(os.path.dirname(manifest_path))
40+
41+
with open(manifest_path, "r", encoding="utf-8") as f:
42+
manifest = json.load(f)
43+
44+
friendly_name = manifest.get("name", domain)
45+
46+
# Dynamic documentation URL logic
47+
docs_url = manifest.get("documentation")
48+
if not docs_url:
49+
if os.path.exists("docs"):
50+
docs_url = f"https://{owner}.github.io/{repo_name}/"
51+
else:
52+
if repo:
53+
docs_url = f"https://github.com/{repo}"
54+
else:
55+
docs_url = f"https://github.com/faserf/{repo_name}"
56+
57+
# Calculate version via version_manager
58+
bump_args = [
59+
"python",
60+
".github/scripts/version_manager.py",
61+
"bump",
62+
"--type",
63+
rtype,
64+
"--level",
65+
bump_level,
66+
]
67+
if version_override and version_override.strip():
68+
bump_args += ["--override", version_override.strip()]
69+
70+
version = (
71+
subprocess.check_output(bump_args)
72+
.decode("utf-8")
73+
.strip()
74+
)
75+
76+
# Revert version bump change in manifest file (since versioning job only calculates it, sync-version actually writes it)
77+
run_git(["checkout", "--", manifest_path])
78+
79+
print(f"Calculated Version: {version}")
80+
tag = f"v{version}"
81+
is_prerelease = "false" if rtype == "stable" else "true"
82+
83+
# Base version without suffixes
84+
base_version = version
85+
m = re.match(r"^(\d+\.\d+\.\d+)", version)
86+
if m:
87+
base_version = m.group(1)
88+
89+
changelog_from = ""
90+
changelog_label = "initial release — full history"
91+
92+
# Get sorted list of tags
93+
tags_raw = run_git(["tag", "-l", "[0-9]*", "v[0-9]*", "--sort=-v:refname"])
94+
tags = [t.strip() for t in tags_raw.splitlines() if t.strip()]
95+
latest_tag = ""
96+
for t in tags:
97+
if re.match(r"^v?\d+\.\d+\.\d+(?:(?:b|-dev|-nightly)\d+)?$", t):
98+
latest_tag = t
99+
break
100+
101+
if rtype == "stable":
102+
for t in tags:
103+
if re.match(r"^v?\d+\.\d+\.\d+$", t):
104+
changelog_from = t
105+
changelog_label = f"since last stable release (`{t}`)"
106+
break
107+
elif rtype == "beta":
108+
prev_beta_pat = re.compile(rf"^v?{re.escape(base_version)}(?:b|-beta)\d+$")
109+
for t in tags:
110+
if prev_beta_pat.match(t):
111+
changelog_from = t
112+
changelog_label = f"since previous beta (`{t}`)"
113+
break
114+
if not changelog_from:
115+
for t in tags:
116+
if re.match(r"^v?\d+\.\d+\.\d+$", t):
117+
changelog_from = t
118+
changelog_label = f"since last stable release (`{t}`) — first beta of {base_version}"
119+
break
120+
else:
121+
if latest_tag:
122+
changelog_from = latest_tag
123+
changelog_label = f"since `{latest_tag}`"
124+
125+
print(f"Changelog range start tag: '{changelog_from}' ({changelog_label})")
126+
127+
# Count commits
128+
total_commit_count = 0
129+
if changelog_from:
130+
count_range = f"{changelog_from}..HEAD"
131+
else:
132+
count_range = "HEAD"
133+
134+
commit_count_raw = run_git(["rev-list", "--count", count_range])
135+
if commit_count_raw.isdigit():
136+
total_commit_count = int(commit_count_raw)
137+
138+
# Generate Changelog
139+
changelog_md = ""
140+
if os.path.exists("scripts/generate_changelog.py"):
141+
try:
142+
changelog_md = (
143+
subprocess.check_output(
144+
[
145+
"python",
146+
"scripts/generate_changelog.py",
147+
"--from-tag",
148+
changelog_from,
149+
"--total-commits",
150+
str(total_commit_count),
151+
"--repo",
152+
repo,
153+
]
154+
)
155+
.decode("utf-8")
156+
.strip()
157+
)
158+
except Exception:
159+
changelog_md = (
160+
"_Changelog could not be generated automatically. See commit history._"
161+
)
162+
else:
163+
changelog_md = "_Changelog script not found._"
164+
165+
if not changelog_md:
166+
changelog_md = "_No categorised changes detected._"
167+
168+
# Channel decorations
169+
channel_badge = {
170+
"stable": "![Stable](https://img.shields.io/badge/channel-stable-brightgreen?style=flat-square)",
171+
"beta": "![Beta](https://img.shields.io/badge/channel-beta-orange?style=flat-square)",
172+
}.get(
173+
rtype,
174+
"![Nightly](https://img.shields.io/badge/channel-nightly-blue?style=flat-square)",
175+
)
176+
177+
# Analyze diff impact
178+
diff_range = f"{changelog_from}..HEAD" if changelog_from else "HEAD"
179+
changed_files_raw = run_git(["diff", "--name-only", diff_range])
180+
changed_files = [f.strip() for f in changed_files_raw.splitlines() if f.strip()]
181+
182+
total_files = len(changed_files)
183+
integration_count = 0
184+
translation_count = 0
185+
ci_count = 0
186+
docs_count = 0
187+
test_count = 0
188+
189+
for f in changed_files:
190+
if f.startswith(f"custom_components/{domain}/translations/"):
191+
translation_count += 1
192+
elif f.startswith("custom_components/"):
193+
integration_count += 1
194+
elif f.startswith("tests/"):
195+
test_count += 1
196+
elif f.startswith(".github/") or f.startswith("scripts/"):
197+
ci_count += 1
198+
elif f.startswith("docs/") or f.endswith(".md"):
199+
docs_count += 1
200+
201+
breaking_count = 0
202+
log_msgs = run_git(["log", diff_range, "--format=%B"])
203+
for msg in log_msgs.split("\n"):
204+
if re.search(r"\bBREAKING CHANGE\b|\bBREAKING:\b|^[a-zA-Z]+!:", msg):
205+
breaking_count += 1
206+
207+
# Determine Risk Severity
208+
severity = "Low"
209+
alert_type = "NOTE"
210+
preamble = "This release introduces minor updates and code improvements."
211+
212+
if breaking_count > 0:
213+
severity = "Critical"
214+
alert_type = "CAUTION"
215+
preamble = f"This release contains **{breaking_count} breaking change(s)**! Please review the changelog carefully before updating."
216+
elif integration_count > 8:
217+
severity = "High"
218+
alert_type = "WARNING"
219+
preamble = "This release contains significant changes to core features. Please verify integration behavior after updating."
220+
elif integration_count > 2 or translation_count > 5:
221+
severity = "Medium"
222+
alert_type = "TIP"
223+
preamble = "This release contains standard updates and feature enhancements to the integration logic or translations."
224+
225+
if rtype != "stable":
226+
preamble = f"ℹ️ **This is a {rtype} build.** It contains preview features for testing.<br><br>{preamble}"
227+
228+
impact_summary = []
229+
if total_files > 0:
230+
if integration_count > 0:
231+
pct = round((integration_count / total_files) * 100)
232+
impact_summary.append(f"⚙️ Core ({integration_count} files · {pct}%)")
233+
if translation_count > 0:
234+
pct = round((translation_count / total_files) * 100)
235+
impact_summary.append(
236+
f"🗣️ Translations ({translation_count} files · {pct}%)"
237+
)
238+
if test_count > 0:
239+
pct = round((test_count / total_files) * 100)
240+
impact_summary.append(f"🧪 Tests ({test_count} files · {pct}%)")
241+
if ci_count > 0:
242+
pct = round((ci_count / total_files) * 100)
243+
impact_summary.append(f"🚀 CI/CD ({ci_count} files · {pct}%)")
244+
if docs_count > 0:
245+
pct = round((docs_count / total_files) * 100)
246+
impact_summary.append(f"📖 Docs ({docs_count} files · {pct}%)")
247+
248+
impact_str = (
249+
" · ".join(impact_summary)
250+
if impact_summary
251+
else "No codebase changes detected."
252+
)
253+
254+
prerelease_note = (
255+
f"\n> [!{alert_type}]\n"
256+
f"> **Release Risk: {severity}**\n"
257+
f"> {preamble}\n"
258+
f">\n"
259+
f"> **Affected areas:** {impact_str}\n"
260+
)
261+
262+
released_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC"
263+
body_parts = [
264+
f"# {friendly_name} {version} {channel_badge}",
265+
"",
266+
prerelease_note,
267+
"## 📋 What's Changed",
268+
"",
269+
changelog_md,
270+
"",
271+
"## 📊 Release Details",
272+
"",
273+
"| | |",
274+
"|---|---|",
275+
f"| **Version** | `{version}` |",
276+
f"| **Channel** | {rtype} |",
277+
f"| **Released** | {released_at} |",
278+
f"| **Commits included** | {total_commit_count}{changelog_label} |",
279+
"",
280+
"---",
281+
"",
282+
f"*📖 [Documentation]({docs_url}) · 🐛 [Report an Issue](https://github.com/{repo}/issues/new/choose) · 📦 [All Releases](https://github.com/{repo}/releases)*",
283+
]
284+
285+
body = "\n".join(body_parts)
286+
with open("release_body.md", "w", encoding="utf-8") as f:
287+
f.write(body)
288+
289+
# Output parameters for GITHUB_OUTPUT
290+
github_output = os.environ.get("GITHUB_OUTPUT", "")
291+
if github_output:
292+
with open(github_output, "a", encoding="utf-8") as f:
293+
f.write(f"version={version}\n")
294+
f.write(f"tag={tag}\n")
295+
f.write(f"is_prerelease={is_prerelease}\n")
296+
import uuid
297+
298+
delimiter = f"gh_release_{uuid.uuid4().hex}"
299+
f.write(f"release_body<<{delimiter}\n")
300+
f.write(body + "\n")
301+
f.write(f"{delimiter}\n")
302+
303+
304+
if __name__ == "__main__":
305+
main()

.github/scripts/changelog_builder.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ def build_changelog(commits, url):
2525
continue
2626
fh, sh, sub = line.split("|", 2)
2727
sl = sub.lower()
28-
if any(
29-
x in sl for x in ["chore: release", "chore: bump", "merge ", "[skip ci]"]
30-
):
28+
if any(x in sl for x in ["chore: release", "chore: bump", "merge ", "[skip ci]"]):
3129
continue
3230
sub = re.sub(r"\(#(\d+)\)", rf"([#\1]({url}/pull/\1))", sub)
3331
entry = f"- {sub} ([{sh}]({url}/commit/{fh}))"

0 commit comments

Comments
 (0)