Pular para o conteúdo
Sunstone Apps
← Voltar para o blog
Release Android com Expo

Versão, versionCode e config gerada: três números que o release Android precisa alinhar

O app já estava na Play com versionCode 9, mas Settings ainda mostrava 0.1.0 e o lint podia trocar a config de produção por development sem você perceber. Não é bug de Gradle — é contrato de versionamento mal definido.

Publicado: 8 min de leitura
Imagem de prévia OpenGraph deste artigo. Versão, versionCode e config gerada: três números que o release Android precisa alinhar

Três números diferentes aparecem em todo release Android do Daily Sudoku: Offline Puzzle, e cada um responde a uma pergunta distinta.

O version em app.json é o que o jogador vê em Settings e o que a Play Store mostra como versionName. O versionCode é o inteiro monotônico que a Google Play exige em todo upload — nunca reutilize. O terceiro número é menos visível: o ambiente embutido em config.generated.ts, que define se o binário roda com ads reais, Sentry ativo, mocks de loja ou atalhos de desenvolvimento.

Com os três desalinhados, o Console parece certo e o device, errado.

Por que 0.1.0 ficou nove builds

Durante a faixa interna e o primeiro lançamento público, subimos versionCode de 1 até 9 com expo.version parado em 0.1.0. A Play aceita. Suporte, testers e release notes pagam o preço.

Testers não sabem se estão na build nova sem olhar versionCode, que não aparece na UI. O modal de boas-vindas compara welcomeSeenVersion com appVersion — subir só o build não reabre o aviso de update. E “mais um 0.1.0” nas release notes não diz se veio correção ou feature.

A regra adotada no monorepo segue SemVer de forma pragmática:

  • PATCH (0.1.00.1.1): correções e polimento sem feature nova relevante.
  • MINOR (0.1.x0.2.0): funcionalidade visível nova (tutorial, dificuldade extra, modo novo).
  • MAJOR (0.x.x1.0.0): marco de produto, não necessariamente breaking técnico.

O versionCode sobe em todo upload, independentemente do PATCH. O Sentry usa com.sunstoneapps.dailysudoku@{version}+{versionCode} com dist = versionCode. Para o candidato pós-correções de tabuleiro, o repo ficou em 0.1.1 + versionCode 10.

O lint que apagava produção

Outro problema apareceu na sequência de release, não no Gradle.

O Sudoku gera src/app/config/config.generated.ts a partir de YAML por ambiente (development, test, production). Scripts de build de loja exportam EXPO_PUBLIC_APP_ENV=production antes de pnpm --filter sudoku config:generate. O arquivo resultante traz generatedConfigEnvironment = "production" e IDs reais de AdMob, Sentry e Firebase.

O lint também chamava config:generate no início, com default development quando a variável não estava no shell. expo lint carrega .env, mas isso roda depois da regeneração.

Sequência que pegamos na prática:

  1. pnpm sudoku:apk:prd deixava config.generated.ts em production.
  2. pnpm run lint sobrescrevia para development.
  3. Quem inspecionava o arquivo achava que o candidato de release estava errado — e, se commitasse sem perceber, o próximo passo do pipeline poderia empacotar config incorreta.

A correção foi remover config:generate de lint e typecheck. Esses checks não precisam mutar artefato gerado; só validam o TypeScript que já está no disco. Regenerar config ficou restrito a comandos que declaram o ambiente de propósito: start, test, sudoku:apk:dev*, sudoku:apk:prd*, sudoku:aab:prd.

Também corrigimos .gitignore, que apontava para apps/*/src/config/config.generated.ts enquanto o arquivo real vive em apps/*/src/app/config/. O caminho errado fazia o artefato continuar versionado sem o ignore funcionar.

Comandos dev e prd: ambiente no nome

Os scripts antigos misturavam velocidade de build com ambiente. pnpm sudoku:apk era incremental, mas sempre com config production. pnpm sudoku:apk:full adicionava prebuild. pnpm sudoku:aab:clean forçava --rerun-tasks. Nada disso dizia “development” ou “production” no nome.

Renomeamos para deixar o contrato explícito:

Comando Ambiente Gradle / prebuild
sudoku:apk:dev development incremental, sem prebuild
sudoku:apk:dev:full development prebuild + --rerun-tasks
sudoku:apk:prd production incremental, sem prebuild
sudoku:apk:prd:full production prebuild + --rerun-tasks
sudoku:aab:prd production prebuild + bundleRelease --rerun-tasks

dev usa mocks, atalhos de desenvolvimento e YAML de development — útil para iterar gameplay sem simular loja. prd valida ads, Sentry, Firebase e RevenueCat como na Play.

Por que fundimos “clean” no “full”

Havia também sudoku:apk:prd:clean e sudoku:aab:prd:clean, que só adicionavam --rerun-tasks ao Gradle. A distinção gerava dúvida real no time: “uso full ou clean para a Play?”

A resposta histórica estava no artigo sobre bundle JS cacheado: um AAB incremental subiu com versionCode=3 no manifest, mas o runtime Sentry ainda anunciava +2. --rerun-tasks corrigiu. Por isso o clean existia.

