Skip to content

Build

Build #654

Workflow file for this run

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