Files
sf-app-platform/docs/codemagic-integration.md
JulianAlcala 298f73b02b ci(codemagic): add staging + production workflows for legacy store builds
Codemagic se usa como herramienta de release (Play Store + App Store),
no como CI general. La validación de develop / feature branches sigue
siendo local.

Workflows:
- staging_android / staging_ios — push a fusion-app → Play Internal Track + TestFlight
- production_android / production_ios — tag 1.0.0(N) → Play (draft) + App Store (manual review)

Todos los workflows publicables buildean en APP_MODE=legacy mientras
el roadmap premium de Treezor no esté listo para producción.

Scripts en codemagic_scripts/:
- project_setup.sh — melos bootstrap (build_runner no es necesario,
  los .freezed.dart y .g.dart están commiteados)
- analyze_and_test.sh — melos run analyze + test con --no-select
  obligatorio (sin TTY melos crashea con StdinException)
- android_dependencies.sh — verifica gradle wrapper
- android_key_properties.sh — escribe key.properties desde CM_KEYSTORE_*
- ios_signing_cocoapods.sh — gem install cocoapods + xcode-project use-profiles
- flutter_build.sh — build aab/ipa con --dart-define=APP_MODE; parser
  para tags formato 1.0.0(N) o v1.0.0(N) en producción
- print_versions.sh — log de versiones y herramientas
- build_status.sh — exit + notificación Google Chat ante fallo

Setup detallado y bloqueantes externos en docs/codemagic-integration.md.
2026-05-12 07:32:52 -05:00

20 KiB
Raw Blame History

Codemagic integration plan

Guía para integrar Codemagic en sf-app-platform. Codemagic se usa exclusivamente como herramienta de release a tiendas (Play Store + App Store), no como CI general. La validación de develop / feature branches sigue siendo local (melos run analyze, melos run test, flutter build manual).

El plan está adaptado a la realidad de este repo (monorepo Melos + pub workspace, configs gitignored, generated files commiteados, fusión legacy↔payment en curso).

Decisiones tomadas

Tema Decisión Por qué
Alcance Solo 4 workflows: staging + production × Android + iOS Codemagic = herramienta de release. Development no se corre en CI — la validación es local
Triggers fusion-app → staging, tag → prod La rama main no está activa; fusion-app es el integration branch durante la fusión legacy↔payment
Formato de tag prod v1.0.0(N) / 1.0.0(N) (formato actual) Mantener convención existente — última release fue v1.0.0(8)
Configs JSON Codemagic Encrypted Files (solo staging + production) Más seguro y auditable que env vars; el repo los tiene gitignored y Codemagic los materializa antes del build
Code generation No correr build_runner en CI 355 .freezed.dart/.g.dart están commiteados — solo melos bootstrap
Quality gates melos run analyze --no-select + melos run test --no-select bloqueantes Si alguien pushea código roto a fusion-app, no queremos que llegue a Play Internal Track ni TestFlight. --no-select es obligatorio (sin TTY melos crashea)
Versionado version desde pubspec.yaml (manual), build number desde tag en prod Como hoy. CI no toca pubspec.yaml
APP_MODE en builds a tiendas Siempre legacy Aunque la rama fusion-app integra legacy+payment, los AAB/IPA publicados a App Store y Play Store deben arrancar en flujo legacy hasta que el roadmap premium de Treezor esté listo para producción
Web No incluido sf-app-platform es mobile-only

