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.
Write the workflow
Section titled “Write the workflow”uv run pyzed build --workflowpyzed build --workflow # or: python -m pyzed build --workflowpyzed build --workflow # or: py -m pyzed build --workflowIt 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).
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.
Run it
Section titled “Run it”-
Commit the workflow and your
uv.lock(the build usesuv sync --locked, so the lock file must be up to date). -
Push a version tag:
Terminal window git tag v1.0.0git push origin v1.0.0Or start it by hand: Actions → Build → Run workflow.
-
When it finishes, download the builds from the run’s page, under Artifacts.
Which machines
Section titled “Which machines”| 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.
Publish a GitHub Release
Section titled “Publish a GitHub Release”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:
[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):
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 --clobberOn 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.
Signing in CI
Section titled “Signing in CI”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.exeStore the Developer ID certificate (.p12) as base64 in MACOS_CERTIFICATE with its password, and your Apple ID
details for notarization:
- name: Sign and notarize if: runner.os == 'macOS' env: CERT: ${{ secrets.MACOS_CERTIFICATE }} CERT_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | keychain="$RUNNER_TEMP/signing.keychain-db" echo "$CERT" | base64 --decode > "$RUNNER_TEMP/cert.p12" security create-keychain -p "" "$keychain" security unlock-keychain -p "" "$keychain" security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" security set-key-partition-list -S apple-tool:,apple: -k "" "$keychain" >/dev/null security list-keychains -d user -s "$keychain" codesign --force --deep --options runtime --timestamp --sign "$IDENTITY" dist/Shop.app ditto -c -k --keepParent dist/Shop.app notarize.zip xcrun notarytool submit notarize.zip --wait \ --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" xcrun stapler staple dist/Shop.appPut the signing step after uv run pyzed build and before zipping. See
Windows and macOS for what each
command does.