# Azure Pipelines com Expo Prebuild

Vou contar um pouquinho como montamos uma pipeline para fazer build dos AABs e IPAs do nosso aplicativo react-native com Expo no Azure DevOps.

Incrivelmente, foi um processo com menos documentação e exemplos do que eu esperava. Esbarrei em alguns problemas, por ser a primeira vez montando algo do tipo e acho que por isso quero compartilhar, para talvez economizar o sofrimento de outras pessoas hehe

Um aviso: se você ainda não tem uma conta de desenvolvedor na Apple, não vai conseguir fazer o build da pipeline, visto que precisa de arquivos que só podem ser gerados dentro do painel do desenvolvedor da Apple.

Para Android, você deve conseguir fazer o build e instalar no seu dispositivo sem problemas mesmo sem uma conta de desenvolvedor Google Play.

### Antes, uma breve reflexão...

Alguns serviços de tecnologia são realmente ótimos quando você utiliza tudo que eles oferecem. Mas muitas vezes isso custa.

Quando decidimos utilizar o Expo no nosso aplicativo, sabíamos que faríamos muita coisa por ele, mas sabíamos que algumas outras faríamos por fora. Na hora da decisão, a gente pensou que isso seria sobre utilizar qualquer implementação ou plugin que precisasse de alguma customização na aplicação nativa, mas na verdade acabou que precisamos muito antes de documentação sobre coisas fora do Expo: o build dos pacotes do aplicativo.

A princípio, parece um processo bem direto: você faz `prebuild` e então segue o processo normal de um aplicativo `react-native` . Bom, não foi tão rápido assim...

### Processo de build básico

Para fazer a construção de um pacote de aplicativo com Expo, você deve rodar os seguintes passos:

1. Rodar `expo prebuild` para construir os arquivos de projeto Android e iOS
    
    1. Para Android:
        
        1. Fazer build do `AAB` com `./gradlew bundleRelease`
            
        2. Assinar o arquivo `AAB`
            
    2. Para iOS, fazer build do projeto com XCode que já gera o `IPA` assinado para você
        

Alguns pré-requisitos:

1. No Android, você precisa gerar ter uma chave assinar o pacote. Apesar do comando `bundleRelease` já gerar assinar para você, para deixar o processo mais controlado, melhor você gerar a sua própria para a pipeline.
    
2. No iOS, você precisa gerar o certificado e o provisioning profile.
    

### Gerando os arquivos para assinatura

#### Android

Para gerar sua chave e certificado de assinatura, utilize o comando:

```bash
keytool -genkeypair -v -storetype PKCS12 -keystore android.jks -alias app -keyalg RSA -keysize 4096 -validity 10000
```

Durante o processo, será solicitada uma senha para o arquivo. Guarde essa senha, pois ela será necessária depois.

#### iOS

Aqui, vamos precisar de um pouco mais de trabalho rs

Você vai precisar gerar pelo menos um arquivo `P12` . Esse arquivo contém um certificado e uma chave privada. Para gerar esse arquivo, você vai precisar pedir um certificado com um arquivo `CSR`. Para gerar o `CSR` , utilize o script bash abaixo:

```bash
openssl genrsa -out app-ios.key 2048
openssl req -new -key app-ios.key -out app-ios.csr
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">A Apple não suporta chaves RSA além de 2048 bits</div>
</div>

O script deve te pedir algumas informações para serem incluídas no certificado (Distinguished names). Preencha de acordo com o que for solicitado na linha de comando.

Com o arquivo `CSR` gerado, vá na [página de certificados](https://developer.apple.com/account/resources/certificates/list) dentro do painel do desenvolvedor da Apple e crie um certificado adequado para sua aplicação. No nosso caso, utilizamos certificados separados para assinar uma build de desenvolvimento (certificado do tipo `iOS App Development`) e outra para produção (certificado do tipo `iOS Distribution`)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1702687490151/628d886b-22d5-4a06-8305-852df189ceec.png align="center")

Depois de seguir o passo a passo, você vai poder baixar o arquivo `CER`. Salve ele como `app-ios.cer` na mesma pasta que estavam os arquivos `CSR` e `KEY` criados no script acima e use o script abaixo para gerar o arquivo `P12`

```bash
openssl x509 -in app-ios.cer -inform DER -out app-ios.pem -outform PEM
openssl pkcs12 -export -legacy -inkey mdr-app-ios.key -in mdr-app-ios.pem -out mdr-app-ios.p12
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Observe que o segundo comando utiliza a flag <code>-legacy</code>. Isso porque utilizei o <code>openssl</code> 3.1.1 via Git Bash e o Mac utiliza o openssl <code>1.x</code> . Sem essa flag, o arquivo não poderá ser utilizado para assinar mais a frente!</div>
</div>

