Como criar um pipeline CI/CD para apps React Native com GitHub Actions — um guia prático do zero
Semana passada, a equipe do React Native lançou a versão 0.74.4 com pequenos ajustes de segurança para Android 14 e correções de regressões no Metro. A novidade chamou pouco a atenção, mas reforça uma tendência cada vez mais forte: a necessidade de automatizar processos que, antigamente, demandavam horas de trabalho manual. Se você ainda envia builds para a Play Store ou TestFlight copiando .aab, .ipa e subindo por interfaces web, este texto é para você.
Vamos construir juntos um pipeline de integração e entrega contínua (CI/CD) para um app React Native usando exclusivamente o GitHub Actions, sem precisar de Jenkins, Bitrise ou CircleCI. A beleza dessa abordagem é que ela serve tanto para quem está começando (basta ter um repositório público no GitHub) quanto para quem já tem um projeto maduro e quer economizar dinheiro em ferramentas pagas.
O que você vai levar deste artigo
Ao final, você terá:
- Um workflow que roda testes, lint e TypeScript em cada Pull Request.
- Builds de Preview que chegam automaticamente no celular via Expo EAS Update ou Firebase App Distribution.
- Uma release de produção gerada toda vez que um PR é mergeado na branch main.
- Tudo isso custando zero reais (ou poucos dólares se precisar de macOS para compilar iOS).
Preparação: estrutura mínima do projeto
Antes de encanar YAML, certifique-se de que seu projeto atenda a estes pré-requisitos:
- React Native 0.73+ (ou Expo SDK 50+ se usar Expo).
package.jsoncontendo:yarn test(ounpm run test) executando Jest.yarn lintexecutando ESLint.yarn type-checkexecutandotsc --noEmit.
- Um arquivo
.nvmrcfixando a versão do Node (ex:18.18.0). - Um repositório público no GitHub (privado também funcila, mas consome minutos de Actions).
Exemplo de scripts úteis no package.json:
{
"scripts": {
"test": "jest --passWithNoTests",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"type-check": "tsc --noEmit",
"build:android": "cd android && ./gradlew assembleRelease",
"build:ios": "cd ios && xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release"
}
}
Passo 1 — Workflow de validação em PR
Crie a pasta .github/workflows e dentro dela o arquivo pr-check.yml:
name: PR Check
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
- name: Install deps
run: yarn install --frozen-lockfile
- name: Lint
run: yarn lint
- name: Type-check
run: yarn type-check
- name: Test
run: yarn test --ci --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Como funciona
- O workflow só dispara quando alguém abre ou atualiza um PR para a
main. - Usamos
runs-on: ubuntu-latestporque testes e lint não precisam de macOS (economia de custo). cache: "yarn"faz o Actions reutilizar a pasta.yarn/cache, reduzindo tempo de CI de 3 min para 40 s na segunda execução.codecov/codecov-actionenvia o relatório de cobertura; basta criar uma conta no Codecov e adicionar o token como secretCODECOV_TOKEN.
Passo 2 — Build de preview automático para Android
Agora que cada PR já tem qualidade garantida, vamos gerar um .apk para teste. Criamos o arquivo preview-android.yml:
name: Preview Android
on:
workflow_run:
workflows: ["PR Check"]
types: [completed]
branches-ignore: [main]
jobs:
build-preview:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
- run: yarn install --frozen-lockfile
- name: Build APK
run: |
cd android
./gradlew assembleRelease
- name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
groups: testers
file: android/app/build/outputs/apk/release/app-release.apk
Dicas práticas
- O trigger
workflow_runespera o PR Check terminar; assim, não gastamos tempo se o código não passar nos testes. - Firebase App Distribution é gratuito para até 500 testers por app. Substitua por Expo EAS se preferir.
Passo 3 — Build de produção assinado (Android)
Para Play Store, precisamos de um .aab. Criamos release-android.yml:
name: Release Android
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
- run: yarn install --frozen-lockfile
- name: Decode keystore
run: |
echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d > android/app/my-upload.keystore
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Build AAB
env:
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
cd android
./gradlew bundleRelease
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.minhaempresa.app
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
track: production
status: completed
Segurança de chaves
- Crie um keystore localmente:
keytool -genkey -v -keystore my-upload.keystore -alias upload -keyalg RSA -keysize 2048 -validity 10000 - Codifique em base64:
base64 -w 0 my-upload.keystore - Guarde o resultado em
ANDROID_KEYSTORE_BASE64(Settings > Secrets and Variables > Actions).
Passo 4 — Build iOS sem Mac próprio
Se você não tem um Mac, use o macos-latest do próprio GitHub. A parte chata é o gerenciamento de perfis e certificados. A comunidade criou a action yukiarrr/ios-build-action que abstrai a dor:
name: Release iOS
on:
push:
branches: [main]
jobs:
release-ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
- run: yarn install --frozen-lockfile
- name: Install pods
run: cd ios && pod install
- name: Build & sign
uses: yukiarrr/[email protected]
with:
project-path: ios/MyApp.xcodeproj
workspace-path: ios/MyApp.xcworkspace
scheme: MyApp
export-method: app-store
configuration: Release
team-id: ${{ secrets.IOS_TEAM_ID }}
app-store-connect-api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
app-store-connect-api-key-issuer