Cosas del repo que importan (chequeadas, no asumidas)

  • Entry points: apps/mobile_app/lib/main_{development,staging,production}.dart
  • Configs (gitignored): apps/mobile_app/config/{development,staging,production}.json — claves: env, apiBaseUrl, apiOrigin, wsUrl, juphoonAppKey
  • iOS schemes por flavor: development.xcscheme, staging.xcscheme, production.xcscheme (ya existen)
  • iOS Firebase configs por flavor: apps/mobile_app/ios/flavors/{dev,stag,prod}/GoogleService-Info.plist (ya existen — el script Build Phase los copia)
  • Android Firebase configs: ya están configuradas. apps/mobile_app/android/app/google-services.json (root) contiene los packages com.savefamily.app.dev Y com.savefamily.app.stag (project sf-platform-pre); src/production/google-services.json override para com.savefamily.app (project sf-platform-pro). Commits de referencia: 81284d7e, e83adbfd
  • Bundle IDs:
    • iOS / Android: com.savefamily.app.dev, com.savefamily.app.stag, com.savefamily.app
  • Bridges nativos: flutter_treezor_entrust_sdk_bridge, sca_treezor son path: dependencies locales. No requieren maven privado ni Pod source extra
  • Native repos: solo google() + mavenCentral(). Cero credenciales adicionales en Gradle
  • AntelopAppDelegate caveat: el swizzling de FCM se rompe si se regenera Runner.xcodeproj. CI no debe regenerar el proyecto Xcode
  • iOS deployment target: 15.0 mínimo
  • Dart SDK: ^3.9.2 → Flutter ≥ 3.27 (requerido por resolution: workspace en pubspec raíz)
  • App Store Connect / Apple Developer: Team KQ73NP6L4Q, Account Holder Jorge Alvarez — la API key para Codemagic la tiene que generar él
  • App Store IDs: staging 6759873957, prod 6759875039
  • Play Internal track: prod Android track id 4700720707717247246

Estrategia de configs vía Encrypted Files

En Codemagic UI → Environment variables → Add encrypted file. Subir cada JSON con Path on virtual machine = path relativo al build dir:

Archivo local Path en VM
apps/mobile_app/config/staging.json apps/mobile_app/config/staging.json
apps/mobile_app/config/production.json apps/mobile_app/config/production.json

Codemagic los desencripta en disco antes de los scripts. No hace falta script de materialización. Si una key rota (ej. juphoonAppKey), se re-sube el archivo desde la UI sin tocar el repo ni env vars.

Archivos a agregar al repo

sf-app-platform/
├── codemagic.yaml
└── codemagic_scripts/
    ├── project_setup.sh           # melos bootstrap (NO build_runner)
    ├── android_dependencies.sh    # verifica gradle wrapper, flutter doctor
    ├── android_key_properties.sh  # escribe key.properties desde CM_KEYSTORE_*
    ├── ios_signing_cocoapods.sh   # gem install cocoapods, xcode-project use-profiles
    ├── flutter_build.sh           # build aab / ipa con parser para 1.0.0(N)
    ├── print_versions.sh          # log de versiones
    ├── analyze_and_test.sh        # melos run analyze && melos run test
    └── build_status.sh            # report final + Google Chat fallo

