El paso que prepara las credenciales hacia `exit 1` si no encontraba GOOGLE_PLAY_SERVICE_ACCOUNT_JSON, y ese secreto no se ha configurado nunca: TODA compilacion de PRO terminaba en rojo por una automatizacion que jamas llego a activarse, mientras los AAB se subian a Play a mano. Un rojo permanente entrena a ignorar los rojos, y entonces el dia que falle algo de verdad tampoco se mira. Ahora el paso se omite con un aviso y expone `disponible`, del que dependen los dos pasos siguientes. No se pierde nada: el AAB ya esta compilado, firmado y subido a ftl-builds antes de llegar aqui. El dia que se configure el secreto, los tres pasos se reactivan solos sin tocar el workflow. El aviso de Telegram deja de afirmar "Publicado en Google Play" cuando la subida se ha omitido. Un mensaje que dice que se publico algo que no se publico es peor que no mandar mensaje.
337 lines
16 KiB
YAML
337 lines
16 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"
|
|
|
|
- name: Bump versión patch + 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))
|
|
# If the triggering commit explicitly pins the version name via the
|
|
# [version set] marker, ship that semver as-is (a milestone like 1.0.0
|
|
# or a major/minor jump the automatic patch bump cannot reach) and only
|
|
# advance the build number, which Google Play requires to stay
|
|
# monotonic. Otherwise keep the default automatic patch+build bump.
|
|
if git log -1 --pretty=%B | grep -q '\[version set\]'; then
|
|
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
|
|
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"
|
|
DESTINO="/opt/ftl-builds/builds/pluriwave/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}"
|
|
|
|
# La publicacion automatica en Google Play es OPCIONAL.
|
|
#
|
|
# Este paso hacia `exit 1` cuando faltaba el secreto, asi que TODA
|
|
# compilacion de PRO terminaba en rojo por una funcion que nunca llego a
|
|
# activarse: el secreto no se configuro nunca y las subidas a Play se han
|
|
# hecho siempre a mano. Un rojo permanente entrena a ignorar los rojos, y
|
|
# entonces el dia que falle algo de verdad tampoco se mira.
|
|
#
|
|
# Ahora se omite con un aviso. El AAB ya esta compilado, firmado y subido
|
|
# a ftl-builds por el paso anterior, asi que no se pierde nada. El dia que
|
|
# se configure el secreto, los tres pasos se activan solos.
|
|
- name: Preparar credenciales de Google Play
|
|
id: credenciales_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 "disponible=no" >> "$GITHUB_OUTPUT"
|
|
echo "AVISO: falta el secreto GOOGLE_PLAY_SERVICE_ACCOUNT_JSON."
|
|
echo "Se omite la publicacion en Google Play; sube el AAB a mano."
|
|
exit 0
|
|
fi
|
|
mkdir -p fastlane/credentials
|
|
printf '%s' "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > fastlane/credentials/google-play-service-account.json
|
|
echo "disponible=si" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Instalar Fastlane
|
|
if: ${{ gitea.ref == 'refs/heads/PRO' && steps.credenciales_play.outputs.disponible == 'si' }}
|
|
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' && steps.credenciales_play.outputs.disponible == 'si' }}
|
|
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"
|
|
# Solo se anuncia la subida a Play cuando de verdad ocurrio: el paso
|
|
# se omite si falta el secreto, y un aviso que dice "publicado"
|
|
# cuando no se publico es peor que no avisar.
|
|
if [ "$BRANCH" = "PRO" ] && [ "${{ steps.credenciales_play.outputs.disponible }}" = "si" ]; then
|
|
MSG="${MSG}%0APublicado en Google Play · Internal Testing"
|
|
elif [ "$BRANCH" = "PRO" ]; then
|
|
MSG="${MSG}%0AEn builds.freetimelab.es · sube el AAB a Play a mano"
|
|
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
|