Desde el 29-08 ningun build de main volvio a aparecer en builds.freetimelab.es, y nada avisaba: el job salia verde porque el scp subia correctamente, solo que a una ruta que el indexador no lee. El portal indexa DOS niveles, <app>/<version>/<ficheros>. La solucion anterior metia la rama como TERCER nivel (pluriwave/main/v1.3.3/), asi que sus ficheros quedaban fuera del indice: 39 versiones listadas, ninguna con "main", ninguna con los codigos 158, 159 ni 160. Encima el echo del propio paso tenia "pluriwave" escrito a fuego y anunciaba la carpeta antigua, congelada desde el +157, que es justo donde se miraba al no encontrar nada. Ahora la rama va en el NOMBRE DE LA APP, que es lo que el portal si entiende. PRO conserva la entrada limpia "pluriwave" y main tiene la suya, igual que ya conviven radar-foral y radar-foral-android. Se cumple el objetivo original -- que el build de desarrollo no aparezca como ultima version de release -- sin romper el indexado. La ruta impresa se deriva de la misma variable, para que no puedan volver a desincronizarse. Se alinea tambien el arreglo del secreto de Google Play que ya esta en PRO: el paso se omite con un aviso en vez de hacer `exit 1`, y el mensaje de Telegram deja de afirmar que se publico algo que no se publico. main nunca ejecuta esos pasos, pero mantener un unico workflow evita conflictos en cada trasvase entre ramas.
381 lines
18 KiB
YAML
381 lines
18 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"
|
|
# La rama va en el NOMBRE DE LA APP, no en una subcarpeta.
|
|
#
|
|
# El objetivo sigue siendo el de siempre: que main y PRO no se mezclen
|
|
# en el portal, que ordena por número de versión y mostraba el build
|
|
# de desarrollo como "última versión" por delante del de release.
|
|
#
|
|
# Pero la primera solución metía la rama como TERCER nivel
|
|
# (pluriwave/main/v1.3.3/) y el portal indexa solo DOS —
|
|
# <app>/<versión>/<ficheros> —, así que desde el 29-08 ningún build de
|
|
# main volvió a aparecer en builds.freetimelab.es aunque el job saliera
|
|
# verde: el scp subía bien, a una ruta que el indexador no lee. Nada
|
|
# avisaba, y el echo de abajo se comía la rama y mandaba a la carpeta
|
|
# antigua, que llevaba congelada desde el +157.
|
|
#
|
|
# Con la rama en el nombre, PRO conserva la entrada limpia "pluriwave"
|
|
# y main tiene la suya, igual que ya conviven radar-foral y
|
|
# radar-foral-android.
|
|
if [ "$BRANCH" = "PRO" ]; then
|
|
APP="pluriwave"
|
|
else
|
|
APP="pluriwave-$(echo "$BRANCH" | tr '/' '-')"
|
|
fi
|
|
DESTINO="/opt/ftl-builds/builds/${APP}/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}"
|
|
# La ruta se imprime desde ${APP}, no a mano: la version anterior tenia
|
|
# "pluriwave" escrito a fuego y mandaba a la carpeta equivocada cada
|
|
# vez que se compilaba algo que no fuera PRO.
|
|
echo "✅ APK: builds.freetimelab.es → ${APP} → v${VERSION} → ${APK_NOMBRE}"
|
|
echo "✅ AAB: builds.freetimelab.es → ${APP} → 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.
|
|
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
|