Skip to content

Build in CI with GitHub Actions

Since each system builds its own app, the easiest way to ship them all is to let GitHub build them on its own Windows, macOS and Linux machines (“runners”), on x64 and ARM, every time you publish a version.

Terminal window
uv run pyzed build --workflow

It writes .github/workflows/build.yml, with one job per target listed in targets of [tool.pyzed.build] (by default Windows x64, macOS on Apple silicon and Linux x64). In Pyzed Design, check the platforms in File → Export app… → Platforms and press Write CI workflow (Exporting your app).

.github/workflows/build.yml
name: Build
# Builds the app for each target with `pyzed build` and keeps the result as an
# artifact (Actions -> this run -> Artifacts). The backends cannot cross-compile, so
# every platform and architecture builds on its own runner.
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { target: windows-x64, runner: windows-latest }
- { target: macos-arm64, runner: macos-latest }
- { target: linux-x64, runner: ubuntu-22.04 }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv sync --locked
- run: uv run pyzed build --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: shop-${{ matrix.target }}
path: dist/

--target makes each job check that its machine builds what the matrix says. The backend, the icon, the splash screen and every other setting come from pyproject.toml, the same as on your computer.

  1. Commit the workflow and your uv.lock (the build uses uv sync --locked, so the lock file must be up to date).

  2. Push a version tag:

    Terminal window
    git tag v1.0.0
    git push origin v1.0.0

    Or start it by hand: Actions → Build → Run workflow.

  3. When it finishes, download the builds from the run’s page, under Artifacts.

Target Runner Builds for
windows-x64 windows-latest Windows 10 and 11 on Intel and AMD
windows-arm64 windows-11-arm Windows 11 on ARM
macos-arm64 macos-latest Macs with Apple silicon
macos-x64 macos-15-intel Intel Macs
linux-x64 ubuntu-22.04 Linux on Intel and AMD
linux-arm64 ubuntu-22.04-arm Linux on ARM

The Linux runners are the oldest Ubuntu GitHub offers on purpose: the app runs on the runner’s glibc or newer (Linux). With backend = "nuitka" every runner already has the C compiler Nuitka needs.

Artifacts expire after a while and need a GitHub login to download. To give people a permanent download page, set archive before writing the workflow:

pyproject.toml
[tool.pyzed.build]
archive = "zip"

Each build then packs the app as <app>-<system>-<arch>.zip (shop-windows-x86_64.zip, shop-macos-arm64.zip, shop-linux-x86_64.zip), the names the self-updater looks for, and the workflow gets a last step that attaches it to the tag’s release (and the permission to create it):

.github/workflows/build.yml (what archive adds)
jobs:
build:
permissions:
contents: write
steps:
# ... the build, then:
- name: Attach to the GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
# the first build to get here creates the release; the others only upload
gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --verify-tag || true
gh release upload "$GITHUB_REF_NAME" dist/*.zip --clobber

On macOS the zip is made with ditto, which keeps the bundle’s symlinks and signature. If you sign the app, sign it before it is packed: set archive only after adding the signing step, or zip after signing yourself.

Keep certificates in GitHub secrets (Settings → Secrets and variables → Actions), never in the repository. Then decode them in a step before signing. This is what Pyzed’s own release workflow does for Pyzed Design:

Store the .pfx file as base64 in WINDOWS_CERTIFICATE and its password in WINDOWS_CERTIFICATE_PASSWORD:

- name: Sign
if: runner.os == 'Windows'
shell: bash
env:
CERT: ${{ secrets.WINDOWS_CERTIFICATE }}
PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
echo "$CERT" | base64 --decode > "$RUNNER_TEMP/cert.pfx"
signtool=$(ls "/c/Program Files (x86)/Windows Kits/10/bin/"*/x64/signtool.exe | tail -1)
"$signtool" sign /f "$RUNNER_TEMP/cert.pfx" /p "$PASSWORD" /fd sha256 \
/tr http://timestamp.digicert.com /td sha256 dist/Shop/Shop.exe

Put the signing step after uv run pyzed build and before zipping. See Windows and macOS for what each command does.