Skip to content

Update release.yml

Update release.yml #34

Workflow file for this run

name: Release (bump tag -> bump version -> Nuitka builds -> GitHub Release)
on:
push:
tags:
- "bump"
- "bump-*"
permissions:
contents: write
env:
PYTHON_VERSION: "3.12"
ENTRYPOINT: "src/spectUI/MainWindow.py"
RESOURCES_SRC_DIR: "src/spectUI/resources"
RESOURCES_DST_DIR: "resources"
WIN_ICON_ICO: "spectHR.ico"
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.final.outputs.version }}
release_tag: ${{ steps.final.outputs.release_tag }}
sha: ${{ steps.sha.outputs.sha }}
steps:
- name: Checkout full history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine target version (tag 'bump' auto-patch OR 'bump-x.y.z' explicit)
id: final
shell: bash
run: |
python - <<'PY'
import os, re
from pathlib import Path
tag = os.environ["GITHUB_REF_NAME"]
pyproj = Path("pyproject.toml")
if not pyproj.exists():
raise SystemExit("pyproject.toml not found at repo root")
txt = pyproj.read_text(encoding="utf-8")
mproj = re.search(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)", txt)
if not mproj:
raise SystemExit("No [project] table found in pyproject.toml")
block = mproj.group(0)
mver = re.search(r'(?m)^version\s*=\s*"([^"]+)"\s*$', block)
if not mver:
raise SystemExit('No [project].version = "..." found in pyproject.toml')
current_raw = mver.group(1).strip()
def normalize_xy_to_xyz(v: str) -> str:
parts = v.split(".")
if len(parts) == 2 and all(p.isdigit() for p in parts):
return f"{parts[0]}.{parts[1]}.0"
return v
def is_stable_xyz(v: str) -> bool:
return re.match(r"^\d+\.\d+\.\d+$", v) is not None
def bump_patch(v: str) -> str:
mm = re.match(r"^(\d+)\.(\d+)\.(\d+)$", v)
if not mm:
raise ValueError(v)
a, b, c = map(int, mm.groups())
return f"{a}.{b}.{c+1}"
if tag == "bump":
base = normalize_xy_to_xyz(current_raw)
if not is_stable_xyz(base):
raise SystemExit(
f"Auto patch bump requires stable X.Y.Z (or X.Y). Current is '{current_raw}'. "
f"Use explicit bump-<version> for pre-releases."
)
version = bump_patch(base)
elif tag.startswith("bump-"):
version = normalize_xy_to_xyz(tag[len("bump-"):])
if not re.match(r"^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*$", version):
raise SystemExit(f"Explicit bump tag not valid enough: '{tag}'")
else:
raise SystemExit(f"Unexpected tag: {tag}")
release_tag = f"v{version}"
out_path = Path(os.environ["GITHUB_OUTPUT"])
out_path.write_text(out_path.read_text() + f"version={version}\nrelease_tag={release_tag}\n", encoding="utf-8")
PY
- name: Bump pyproject.toml [project].version to target version
env:
VERSION: ${{ steps.final.outputs.version }}
shell: bash
run: |
python - <<'PY'
import os, re
from pathlib import Path
version = os.environ["VERSION"]
path = Path("pyproject.toml")
txt = path.read_text(encoding="utf-8")
mproj = re.search(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)", txt)
if not mproj:
raise SystemExit("No [project] table found in pyproject.toml")
block = mproj.group(0)
new_block, n = re.subn(
r'(?m)^version\s*=\s*".*?"\s*$',
f'version = "{version}"',
block,
count=1
)
if n != 1:
raise SystemExit("Could not replace [project].version exactly once")
path.write_text(txt[:mproj.start()] + new_block + txt[mproj.end():], encoding="utf-8")
PY
- name: Commit bump and push to default branch; create vX.Y.Z tag
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add pyproject.toml
git commit -m "chore(release): bump version to ${{ steps.final.outputs.version }}" || echo "No changes to commit"
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
git push origin "HEAD:${DEFAULT_BRANCH}"
git tag -f "${{ steps.final.outputs.release_tag }}"
git push -f origin "${{ steps.final.outputs.release_tag }}"
- name: Export SHA used for builds
id: sha
shell: bash
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build:
needs: prepare
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout prepared commit
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.sha }}
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: "pip"
- name: Linux system deps (Qt runtime + Nuitka tooling)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
patchelf \
libgl1 \
libegl1 \
libglib2.0-0 \
libdbus-1-3 \
libxkbcommon0 \
libxkbcommon-x11-0 \
libx11-xcb1 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-shape0 \
libxcb-xinerama0 \
libxcb-xfixes0 \
libxrender1 \
libxi6 \
libxext6 \
libfontconfig1 \
libfreetype6 \
libnss3 \
libasound2t64 \
fonts-dejavu-core \
libxcb-xinput0 \
libxcb-util1 \
libxcb-icccm4 \
libxcb-keysyms1 \
libxcb-render0 \
libxcb-render-util0 \
libxcb-shape0 \
libxcb-xfixes0 \
libxcb-xinerama0 \
libxcb-dri2-0 \
libxcb-dri3-0 \
libxcb-present0 \
libxcb-sync1 \
libxshmfence1 \
fonts-dejavu-core
- name: Install project + Nuitka tooling
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install .
python -m pip install nuitka zstandard
- name: Build + package (Windows/Linux onefile; macOS .app + iconset -> icns -> zip)
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.prepare.outputs.version }}"
OUTDIR="dist/${{ runner.os }}"
mkdir -p "$OUTDIR"
ENTRY="${{ env.ENTRYPOINT }}"
if [ ! -f "$ENTRY" ]; then
echo "Entry not found: $ENTRY"
exit 1
fi
COMMON_ARGS=(
"$ENTRY"
--standalone
--assume-yes-for-downloads
--enable-plugin=pyside6
--include-package=spectHR
--include-package=spectUI
--include-data-dir="${{ env.RESOURCES_SRC_DIR }}=${{ env.RESOURCES_DST_DIR }}"
--output-dir="$OUTDIR"
)
case "${{ runner.os }}" in
Windows)
if [ ! -f "${{ env.WIN_ICON_ICO }}" ]; then
echo "Missing icon file: ${{ env.WIN_ICON_ICO }} (repo root)"
exit 1
fi
python -m nuitka \
"${COMMON_ARGS[@]}" \
--onefile \
--windows-icon-from-ico="${{ env.WIN_ICON_ICO }}" \
--output-filename="spectHR.exe"
;;
macOS)
if [ ! -d "spectHR.iconset" ]; then
echo "Missing folder: spectHR.iconset (expected in repo root)"
exit 1
fi
/usr/bin/iconutil -c icns "spectHR.iconset" -o "spectHR.icns"
if [ ! -f "spectHR.icns" ]; then
echo "Failed to create spectHR.icns from spectHR.iconset"
exit 1
fi
python -m nuitka \
"${COMMON_ARGS[@]}" \
--macos-create-app-bundle \
--macos-app-name="spectHR" \
--macos-app-icon="spectHR.icns" \
--output-filename="spectHR"
;;
Linux)
python -m nuitka \
"${COMMON_ARGS[@]}" \
--onefile \
--output-filename="spectHR-Linux-v$VERSION"
;;
*)
echo "Unsupported OS: ${{ runner.os }}"
exit 1
;;
esac
env:
VERSION: ${{ needs.prepare.outputs.version }}
- name: Package Windows artifact
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = "${{ needs.prepare.outputs.version }}"
$outdir = "dist/Windows"
if (!(Test-Path "$outdir/spectHR.exe")) { throw "Missing $outdir/spectHR.exe" }
if (!(Test-Path "ExampleData")) { throw "Missing ExampleData/ at repo root" }
$zip = "$outdir/spectHR-Windows-v$version.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
# ``-Path`` accepts multiple roots; each one lands at the top
# of the archive, so the .exe and the ExampleData/ folder sit
# side-by-side inside the zip.
Compress-Archive -Path "$outdir/spectHR.exe","ExampleData" -DestinationPath $zip
Remove-Item "$outdir/spectHR.exe" -Force
- name: Package macOS artifact
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.prepare.outputs.version }}"
OUTDIR="dist/macOS"
APP_PATH="$(find "$OUTDIR" -maxdepth 1 -type d -name "*.app" | head -n 1)"
if [ -z "${APP_PATH:-}" ]; then
echo "No .app produced in $OUTDIR"
ls -la "$OUTDIR"
exit 1
fi
if [ ! -d "ExampleData" ]; then
echo "Missing ExampleData/ at repo root"
exit 1
fi
APP_NAME="spectHR-macOS-v$VERSION.app"
mv "$APP_PATH" "$OUTDIR/$APP_NAME"
# Stage ExampleData/ next to the .app so both end up at the
# archive root after ``cd "$OUTDIR" && zip -r``.
cp -R ExampleData "$OUTDIR/ExampleData"
( cd "$OUTDIR" && /usr/bin/zip -r "spectHR-macOS-v$VERSION.zip" "$APP_NAME" "ExampleData" )
rm -rf "$OUTDIR/$APP_NAME" "$OUTDIR/ExampleData"
- name: Package Linux artifact
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.prepare.outputs.version }}"
OUTDIR="dist/Linux"
BIN="$OUTDIR/spectHR-Linux-v$VERSION"
if [ ! -f "$BIN" ]; then
echo "Missing $BIN"
ls -la "$OUTDIR"
exit 1
fi
if [ ! -d "ExampleData" ]; then
echo "Missing ExampleData/ at repo root"
exit 1
fi
# Stage ExampleData/ next to the binary so both sit at the
# archive root after ``cd "$OUTDIR" && tar``.
cp -R ExampleData "$OUTDIR/ExampleData"
( cd "$OUTDIR" && tar -czf "spectHR-Linux-v$VERSION.tar.gz" "spectHR-Linux-v$VERSION" "ExampleData" )
rm -f "$BIN"
rm -rf "$OUTDIR/ExampleData"
- name: Upload artifact (only packaged files)
uses: actions/upload-artifact@v4
with:
name: spectHR-${{ runner.os }}-v${{ needs.prepare.outputs.version }}
path: |
dist/${{ runner.os }}/*.zip
dist/${{ runner.os }}/*.tar.gz
release:
needs: [prepare, build]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create GitHub Release and upload assets
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.release_tag }}
name: ${{ needs.prepare.outputs.release_tag }}
generate_release_notes: true
files: |
artifacts/**/*.zip
artifacts/**/*.tar.gz