DEV Community

Alair Joao Tavares
Alair Joao Tavares

Posted on • Originally published at activi.dev

Arquitetando CI/CD para Monorepos Mobile: Integrando npm Workspaces, EAS Builds e GitHub Actions

Arquitetando CI/CD para Monorepos Mobile: Integrando npm Workspaces, EAS Builds e GitHub Actions

O desenvolvimento de aplicações mobile modernas frequentemente exige o compartilhamento de código entre diferentes plataformas, como web, backend e mobile. É nesse cenário que a arquitetura de monorepo brilha, permitindo que você mantenha todo o seu ecossistema em um único repositório. No entanto, quando introduzimos o React Native na equação, especialmente utilizando o ecossistema do Expo, a configuração de pipelines de CI/CD (Continuous Integration e Continuous Deployment) pode se tornar um desafio considerável.

Neste artigo, vamos explorar como construir um pipeline de CI/CD robusto e eficiente para um monorepo mobile. Combinaremos o poder do npm Workspaces para o gerenciamento de pacotes, o Expo Application Services (EAS) para a automação de builds em nuvem, e o GitHub Actions para orquestrar as rotinas de release, com foco especial no fluxo de iOS.


O Desafio dos Monorepos no Desenvolvimento Mobile

Quando trabalhamos com um repositório simples contendo apenas o app mobile, as ferramentas de build conseguem encontrar facilmente o package.json, o node_modules e os arquivos de configuração. Porém, em um monorepo, a estrutura muda. As dependências muitas vezes são içadas (hoisted) para a raiz do repositório, e o código do aplicativo precisa resolver módulos que vivem em pastas adjacentes (ex: packages/shared-ui).

Se o seu serviço de build (como o EAS) não for instruído corretamente sobre como lidar com essa estrutura, os builds falharão por não encontrarem pacotes internos ou por erros de resolução no Metro Bundler (o empacotador padrão do React Native).

Para resolver isso, utilizaremos o npm Workspaces, que tem suporte nativo para linkagem de pacotes locais.


1. Estruturando o Monorepo com npm Workspaces

A base da nossa arquitetura começa na organização das pastas e no arquivo package.json raiz. Vamos assumir que estamos construindo uma aplicação em TypeScript, o padrão ouro para aplicações React Native escaláveis.

Uma estrutura de monorepo ideal se parece com isso:

meu-monorepo/
├── package.json
├── package-lock.json
├── tsconfig.base.json
├── apps/
│   └── mobile-app/         # Nosso app React Native/Expo
│       ├── package.json
│       ├── eas.json
│       ├── App.tsx
│       └── tsconfig.json
└── packages/
    └── shared-types/       # Pacote TypeScript compartilhado
        ├── package.json
        ├── src/index.ts
        └── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Na raiz do repositório, o seu package.json deve declarar os workspaces:

{
  "name": "example-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build:shared": "npm run build -w packages/shared-types",
    "start:mobile": "npm run start -w apps/mobile-app"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configurando um Pacote Compartilhado (TypeScript)

Vamos criar um arquivo genérico no nosso pacote shared-types para garantir que a tipagem flua perfeitamente pelo monorepo.

// packages/shared-types/src/index.ts
export interface UserProfile {
  id: string;
  username: string;
  email: string;
  preferences: {
    notificationsEnabled: boolean;
    theme: 'light' | 'dark';
  };
}

export const formatUsername = (user: UserProfile): string => {
  return `@${user.username.toLowerCase()}`;
};
Enter fullscreen mode Exit fullscreen mode

No package.json do seu app mobile (apps/mobile-app/package.json), você adiciona a dependência referenciando a versão local:

{
  "name": "@example/mobile-app",
  "version": "1.0.0",
  "dependencies": {
    "expo": "~51.0.0",
    "react": "18.2.0",
    "react-native": "0.74.1",
    "@example/shared-types": "*"
  }
}
Enter fullscreen mode Exit fullscreen mode

Ao rodar npm install na raiz do repositório, o npm criará symlinks (links simbólicos) automáticos. O app mobile agora consegue importar UserProfile como se fosse um pacote npm público.


2. Configurando o EAS Build para o Monorepo

O Expo Application Services (EAS) é um serviço de nuvem incrivelmente poderoso para compilar apps React Native. No entanto, ele precisa ser configurado especificamente para entender que está rodando dentro de um monorepo.

Primeiro, certifique-se de que o Metro Bundler do React Native entenda os symlinks criados pelo npm workspaces. No seu apps/mobile-app/metro.config.js:

// apps/mobile-app/metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

// Adiciona a raiz do monorepo para que pacotes internos sejam resolvidos
config.watchFolders = [monorepoRoot];

// Configura a resolução de nós do Metro
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(monorepoRoot, 'node_modules'),
];

module.exports = config;
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos ajustar o arquivo eas.json no app mobile. O segredo aqui é garantir que o EAS instale as dependências a partir da raiz do monorepo, e não apenas na pasta do aplicativo.

