Files
pluriwave/.gitea/workflows/build.yml
T
FreeTLab a99df5d055
Build & Deploy PluriWave / Análisis de código (push) Successful in 28s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m44s
ci: let PRO own the version name and split artifacts by branch [version set]
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.
2026-08-29 10:59:20 +02:00

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