# Compilar en CI con GitHub Actions

> Compila una app Pyzed para Windows, macOS y Linux en x64 y ARM en cada versión con GitHub Actions: el flujo que escribe pyzed build --workflow, publicar una GitHub Release y firmar con secretos.

Source: https://albakore.github.io/pyzed/es/packaging/ci/

Como cada sistema compila su propia app, la forma más fácil de publicarlas todas es dejar que GitHub las compile en
sus propias máquinas Windows, macOS y Linux ("runners"), en x64 y ARM, cada vez que publicas una versión.

## Escribir el flujo

**uv:**

```bash
uv run pyzed build --workflow
```

**pip:**

```bash title="macOS y Linux"
pyzed build --workflow          # o: python -m pyzed build --workflow
```

```powershell title="Windows (PowerShell)"
pyzed build --workflow          # o: py -m pyzed build --workflow
```

Escribe `.github/workflows/build.yml`, con un trabajo por cada destino de `targets` en `[tool.pyzed.build]` (por
defecto Windows x64, macOS en Apple silicon y Linux x64). En Pyzed Design, marca las plataformas en
**Archivo → Exportar aplicación… → Plataformas** y pulsa **Escribir workflow de CI**
([Exportar tu app](https://albakore.github.io/pyzed/es/editor/exporting/#plataformas-y-arquitecturas)).

```yaml title=".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` hace que cada trabajo compruebe que su máquina compila lo que dice la matriz. El empaquetador, el icono, la
pantalla de inicio y el resto de ajustes salen de `pyproject.toml`, igual que en tu equipo.

## Ejecutarlo

1. Haz commit del flujo y de tu `uv.lock` (la compilación usa `uv sync --locked`, así que el archivo de bloqueo tiene
   que estar al día).

2. Sube una etiqueta de versión:

```bash
git tag v1.0.0
git push origin v1.0.0
```

   O lánzalo a mano: **Actions → Build → Run workflow**.

3. Cuando termine, descarga las compilaciones desde la página de la ejecución, en **Artifacts**.

## Qué máquinas

| Destino | Runner | Compila para |
| --- | --- | --- |
| `windows-x64` | `windows-latest` | Windows 10 y 11 en Intel y AMD |
| `windows-arm64` | `windows-11-arm` | Windows 11 en ARM |
| `macos-arm64` | `macos-latest` | Macs con Apple silicon |
| `macos-x64` | `macos-15-intel` | Macs con Intel |
| `linux-x64` | `ubuntu-22.04` | Linux en Intel y AMD |
| `linux-arm64` | `ubuntu-22.04-arm` | Linux en ARM |

Los runners de Linux son a propósito el Ubuntu más antiguo que ofrece GitHub: la app funciona con la glibc del runner
o una más nueva ([Linux](https://albakore.github.io/pyzed/es/packaging/linux/#en-qué-distribución-compilar)). Con `backend = "nuitka"` todos los
runners ya tienen el compilador de C que necesita Nuitka.

## Publicar una GitHub Release

Los artifacts caducan pasado un tiempo y piden iniciar sesión en GitHub para descargarlos. Para dar a la gente una
página de descarga permanente, fija `archive` antes de escribir el flujo:

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

Cada compilación empaqueta entonces la app como `<app>-<sistema>-<arq>.zip` (`shop-windows-x86_64.zip`,
`shop-macos-arm64.zip`, `shop-linux-x86_64.zip`), los nombres que busca la
[autoactualización](https://albakore.github.io/pyzed/es/packaging/updates/), y el flujo gana un último paso que lo adjunta a la Release de la
etiqueta (y el permiso para crearla):

```yaml title=".github/workflows/build.yml (lo que añade archive)"
jobs:
  build:
    permissions:
      contents: write
    steps:
      # ... la compilación, y luego:
      - 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
```

En macOS el zip se crea con `ditto`, que conserva los enlaces simbólicos y la firma del paquete. Si
[firmas](#firmar-en-ci) la app, fírmala antes de comprimirla: fija `archive` solo después de añadir el paso de firma, o
comprime tú tras firmar.

## Firmar en CI

Guarda los certificados en **secretos de GitHub** (Settings → Secrets and variables → Actions), nunca en el
repositorio. Luego decodifícalos en un paso antes de firmar. Es lo que hace el flujo de versiones de Pyzed para Pyzed
Design:

**Windows:**

Guarda el archivo `.pfx` en base64 en `WINDOWS_CERTIFICATE` y su contraseña en `WINDOWS_CERTIFICATE_PASSWORD`:

```yaml
      - 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
```

**macOS:**

Guarda el certificado Developer ID (`.p12`) en base64 en `MACOS_CERTIFICATE` con su contraseña, y los datos de tu
Apple ID para la notarización:

```yaml
      - 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.app
```

Pon el paso de firma **después** de `uv run pyzed build` y **antes** de comprimir. Mira
[Windows](https://albakore.github.io/pyzed/es/packaging/windows/#firmarla) y [macOS](https://albakore.github.io/pyzed/es/packaging/macos/#firmar-y-notarizar) para saber
qué hace cada comando.

**Cómo se publica Pyzed Design:**

El flujo que compila el propio Pyzed Design,
[`.github/workflows/release.yml`](https://github.com/albakore/pyzed/blob/main/.github/workflows/release.yml), es un
ejemplo real completo: cinco plataformas, firma y notarización opcionales, un instalador de Inno Setup en Windows, un
`.dmg` en macOS, `SHA256SUMS`, atestaciones de procedencia y una GitHub Release con notas.