Guarde também a senha utilizada na geração do arquivo `P12`. Ela será necessária mais a frente.

Agora precisamos gerar um `Provisioning Profile`. Basta entrar na [página dos profiles](https://developer.apple.com/account/resources/profiles/list) dentro do painel do desenvolvedor da Apple e criar um. No nosso caso, geramos 1 para desenvolvimento (tipo `iOS App Development`) e um para produção (tipo `App Store`)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1702688389244/9192635e-2a2e-40ec-b8a5-38cad0c531a8.png align="center")

Logo depois dessa tela, você deverá selecionar as `capabilities` do seu aplicativo. No nosso projeto, foi necessário selecionar o `Push Notification`. Se você gerar um `profile` sem as `capabilities` necessárias, você irá tomar um erro na etapa de geração do IPA que não vai te dizer bem que esse é o problema. Para ver quais são as necessárias, veja [via XCode](https://developer.apple.com/documentation/xcode/adding-capabilities-to-your-app/#Add-a-capability) no projeto.

Concluído o processo, você poderá baixar um arquivo `mobileprovision` .

Todos esses arquivos devem ser enviados para a pipeline do Azure como arquivos seguros, no menu `Pipelines > Library`, na aba `Secure Files` .

Aproveite que está aqui e crie um `Variable Group` e salve as senhas utilizadas para o certificado android e arquivo `p12` iOS. Vamos considerar que o nome desse grupo é `app-vg` e que ele tem as propriedades `androidKeyStorePassword` e `iosP12Password` .

### LogRocket: uma fonte de alívio

Quando começamos a construção, seguimos o modelo proposto no blog da LogRocket para `react-native`: [Continuous deployment of React Native app with Azure DevOps - LogRocket Blog](https://blog.logrocket.com/continuous-deployment-of-react-native-app-with-azure-devops/)

Lendo o artigo, de cara percebemos alguns ajustes a serem feitos:

1. Não vamos manter as pastas `android` e `ios` no repositório Git. Podemos criá-las na execução da pipeline.
    
2. Vamos controlar a versão programaticamente com o número do build da pipeline Azure, utilizando variáveis de ambiente na hora do build.
    
3. No link, a pipeline utiliza um único job para fazer a construção do `APK` Android e do `IPA` iOS. Vamos separar o build em 2 `stages`, um para cada OS.
    

Como no link, nós também estamos utilizando o `App Center` para fazer publicação das versões, mas essa é uma etapa opcional que não vou entrar em detalhes aqui.

### Configurando a versão da aplicação

O formato de versão que vamos utilizar é o recomendado pela Apple, com 3 inteiros, separados por ponto.

No arquivo `app.json`, vamos deixar os dois primeiros números e o terceiro será concatenado com uma variável de ambiente chamada `BUILD_NUMBER`. Para evitar erros durante o desenvolvimento, vamos deixar o valor padrão 1 caso essa variável esteja vazia.

Crie um arquivo `app.config.ts` na raiz da sua aplicação com o seguinte código

```typescript
import type { ConfigContext, ExpoConfig } from '@expo/config'

export default function ConfigExpo({ config }: ConfigContext): ExpoConfig {
	if (!config.version) {
		throw new Error('No version configured in app.json.')
	}

	const buildNumber = parseInt(process.env.BUILD_NUMBER ?? '1')
	const version = `${config.version}.${buildNumber}`

	return {
		...config,
		version,
		android: {
			...config.android,
			versionCode: buildNumber
		},
		ios: {
			...config.ios,
			buildNumber: buildNumber.toString()
		},
		name: config.name!,
		slug: config.slug!
	}
}
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">✅</div>
<div data-node-type="callout-text">A configuração customizada foi uma ideia desse outro ótimo artigo: <a target="_blank" rel="noopener noreferrer nofollow" href="https://elazizi.com/posts/environment-variables-in-react-native-the-right-way/" style="pointer-events: none">Environment Variables in React Native: The Right Way! (elazizi.com)</a></div>
</div>

### Agora sim, a pipeline!

Nós gostamos muito de usar templates de pipeline aqui na Mdr. Ajuda muito a reaproveitar declarações. Como temos build para desenvolvimento e produção, sem essa funcionalidade ficaria muito código duplicado na nossa definição de pipeline rs

Para facilitar o exemplo, vou fazer como se fosse apenas um ambiente. Não se esqueça de ajustar o nome dos arquivos de acordo com o que você colocou no `Secure Files`!

Ah, os exemplos abaixo estão com `pnpm`. Se você utiliza `yarn` ou `npm`, faça os ajustes necessários no template de `prebuild`.

Lembrando que para usar o `pnpm`, você deve ter o arquivo `.npmrc` na raíz do seu projeto Expo com o seguinte conteúdo:

```yaml
engine-strict=true
node-linker=hoisted
```

Arquivo `.azuredevops/prebuild.yml`

```yaml
parameters:
  - name: buildNumber
    type: string
  - name: platform
    type: string

steps:
  - task: UseNode@1
    inputs:
      version: '20.10.0'

  - script: corepack enable && corepack prepare pnpm@latest-8 --activate
    displayName: Setup pnpm

  - bash: pnpm i --frozen-lockfile
    displayName: Install dependencies

  - bash: npx expo prebuild --platform ${{ parameters.platform }}
    displayName: Expo PreBuild
    env:
      BUILD_NUMBER: ${{ parameters.buildNumber }}
```

Arquivo `.azuredevops/build-android.yml`

```yaml
parameters:
  - name: keyStoreFile
    type: string
  - name: variableGroup
    type: string

jobs:
  - job: BuildAndroid
    displayName: Build Android App Bundle
    variables:
      - group: ${{ parameters.variableGroup }}
    steps:
      # Use o Java 17, porque o Expo usa Gradle 8.0, sem suporte ao Java 20 ainda
      - task: JavaToolInstaller@0
        displayName: Install Java 17
        inputs:
          jdkArchitectureOption: x64
          jdkSourceOption: "PreInstalled"
          versionSpec: "17"

      - template: prebuild.yml
        parameters:
          buildNumber: $(Build.BuildId)
          platform: android

      - bash: ./gradlew bundleRelease
        displayName: Build Unsigned App Bundle
        workingDirectory: android/
        env:
          BUILD_NUMBER: $(Build.BuildId)
      
      # Precisamos remover a assinatura antiga, ou o Google Play vai
      # reclamar da dupla assinatura ao submeter a versão
      - bash: zip -d app-release.aab 'META-INF/*.SF' 'META-INF/*.RSA'
        displayName: Remove debug signature from App Bundle
        workingDirectory: android/app/build/outputs/bundle/release/

      # Importante especificar aqui os 'arguments'. Por padrão, essa task
      # faz a assinatura com SHA1 e isso falha na hora de submeter o aplicativo
      # na Google Play. Esse foi um dos problemas que mais demoramos para descobrir rs
      - task: AndroidSigning@2
        displayName: Sign App Bundle
        inputs:
          apkFiles: android/app/build/outputs/bundle/release/app-release.aab
          jarsignerKeyPassword: $(androidKeyStorePassword)
          jarsignerKeystoreFile: ${{ parameters.keyStoreFile }}
          jarsignerKeystorePassword: $(androidKeyStorePassword)
          jarsignerKeystoreAlias: app
          jarsignerArguments: '-verbose -sigalg SHA256withRSA -digestalg SHA-256'
          zipalign: true

      - task: PublishBuildArtifacts@1
        displayName: Publish App Bundle to Pipeline
        inputs:
          ArtifactName: android
          PathtoPublish: android/app/build/outputs/bundle/release/
          publishLocation: Container
```

Arquivo `.azuredevops/build-ios.yml`

```yaml
parameters:
  - name: appCenterAppSlug
    type: string
  - name: appCenterServiceConnection
    type: string
  - name: environmentPrefix
    type: string
  - name: iosCertificateFile
    type: string
  - name: iosProvisioningProfileFile
    type: string
  - name: variableGroup
    type: string

jobs:
  - job: BuildIos
    displayName: Build iOS IPA
    pool:
      vmImage: macos-latest
    variables:
      - group: ${{ parameters.variableGroup }}
    steps:
      - task: InstallAppleCertificate@2
        displayName: Install Apple Certificate
        inputs:
          certSecureFile: ${{ parameters.iosCertificateFile }}
          certPwd: $(iosP12Password)
          keychain: temp

      - task: InstallAppleProvisioningProfile@1
        displayName: Install Apple Provisioning Profile
        inputs:
          provisioningProfileLocation: secureFiles
          provProfileSecureFile: ${{ parameters.iosProvisioningProfileFile }}
          removeProfile: true

      - template: prebuild.yml
        parameters:
          buildNumber: $(Build.BuildId)
          environmentPrefix: ${{ parameters.environmentPrefix }}
          platform: ios

      # As variáveis APPLE_CERTIFICATE_SIGNING_IDENTITY and APPLE_PROV_PROFILE_UUID
      # são definidas pelas tasks anteriores InstallAppleCertificate & InstallAppleProvisioningProfile
      # Outro ponto de atenção aqui: tanto o scheme quando o nome do arquivo xcworkspace
      # refletem os valores especificados no app.json. Vale fazer um build local para ver qual 
      # o nome gerado para o seu projeto
      - task: Xcode@5
        displayName: Build IPA
        inputs:
          actions: build
          configuration: Release
          exportPath: output
          packageApp: true
          provisioningProfileUuid: $(APPLE_PROV_PROFILE_UUID)
          scheme: <seu_scheme>
          sdk: iphoneos
          signingOption: manual
          signingIdentity: $(APPLE_CERTIFICATE_SIGNING_IDENTITY)
          xcWorkspacePath: ios/<nome_do_app>.xcworkspace
        env:
          BUILD_NUMBER: $(Build.BuildId)

      # Como não sei exatamente onde o IPA é construído, por não ter um
      # mac para fazer o teste local, eu jogo todos os arquivos IPA
      # para a pasta de staging do agente que está executando a pipeline
      - task: CopyFiles@2
        displayName: Copy IPA
        inputs:
          contents: '**/*.ipa'
          flattenFolders: true
          overWrite: true
          targetFolder: $(build.artifactStagingDirectory)

      # Ao invés de enviar só o IPA, mando a pasta toda. Assim, não preciso saber
      # exatamente onde o IPA está.
      - task: PublishBuildArtifacts@1
        displayName: Publish IPA to Pipeline
        inputs:
          PathtoPublish: $(build.artifactStagingDirectory)
          ArtifactName: ios
          publishLocation: Container
```

Arquivo `azure-pipelines.yml`

```yaml
pool:
  vmImage: 'ubuntu-22.04'

stages:
  - stage: BuildAndroidApp
    displayName: Build Android App Bundle
    dependsOn: []
    jobs:
      - template: .azuredevops/build-android.yml
        parameters:
          keyStoreFile: app-android.jks
          variableGroup: app-vg

  - stage: BuildIOSApp
    displayName: Build iOS IPA
    dependsOn: []
    jobs:
      - template: .azuredevops/build-ios.yml
        parameters:
          iosCertificateFile: app-ios.p12
          iosProvisioningProfileFile: app-ios.mobileprovision
          variableGroup: app-vg
```

E é isso! Espero ter ajudado alguém com esse exemplo. Sem dúvida foi muito difícil chegar nele (a etapa de assinatura do Android me deixou bem maluco rs)

Esses arquivos são alterações dos nossos originais e foram feitos para este artigo sem validar a sua execução. Caso você tenha algum erro montando a sua pipeline a partir desse exemplo, fique a vontade para entrar em contato comigo (`marcos.romero@mercadoderecebiveis.com.br`) que ficarei feliz em ajudar a corrigir!
