Skip to content

release: prepare v0.5.0 #30

release: prepare v0.5.0

release: prepare v0.5.0 #30

name: Build Multi-Platform
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: read
jobs:
detect-release:
name: Detect Release Version Change
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.detect.outputs.should_release }}
version: ${{ steps.detect.outputs.version }}
tag_name: ${{ steps.detect.outputs.tag_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect version change
id: detect
run: |
python - <<'PY'
import os
import pathlib
import subprocess
import tomllib
def set_output(name: str, value: str) -> None:
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"{name}={value}\n")
before = os.environ.get("GITHUB_EVENT_BEFORE", "")
current = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
previous = None
if before and before != "0000000000000000000000000000000000000000":
try:
previous_text = subprocess.check_output(
["git", "show", f"{before}:pyproject.toml"],
text=True,
stderr=subprocess.DEVNULL,
)
previous = tomllib.loads(previous_text)["project"]["version"]
except Exception:
previous = None
should_release = previous != current
tag_name = f"v{current}"
print(f"previous={previous}")
print(f"current={current}")
print(f"should_release={str(should_release).lower()}")
set_output("should_release", str(should_release).lower())
set_output("version", current)
set_output("tag_name", tag_name)
PY
env:
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
build-windows:
name: Build Windows EXE & Installer
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: |
requirements-windows.txt
pyproject.toml
- name: Validate version consistency
run: |
$code = @'
import pathlib
import re
import sys
import tomllib
py = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
v1 = py["project"]["version"]
txt = pathlib.Path("src/__init__.py").read_text(encoding="utf-8")
m = re.search(r"__version__\s*=\s*\"([^\"]+)\"", txt)
v2 = m.group(1) if m else None
print(f"pyproject={v1}, src={v2}")
sys.exit(0 if (v1 and v1 == v2) else 1)
'@
python -c $code
shell: pwsh
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "numpy>=1.24.0,<2.0.0" --only-binary=:all:
pip install -r requirements-windows.txt --verbose
shell: pwsh
- name: Build Single-File EXE
run: |
New-Item -ItemType Directory -Force -Path build-logs | Out-Null
python setup.py --build *>&1 | Tee-Object -FilePath build-logs/pyinstaller-single.log
if ($LASTEXITCODE -ne 0) {
Write-Error "PyInstaller single-file build failed"
exit $LASTEXITCODE
}
shell: pwsh
- name: Build Directory Mode for Installer
run: |
python setup.py --build-dir *>&1 | Tee-Object -FilePath build-logs/pyinstaller-dir.log
if ($LASTEXITCODE -ne 0) {
Write-Error "PyInstaller directory build failed"
exit $LASTEXITCODE
}
shell: pwsh
- name: Install NSIS
run: |
winget install --id NSIS.NSIS --exact --accept-source-agreements --accept-package-agreements -e --silent
Start-Sleep -Seconds 10
shell: pwsh
- name: Build NSIS Installer
run: |
$nsisPaths = @(
"C:\Program Files (x86)\NSIS\makensis.exe",
"C:\Program Files\NSIS\makensis.exe",
"C:\NSIS\makensis.exe"
)
$makensis = $null
foreach ($path in $nsisPaths) {
if (Test-Path $path) {
$makensis = $path
break
}
}
if (-not $makensis) {
Write-Error "NSIS makensis.exe not found"
exit 1
}
& $makensis "installer\installer.nsi" *>&1 | Tee-Object -FilePath build-logs/nsis.log
if ($LASTEXITCODE -ne 0) {
Write-Error "Installer build failed!"
exit 1
}
shell: pwsh
- name: Upload Windows build logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: windows-build-logs
path: |
build-logs/**
build/**
retention-days: 14
if-no-files-found: warn
- name: Upload Windows EXE
uses: actions/upload-artifact@v4
with:
name: ha-assistant-windows-exe
path: dist/HomeAssistantWindows.exe
retention-days: 30
- name: Upload Windows Installer
uses: actions/upload-artifact@v4
with:
name: ha-assistant-windows-installer
path: dist/HomeAssistantWindows_Setup.exe
retention-days: 30
build-macos:
name: Build macOS App
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: |
requirements-macos.txt
pyproject.toml
- name: Validate version consistency
run: |
python - <<'PY'
import pathlib
import re
import sys
import tomllib
py = tomllib.loads(pathlib.Path('pyproject.toml').read_text(encoding='utf-8'))
v1 = py['project']['version']
txt = pathlib.Path('src/__init__.py').read_text(encoding='utf-8')
m = re.search(r'__version__\s*=\s*"([^"]+)"', txt)
v2 = m.group(1) if m else None
print(f'pyproject={v1}, src={v2}')
raise SystemExit(0 if (v1 and v1 == v2) else 1)
PY
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-macos.txt
- name: Build macOS App
run: |
mkdir -p build-logs
set -o pipefail
python setup.py --build 2>&1 | tee build-logs/pyinstaller-macos.log
- name: Create DMG
run: |
set -o pipefail
hdiutil create -volname "Home Assistant" -srcfolder dist/HomeAssistant.app -ov -format UDZO dist/HomeAssistant-macOS.dmg 2>&1 | tee build-logs/dmg.log
- name: Upload macOS build logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: macos-build-logs
path: |
build-logs/**
build/**
retention-days: 14
if-no-files-found: warn
- name: Upload macOS DMG
uses: actions/upload-artifact@v4
with:
name: ha-assistant-macos
path: dist/HomeAssistant-macOS.dmg
retention-days: 30
release:
name: Create Release
needs: [detect-release, build-windows, build-macos]
if: needs.detect-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download Windows EXE
uses: actions/download-artifact@v4
with:
name: ha-assistant-windows-exe
path: dist/
- name: Download Windows Installer
uses: actions/download-artifact@v4
with:
name: ha-assistant-windows-installer
path: dist/
- name: Download macOS DMG
uses: actions/download-artifact@v4
with:
name: ha-assistant-macos
path: dist/
- name: Generate latest.json
run: |
version="${{ needs.detect-release.outputs.version }}"
echo "{\"version\": \"$version\"}" > dist/latest.json
shell: bash
- name: Generate release notes
run: |
python - <<'PY'
import json
import os
import textwrap
import urllib.error
import urllib.parse
import urllib.request
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GITHUB_TOKEN"]
current_tag = os.environ["CURRENT_TAG"]
current_sha = os.environ["CURRENT_SHA"]
def api(path: str):
url = f"https://api.github.com{path}"
request = urllib.request.Request(
url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
},
)
with urllib.request.urlopen(request) as response:
return json.load(response)
def get_latest_release_tag() -> str | None:
try:
release = api(f"/repos/{repo}/releases/latest")
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
raise
return release.get("tag_name")
def summarize_body(body: str | None) -> str | None:
if not body:
return None
for raw_line in body.splitlines():
line = raw_line.strip()
if not line:
continue
if line.startswith("#"):
continue
if line.startswith("-") or line.startswith("*"):
line = line[1:].strip()
if line:
return textwrap.shorten(line, width=160, placeholder="...")
return None
previous_tag = get_latest_release_tag()
pr_map: dict[int, dict] = {}
if previous_tag:
compare = api(f"/repos/{repo}/compare/{urllib.parse.quote(previous_tag, safe='')}...{current_sha}")
for commit in compare.get("commits", []):
commit_sha = commit["sha"]
pulls = api(f"/repos/{repo}/commits/{commit_sha}/pulls")
for pull in pulls:
pr_map[pull["number"]] = {
"number": pull["number"],
"title": pull["title"].strip(),
"body": summarize_body(pull.get("body")),
"url": pull["html_url"],
"merged_at": pull.get("merged_at") or "",
}
prs = sorted(pr_map.values(), key=lambda pr: (pr["merged_at"], pr["number"]))
lines = [f"## What's Changed", ""]
if previous_tag:
lines.append(f"- Compare: https://github.com/{repo}/compare/{previous_tag}...{current_tag}")
else:
lines.append("- Initial release")
lines.append("")
lines.append("## Pull Requests")
lines.append("")
if prs:
for pr in prs:
lines.append(f"- #{pr['number']} {pr['title']} ({pr['url']})")
if pr["body"]:
lines.append(f" - {pr['body']}")
else:
lines.append("- No pull requests found between the previous release and this release.")
with open("dist/release_notes.md", "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
PY
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_TAG: ${{ needs.detect-release.outputs.tag_name }}
CURRENT_SHA: ${{ github.sha }}
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.detect-release.outputs.tag_name }}
target_commitish: ${{ github.sha }}
name: ${{ needs.detect-release.outputs.tag_name }}
files: |
dist/HomeAssistantWindows.exe
dist/HomeAssistantWindows_Setup.exe
dist/HomeAssistant-macOS.dmg
dist/latest.json
body_path: dist/release_notes.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}