A simplificação foi direta: todo *:full e o único aab:prd passam a rodar --rerun-tasks sempre. O caminho rápido (apk:dev / apk:prd) continua incremental para mudanças só de JS/TS quando o projeto Android já existe. O tradeoff é build mais lento nos fluxos “full”, em troca de menos decisões e menos risco de enviar bundle velho.

Não existe mais AAB incremental no monorepo. Para a Play Store, só pnpm sudoku:aab:prd.

Quando o incremental engana

apk:prd incremental é legítimo para mudanças só em JS/TS — por exemplo ajustar texto de modal ou cor de nota — desde que o diretório android/ já exista e nenhum config plugin nativo tenha mudado.

O candidato 0.1.1 incluiu alteração em plugins/withKeyEvent.cjs, que injeta onKeyDown no MainActivity.kt gerado. Esse tipo de mudança não entra num APK incremental: sem expo prebuild, o binário continua com o Activity antigo e o teclado físico simplesmente não responde. Por isso o smoke de release usou sudoku:apk:prd:full antes do AAB, e o guia de Play recomenda *:full sempre que plugins nativos ou dependências com código Android mudarem.

A distinção não é burocracia. É a diferença entre “o Metro recarregou” e “o Gradle recompilou o que o jogador instala”.

O que entrou no PATCH 0.1.1

O bump de PATCH não foi cosmético. O pacote agrupa correções que testers já tinham relatado em builds 0.1.0 com versionCode diferente:

  • Game over obrigatório — modal não-dismissável com saídas explícitas (Restart, New Game, Home), para o back button não devolver o jogador ao tabuleiro terminado.
  • Notas mais legíveis — tamanho proporcional à célula nos dois renderers (Skia e Native), com anti-exploit em notas manuais.
  • Confete na vitória — overlay como último filho do screen, com zIndex/elevation, para não ficar atrás do conteúdo.
  • Teclado físico no Android — dígitos, apagar e atalhos h/a/n via react-native-keyevent no Activity, não via TextInput escondido (que no Fabric ainda abre o teclado virtual).

Release notes curtas para a Play ficam em docs/app-sudoku/SUDOKU_LOJA_LISTING.md. O texto longo fica no histórico e neste artigo; a loja recebe duas linhas que um jogador consegue ler.

Validar Sentry e Settings no device

Antes de subir faixa interna, conferimos três sinais no APK/AAB de produção:

  1. Settings — linha Versão 0.1.1 · Build 10 (ou equivalente i18n).
  2. Sentry — evento de bootstrap com release [email protected]+10 e dist=10.
  3. Ads/consent — fluxo UMP e banner de teste ausente; IDs vêm do YAML de produção embutido no generate.

Se Settings mostra 0.1.0 com build 10, o app.json não foi commitado ou o bundle JS veio de cache — volta para *:full ou aab:prd.

Checklist antes de subir 0.1.1

Depois dessas mudanças, o fluxo de release do Sudoku ficou assim:

  1. Conferir no Play Console o maior versionCode publicado (produção era 9; o próximo é 10).
  2. Ajustar expo.version pelo tipo de mudança (PATCH para correções atuais).
  3. Incrementar versionCode e alinhar ios.buildNumber.
  4. Rodar pnpm run typecheck, pnpm run lint, pnpm run format:check — sem regenerar config para development.
  5. Gerar artefato com pnpm sudoku:aab:prd (ou apk:prd:full para smoke em device).
  6. Confirmar no arquivo gerado generatedConfigEnvironment = "production" se ainda estiver inspecionando o repo.
  7. Validar no device: Settings mostra Versão 0.1.1 · Build 10, Sentry inicializa com +10, fluxos corrigidos (game over, notas, confete, teclado físico).

.env não escolhe ambiente

apps/sudoku/.env guarda secrets e chaves de vendor (Sentry, Firebase, RevenueCat, ads web). Não coloque EXPO_PUBLIC_APP_ENV ali — os scripts de build exportam o ambiente (apk:dev*development, apk:prd* / aab:prdproduction, E2E → test). Para Expo no dia a dia, pnpm sudoku já cai em development no config:generate; para web production use pnpm sudoku:production.

O que não mudou

A regra de não subir AAB pelo browser automation continua. O path local é apps/sudoku/android/app/build/outputs/bundle/release/app-release.aab. Release notes EN/pt-BR vivem em docs/app-sudoku/SUDOKU_LOJA_LISTING.md.

Posts e entradas de histórico que citam sudoku:aab:clean ou sudoku:apk sem sufixo registram comandos da época; a referência operacional atual está em README.md, docs/GUIA_APK.md e docs/app-sudoku/SUDOKU_GUIA_GOOGLE_PLAY.md (seção Versionamento).

Release Android confiável não é só incrementar versionCode. É o nome que o jogador vê, o build que a Play indexa, o YAML embutido no binário e o script que você rodou cinco minutos antes do upload baterem no mesmo ambiente.

Congelar 0.1.0 enquanto o versionCode subia era atalho de pré-lançamento. Em produção pública, um PATCH no SemVer custa pouco. O config:generate dentro do lint era pior: side effect silencioso que só aparece quando você abre o arquivo certo na ordem errada.

Se o app gera config por ambiente, liste quais scripts npm tocam nesse arquivo e com qual EXPO_PUBLIC_APP_ENV. O lint não deveria estar na lista.

Falar com a Sunstone Apps