Three related fixes to the release plumbing: - Only PRO bumps the semver now. main was bumping its patch on every push, so it raced permanently ahead of the branch that actually ships (main hit 1.3.3 while PRO sat at 1.3.0), buried release artifacts under a dev branch on the portal, and made every main<->PRO merge conflict on pubspec.yaml. The build number still advances on every branch, since Play requires it to be monotonic across the whole app. - The [version set] marker is now searched across every commit the push introduced, not just the tip. A plain 'git pull' inserts a merge commit with no marker, which silently dropped a pinned name and turned 1.3.0 into 1.3.1. - Artifacts land in a per-branch folder so the portal stops interleaving development and release builds.
341 lines
15 KiB
YAML
341 lines
15 KiB
YAML
name: Build & Deploy PluriWave
|
|
|
|
on:
|
|
push:
|
|
branches: [main, PRO]
|
|
|
|
env:
|
|
PATH: /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
|
ANDROID_HOME: /Users/freetlab/Library/Android/sdk
|
|
KEYSTORE_PATH: /Users/freetlab/.openclaw/workspace/.secure/pluriwave/pluriwave-upload.jks
|
|
KEYSTORE_ALIAS: pluriwave-upload
|
|
PLAY_PACKAGE_NAME: es.freetimelab.pluriwave
|
|
CURRENT_REF: ${{ gitea.ref }}
|
|
|
|
jobs:
|
|
analizar:
|
|
name: Análisis de código
|
|
runs-on: [self-hosted, macos, arm64, flutter]
|
|
steps:
|
|
- name: Clonar rama actual
|
|
run: |
|
|
BRANCH="${CURRENT_REF#refs/heads/}"
|
|
git clone https://ShanaiaBot:${{ secrets.GITEA_TOKEN }}@git.freetimelab.es/FreeTLab/pluriwave.git .
|
|
git fetch origin "$BRANCH"
|
|
git checkout "$BRANCH"
|
|
|
|
- name: Obtener dependencias
|
|
run: flutter pub get
|
|
|
|
- name: Verificar integridad de literales i18n
|
|
run: python3 tool/check_arb_placeholder_corruption.py
|
|
|
|
- name: Analizar código
|
|
run: flutter analyze --no-fatal-infos --no-fatal-warnings
|
|
|
|
- name: Ejecutar tests criticos
|
|
timeout-minutes: 15
|
|
run: |
|
|
flutter test test/servicios/servicio_programacion_alarmas_test.dart test/estado/estado_alarmas_test.dart --concurrency=1 --timeout=60s
|
|
|
|
- name: Limpiar procesos Flutter de tests
|
|
if: always()
|
|
run: pkill -f 'flutter_tester|flutter_tools.snapshot|dartaotruntime' 2>/dev/null || true
|
|
|
|
build:
|
|
name: Build APK + AAB release
|
|
runs-on: [self-hosted, macos, arm64, flutter]
|
|
needs: analizar
|
|
steps:
|
|
- name: Clonar rama actual
|
|
run: |
|
|
BRANCH="${CURRENT_REF#refs/heads/}"
|
|
git clone https://ShanaiaBot:${{ secrets.GITEA_TOKEN }}@git.freetimelab.es/FreeTLab/pluriwave.git .
|
|
git fetch origin "$BRANCH"
|
|
git checkout "$BRANCH"
|
|
|
|
- name: Configurar keystore de firma
|
|
env:
|
|
KEYSTORE_PASSWORD: ${{ secrets.PLURIWAVE_KEYSTORE_PASSWORD }}
|
|
run: |
|
|
if [ ! -f "$KEYSTORE_PATH" ]; then
|
|
echo "ERROR: Keystore no encontrado en $KEYSTORE_PATH"
|
|
exit 1
|
|
fi
|
|
echo "storeFile=$KEYSTORE_PATH" > android/key.properties
|
|
echo "storePassword=$KEYSTORE_PASSWORD" >> android/key.properties
|
|
echo "keyAlias=$KEYSTORE_ALIAS" >> android/key.properties
|
|
echo "keyPassword=$KEYSTORE_PASSWORD" >> android/key.properties
|
|
echo "✅ Keystore configurado"
|
|
|
|
# PRO owns the version NAME; every branch advances the build NUMBER.
|
|
#
|
|
# Previously main also bumped its patch on every push, so main's semver
|
|
# raced permanently ahead of PRO's (main hit 1.3.3 while the branch that
|
|
# actually ships sat at 1.3.0). That buried the release artifacts under a
|
|
# dev branch on builds.freetimelab.es, which sorts by version, and made
|
|
# every main<->PRO merge conflict on pubspec.yaml.
|
|
#
|
|
# The build number still advances everywhere: Google Play requires it to
|
|
# be monotonic across the whole app, so two branches must never mint the
|
|
# same code.
|
|
- name: Bump versión + commit
|
|
run: |
|
|
BRANCH="${CURRENT_REF#refs/heads/}"
|
|
git config user.name "ShanaiaBot"
|
|
git config user.email "shanaia@freetimelab.es"
|
|
CURRENT=$(grep '^version:' pubspec.yaml | awk '{print $2}')
|
|
SEMVER=$(echo "$CURRENT" | cut -d'+' -f1)
|
|
BUILD=$(echo "$CURRENT" | cut -d'+' -f2)
|
|
NEW_BUILD=$((BUILD + 1))
|
|
|
|
# Look for [version set] across EVERY commit this push introduced,
|
|
# not just the tip. `git pull` inserts an auto-generated merge commit
|
|
# whose message carries no marker, which silently discarded a pinned
|
|
# version name and bumped 1.3.0 to 1.3.1 behind our backs.
|
|
RANGO="${{ gitea.event.before }}..${{ gitea.sha }}"
|
|
if git log "$RANGO" --pretty=%B 2>/dev/null | grep -q '\[version set\]'; then
|
|
MARCADOR="si"
|
|
else
|
|
MARCADOR="no"
|
|
fi
|
|
|
|
if [ "$BRANCH" != "PRO" ] || [ "$MARCADOR" = "si" ]; then
|
|
# Non-release branches never touch the name; PRO respects a pin.
|
|
NEW_VERSION="${SEMVER}+${NEW_BUILD}"
|
|
else
|
|
MAJOR=$(echo "$SEMVER" | cut -d. -f1)
|
|
MINOR=$(echo "$SEMVER" | cut -d. -f2)
|
|
PATCH=$(echo "$SEMVER" | cut -d. -f3)
|
|
NEW_PATCH=$((PATCH + 1))
|
|
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}+${NEW_BUILD}"
|
|
fi
|
|
|
|
echo "rama=${BRANCH} marcador=${MARCADOR} ${CURRENT} -> ${NEW_VERSION}"
|
|
sed -i '' "s/^version: .*/version: ${NEW_VERSION}/" pubspec.yaml
|
|
git add pubspec.yaml
|
|
git commit -m "chore: bump version to ${NEW_VERSION} [ci skip]"
|
|
git push origin "HEAD:${BRANCH}"
|
|
|
|
- name: Extraer versión
|
|
id: version
|
|
run: |
|
|
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f1)
|
|
BUILD_NUMBER=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f2)
|
|
COMMIT=$(git rev-parse --short HEAD)
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT"
|
|
echo "commit=$COMMIT" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Obtener dependencias
|
|
run: flutter pub get
|
|
|
|
# OBLIGATORIO en este runner autoalojado, no es higiene opcional.
|
|
#
|
|
# El directorio build/ sobrevive entre ejecuciones y el merge
|
|
# incremental de recursos de Gradle se queda rancio: los drawables
|
|
# ic_auto_eq_on/ic_auto_eq_off (anadidos el 31-07 en 2540556) NUNCA
|
|
# llegaron a entrar en el APK, mientras que ic_stat_pluriwave -- misma
|
|
# carpeta, anadido el 02-07 -- si estaba. Verificado extrayendo el
|
|
# base.apk instalado en el dispositivo: los ficheros no existen ni como
|
|
# entrada del zip ni en resources.arsc.
|
|
#
|
|
# El coste fue semanas de diagnostico equivocado. Cada setState
|
|
# publicaba una CustomAction cuyo icono resolvia a 0, y
|
|
# PlaybackStateCompat.CustomAction.Builder lanza en ese caso, abortando
|
|
# setState antes de activar la sesion de medios: Android Auto se
|
|
# quedaba con la sesion congelada e inactiva. El codigo Dart siempre
|
|
# llegaba porque se recompila; el recurso Android no.
|
|
- name: Limpiar artefactos de compilacion
|
|
run: flutter clean
|
|
|
|
- name: Reinstalar dependencias tras limpiar
|
|
run: flutter pub get
|
|
|
|
- name: Build APK release
|
|
run: flutter build apk --release
|
|
|
|
# Guardian de recursos: el APK debe contener los drawables que el codigo
|
|
# resuelve POR NOMBRE en runtime (getResources().getIdentifier).
|
|
#
|
|
# Un nombre que no resuelve devuelve id 0, y eso no falla la
|
|
# compilacion: falla en el coche. Concretamente
|
|
# PlaybackStateCompat.CustomAction.Builder lanza con icono 0, ese throw
|
|
# aborta AudioService.setState antes de activar la sesion de medios, y
|
|
# Android Auto se queda con la interfaz congelada. Paso exactamente eso
|
|
# entre el 31-07 (commit 2540556) y el 07-08 sin que nada lo detectara.
|
|
#
|
|
# Anadir un drawable nuevo referenciado por nombre => anadirlo aqui.
|
|
# Guardian de recursos: el APK debe contener los drawables que el codigo
|
|
# resuelve POR NOMBRE en runtime (getResources().getIdentifier).
|
|
#
|
|
# Un nombre que no resuelve devuelve id 0, y eso no falla la
|
|
# compilacion: falla en el coche. PlaybackStateCompat.CustomAction
|
|
# .Builder lanza con icono 0, ese throw aborta AudioService.setState
|
|
# antes de activar la sesion de medios, y Android Auto se queda con la
|
|
# interfaz congelada. Paso exactamente eso desde el 31-07 (commit
|
|
# 2540556) sin que nada lo detectara.
|
|
#
|
|
# La primera version de este paso daba FALSOS POSITIVOS: no comprobaba
|
|
# que el APK existiera ni que unzip estuviera disponible, asi que
|
|
# cualquier fallo de la tuberia se reportaba como "faltan todos los
|
|
# recursos". Un guardian que miente es peor que no tener guardian:
|
|
# manda a buscar fantasmas. De ahi que ahora verifique primero sus
|
|
# propias herramientas y vuelque el inventario real antes de juzgar.
|
|
#
|
|
# Anadir un drawable nuevo referenciado por nombre => anadirlo aqui.
|
|
# Guardian de recursos: el APK debe contener los drawables que el codigo
|
|
# resuelve POR NOMBRE en runtime (getResources().getIdentifier).
|
|
#
|
|
# Un nombre que no resuelve devuelve id 0. Eso no falla la compilacion:
|
|
# falla en el coche. PlaybackStateCompat.CustomAction.Builder lanza con
|
|
# icono 0, ese throw aborta AudioService.setState antes de activar la
|
|
# sesion de medios, y Android Auto se queda con la interfaz congelada.
|
|
# Paso exactamente eso desde el 31-07 (commit 2540556) sin deteccion.
|
|
#
|
|
# Se inspecciona resources.arsc, NO las rutas del zip: el APK release
|
|
# acorta/renombra las rutas de recursos (una version anterior de este
|
|
# paso listo "ningun drawable" en un APK de 105MB, que es imposible).
|
|
# Los NOMBRES de recurso siguen en la tabla pase lo que pase.
|
|
#
|
|
# El centinela existe porque este guardian ya mintio una vez: al no
|
|
# validar su propio metodo, reporto como ausente hasta un recurso que
|
|
# estaba verificado presente. Si el centinela no aparece, la inspeccion
|
|
# no es fiable y NO tenemos derecho a declarar nada ausente.
|
|
#
|
|
# Anadir un drawable nuevo referenciado por nombre => anadirlo aqui.
|
|
- name: Verificar recursos criticos en el APK
|
|
run: |
|
|
set -u
|
|
APK=build/app/outputs/flutter-apk/app-release.apk
|
|
CENTINELA=station_art_nova
|
|
|
|
if [ ! -f "$APK" ]; then
|
|
echo "El APK no esta donde se esperaba: $APK"
|
|
find build/app/outputs -name '*.apk' 2>/dev/null || echo " (nada)"
|
|
exit 1
|
|
fi
|
|
echo "APK: $APK ($(wc -c < "$APK") bytes)"
|
|
|
|
if ! command -v unzip >/dev/null 2>&1; then
|
|
echo "unzip no esta disponible: no se puede inspeccionar el APK."
|
|
exit 1
|
|
fi
|
|
|
|
ARSC=$(mktemp)
|
|
unzip -p "$APK" resources.arsc > "$ARSC" 2>/dev/null || true
|
|
if [ ! -s "$ARSC" ]; then
|
|
echo "No se pudo extraer resources.arsc del APK."
|
|
exit 1
|
|
fi
|
|
echo "resources.arsc: $(wc -c < "$ARSC") bytes"
|
|
|
|
if ! grep -a -q "$CENTINELA" "$ARSC"; then
|
|
echo "El centinela '$CENTINELA' no aparece en la tabla de recursos."
|
|
echo "La inspeccion no es fiable; no se declara nada ausente."
|
|
exit 1
|
|
fi
|
|
echo "Centinela '$CENTINELA' localizado: la inspeccion es fiable."
|
|
|
|
FALTAN=0
|
|
for RECURSO in ic_auto_eq_on ic_auto_eq_off ic_stat_pluriwave; do
|
|
if grep -a -q "$RECURSO" "$ARSC"; then
|
|
echo "OK $RECURSO"
|
|
else
|
|
echo "FALTA $RECURSO"
|
|
FALTAN=$((FALTAN + 1))
|
|
fi
|
|
done
|
|
|
|
if [ "$FALTAN" -ne 0 ]; then
|
|
echo ""
|
|
echo "$FALTAN drawable(s) resueltos por nombre NO estan en el APK."
|
|
echo "En runtime resolveran a id 0 y tumbaran la sesion de medios."
|
|
exit 1
|
|
fi
|
|
echo "Todos los recursos criticos viajan en el APK."
|
|
|
|
- name: Build AAB release
|
|
run: flutter build appbundle --release
|
|
|
|
# El nombre lleva RAMA y CÓDIGO DE VERSIÓN, no solo el semver.
|
|
#
|
|
# Antes, cada build de 1.3.0 se llamaba `pluriwave-v1.3.0.aab` y caía en
|
|
# la misma carpeta, así que main y PRO se pisaban y tres builds distintos
|
|
# eran indistinguibles una vez descargados: el navegador los guarda como
|
|
# "(1)", "(2)"... y ya no se sabe cuál es cuál. Eso costó subir a Play
|
|
# Console un código de versión ya usado, dos veces.
|
|
#
|
|
# Con `pluriwave-PRO-v1.3.0+156.aab` el archivo se identifica solo,
|
|
# incluso semanas después y fuera de este repo.
|
|
- name: Publicar en ftl-builds (Zimaboard)
|
|
run: |
|
|
VERSION="${{ steps.version.outputs.version }}"
|
|
BUILD_NUMBER="${{ steps.version.outputs.build_number }}"
|
|
BRANCH="${CURRENT_REF#refs/heads/}"
|
|
ETIQUETA="${BRANCH}-v${VERSION}+${BUILD_NUMBER}"
|
|
APK_NOMBRE="pluriwave-${ETIQUETA}.apk"
|
|
AAB_NOMBRE="pluriwave-${ETIQUETA}.aab"
|
|
# Carpeta por rama: main y PRO ya no se mezclan en el portal, que
|
|
# ordena por número de versión y por tanto mostraba el build de
|
|
# desarrollo como "última versión" por delante del de release.
|
|
DESTINO="/opt/ftl-builds/builds/pluriwave/${BRANCH}/v${VERSION}"
|
|
SSH_KEY="/Users/freetlab/.openclaw/workspace/.secure/zimaboard_ed25519"
|
|
|
|
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ShanaiaBot@192.168.0.33 "mkdir -p ${DESTINO}"
|
|
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no \
|
|
build/app/outputs/flutter-apk/app-release.apk \
|
|
"ShanaiaBot@192.168.0.33:${DESTINO}/${APK_NOMBRE}"
|
|
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no \
|
|
build/app/outputs/bundle/release/app-release.aab \
|
|
"ShanaiaBot@192.168.0.33:${DESTINO}/${AAB_NOMBRE}"
|
|
echo "✅ APK: builds.freetimelab.es → pluriwave → v${VERSION} → ${APK_NOMBRE}"
|
|
echo "✅ AAB: builds.freetimelab.es → pluriwave → v${VERSION} → ${AAB_NOMBRE}"
|
|
|
|
- name: Preparar credenciales de Google Play
|
|
if: ${{ gitea.ref == 'refs/heads/PRO' }}
|
|
env:
|
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
|
run: |
|
|
if [ -z "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" ]; then
|
|
echo "ERROR: falta el secreto GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"
|
|
exit 1
|
|
fi
|
|
mkdir -p fastlane/credentials
|
|
printf '%s' "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > fastlane/credentials/google-play-service-account.json
|
|
|
|
- name: Instalar Fastlane
|
|
if: ${{ gitea.ref == 'refs/heads/PRO' }}
|
|
run: |
|
|
gem list -i fastlane >/dev/null 2>&1 || gem install fastlane --no-document
|
|
|
|
- name: Publicar AAB en Google Play Internal Testing
|
|
if: ${{ gitea.ref == 'refs/heads/PRO' }}
|
|
env:
|
|
PLAY_JSON_KEY_PATH: fastlane/credentials/google-play-service-account.json
|
|
PLAY_AAB_PATH: build/app/outputs/bundle/release/app-release.aab
|
|
PLAY_TRACK: internal
|
|
PLAY_RELEASE_STATUS: completed
|
|
run: fastlane android upload_internal
|
|
|
|
- name: Notificar Telegram
|
|
if: always()
|
|
run: |
|
|
VERSION="${{ steps.version.outputs.version }}"
|
|
COMMIT="${{ steps.version.outputs.commit }}"
|
|
BRANCH="${CURRENT_REF#refs/heads/}"
|
|
BOT_TOKEN=$(plutil -extract 'EnvironmentVariables:TELEGRAM_BOT_TOKEN' raw /Users/freetlab/Library/LaunchAgents/ai.openclaw.gateway.plist 2>/dev/null || echo "")
|
|
if [ -z "$BOT_TOKEN" ]; then exit 0; fi
|
|
if [ "${{ job.status }}" = "success" ]; then
|
|
MSG="✅ *PluriWave* v${VERSION} · rama ${BRANCH} · ${COMMIT}%0AAPK + AAB generados"
|
|
if [ "$BRANCH" = "PRO" ]; then
|
|
MSG="${MSG}%0APublicado en Google Play · Internal Testing"
|
|
else
|
|
MSG="${MSG}%0APublicado en builds.freetimelab.es"
|
|
fi
|
|
else
|
|
MSG="❌ *PluriWave* build FAILED · rama ${BRANCH} · ${COMMIT}"
|
|
fi
|
|
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
-d "chat_id=221721467" -d "parse_mode=Markdown" -d "text=${MSG}" || true
|