Build #654
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: Build | |
| on: | |
| schedule: | |
| # Nightly at 00:17 UTC (8:17 PM EDT / 7:17 PM EST). | |
| # GitHub Actions schedules are UTC-only and can queue late, so avoid :00/:30. | |
| - cron: "17 0 * * *" | |
| push: | |
| tags: ['v*.*.*'] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Skip release creation' | |
| type: boolean | |
| default: false | |
| permissions: | |
| actions: read | |
| contents: write | |
| concurrency: | |
| group: build-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| PYTHON_VERSION: "3.12" | |
| NUITKA_CACHE_DIR: ${{ github.workspace }}/.nuitka-cache | |
| PIP_DEFAULT_TIMEOUT: "120" | |
| PIP_RETRIES: "8" | |
| jobs: | |
| prepare: | |
| name: Prepare | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| version: ${{ steps.check.outputs.version }} | |
| is_nightly: ${{ steps.check.outputs.is_nightly }} | |
| tag: ${{ steps.check.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} | |
| fetch-depth: 0 | |
| - name: Check build conditions | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") | |
| if [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| # Tagged release | |
| echo "is_nightly=false" >> $GITHUB_OUTPUT | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT | |
| elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then | |
| # Manual nightly dispatches are intentional force-builds. | |
| echo "is_nightly=true" >> $GITHUB_OUTPUT | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| echo "tag=nightly-$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| else | |
| # Nightly build - check for changes | |
| echo "is_nightly=true" >> $GITHUB_OUTPUT | |
| echo "tag=nightly-$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| LAST_TAG=$(git tag --list "nightly-*" --sort=-version:refname | head -1) | |
| if [ -z "$LAST_TAG" ]; then | |
| # No prior nightly: build once | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| else | |
| gh release view "$LAST_TAG" --json body -q .body > previous-notes.md || true | |
| : > latest-stable-notes.md | |
| LATEST_STABLE_TAG=$(gh api "repos/${GITHUB_REPOSITORY}/releases" \ | |
| --jq '[.[] | select(.draft == false and .prerelease == false)][0].tag_name // ""') | |
| if [ -n "$LATEST_STABLE_TAG" ]; then | |
| gh release view "$LATEST_STABLE_TAG" --json body -q .body > latest-stable-notes.md || true | |
| fi | |
| python scripts/changelog_tools.py should-build-nightly \ | |
| --previous-tag "$LAST_TAG" \ | |
| --exclude-notes previous-notes.md \ | |
| --latest-stable-tag "$LATEST_STABLE_TAG" \ | |
| --exclude-stable-notes latest-stable-notes.md \ | |
| >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| build-windows: | |
| name: Windows | |
| needs: prepare | |
| if: needs.prepare.outputs.should_build == 'true' | |
| runs-on: windows-latest | |
| timeout-minutes: 210 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v6.2.0 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/cache@v5.0.5 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/Library/Caches/pip | |
| ~\AppData\Local\pip\Cache | |
| key: pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| pip-${{ runner.os }}- | |
| - uses: actions/cache/restore@v5.0.5 | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| restore-keys: | | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}- | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| - name: Install dependencies | |
| run: | | |
| pip install --only-binary wxPython wxPython | |
| pip install -e .[build] | |
| - name: Install Inno Setup | |
| run: choco install innosetup -y --no-progress | |
| - name: Build | |
| timeout-minutes: 180 | |
| run: | | |
| python scripts/generate_build_meta.py ${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }} | |
| python installer/create_icons.py | |
| python installer/build_nuitka.py --tag "${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }}" | |
| - uses: actions/upload-artifact@v7.0.1 | |
| with: | |
| name: windows | |
| path: | | |
| dist/AccessiWeather_Setup_*.exe | |
| dist/AccessiWeather_Portable_*.zip | |
| - uses: actions/cache/save@v5.0.5 | |
| if: always() | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| build-macos: | |
| name: macOS | |
| needs: prepare | |
| if: needs.prepare.outputs.should_build == 'true' | |
| runs-on: macos-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v6.2.0 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/cache@v5.0.5 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/Library/Caches/pip | |
| ~\AppData\Local\pip\Cache | |
| key: pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| pip-${{ runner.os }}- | |
| - uses: actions/cache/restore@v5.0.5 | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| restore-keys: | | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}- | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| - name: Install ccache | |
| run: brew install ccache | |
| - name: Install dependencies | |
| run: | | |
| pip install --only-binary wxPython wxPython | |
| pip install -e .[build] | |
| - name: Build | |
| timeout-minutes: 30 | |
| run: | | |
| python scripts/generate_build_meta.py ${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }} | |
| python installer/create_icons.py | |
| python installer/build_nuitka.py --tag "${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }}" | |
| - uses: actions/upload-artifact@v7.0.1 | |
| with: | |
| name: macos | |
| path: dist/AccessiWeather_macOS_*.zip | |
| - uses: actions/cache/save@v5.0.5 | |
| if: always() | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| build-linux: | |
| name: Linux | |
| needs: prepare | |
| if: needs.prepare.outputs.should_build == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 75 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@v6.2.0 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: actions/cache@v5.0.5 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/Library/Caches/pip | |
| ~\AppData\Local\pip\Cache | |
| key: pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| pip-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| pip-${{ runner.os }}- | |
| - uses: actions/cache/restore@v5.0.5 | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| restore-keys: | | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}- | |
| nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}- | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| ccache \ | |
| libgtk-3-dev \ | |
| libnotify-dev \ | |
| libsdl2-dev \ | |
| libwebkit2gtk-4.1-dev \ | |
| freeglut3-dev \ | |
| xvfb | |
| - name: Install dependencies | |
| run: | | |
| WX_PYTHON_VERSION=4.2.5 | |
| PY_TAG=$(python -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")') | |
| pip install \ | |
| "https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-24.04/wxpython-${WX_PYTHON_VERSION}-${PY_TAG}-${PY_TAG}-linux_x86_64.whl" | |
| pip install -e .[build] | |
| - name: Build | |
| timeout-minutes: 55 | |
| run: | | |
| python scripts/generate_build_meta.py ${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }} | |
| python installer/create_icons.py | |
| python installer/build_nuitka.py --tag "${{ needs.prepare.outputs.is_nightly == 'true' && needs.prepare.outputs.tag || '' }}" | |
| - name: Smoke test packaged app | |
| env: | |
| ACCESSIWEATHER_TEST_MODE: "1" | |
| XDG_DATA_HOME: ${{ runner.temp }}/accessiweather-data | |
| XDG_CONFIG_HOME: ${{ runner.temp }}/accessiweather-config | |
| run: | | |
| set +e | |
| timeout 25s xvfb-run -a dist/AccessiWeather/AccessiWeather > linux-smoke.log 2>&1 | |
| status=$? | |
| set -e | |
| cat linux-smoke.log | |
| if grep -E "cannot register existing type 'G(Task|AsyncResult|InputStream|Seekable|PollableInputStream)'|GLib-GObject-CRITICAL" linux-smoke.log; then | |
| echo "Packaged Linux app hit the GLib/GIO duplicate type-registration crash." | |
| exit 1 | |
| fi | |
| if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then | |
| echo "Packaged Linux app exited unexpectedly with status $status." | |
| exit "$status" | |
| fi | |
| - name: Package AppImage | |
| timeout-minutes: 15 | |
| run: python installer/build_appimage.py | |
| - name: Smoke test AppImage on Fedora | |
| timeout-minutes: 15 | |
| run: | | |
| cat > fedora-smoke.sh <<'EOF' | |
| set -e | |
| dnf install -y -q gtk3 libnotify alsa-lib xorg-x11-server-Xvfb dejavu-sans-fonts >/dev/null | |
| cd /work | |
| appimage=$(find dist -maxdepth 1 -name 'AccessiWeather_Linux_*.AppImage' -print -quit) | |
| if [ -z "$appimage" ]; then | |
| echo "AppImage was not created." | |
| exit 1 | |
| fi | |
| chmod +x "$appimage" | |
| export ACCESSIWEATHER_TEST_MODE=1 | |
| export XDG_DATA_HOME=/tmp/aw-data XDG_CONFIG_HOME=/tmp/aw-config | |
| set +e | |
| timeout 25s xvfb-run -a "$appimage" --appimage-extract-and-run > fedora-smoke.log 2>&1 | |
| status=$? | |
| set -e | |
| cat fedora-smoke.log | |
| if grep -E "error while loading shared libraries|cannot open shared object file" fedora-smoke.log; then | |
| echo "AppImage is missing a shared library on Fedora." | |
| exit 1 | |
| fi | |
| if grep -E "cannot register existing type 'G(Task|AsyncResult|InputStream|Seekable|PollableInputStream)'|GLib-GObject-CRITICAL" fedora-smoke.log; then | |
| echo "AppImage hit the GLib/GIO duplicate type-registration crash on Fedora." | |
| exit 1 | |
| fi | |
| if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then | |
| echo "AppImage exited unexpectedly with status $status on Fedora." | |
| exit "$status" | |
| fi | |
| echo "Fedora smoke test passed." | |
| EOF | |
| docker run --rm -v "$PWD:/work" fedora:43 bash /work/fedora-smoke.sh | |
| - name: Verify Linux tarball avoids bundled OpenSSL | |
| run: | | |
| tarball=$(find dist -maxdepth 1 -name 'AccessiWeather_Linux_*.tar.gz' -print -quit) | |
| if [ -z "$tarball" ]; then | |
| echo "Linux tarball was not created." | |
| exit 1 | |
| fi | |
| if tar -tzf "$tarball" | grep -E '(^|/)lib(ssl|crypto)\.so'; then | |
| echo "Linux tarball must not bundle OpenSSL libraries; use the target system OpenSSL instead." | |
| exit 1 | |
| fi | |
| - name: Verify Linux tarball includes sound_lib x64 libraries | |
| run: | | |
| tarball=$(find dist -maxdepth 1 -name 'AccessiWeather_Linux_*.tar.gz' -print -quit) | |
| if [ -z "$tarball" ]; then | |
| echo "Linux tarball was not created." | |
| exit 1 | |
| fi | |
| for lib in libbass.so libbassmix.so libbassopus.so libtags.so; do | |
| if ! tar -tzf "$tarball" | grep -qx "AccessiWeather/sound_lib/lib/x64/$lib"; then | |
| echo "Linux tarball is missing sound_lib/lib/x64/$lib." | |
| exit 1 | |
| fi | |
| done | |
| - uses: actions/upload-artifact@v7.0.1 | |
| with: | |
| name: linux | |
| path: | | |
| dist/AccessiWeather_Linux_*.tar.gz | |
| dist/AccessiWeather_Linux_*.AppImage | |
| - uses: actions/cache/save@v5.0.5 | |
| if: always() | |
| with: | |
| path: .nuitka-cache | |
| key: nuitka-${{ runner.os }}-py${{ env.PYTHON_VERSION }}-${{ hashFiles('pyproject.toml', 'installer/build_nuitka.py') }}-${{ github.run_id }} | |
| release: | |
| name: Release | |
| needs: [prepare, build-windows, build-macos, build-linux] | |
| if: always() && needs.prepare.outputs.should_build == 'true' && (needs.build-windows.result == 'success' || needs.build-macos.result == 'success' || needs.build-linux.result == 'success') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} | |
| fetch-depth: 0 | |
| - name: Download artifacts | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --dir dist | |
| - name: Prepare release | |
| id: prep | |
| env: | |
| IS_NIGHTLY: ${{ needs.prepare.outputs.is_nightly }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| CLIFF_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p assets | |
| TIMESTAMP=$(date -u +%Y%m%d) | |
| # Rename assets based on release type | |
| while IFS= read -r f; do | |
| name=$(basename "$f") | |
| if [ "$IS_NIGHTLY" = "true" ]; then | |
| case "$name" in | |
| *Setup*.exe) mv "$f" "assets/AccessiWeather-nightly-${TIMESTAMP}-windows-setup.exe" ;; | |
| *Portable*.zip) mv "$f" "assets/AccessiWeather-nightly-${TIMESTAMP}-windows-portable.zip" ;; | |
| *macOS*.zip) mv "$f" "assets/AccessiWeather-nightly-${TIMESTAMP}-macOS.zip" ;; | |
| *Linux*.tar.gz) mv "$f" "assets/AccessiWeather-nightly-${TIMESTAMP}-linux.tar.gz" ;; | |
| *Linux*.AppImage) mv "$f" "assets/AccessiWeather-nightly-${TIMESTAMP}-linux-x86_64.AppImage" ;; | |
| esac | |
| else | |
| case "$name" in | |
| *Setup*.exe) mv "$f" "assets/AccessiWeather-${VERSION}-windows-setup.exe" ;; | |
| *Portable*.zip) mv "$f" "assets/AccessiWeather-${VERSION}-windows-portable.zip" ;; | |
| *macOS*.zip) mv "$f" "assets/AccessiWeather-${VERSION}-macOS.zip" ;; | |
| *Linux*.tar.gz) mv "$f" "assets/AccessiWeather-${VERSION}-linux.tar.gz" ;; | |
| *Linux*.AppImage) mv "$f" "assets/AccessiWeather-${VERSION}-linux-x86_64.AppImage" ;; | |
| esac | |
| fi | |
| done < <(find dist -type f \( -name '*.exe' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.AppImage' \)) | |
| # Generate checksums | |
| cd assets && sha256sum * > checksums.txt 2>/dev/null || true | |
| # Generate release notes from curated CHANGELOG.md entries. | |
| cd .. | |
| if [ "$IS_NIGHTLY" = "true" ]; then | |
| LAST_TAG=$(git tag --list "nightly-*" --sort=-version:refname | head -1) | |
| if [ -n "$LAST_TAG" ]; then | |
| gh release view "$LAST_TAG" --json body -q .body > previous-notes.md || true | |
| python scripts/changelog_tools.py notes \ | |
| --kind nightly \ | |
| --previous-tag "$LAST_TAG" \ | |
| --exclude-notes previous-notes.md \ | |
| --output notes.md | |
| else | |
| python scripts/changelog_tools.py notes --kind nightly --output notes.md | |
| fi | |
| else | |
| python scripts/changelog_tools.py notes \ | |
| --kind stable \ | |
| --version "$TAG" \ | |
| --output notes.md | |
| fi | |
| - name: Create release | |
| if: ${{ inputs.dry_run != true }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IS_NIGHTLY: ${{ needs.prepare.outputs.is_nightly }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| if [ "$IS_NIGHTLY" = "true" ]; then | |
| # Nightly: always recreate | |
| gh release delete "$TAG" -y --cleanup-tag 2>/dev/null || true | |
| gh release create "$TAG" assets/* --notes-file notes.md \ | |
| --title "Nightly $(date -u +%Y-%m-%d)" --prerelease --target dev | |
| else | |
| # Stable: upload assets to existing release, or create new one | |
| if gh release view "$TAG" &>/dev/null; then | |
| # Release exists (created manually) — just upload assets | |
| gh release upload "$TAG" assets/* --clobber | |
| gh release edit "$TAG" --notes-file notes.md --title "AccessiWeather v${VERSION}" | |
| else | |
| gh release create "$TAG" assets/* --notes-file notes.md \ | |
| --title "AccessiWeather v${VERSION}" | |
| fi | |
| fi | |
| - name: Send build notification | |
| if: ${{ inputs.dry_run != true }} | |
| env: | |
| ORINKS_BUILD_NOTIFICATION_TOKEN: ${{ secrets.ORINKS_BUILD_NOTIFICATION_TOKEN }} | |
| IS_NIGHTLY: ${{ needs.prepare.outputs.is_nightly }} | |
| run: | | |
| if [ -z "$ORINKS_BUILD_NOTIFICATION_TOKEN" ]; then | |
| echo "ORINKS_BUILD_NOTIFICATION_TOKEN is not configured; skipping notification." | |
| exit 0 | |
| fi | |
| python - <<'PY' | |
| import json | |
| import os | |
| import urllib.request | |
| product = "AccessiWeather" | |
| release_kind = "nightly build" if os.environ["IS_NIGHTLY"] == "true" else "stable release" | |
| payload = { | |
| "product": product, | |
| "title": f"{product} {release_kind} available", | |
| "body": f"A new {product} {release_kind} is ready to download.", | |
| "url": "/accessiweather/downloads", | |
| } | |
| request = urllib.request.Request( | |
| "https://www.orinks.net/api/notifications/send", | |
| data=json.dumps(payload).encode("utf-8"), | |
| headers={ | |
| "Authorization": f"Bearer {os.environ['ORINKS_BUILD_NOTIFICATION_TOKEN']}", | |
| "Content-Type": "application/json", | |
| }, | |
| method="POST", | |
| ) | |
| with urllib.request.urlopen(request, timeout=30) as response: | |
| print(response.read().decode("utf-8")) | |
| PY |