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.
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.0→0.1.1): correções e polimento sem feature nova relevante. - MINOR (
0.1.x→0.2.0): funcionalidade visível nova (tutorial, dificuldade extra, modo novo). - MAJOR (
0.x.x→1.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:
pnpm sudoku:apk:prddeixavaconfig.generated.tsemproduction.pnpm run lintsobrescrevia paradevelopment.- 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/nviareact-native-keyeventno Activity, não viaTextInputescondido (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:
- Settings — linha
Versão 0.1.1 · Build 10(ou equivalente i18n). - Sentry — evento de bootstrap com release
[email protected]+10edist=10. - 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:
- Conferir no Play Console o maior
versionCodepublicado (produção era9; o próximo é10). - Ajustar
expo.versionpelo tipo de mudança (PATCH para correções atuais). - Incrementar
versionCodee alinharios.buildNumber. - Rodar
pnpm run typecheck,pnpm run lint,pnpm run format:check— sem regenerar config paradevelopment. - Gerar artefato com
pnpm sudoku:aab:prd(ouapk:prd:fullpara smoke em device). - Confirmar no arquivo gerado
generatedConfigEnvironment = "production"se ainda estiver inspecionando o repo. - 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:prd →
production, 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.