{
  "cli": {
    "version": ">= 7.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "base": {
      "env": {
        "EXPO_USE_PATH_ALIASES": "1"
      }
    },
    "production": {
      "extends": "base",
      "node": "18.x",
      "ios": {
        "image": "latest"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Por padrão, se você acionar um build na pasta do app, o EAS tentará subir apenas o código do app. É necessário garantir que o comando seja rodado a partir do root, ou configurar o eas ignore adequadamente. Vamos lidar com isso elegantemente no próximo passo usando o GitHub Actions.


3. Automatizando Releases de iOS com GitHub Actions

Aqui é onde a mágica da automação acontece. Em vez de rodar o processo de build do EAS manualmente na sua máquina, correndo o risco de compilar código desatualizado, vamos configurar um workflow no GitHub Actions que será disparado toda vez que fizermos push na branch main ou criarmos uma tag de release.

Crie um arquivo em .github/workflows/ios-release.yml:

name: iOS Production Release

on:
  push:
    branches:
      - main
    paths:
      - 'apps/mobile-app/**'
      - 'packages/**'
      - 'package.json'
      - 'package-lock.json'

jobs:
  build-ios:
    name: Build & Submit iOS App via EAS
    runs-on: ubuntu-latest

    # Usamos o diretório raiz como padrão para os comandos iniciais
    defaults:
      run:
        working-directory: ./

    steps:
      - name: 🏗 Checkout do repositório
        uses: actions/checkout@v4

      - name: ⚙️ Setup do Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
          cache-dependency-path: 'package-lock.json'

      - name: 📦 Instalar Dependências (Monorepo)
        run: npm ci

      - name: 🚀 Setup do Expo e EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: 🍎 Build para iOS
        # Mudamos para o diretório do app especificamente para rodar o comando EAS
        working-directory: ./apps/mobile-app
        run: |
          eas build --platform ios \
                    --profile production \
                    --non-interactive \
                    --auto-submit
        env:
          EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }}
Enter fullscreen mode Exit fullscreen mode

Entendendo o Workflow

  1. Condições de Execução (on.push.paths): O workflow só é disparado se houver modificações na pasta do app mobile, nos pacotes compartilhados ou nas dependências principais. Isso economiza valiosos minutos de CI e não aciona builds mobile desnecessários se você alterar apenas o backend.
  2. Setup e Cache (actions/setup-node): Uma etapa essencial. Utilizamos o arquivo package-lock.json para criar um cache eficiente do npm. Em um monorepo, isso corta o tempo de instalação pela metade.
  3. Instalação das dependências (npm ci): É executado na raiz do repositório. Isso garante que todos os workspaces sejam inicializados e os symlinks criados corretamente.
  4. Expo Action (expo/expo-github-action): Esta action oficial injeta as credenciais do EAS no ambiente. Você deve criar um Personal Access Token no portal do Expo e adicioná-lo nas Secrets do repositório (EXPO_TOKEN).
  5. O Comando de Build: Observe que alteramos o diretório de trabalho apenas no momento do build (./apps/mobile-app). A flag --non-interactive é vital para impedir que o terminal trave aguardando input de usuário. A flag --auto-submit envia o app automaticamente para o TestFlight ou App Store após o build bem-sucedido (desde que as credenciais da Apple estejam devidamente preenchidas via secrets do EAS ou variáveis de ambiente).

Boas Práticas e Dicas de Ouro

1. Lidando com Credenciais da Apple

A submissão automática para iOS exige que o EAS tenha acesso à sua conta da Apple Developer. O recomendado é usar a integração oficial do EAS. Em vez de armazenar o password no repositório, você pode criar uma App-Specific Password na Apple e cadastrá-la com a variável EXPO_APPLE_APP_SPECIFIC_PASSWORD para submissões sem falhas de MFA (Multi-Factor Authentication).

2. Versionamento e Gerenciamento de Configuração

No EAS, prefira o padrão "appVersionSource": "remote" no eas.json e gerencie o número da versão dinamicamente através de scripts pré-build ou utilizando o auto-incremento nativo do EAS (eas build:version:set). Isso evita conflitos de merge desagradáveis em arquivos app.json no monorepo.

3. Evite Subir a Pasta node_modules

Em monorepos muito grandes, o upload dos arquivos para a nuvem do EAS pode ser lento. Configure corretamente um arquivo .easignore no diretório do seu app mobile para garantir que ele não copie o node_modules (o EAS irá rodar o npm install novamente em sua própria máquina de build) nem pastas de outros aplicativos irrelevantes para o mobile.

Exemplo de arquivo .easignore no root do monorepo (se for subir do root):

apps/web-app/
apps/backend-api/
**/node_modules/
.git/
Enter fullscreen mode Exit fullscreen mode

Conclusão

Configurar um pipeline de CI/CD em um ambiente monorepo pode parecer desafiador no início. A separação estrita de escopos, gerenciada pelo npm workspaces, interage de forma complexa com os empacotadores nativos como o Metro.

Entretanto, ao configurar as rotas corretamente no Metro Bundler, orquestrar as instalações a partir da raiz com as Actions do GitHub, e entregar o pacote completo para o EAS, você cria uma máquina de automação formidável.

Aplicar essas práticas significa que seu time poderá focar no que realmente importa: desenvolver funcionalidades incríveis e resolver problemas dos usuários, deixando o processo burocrático de build, compilação de chaves e distribuição para a nuvem. Menos horas gastas no Xcode, mais tempo programando em TypeScript.

Top comments (0)