diff --git a/.gitea/workflows/apk.yml b/.gitea/workflows/apk.yml new file mode 100644 index 0000000..19e7429 --- /dev/null +++ b/.gitea/workflows/apk.yml @@ -0,0 +1,53 @@ +# Construye la APK y la deja publicada como borrador en las releases. +# +# NO se dispara sola con cada commit: la APK son mas de 100 MB y el backend tarda +# en empaquetarse. Se lanza a mano desde la interfaz de gitea, o al crear un tag. +# +# El runner necesita: JDK 17 con compilador, el SDK de Android (plataforma 35 y +# build-tools), node, zip y unzip. Un runner pelado NO vale. +# +# Secretos que hay que dar de alta en el repositorio: +# OASIS_KEYSTORE_B64 el almacen de claves en base64 +# OASIS_KEYSTORE_PASS su contraseƱa +# OASIS_KEY_ALIAS el alias (por defecto "oasis") +# +# El token para publicar lo pone gitea solo en GITEA_TOKEN. + +name: APK + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # release.sh cuenta commits para el versionCode + + - name: Restaurar el almacen de claves + run: | + echo "${{ secrets.OASIS_KEYSTORE_B64 }}" | base64 -d > /tmp/oasis.jks + chmod 600 /tmp/oasis.jks + + - name: Construir, verificar y publicar + env: + JAVA_HOME: /usr/lib/jvm/temurin-17-jdk-amd64 + ANDROID_HOME: /opt/android-sdk + OASIS_KEYSTORE: /tmp/oasis.jks + OASIS_KEYSTORE_PASS: ${{ secrets.OASIS_KEYSTORE_PASS }} + OASIS_KEY_ALIAS: ${{ secrets.OASIS_KEY_ALIAS }} + OASIS_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + OASIS_GITEA_URL: ${{ github.server_url }} + OASIS_GITEA_REPO: ${{ github.repository }} + run: | + cd android + ./scripts/release.sh --release --publish + + - name: Borrar el almacen de claves + if: always() + run: rm -f /tmp/oasis.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9d38c5f..6ad85c7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -14,8 +14,9 @@ android { applicationId = "net.laenre.oasis" minSdk = 24 targetSdk = 35 - versionCode = 1 - versionName = "0.9.5" + // el pipeline los pasa por entorno; a mano valen los de aqui + versionCode = (System.getenv("OASIS_VERSION_CODE") ?: "1").toInt() + versionName = System.getenv("OASIS_VERSION_NAME") ?: "0.9.5" ndk { abiFilters += listOf("arm64-v8a") } } diff --git a/android/scripts/release.sh b/android/scripts/release.sh new file mode 100755 index 0000000..5fedb05 --- /dev/null +++ b/android/scripts/release.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# +# Construye la APK, la verifica y (si se le pasa un token) publica la release en +# gitea. Pensado para lanzarlo a mano o desde un runner; hace lo mismo en los dos +# sitios, que es lo que evita el clasico "en mi maquina si sale". +# +# Uso: +# ./scripts/release.sh # compila debug y verifica +# ./scripts/release.sh --release # compila release firmada +# ./scripts/release.sh --release --publish +# +# Variables: +# OASIS_KEYSTORE, OASIS_KEYSTORE_PASS, OASIS_KEY_ALIAS firma (obligatorias con --release) +# OASIS_GITEA_TOKEN publicar (obligatoria con --publish) +# OASIS_APK_BASE APK del que sacar node_modules la primera vez +# OASIS_GITEA_URL por defecto https://gitea.laenre.net +# OASIS_GITEA_REPO por defecto hacklab/OASIS_MOBILE +# ANDROID_HOME por defecto ~/Android/Sdk +# JAVA_HOME un JDK 17 o superior CON compilador +# +set -euo pipefail + +HERE="$(cd "$(dirname "$0")/.." && pwd)" +REPO="$(cd "$HERE/.." && pwd)" +cd "$HERE" + +MODE=debug +PUBLISH=0 +for arg in "$@"; do + case "$arg" in + --release) MODE=release ;; + --publish) PUBLISH=1 ;; + *) echo "opcion desconocida: $arg"; exit 2 ;; + esac +done + +GITEA_URL="${OASIS_GITEA_URL:-https://gitea.laenre.net}" +GITEA_REPO="${OASIS_GITEA_REPO:-hacklab/OASIS_MOBILE}" +SDK="${ANDROID_HOME:-$HOME/Android/Sdk}" + +say() { printf '\n== %s\n' "$*"; } +die() { printf '\n!! %s\n' "$*" >&2; exit 1; } + +# --- 1) version ------------------------------------------------------------- +# El nombre sale de package.json, que es de donde lo saca tambien el backend, para +# que la version que muestra la aplicacion y la de la APK no puedan discrepar. +VERSION_NAME="${OASIS_VERSION_NAME:-$(node -p "require('$REPO/package.json').version" 2>/dev/null || echo 0.0.0)}" +# El codigo tiene que subir en cada publicacion o Android rechaza la instalacion: +# el numero de commits sirve y no hay que llevarlo a mano. +VERSION_CODE="${OASIS_VERSION_CODE:-$(git -C "$REPO" rev-list --count HEAD 2>/dev/null || echo 1)}" +TAG="v${VERSION_NAME}-${VERSION_CODE}" +export OASIS_VERSION_NAME="$VERSION_NAME" OASIS_VERSION_CODE="$VERSION_CODE" +say "version ${VERSION_NAME} (codigo ${VERSION_CODE}), modo ${MODE}" + +# --- 2) comprobaciones previas --------------------------------------------- +[ -n "${JAVA_HOME:-}" ] || die "define JAVA_HOME apuntando a un JDK con compilador" +[ -x "${JAVA_HOME}/bin/javac" ] || die "en ${JAVA_HOME} no hay javac: es un runtime, no un JDK" +[ -d "$SDK" ] || die "no encuentro el SDK de Android en $SDK (define ANDROID_HOME)" + +if [ "$MODE" = release ]; then + [ -n "${OASIS_KEYSTORE:-}" ] || die "--release necesita OASIS_KEYSTORE" + [ -f "$OASIS_KEYSTORE" ] || die "no existe el almacen de claves: $OASIS_KEYSTORE" + [ -n "${OASIS_KEYSTORE_PASS:-}" ] || die "--release necesita OASIS_KEYSTORE_PASS" +fi +if [ "$PUBLISH" = 1 ]; then + [ -n "${OASIS_GITEA_TOKEN:-}" ] || die "--publish necesita OASIS_GITEA_TOKEN" + [ "$MODE" = release ] || die "no se publica una APK de depuracion" +fi + +# --- 3) backend empaquetado ------------------------------------------------- +# prepare.sh es idempotente pero tarda; se rehace siempre porque el backend cambia +# en cada commit y una APK con el backend viejo es peor que no tener APK. +# La primera vez hace falta un APK base del que sacar node_modules: el del movil +# esta podado y sin binarios nativos, y un npm install normal no lo reproduce. +say "empaquetando el backend" +./scripts/prepare.sh ${OASIS_APK_BASE:+"$OASIS_APK_BASE"} \ + || die "prepare.sh ha fallado (define OASIS_APK_BASE con un APK del que sacar node_modules)" + +ZIP=app/src/main/assets/nodejs-project.zip +[ -f "$ZIP" ] || die "falta $ZIP" +ZIP_MB=$(( $(stat -c%s "$ZIP") / 1024 / 1024 )) +[ "$ZIP_MB" -ge 20 ] || die "el backend empaquetado son $ZIP_MB MB: se ha quedado a medias" +say "backend: ${ZIP_MB} MB" + +# --- 4) compilar ------------------------------------------------------------ +say "compilando" +if [ "$MODE" = release ]; then + ./gradlew --quiet assembleRelease + APK=$(ls app/build/outputs/apk/release/*.apk | head -1) +else + ./gradlew --quiet assembleDebug + APK=$(ls app/build/outputs/apk/debug/*.apk | head -1) +fi +[ -f "$APK" ] || die "no se ha generado ninguna APK" +say "APK: $APK ($(( $(stat -c%s "$APK") / 1024 / 1024 )) MB)" + +# --- 5) verificar ----------------------------------------------------------- +# Estas tres comprobaciones son las que han cazado fallos de verdad: una APK que +# compila e instala puede no llevar el backend dentro, o haber perdido los permisos. +BT=$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1) +[ -n "$BT" ] || die "no encuentro build-tools en $SDK" + +say "verificando" +# ojo con pipefail: grep -q cierra la tuberia y el productor muere con SIGPIPE, +# asi que se vuelca a fichero antes de mirar +LISTING=$(mktemp); PERMS=$(mktemp) +trap 'rm -f "$LISTING" "$PERMS"' EXIT +unzip -l "$APK" > "$LISTING" +"$BT/aapt2" dump permissions "$APK" > "$PERMS" 2>/dev/null || true + +grep -q "assets/nodejs-project.zip" "$LISTING" || die "la APK no lleva el backend dentro" +grep -q "android.permission.CAMERA" "$PERMS" || die "la APK no declara CAMERA: no podria hacer videollamadas" +grep -q "android.permission.RECORD_AUDIO" "$PERMS" || die "la APK no declara RECORD_AUDIO" +grep -q "libnode.so" "$LISTING" || die "la APK no lleva el runtime de node" + +if [ "$MODE" = release ]; then + "$BT/apksigner" verify --print-certs "$APK" > /tmp/oasis-signer.txt 2>&1 \ + || die "la APK no esta firmada correctamente" + FINGERPRINT=$(grep -m1 "SHA-256 digest" /tmp/oasis-signer.txt | awk '{print $NF}') + say "huella del certificado: $FINGERPRINT" +fi + +SHA=$(sha256sum "$APK" | cut -d' ' -f1) +say "sha256 de la APK: $SHA" + +# --- 6) publicar ------------------------------------------------------------ +if [ "$PUBLISH" != 1 ]; then + say "listo (sin publicar)" + exit 0 +fi + +say "publicando $TAG en $GITEA_REPO" +API="$GITEA_URL/api/v1/repos/$GITEA_REPO" +AUTH="Authorization: token $OASIS_GITEA_TOKEN" + +NOTES=$(printf 'Rama experimental del hacklab sobre el Oasis de epsylon.\n\nNO es Oasis oficial: https://github.com/epsylon/oasis\n\nVersion base: %s (codigo %s)\n\nsha256 de la APK: %s\nHuella del certificado: %s\n\nComprueba la huella antes de instalar:\n\n apksigner verify --print-certs oasis.apk\n' \ + "$VERSION_NAME" "$VERSION_CODE" "$SHA" "${FINGERPRINT:-sin firmar}") + +RELEASE_ID=$(curl -sf -X POST "$API/releases" -H "$AUTH" -H "Content-Type: application/json" \ + -d "$(node -e ' + const [tag, name, body] = process.argv.slice(1); + process.stdout.write(JSON.stringify({ tag_name: tag, name, body, draft: true, prerelease: true })); + ' "$TAG" "Oasis Mobile $TAG" "$NOTES")" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id' 2>/dev/null) \ + || die "no se ha podido crear la release (token sin permiso, o el tag ya existe)" + +curl -sf -X POST "$API/releases/$RELEASE_ID/assets?name=oasis-$TAG.apk" \ + -H "$AUTH" -F "attachment=@$APK" >/dev/null \ + || die "la release se ha creado pero no se ha podido subir la APK" + +say "publicada como borrador: $GITEA_URL/$GITEA_REPO/releases" +say "revisala y quita el borrador a mano; no se publica sola a proposito"