Permisos: chmod +x codemagic_scripts/*.sh.

Scripts

project_setup.sh

#!/bin/bash
set -eu

dart pub global activate melos 7.5.1
melos bootstrap

Nada de build_runner: los .freezed.dart y .g.dart ya están commiteados (355 archivos). Si en el futuro decidimos dejar de commitearlos, se agrega melos run generate acá.

analyze_and_test.sh

#!/bin/bash
set -euo pipefail

melos run analyze --no-select
melos run test --no-select

Los dos targets ya están definidos en el bloque melos.scripts del pubspec.yaml raíz.

Críticos (aprendidos en Fase 0):

  • --no-select es obligatorio en non-TTY. Sin esto, melos prompts interactivamente y crashea con StdinException
  • set -o pipefail evita que pipes downstream enmascaren un exit code de fallo

android_dependencies.sh

#!/bin/bash
set -eu

cd apps/mobile_app/android
./gradlew --version

Diferencia con effi: no se regenera el wrapper (gradle wrapper --gradle-version 7.4). El repo ya commitea el wrapper actual; regenerarlo en cada build es ruido.

android_key_properties.sh

#!/bin/bash
set -eu

cat > "$CM_BUILD_DIR/apps/mobile_app/android/key.properties" <<EOF
storePassword=$CM_KEYSTORE_PASSWORD
keyPassword=$CM_KEY_PASSWORD
keyAlias=$CM_KEY_ALIAS
storeFile=$CM_KEYSTORE_PATH
EOF

El build.gradle.kts busca rootProject.file("key.properties")apps/mobile_app/android/key.properties.

ios_signing_cocoapods.sh

#!/bin/bash
set -eu

gem install cocoapods -v $COCOAPODS_VERSION
xcode-project use-profiles --project apps/mobile_app/ios/Runner.xcodeproj

xcode-project viene preinstalado en imágenes Mac mini de Codemagic.

flutter_build.sh

Parser adaptado al formato real 1.0.0(N) y pubspec.yaml: version: 1.0.0+10. APP_MODE por flavor: staging y production fuerzan legacy (lo que se publica a tiendas); development queda en el default del código (legacy, pero se puede sobreescribir vía un 3er argumento si en el futuro queremos un smoke test del flujo payment).

#!/bin/bash
set -eu

APP_PLATFORM=$1   # "android" o "ios"
ENVIRONMENT=$2    # "development", "staging" o "production"
APP_MODE=${3:-legacy}   # "legacy" o "payment". Default legacy (publicable a tiendas)

APP_DIR="apps/mobile_app"
PUBSPEC="$APP_DIR/pubspec.yaml"

case "$ENVIRONMENT" in
  "development"|"staging")
    PUBSPEC_VERSION=$(grep '^version:' "$PUBSPEC" | sed -E 's/^version:[[:space:]]+//')
    VERSION_NUMBER=${PUBSPEC_VERSION%+*}
    ENV_BUILD_NUMBER=${PUBSPEC_VERSION#*+}
    ;;
  "production")
    VERSION_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?([0-9]+(\.[0-9]+)+)\([0-9]+\)$/\1/')
    ENV_BUILD_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?[0-9]+(\.[0-9]+)+\(([0-9]+)\)$/\2/')
    if [ -z "$VERSION_NUMBER" ] || [ -z "$ENV_BUILD_NUMBER" ]; then
      echo "Tag inválido (esperado 1.0.0(N) o v1.0.0(N)): $CM_TAG"; exit 1
    fi
    ;;
  *)
    echo "ENVIRONMENT inválido: $ENVIRONMENT"; exit 1 ;;
esac

cd "$APP_DIR"

COMMON_FLAGS=(
  --release
  --flavor "$ENVIRONMENT"
  -t "lib/main_$ENVIRONMENT.dart"
  --build-name="$VERSION_NUMBER"
  --build-number="$ENV_BUILD_NUMBER"
  --dart-define-from-file="config/$ENVIRONMENT.json"
  --dart-define=APP_MODE="$APP_MODE"
)

case "$APP_PLATFORM" in
  "android")
    flutter build appbundle "${COMMON_FLAGS[@]}"
    ;;
  "ios")
    flutter build ipa \
      "${COMMON_FLAGS[@]}" \
      --export-options-plist=/Users/builder/export_options.plist
    ;;
  *) exit 1 ;;
esac

echo "Build OK: $APP_PLATFORM / $ENVIRONMENT / mode=$APP_MODE (v$VERSION_NUMBER+$ENV_BUILD_NUMBER)"

print_versions.sh

Mismo concepto que effi, ajustado al parser:

#!/bin/bash
set -eu

APP_PLATFORM=$1
ENVIRONMENT=$2

case "$ENVIRONMENT" in
  "development"|"staging")
    PUBSPEC_VERSION=$(grep '^version:' apps/mobile_app/pubspec.yaml | sed -E 's/^version:[[:space:]]+//')
    VERSION_NUMBER=${PUBSPEC_VERSION%+*}
    ENV_BUILD_NUMBER=${PUBSPEC_VERSION#*+}
    ;;
  "production")
    VERSION_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?([0-9]+(\.[0-9]+)+)\([0-9]+\)$/\1/')
    ENV_BUILD_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v?[0-9]+(\.[0-9]+)+\(([0-9]+)\)$/\2/')
    ;;
esac

printf "\033[32m%s\033[0m %s\n" "Flutter version:" "$FLUTTER_VERSION"
printf "\033[32m%s\033[0m %s\n" "App version:" "$VERSION_NUMBER"
printf "\033[32m%s\033[0m %s\n" "Build number:" "$ENV_BUILD_NUMBER"

if [ "$APP_PLATFORM" = "ios" ]; then
  pod --version
  xcodebuild -version
fi

build_status.sh

Idéntico a effi (notificación Google Chat ante fallo).

codemagic.yaml

Cuatro workflows, organizados por (flavor × plataforma). Sin workflows de development — la validación de develop branch se hace local.

workflows:
  # Staging: build legacy publicable a Play Internal Track / TestFlight desde fusion-app
  staging_android:
    name: Staging Android
    instance_type: linux_x2
    environment:
      flutter: $FLUTTER_VERSION
      android_signing: [upload_keystore]
      groups:
        - codemagic-staging
        - codemagic-common-vars
    triggering:
      events: [push]
      branch_patterns:
        - pattern: fusion-app
          include: true
    scripts:
      - name: Setup project
        script: sh ./codemagic_scripts/project_setup.sh
      - name: Analyze + test (bloqueante)
        script: sh ./codemagic_scripts/analyze_and_test.sh
      - name: Print versions
        script: sh ./codemagic_scripts/print_versions.sh android staging
      - name: Android dependencies
        script: sh ./codemagic_scripts/android_dependencies.sh
      - name: Set up key.properties
        script: sh ./codemagic_scripts/android_key_properties.sh
      - name: Build Android app (APP_MODE=legacy)
        script: sh ./codemagic_scripts/flutter_build.sh android staging legacy
      - name: Mark success
        script: touch ~/SUCCESS
    artifacts:
      - apps/mobile_app/build/app/outputs/bundle/stagingRelease/app-staging-release.aab
    publishing:
      google_play:
        track: internal
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        submit_as_draft: false
      scripts:
        - name: Report status
          script: sh ./codemagic_scripts/build_status.sh android staging

  staging_ios:
    name: Staging iOS
    instance_type: mac_mini_m2
    integrations:
      app_store_connect: SaveFamily ASC Key   # crear en CM con la key generada por Jorge Alvarez
    environment:
      flutter: $FLUTTER_VERSION
      xcode: latest
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.savefamily.app.stag
      groups:
        - codemagic-staging
        - codemagic-common-vars
    triggering:
      events: [push]
      branch_patterns:
        - pattern: fusion-app
          include: true
    scripts:
      - name: Setup project
        script: sh ./codemagic_scripts/project_setup.sh
      - name: Analyze + test (bloqueante)
        script: sh ./codemagic_scripts/analyze_and_test.sh
      - name: iOS signing + CocoaPods
        script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
      - name: Print versions
        script: sh ./codemagic_scripts/print_versions.sh ios staging
      - name: Build iOS app (APP_MODE=legacy)
        script: sh ./codemagic_scripts/flutter_build.sh ios staging legacy
      - name: Mark success
        script: touch ~/SUCCESS
    artifacts:
      - apps/mobile_app/build/ios/ipa/*.ipa
    publishing:
      app_store_connect:
        auth: integration
        submit_to_testflight: true
      scripts:
        - name: Report status
          script: sh ./codemagic_scripts/build_status.sh ios staging

  # Production: build legacy disparado por tag 1.0.0(N) o v1.0.0(N)
  production_android:
    name: Production Android
    instance_type: linux_x2
    environment:
      flutter: $FLUTTER_VERSION
      android_signing: [upload_keystore]
      groups:
        - codemagic-production
        - codemagic-common-vars
    triggering:
      events: [tag]
    scripts:
      - name: Setup project
        script: sh ./codemagic_scripts/project_setup.sh
      - name: Analyze + test (bloqueante)
        script: sh ./codemagic_scripts/analyze_and_test.sh
      - name: Print versions
        script: sh ./codemagic_scripts/print_versions.sh android production
      - name: Android dependencies
        script: sh ./codemagic_scripts/android_dependencies.sh
      - name: Set up key.properties
        script: sh ./codemagic_scripts/android_key_properties.sh
      - name: Build Android app (APP_MODE=legacy)
        script: sh ./codemagic_scripts/flutter_build.sh android production legacy
      - name: Mark success
        script: touch ~/SUCCESS
    artifacts:
      - apps/mobile_app/build/app/outputs/bundle/productionRelease/app-production-release.aab
    publishing:
      google_play:
        track: internal
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        submit_as_draft: true   # producción se manda como draft para revisión humana

  production_ios:
    name: Production iOS
    instance_type: mac_mini_m2
    integrations:
      app_store_connect: SaveFamily ASC Key
    environment:
      flutter: $FLUTTER_VERSION
      xcode: latest
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.savefamily.app
      groups:
        - codemagic-production
        - codemagic-common-vars
    triggering:
      events: [tag]
    scripts:
      - name: Setup project
        script: sh ./codemagic_scripts/project_setup.sh
      - name: Analyze + test (bloqueante)
        script: sh ./codemagic_scripts/analyze_and_test.sh
      - name: iOS signing + CocoaPods
        script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
      - name: Print versions
        script: sh ./codemagic_scripts/print_versions.sh ios production
      - name: Build iOS app (APP_MODE=legacy)
        script: sh ./codemagic_scripts/flutter_build.sh ios production legacy
      - name: Mark success
        script: touch ~/SUCCESS
    artifacts:
      - apps/mobile_app/build/ios/ipa/*.ipa
    publishing:
      app_store_connect:
        auth: integration
        submit_to_app_store: true
        release_type: MANUAL
        cancel_previous_submissions: true
        copyright: 2026 SaveFamily

Environment groups en Codemagic

codemagic-common-vars

  • FLUTTER_VERSION = 3.27.0 (mínimo para resolution: workspace)
  • COCOAPODS_VERSION = 1.15.2
  • GOOGLE_BUILDS_CHAT_URL (opcional, webhook fallos)
  • GOOGLE_CHAT_URL (opcional, webhook releases)

codemagic-staging

  • GCLOUD_SERVICE_ACCOUNT_CREDENTIALS

codemagic-production

  • GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
  • RELEASE_NOTES_LANGUAGE = es-ES (o lo que defina el equipo)
  • RELEASE_NOTES = texto multilinea de release notes

Code Signing (UI separada de groups)

  • Android keystore → uploaded como upload_keystore. Codemagic expone CM_KEYSTORE_PATH, CM_KEYSTORE_PASSWORD, CM_KEY_PASSWORD, CM_KEY_ALIAS
  • App Store Connect API integration → la genera Jorge Alvarez desde App Store Connect → Users and Access → Keys. En CM se llama SaveFamily ASC Key (referenciado desde integrations.app_store_connect)

Encrypted Files (UI separada también)

  • apps/mobile_app/config/staging.json
  • apps/mobile_app/config/production.json

Bloqueantes antes de prender CI

Cosas que hay que resolver antes de que el primer build dev verde sea posible:

  1. App Store Connect API key — Jorge Alvarez (Account Holder) tiene que generar la key desde App Store Connect → Users and Access → Integrations → Team Keys, descargar el .p8 (solo se puede descargar una vez), y configurar la integration en Codemagic
  2. Provisioning profiles:
    • com.savefamily.app.stag → tipo app_store (para TestFlight)
    • com.savefamily.app → tipo app_store (para release)
    • El perfil com.savefamily.app.dev no se usa en CI (solo desarrollo local en Xcode)
  3. Upload keystore Android — verificar si ya existe el upload key actual del equipo. Si no, generar uno (keytool -genkey -v -keystore upload.keystore ...) y registrarlo en Play Console Internal App Sharing
  4. Google Play service account — necesita androidpublisher API enabled + permisos en Play Console (Release Manager) para com.savefamily.app.stag (staging) y com.savefamily.app (production)

Pasos para arrancar (orden)

  1. Resolver bloqueantes (sección anterior)
  2. Crear archivos en el repo:
    • codemagic.yaml
    • codemagic_scripts/*.sh (8 scripts) con chmod +x
    • Commitear en feature branch + PR a fusion-app
  3. Conectar repo a Codemagic UI (SaveFamily/sf-app-platform)
  4. Crear los 3 Environment Groups (common-vars, staging, production) y poblar variables
  5. Subir Encrypted Files (2 JSON: staging + production)
  6. Subir keystore Android + configurar App Store Connect integration
  7. Disparar staging_android MANUAL desde la UI (sin esperar push) para validar melos bootstrap + analyze + test + build end-to-end. Validar el AAB en Play Internal Track con un tester
  8. Disparar staging_ios MANUAL — más puntos de falla por signing/Pods/CocoaPods, esperar fricción. Validar el IPA en TestFlight
  9. Activar trigger automático en push a fusion-app solo cuando ambos staging estén verdes
  10. Tag de RC para producción (ej. 0.99.0(1)) para validar el pipeline de tag-driven release sin afectar producción real
  11. Primer release real con tag 1.0.0(N) solo después de coordinación con stakeholders y validación del RC

Riesgos y caveats específicos

  • Tiempo de build estimado: melos bootstrap (~1 min) + melos run analyze (~2-3 min, 30+ paquetes) + melos run test (depende, ~2-5 min) + build (~5-10 min). Total: 10-20 min por workflow. Mac mini m2 escala mejor que linux_x2 para iOS
  • AntelopAppDelegate: cualquier pod install con --repo-update puede regenerar archivos que rompen el swizzling. El Podfile.lock ya está commiteado — CI debe usar pod install plain, no pod update
  • resolution: workspace requiere Flutter ≥ 3.27. Pinear FLUTTER_VERSION exacto en codemagic-common-vars, no latest
  • flutter_secure_storage está pinned a 9.2.4 vía dependency_overrides. CI no debería romper esto pero si melos hace algo raro con overrides en workspace, validar localmente primero
  • Tests con backend mock: hay tests que llaman APIs (questia_api_test?). Si fallan en CI sin red, hay que skiparlos con @Skip() o un tag de melos. Validar antes del primer push
  • Flag --no-select obligatorio en melos run: en non-TTY (cualquier CI) melos prompts y crashea con StdinException. Validado en Fase 0
  • set -o pipefail en todos los scripts: pipes como melos run test | tail -50 enmascaran el exit code del comando upstream. Sin pipefail, un fallo de melos se reporta como success. Validado en Fase 0
  • Release notes en prod: actualmente se hace manual. Si automatizamos via android_release.sh (estilo effi), las release notes vienen de la env var RELEASE_NOTES. Cambiar la convención del equipo o seguir manual via Play Console UI

Referencias

  • Setup base: ~/Desktop/apps/effi-app-flutter/codemagic.yaml + codemagic_scripts/
  • Release workflow manual actual: memory/project_savefamily_release_workflow.md
  • Apple Dev / Account Holder: memory/project_apple_developer_team.md, memory/project_apns_setup.md
  • App Store / Play Store IDs: memory/project_app_store_ids.md
  • iOS deployment target: memory/project_ios_deployment_target.md
  • AppDelegate Antelop caveat: memory/project_appdelegate_antelop_firebase.md