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.
20 KiB
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 packagescom.savefamily.app.devYcom.savefamily.app.stag(projectsf-platform-pre);src/production/google-services.jsonoverride paracom.savefamily.app(projectsf-platform-pro). Commits de referencia:81284d7e,e83adbfd - Bundle IDs:
- iOS / Android:
com.savefamily.app.dev,com.savefamily.app.stag,com.savefamily.app
- iOS / Android:
- Bridges nativos:
flutter_treezor_entrust_sdk_bridge,sca_treezorsonpath: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 porresolution: workspaceen 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, prod6759875039 - 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-selectes obligatorio en non-TTY. Sin esto, melos prompts interactivamente y crashea conStdinExceptionset -o pipefailevita 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 pararesolution: workspace)COCOAPODS_VERSION=1.15.2GOOGLE_BUILDS_CHAT_URL(opcional, webhook fallos)GOOGLE_CHAT_URL(opcional, webhook releases)
codemagic-staging
GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
codemagic-production
GCLOUD_SERVICE_ACCOUNT_CREDENTIALSRELEASE_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 exponeCM_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 desdeintegrations.app_store_connect)
Encrypted Files (UI separada también)
apps/mobile_app/config/staging.jsonapps/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:
- 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 - Provisioning profiles:
com.savefamily.app.stag→ tipo app_store (para TestFlight)com.savefamily.app→ tipo app_store (para release)- El perfil
com.savefamily.app.devno se usa en CI (solo desarrollo local en Xcode)
- 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 - Google Play service account — necesita
androidpublisherAPI enabled + permisos en Play Console (Release Manager) paracom.savefamily.app.stag(staging) ycom.savefamily.app(production)
Pasos para arrancar (orden)
- Resolver bloqueantes (sección anterior)
- Crear archivos en el repo:
codemagic.yamlcodemagic_scripts/*.sh(8 scripts) conchmod +x- Commitear en feature branch + PR a
fusion-app
- Conectar repo a Codemagic UI (
SaveFamily/sf-app-platform) - Crear los 3 Environment Groups (common-vars, staging, production) y poblar variables
- Subir Encrypted Files (2 JSON: staging + production)
- Subir keystore Android + configurar App Store Connect integration
- Disparar
staging_androidMANUAL desde la UI (sin esperar push) para validarmelos bootstrap+ analyze + test + build end-to-end. Validar el AAB en Play Internal Track con un tester - Disparar
staging_iosMANUAL — más puntos de falla por signing/Pods/CocoaPods, esperar fricción. Validar el IPA en TestFlight - Activar trigger automático en push a
fusion-appsolo cuando ambos staging estén verdes - Tag de RC para producción (ej.
0.99.0(1)) para validar el pipeline de tag-driven release sin afectar producción real - 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 installcon--repo-updatepuede regenerar archivos que rompen el swizzling. ElPodfile.lockya está commiteado — CI debe usarpod installplain, nopod update resolution: workspacerequiere Flutter ≥ 3.27. PinearFLUTTER_VERSIONexacto encodemagic-common-vars, nolatestflutter_secure_storageestá pinned a9.2.4víadependency_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-selectobligatorio enmelos run: en non-TTY (cualquier CI) melos prompts y crashea conStdinException. Validado en Fase 0 set -o pipefailen todos los scripts: pipes comomelos run test | tail -50enmascaran 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 varRELEASE_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