Update release.yml #34
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |