diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml new file mode 100644 index 0000000..3c6b374 --- /dev/null +++ b/.github/workflows/backup.yml @@ -0,0 +1,69 @@ +name: Backup semanal de la base de datos + +# Dump cifrado de los esquemas public + internal (sin auth) a Google Drive, +# con rotación automática. Implementa Documentation/Backlog/Backups.md — ver +# ese doc para el diseño completo (invariantes, esquema de retención, y por +# qué la subida se hace con rclone autenticado como el dueño del Drive en +# vez de un Service Account). +# +# Falla ruidoso a propósito (invariante 3 del backlog): si el dump, el +# cifrado, la subida o la rotación fallan, este job queda en rojo y GitHub +# le manda un mail al dueño del repo. Ningún paso sensible usa +# continue-on-error. +# +# El remote de rclone dentro de RCLONE_CONFIG tiene que llamarse "gdrive" +# (ver database/backup/dump.sh y rotate.sh, que asumen ese nombre). + +on: + schedule: + - cron: "0 5 * * 1" # lunes 05:00 UTC (~02:00 hora Argentina) + workflow_dispatch: {} # para probar a mano desde la pestaña Actions + +jobs: + backup: + runs-on: ubuntu-latest + env: + RCLONE_REMOTE_NAME: gdrive + steps: + - uses: actions/checkout@v4 + + - name: Instalar postgresql-client 17 y rclone + # El postgresql-client generico de Ubuntu puede venir en una version + # mas vieja que el servidor (Postgres 17 en Supabase), y pg_dump con + # un cliente mas viejo que el servidor puede fallar o dumpear + # incompleto. Se instala la 17 explicita desde el repo oficial PGDG. + run: | + sudo apt-get update + sudo apt-get install -y curl ca-certificates gnupg + sudo install -d /usr/share/postgresql-common/pgdg + sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt-get update + sudo apt-get install -y postgresql-client-17 + curl -fsSL https://rclone.org/install.sh | sudo bash + # El runner ya trae un pg_dump mas viejo preinstalado que gana en + # el PATH aunque se instale la 17 al lado. Se antepone la carpeta + # de la 17 explicitamente para que no haya ambiguedad. + echo "/usr/lib/postgresql/17/bin" >> "$GITHUB_PATH" + + - name: Configurar rclone + env: + RCLONE_CONFIG_CONTENT: ${{ secrets.RCLONE_CONFIG }} + run: | + mkdir -p ~/.config/rclone + printf '%s' "$RCLONE_CONFIG_CONTENT" > ~/.config/rclone/rclone.conf + chmod 600 ~/.config/rclone/rclone.conf + + - name: Dump + comprimir + cifrar + subir + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + BACKUP_PASSPHRASE: ${{ secrets.BACKUP_PASSPHRASE }} + GDRIVE_FOLDER_ID: ${{ secrets.GDRIVE_FOLDER_ID }} + run: bash database/backup/dump.sh + + - name: Rotar backups viejos + env: + GDRIVE_FOLDER_ID: ${{ secrets.GDRIVE_FOLDER_ID }} + run: bash database/backup/rotate.sh diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 0000000..a9965c9 --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,177 @@ +name: Build macOS release + +# Compila la app de escritorio para macOS y arma el paquete que consume el +# auto-updater (soma_pro_updater). Corre en un runner macOS de GitHub: un build +# de Mac NO se puede compilar desde Linux ni Windows (necesita Xcode), así que +# esto reemplaza a tener una Mac propia. +# +# Se dispara a mano desde la pestaña Actions ("Run workflow") y elegís a qué +# Supabase apunta el build (prod o dev). +# +# Qué deja: +# - Un .dmg para la PRIMERA instalación (contiene la carpeta SOMA PRO con el +# updater y la app como hermanos + version.json). Sale como artifact. +# - El paquete de actualización (zip del SOMA PRO.app) + manifest.json. +# También como artifact, y si el build es de prod se publica solo al feed +# de GitHub Pages (repo peblo423/soma-pro-releases, carpeta macos/), que es +# de donde el updater baja las nuevas versiones. +# +# Por qué updater + app como hermanos y no una subcarpeta app/ (como Windows): +# en macOS cada .app es un bundle autocontenido, así que el updater y la app +# real conviven en la misma carpeta sin pisarse. Lo espera así updater_service.dart. +# +# Firma / Gatekeeper. El .app sale con firma ad-hoc (sin cuenta de Apple +# Developer, USD 99/año), así que la PRIMERA vez macOS lo bloquea por venir "sin +# verificar". Se destraba una única vez, tras arrastrar la carpeta a Aplicaciones: +# xattr -dr com.apple.quarantine "/Applications/SOMA PRO" +# De ahí en más las actualizaciones son automáticas: el updater las baja por HTTP +# (no quedan en cuarentena) y las aplica con ditto, que preserva permisos y +# symlinks del bundle. La única forma de borrar hasta ese paso inicial es pagar +# Apple Developer y notarizar. + +on: + workflow_dispatch: + inputs: + environment: + description: "Entorno / Supabase al que apunta la app" + type: choice + options: + - prod + - dev + default: dev + +jobs: + build: + runs-on: macos-latest + steps: + - name: Checkout del repo + uses: actions/checkout@v4 + + # Barrera contra publicar código de desarrollo en producción: solo master + # puede generar un build de prod. El dropdown sigue permitiendo elegir + # "prod" desde otra rama (útil para compilar y probar el artifact), pero + # el paso "Publicar al feed" ya tiene su propio if, y este step corta + # antes de gastar tiempo compilando si la intención no tiene sentido. + - name: Verificar rama para builds de prod + if: ${{ inputs.environment == 'prod' && github.ref != 'refs/heads/master' }} + run: | + echo "::error::Los builds de prod solo se publican desde master, no desde ${GITHUB_REF#refs/heads/}." + echo "Si querés compilar desde esta rama para probar, elegí 'dev' en el dropdown." + exit 1 + + # Misma versión que usa Pablo localmente, para que el build sea reproducible. + - name: Instalar Flutter 3.41.6 + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.41.6 + channel: stable + cache: true + + - name: Habilitar macOS desktop + run: flutter config --enable-macos-desktop + + # --- App principal --- + - name: Dependencias app (pub get) + working-directory: flutter_soma_app + run: flutter pub get + + - name: Compilar app release (${{ inputs.environment }}) + working-directory: flutter_soma_app + run: flutter build macos --release --dart-define-from-file=config/${{ inputs.environment }}.json + + # --- Updater --- + - name: Dependencias updater (pub get) + working-directory: soma_pro_updater + run: flutter pub get + + - name: Compilar updater release + working-directory: soma_pro_updater + run: flutter build macos --release + + # --- Empaquetado --- + - name: Armar layout, paquete de update y manifest + id: package + run: | + set -euo pipefail + APP="flutter_soma_app/build/macos/Build/Products/Release/SOMA PRO.app" + UPDATER="soma_pro_updater/build/macos/Build/Products/Release/soma_pro_updater.app" + test -d "$APP" || { echo "No se encontró la app en $APP"; exit 1; } + test -d "$UPDATER" || { echo "No se encontró el updater en $UPDATER"; exit 1; } + + VERSION=$(grep -m1 '^version:' flutter_soma_app/pubspec.yaml \ + | sed 's/version:[[:space:]]*//' | cut -d'+' -f1 | tr -d '[:space:]') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Empaquetando SOMA PRO v$VERSION (${{ inputs.environment }})" + + # Carpeta de instalación: updater y app como hermanos + version.json. + # ditto (no cp) para preservar permisos y symlinks de cada bundle. + STAGE="release/macos/SOMA PRO" + rm -rf release/macos && mkdir -p "$STAGE" + ditto "$UPDATER" "$STAGE/soma_pro_updater.app" + ditto "$APP" "$STAGE/SOMA PRO.app" + echo "{\"version\": \"$VERSION\"}" > "$STAGE/version.json" + + # Paquete de actualización: SOLO el bundle de la app, que es lo que el + # updater baja y extrae sobre la carpeta de instalación. `ditto -c -k` + # preserva permisos y symlinks (un zip común los rompe); --keepParent + # deja el .app como carpeta raíz del zip, tal como el updater lo espera. + mkdir -p out + ZIP="out/soma_pro-macos-$VERSION.zip" + ditto -c -k --keepParent "$APP" "$ZIP" + SHA=$(shasum -a 256 "$ZIP" | cut -d' ' -f1) + + # Manifest que consume el updater desde GitHub Pages. + cat > out/manifest.json <.json se hornea en el bundle. +# +# Qué deja: +# - Un .exe (Inno Setup) para la PRIMERA instalación: instala el updater en +# la raíz y la app en app\, con acceso directo a la app. Sale como artifact. +# - El paquete de actualización (zip de la carpeta app\) + manifest.json. +# También como artifact, y si el build es de prod se publica solo al feed +# de GitHub Pages (repo peblo423/soma-pro-releases, carpeta windows/), que +# es de donde el updater baja las nuevas versiones. +# +# Por qué updater en la raíz y app en app\ (a diferencia de macOS, donde son +# hermanos): en Windows cada carpeta de build es un montón de archivos sueltos +# (exe + dlls + data\), no un bundle autocontenido como el .app de macOS: si +# updater y app compartieran carpeta, sus data\ se pisarían entre sí. Lo espera +# así updater_service.dart, y lib/main.dart busca al updater un nivel arriba +# de donde corre la app (ver _redirectToUpdaterIfNeeded). +# +# No usa secretos para compilar (URL + anon key son públicas, ya están en +# config/*.json); sí usa RELEASES_REPO_TOKEN (PAT con Contents:write sobre +# soma-pro-releases) para publicar al feed en prod. + +on: + workflow_dispatch: + inputs: + environment: + description: "Entorno / Supabase al que apunta la app" + type: choice + options: + - prod + - dev + default: dev + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout del repo + uses: actions/checkout@v4 + + # Barrera contra publicar código de desarrollo en producción: solo master + # puede generar un build de prod. Mismo motivo que en build-macos.yml. + - name: Verificar rama para builds de prod + if: ${{ inputs.environment == 'prod' && github.ref != 'refs/heads/master' }} + shell: bash + run: | + echo "::error::Los builds de prod solo se publican desde master, no desde ${GITHUB_REF#refs/heads/}." + echo "Si querés compilar desde esta rama para probar, elegí 'dev' en el dropdown." + exit 1 + + # Misma versión que usa Pablo localmente, para que el build sea reproducible. + - name: Instalar Flutter 3.41.6 + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.41.6 + channel: stable + cache: true + + - name: Habilitar Windows desktop + run: flutter config --enable-windows-desktop + + # --- App principal --- + - name: Dependencias app (pub get) + working-directory: flutter_soma_app + run: flutter pub get + + - name: Compilar app release (${{ inputs.environment }}) + working-directory: flutter_soma_app + run: flutter build windows --release --dart-define-from-file=config/${{ inputs.environment }}.json + + # --- Updater --- + - name: Dependencias updater (pub get) + working-directory: soma_pro_updater + run: flutter pub get + + - name: Compilar updater release + working-directory: soma_pro_updater + run: flutter build windows --release + + # --- Empaquetado --- + - name: Armar layout, paquete de update y manifest + id: package + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $pubspec = Get-Content flutter_soma_app\pubspec.yaml -Raw + if ($pubspec -notmatch "version:\s*([\d\.]+)") { + throw "No se pudo leer la versión de pubspec.yaml" + } + $version = $Matches[1] + "version=$version" >> $env:GITHUB_OUTPUT + Write-Host "Empaquetando SOMA PRO v$version (${{ inputs.environment }})" + + # El updater vive en la raíz de instalación; la app real va en `app/` + # para que sus carpetas `data/` no se pisen (ver updater_service.dart). + $stage = "release\windows\SOMA PRO" + if (Test-Path "release") { Remove-Item "release" -Recurse -Force } + New-Item -ItemType Directory -Path "$stage\app" -Force | Out-Null + Copy-Item "soma_pro_updater\build\windows\x64\runner\Release\*" $stage -Recurse + Copy-Item "flutter_soma_app\build\windows\x64\runner\Release\*" "$stage\app" -Recurse + @{ version = $version } | ConvertTo-Json | Set-Content "$stage\version.json" + + # Paquete de actualización: solo lo que va dentro de app/, es lo que + # el updater descarga y extrae sobre app/ cuando hay una versión nueva. + New-Item -ItemType Directory -Path "out" -Force | Out-Null + $updateZip = "out\soma_pro-windows-$version.zip" + Compress-Archive -Path "$stage\app\*" -DestinationPath $updateZip + $sha = (Get-FileHash $updateZip -Algorithm SHA256).Hash.ToLower() + + @{ + version = $version + url = "https://peblo423.github.io/soma-pro-releases/windows/soma_pro-windows-$version.zip" + sha256 = $sha + } | ConvertTo-Json | Set-Content "out\manifest.json" + + - name: Instalar Inno Setup + run: choco install innosetup --no-progress -y + + - name: Armar el instalador (primera instalación) + run: '& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "/DAppEnv=${{ inputs.environment }}" "/DAppVersion=${{ steps.package.outputs.version }}" flutter_soma_app\installer\soma.iss' + + - name: Mover el instalador a out/ + shell: pwsh + run: Copy-Item "flutter_soma_app\installer\Output\*.exe" "out\" + + - name: Subir artifacts + uses: actions/upload-artifact@v4 + with: + name: SOMA-${{ inputs.environment }}-windows + path: | + out/*.exe + out/*.zip + out/manifest.json + if-no-files-found: error + + # Publica el paquete de update al feed público SOLO en prod: el feed es + # único (no separa dev/prod), así que un build dev no debe llegarle a nadie. + - name: Publicar al feed (solo prod) + if: ${{ inputs.environment == 'prod' }} + shell: bash + env: + RELEASES_REPO_TOKEN: ${{ secrets.RELEASES_REPO_TOKEN }} + run: | + set -euo pipefail + if [ -z "${RELEASES_REPO_TOKEN:-}" ]; then + echo "Falta el secret RELEASES_REPO_TOKEN (PAT con Contents:write sobre soma-pro-releases)"; exit 1 + fi + VERSION="${{ steps.package.outputs.version }}" + git clone --depth 1 \ + "https://x-access-token:${RELEASES_REPO_TOKEN}@github.com/peblo423/soma-pro-releases.git" feed + mkdir -p feed/windows + cp "out/soma_pro-windows-$VERSION.zip" feed/windows/ + cp "out/manifest.json" feed/windows/manifest.json + cd feed + git config user.name "soma-ci" + git config user.email "ci@soma.local" + git add windows + if git diff --cached --quiet; then + echo "Sin cambios para publicar." + else + git commit -m "Windows release v$VERSION" + git push + fi