The Social App: Integrando dados de Cobertura de Código

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último post realizamos uma configuração inicial do tema do nosso aplicativo de forma a diferenciar as builds de debug e release. No post de hoje, vamos configurar uma métrica importante para o desenvolvimento de qualquer projeto de software, a cobertura de código.

Simplificando ao máximo o conceito (já que a explicação dele foge um pouco ao escopo deste post), a métrica diz respeito a quanto do seu código é “exercitado” durante a execução dos testes. Os relatórios de cobertura geralmente fornecem formas de mensurar não só percentualmente quanto do código foi coberto pelos testes, mas também pode dar insights sobre quais partes do código poderiam receber mais testes.

O primeiro passo é habilitarmos a geração desse dado no projeto. Porém, neste momento não é possível gerar este dado, já que nosso projeto não possui nenhum teste. Na verdade, ele praticamente não possui código, somente uma Activity vazia que ainda não faz nada. Poderíamos criar um teste simples de Espresso, para validar a inicialização da Activity, porém ainda não resolvemos o problema da execução de testes instrumentados no nosso CI (inclusive, se você quer ler mais sobre a diferença entre testes instrumentados e locais, tem um post excelente do Victor Nascimento no Medium). Dado esse cenário, vou utilizar o Robolectric para criar um teste que, apesar de utilizar classes do Android, roda localmente na JVM.

Para integrarmos o Robolectric em nosso projeto, é necessário adicionar a sua dependência, dentro do escopo de testes (testImplementation). Além disso, como o Robolectric utiliza resources do Android, precisamos habilitar a disponibilização dos resources para os testes unitários. Caso contrário, nossos testes falharão por não encontrar qualquer referência a resources adicionados por nós no projeto (como layouts, strings, drawables e outros).

<br />
android {<br />
  ...<br />
  testOptions {<br />
    unitTests.includeAndroidResources true<br />
  }<br />
}</p>
<p>dependencies {<br />
  ...<br />
  testImplementation 'org.robolectric:robolectric:3.8'<br />
}<br />

Dentro da pasta src/test/kotlin, vamos criar uma classe de teste bem simples, chamada MainActivityTest, que simplesmente fará uma validação se a Activity foi criada com sucesso pelo Robolectric. Apesar de ser um teste sem muito valor para o app (afinal, estamos testando se o Robolectric funciona), esse teste gerará alguma cobertura para o nosso projeto.

package net.rafaeltoledo.social</p>
<p>import org.junit.Assert.assertNotNull<br />
import org.junit.Test<br />
import org.junit.runner.RunWith<br />
import org.robolectric.Robolectric<br />
import org.robolectric.RobolectricTestRunner</p>
<p>@RunWith(RobolectricTestRunner::class)<br />
class MainActivityTest {</p>
<p>    @Test<br />
    fun checkIfActivityIsSuccessfullyCreated() {<br />
        assertNotNull(Robolectric.setupActivity(MainActivity::class.java))<br />
    }<br />
}

Com o teste criado (e passando!), vamos configurar a geração dos dados de cobertura. Eu já escrevi dois posts sobre o assunto, então não vou me alongar muito sobre os detalhes de implementação aqui para não deixar o este post muito longo 🙂

Criei um script chamado coverage.gradle, que coloquei na pasta gradle/ do projeto (inicialmente, tinha colocado numa pasta chamada tools/, mas após algumas discussões a respeito, pareceu fazer mais sentido a primeira opção).

apply plugin: 'jacoco'</p>
<p>jacoco.toolVersion versions.jacoco</p>
<p>tasks.withType(Test) {<br />
    jacoco.includeNoLocationClasses = true<br />
}</p>
<p>def classes = fileTree(dir: &quot;$buildDir/tmp/kotlin-classes/debug&quot;)<br />
def sources = files(&quot;$projectDir/src/main/kotlin&quot;)<br />
def report = &quot;$buildDir/reports/jacoco/report.xml&quot;</p>
<p>task createCombinedCoverageReport(type: JacocoReport,<br />
        dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {</p>
<p>    sourceDirectories = sources<br />
    classDirectories = files(classes)<br />
    executionData = fileTree(dir: buildDir, includes: [<br />
            'jacoco/testDebugUnitTest.exec',<br />
            'outputs/code-coverage/connected/*coverage.ec'<br />
    ])</p>
<p>    reports {<br />
        xml.enabled = true<br />
        xml.destination file(report)</p>
<p>        html.enabled = true<br />
    }<br />
}

Bom, a parte de configuração do relatório em si é muito próxima do que expliquei nos dois posts sobre o assunto. Algumas coisas estão isoladas em variáveis (sourcesreport), pois vamos reutilizá-las já já.

Com isso, vamos incluir o arquivo no build.gradle do módulo app e habilitar a geração de cobertura de código para os testes instrumentados:

apply plugin: 'com.android.application'<br />
apply plugin: 'org.jetbrains.kotlin.android'</p>
<p>apply from: &quot;$rootDir/gradle/coverage.gradle&quot;</p>
<p>android {<br />
  ...<br />
  buildTypes {<br />
    debug {<br />
      testCoverageEnabled true<br />
      ...<br />
    }<br />
    ...<br />
  }<br />
}<br />
...

Para finalizar, vamos setar a versão do jacoco no classpath para a última versão, modificando o build.gradle na raiz do nosso projeto:

buildscript {</p>
<p>  ext.versions = [<br />
      'kotlin': '1.2.31',<br />
      'supportLibrary': '27.1.1',<br />
      'googleServices': '12.0.1',<br />
      'jacoco': '0.8.1'<br />
  ]</p>
<p>  repositories {<br />
    google()<br />
    jcenter()<br />
  }</p>
<p>  dependencies {<br />
    classpath 'com.android.tools.build:gradle:3.1.1'<br />
    classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin&quot;<br />
    classpath 'com.google.gms:google-services:3.2.1'<br />
    classpath &quot;org.jacoco:org.jacoco.core:$versions.jacoco&quot;<br />
  }<br />
}

Feito isso, já podemos executar a task createCombinedCoverageReport e deveremos obter o relatório com incríveis 100% de cobertura, disponível dentro da pasta app/build/reports/jacoco

Perceba que ele foi gerado pela versão correta do Jacoco que configuramos, 0.8.1.

Com a métrica sendo gerada, é importante que ela esteja visível dentro do nosso fluxo de trabalho, para que possamos acompanhar a sua evolução ao longo do desenvolvimento do projeto. Para isso, precisamos “publicar” essa informação em algum lugar.

O Jenkins, por exemplo, oferece formas de acompanhar isso, caso que não ocorre com o CircleCI. Para isso, vamos utilizar um serviço externo, no caso o Coveralls. Durante o desenvolvimento, validei também o Codecov, porém ele não se comportou muito bem com o Kotlin. O Codecov irá se plugar ao nosso fluxo, validando a cobertura atual em cada Pull Request, e exibirá um histórico de como essa métrica evolui a cada commit.

Para que essa integração funcione, precisamos de enviar essa métrica para o Coveralls. Faremos isso através de um plugin. A configuração dele é bem simples:

// build.gradle<br />
buildscript {<br />
  ...<br />
  repositories {<br />
    google()<br />
    jcenter()<br />
    gradlePluginPortal()<br />
  }<br />
  dependencies {<br />
    classpath 'com.android.tools.build:gradle:3.1.1'<br />
    classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin&quot;<br />
    classpath 'com.google.gms:google-services:3.2.1'<br />
    classpath &quot;org.jacoco:org.jacoco.core:$versions.jacoco&quot;<br />
    classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'<br />
  }<br />
}

No arquivo coverage.gradle, faremos a aplicação do plugin, bem como a sua configuração:

// coverage.gradle<br />
apply plugin: 'jacoco'<br />
apply plugin: 'com.github.kt3k.coveralls'</p>
<p>...</p>
<p>def classes = fileTree(dir: &quot;$buildDir/tmp/kotlin-classes/debug&quot;)<br />
def sources = files(&quot;$projectDir/src/main/kotlin&quot;)<br />
def report = &quot;$buildDir/reports/jacoco/report.xml&quot;</p>
<p>...</p>
<p>coveralls {<br />
    sourceDirs = sources.flatten()<br />
    jacocoReportPath = report<br />
}<br />

Com isso, o último passo é configurar a chave de upload dos relatórios no CI. Para isso, basta criarmos uma variável de ambiente chamada COVERALLS_REPO_TOKEN e colocar o valor fornecido no painel do Coveralls.

Feito isso, o último passo é editar o nosso arquivo de configuração do CI para incluir algumas coisas:

version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      - image: circleci/android:api-27-alpha</p>
<p>    working_directory: ~/social-app</p>
<p>    environment:<br />
      JVM_OPTS: -Xmx3200m<br />
      CIRCLE_JDK_VERSION: oraclejdk8</p>
<p>    steps:<br />
      - checkout</p>
<p>      - restore_cache:<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}</p>
<p>      - run:<br />
          name: Accept licenses<br />
          command: yes | sdkmanager --licenses || true</p>
<p>      - run:<br />
          name: Decrypt release key<br />
          command: openssl aes-256-cbc -d -in distribution/release.keystore-cipher -out distribution/release.keystore -md sha256 -k $CIPHER_DECRYPT_KEY</p>
<p>      - run:<br />
          name: Setup Google Services JSON<br />
          command: |<br />
            mkdir -p app/src/debug/ &amp;amp;&amp;amp; touch app/src/debug/google-services.json<br />
            echo &quot;${JSON_FIREBASE_DEVELOPMENT}&quot; &amp;gt;&amp;gt; &quot;app/src/debug/google-services.json&quot;<br />
            mkdir -p app/src/release/ &amp;amp;&amp;amp; touch app/src/release/google-services.json<br />
            echo &quot;${JSON_FIREBASE_RELEASE}&quot; &amp;gt;&amp;gt; &quot;app/src/release/google-services.json&quot;</p>
<p>      - run:<br />
          name: Run Linters<br />
          command: ./gradlew check</p>
<p>      - run:<br />
          name: Run Tests and generate Code Coverage<br />
          command: ./gradlew createCombinedCoverageReport</p>
<p>      - run:<br />
          name: Upload code coverage data<br />
          command: ./gradlew coveralls</p>
<p>      - run:<br />
          name: Build<br />
          command: ./gradlew assemble assembleAndroidTest</p>
<p>      - store_artifacts:<br />
          path: app/build/reports/jacoco/createCombinedCoverageReport<br />
          destination: coverage-report</p>
<p>      - store_artifacts:<br />
          path: app/build/reports/tests/testDebugUnitTest<br />
          destination: local-test-report</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}<br />

Aqui eu fiz algumas modificações. Primeiramente, separei as tasks de linters, testes e build (a partir da linha 35), adicionando a task que vai enviar os dados para o Coveralls (Upload code coverage data). Além disso, comecei a disponibilizar no CircleCI os relatórios e de cobertura e execução de testes (utilizando a configuração store_artifacts). Com isso, após a execução de uma build com sucesso, é possível navegar por esses artefatos, desde que esteja logado.

Ufa! É isso! Você pode conferir no repositório o resultado dessas mudanças. A partir de agora, a cobertura de código é exibida também no README do projeto e como uma etapa de verificação nos PRs abertos!

The Social Project: Configurando o Firebase no Projeto

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último post configuramos as chaves de assinatura do nosso app de maneira segura, garantindo que os dados não estejam expostos no repositório e viabilizando que o CI consiga buildar e assinar os APKs. No post de hoje, vamos fazer a configuração do projeto no Firebase.

O primeiro passo é criarmos os projetos em nossa conta Google. Por que escrevi no plural? Porque faz todo sentido separarmos os ambientes, tendo um projeto para desenvolvimento (onde poderemos testar das mais diversas formas desde formato de dados, notificações, configurações e outros) e outro para produção, podendo assim realizar diversos experimentos sem impactar o app já publicado.

No meu caso, criei dois projetos no painel do Firebase, dando o nome de Social App DevelopmentSocial App Production.

Com os dois projetos criados, vamos configurar o nosso app. Primeiramente o build.gradle principal do projeto:

buildscript {</p>
<p>  ext.versions = [<br />
      'kotlin': '1.2.30',<br />
      'supportLibrary': '27.1.0',<br />
      'googleServices': '12.0.0'<br />
  ]</p>
<p>  repositories {<br />
    google()<br />
    jcenter()<br />
  }<br />
  dependencies {<br />
    classpath 'com.android.tools.build:gradle:3.0.1'<br />
    classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin&quot;<br />
    classpath 'com.google.gms:google-services:3.2.0'<br />
  }<br />
}</p>
<p>allprojects {<br />
  repositories {<br />
    google()<br />
    jcenter()<br />
  }</p>
<p>  configurations.all {<br />
    resolutionStrategy {<br />
      eachDependency { details -&amp;gt;<br />
        if (details.requested.group == 'com.android.support'<br />
            &amp;amp;&amp;amp; details.requested.name != 'multidex'<br />
            &amp;amp;&amp;amp; details.requested.name != 'multidex-instrumentation') {<br />
          details.useVersion versions.supportLibrary<br />
        }<br />
      }<br />
    }<br />
  }<br />
}</p>
<p>task clean(type: Delete) {<br />
    delete rootProject.buildDir<br />
}

Fizemos algumas boas mudanças no arquivo. Primeiramente criei um bloco para padronizar a configuração de versões que se repetem em múltiplos locais do projeto – a versão do Kotlin, da Support Library e das bibliotecas Google (Firebase e Play Services). Em seguida, adicionamos o plugin google-services em sua última versão, que se encarregará de configurar o nosso projeto.

Nesse arquivo, também, coloquei uma pequena configuração para forçar todas as Support Libraries a compartilharem da mesma versão. Por quê? Muitas vezes, ao adicionar uma outra dependência – por exemplo, o próprio Firebase – ele depende de uma versão de alguma Support Library mais antiga que a utilizada no projeto. Uma das formas de se resolver isso é dando um exclude na dependência e adicionando manualmente a versão mais recente. Particularmente não gosto dessa abordagem por poluir muito o bloco de dependências. Dessa forma, temos esse bloco de configuração que vai garantir que as Support Libraries estejam todas na versão 27.1.0 (exceto as bibliotecas de Multidex, que possuem um versionamento próprio).

Em seguida, vamos configurar o build.gradle do projeto app.

android {<br />
  ...<br />
  buildTypes {<br />
    debug {<br />
      applicationIdSuffix '.dev'<br />
      signingConfig signingConfigs.debug<br />
    }<br />
    ...<br />
  }<br />
}</p>
<p>dependencies {<br />
  implementation &quot;com.android.support:appcompat-v7:$versions.supportLibrary&quot;</p>
<p>  implementation &quot;org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin&quot;</p>
<p>  implementation &quot;com.google.firebase:firebase-core:$versions.googleServices&quot;<br />
  ...<br />
}</p>
<p>apply plugin: 'com.google.gms.google-services'

Aqui, primeiramente, adicionamos um sufixo ao applicationId para o build type debug. Isso vai facilitar mantermos tanto a versão de produção quanto a de desenvolvimento no mesmo dispositivo. Em seguida, configurei as dependências para utilizar as versões definidas globalmente e adicionei a dependência firebase-core, que já nos adiciona o analytics, por exemplo. Por fim, aplicamos o plugin google-services no final do arquivo, conforme a documentação.

Bom, feito isso, vamos configurar os aplicativos no painel do Firebase.

Aqui, vamos inserir o nome do pacote (net.rafaeltoledo.social para o app de produção, net.rafaeltoledo.social.dev para o app de desenvolvimento), opcionalmente um apelido e o hash SHA-1 de assinatura. Podemos facilmente conseguir esse hash através da task signingReport do Gradle. Para executá-la, certifique-se de que a chave de produção esteja descriptografada e que as variáveis de ambiente com as senhas estejam exportadas.

Com o resultado dessa task, copie o hash SHA-1 de debug e release e coloque em cada um dos projetos. Com isso, teremos dois arquivos google-services.json, um para cada projeto.

Pronto, agora é só commitar e seguir o jogo? Não necessariamente…

Quando você está desenvolvendo um projeto, idealmente você não deveria deixar estes arquivos no seu controle de versão. Primeiramente, porque alguém poderia criar um app com o mesmo applicationId e utilizá-lo para acessar o seu projeto. Segundo, que poderia com isso também comprometer a sua cota de serviços e, caso você utilize um dos serviços pagos do Firebase, ocasionar em gastos para você. Portanto, trate o arquivo google-services.json também como uma chave do app.

Para resolver esse problema, primeiramente coloquei o arquivo no .gitignore, para que ele não seja adicionado ao Git. Em seguida, vamos colocar o conteúdo do arquivo no CI, para que sejam colocados no projeto na hora do build. Farei isso colocando o conteúdo em uma variável de ambiente e, através do comando echo, mandando o conteúdo pra um arquivo.

O conteúdo desse arquivo é simplesmente um JSON, então a minha recomendação é, antes de criar as variáveis, utilizar algum serviço de minificação (ou minify) para remover espaços e quebras de linha desnecessárias.

Por fim, basta adicionar uma etapa no arquivo de configuração do CI para obter o valor da variável e jogar no arquivo corretamente dentro de cada uma das pastas (app/src/release app/src/debug).

- run:<br />
    name: Setup Google Services JSON<br />
    command: |<br />
      mkdir -p app/src/debug/ &amp;amp;&amp;amp; touch app/src/debug/google-services.json<br />
      echo &quot;${JSON_FIREBASE_DEVELOPMENT}&quot; &amp;gt;&amp;gt; &quot;app/src/debug/google-services.json&quot;<br />
      mkdir -p app/src/release/ &amp;amp;&amp;amp; touch app/src/release/google-services.json<br />
      echo &quot;${JSON_FIREBASE_RELEASE}&quot; &amp;gt;&amp;gt; &quot;app/src/release/google-services.json&quot;

E é isso. Configuramos corretamente o projeto e mantivemos nossos segredos em segurança 🙂

O código já foi mergeado no repositório após um PR e a build no CI 🙂

The Social Project: Configurando a Assinatura do App no CI

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último artigo, configuramos o nosso build script para suportar o Kotlin. No post de hoje, vamos configurar a assinatura do app e levantar um ponto importante: como guardar seus segredos, principalmente quando o app é open source?

Com certeza, se você procurar um pouco, você encontrará diversos repositório com todo tipo de informação privada, desde chaves de APIs a ferramentas pagas. Todas, commitadas de forma acidental (ou mesmo proposital, mas de forma inadvertida) no repositório público do projeto.

No Android, um dos maiores “segredos” que mantemos é a chave de assinatura do app. Ela, juntamente com o Application ID, fazem um app se tornar único. Como o identificador é público, o vazamento de uma chave pode gerar um seríssimo risco de segurança, possibilitando que versões modificadas do seu app sejam passíveis de instalação sobre o app original, não só possibilitando comportamentos potencialmente perigosos para o usuário, como o acesso a informações internas e privadas que eventualmente o seu app tenha salvo localmente (como dados de autenticação na sua API, por exemplo).

A perda dessa chave também pode ser um problemão. Sem ela, é impossível enviar atualizações do seu app para o Google Play, por exemplo. Hoje felizmente já temos formas de minimizar esse risco, utilizando o Google Play App Signing (assunto que com certeza será abordado nessa série de posts).

Falando especificamente do nosso app, temos então um problema com relação a isso. Para a assinatura, precisamos de um arquivo (geralmente com a extensão .keystore ou .jks), duas senhas e um alias. O primeiro questionamento é sobre o arquivo em si. Colocaremos ele no repositório? Por mais que a chave necessite das senhas, ter o arquivo exposto envolve algum grau de risco.

Para solucionar esse problema temos basicamente duas abordagens: a primeira delas seria colocar o arquivo de assinatura em algum serviço externo, como Google Drive ou Dropbox, e baixá-lo no momento da build utilizando curl. Nesse cenário, teríamos uma chave de API exposta via variável de ambiente. Apesar de viável, esse cenário tem o problema de estarmos acoplando uma dependência externa ao simples processo de build do APK. Além de oscilações de rede que podem causar falha da build, também temos que manter essa request em dia, tendo que ajustar a chamada de download no caso de eventuais mudanças nas APIs desses serviços.

A segunda abordagem, e é a que vou utilizar aqui, será criptografar o arquivo e colocá-lo no repositório junto com o projeto. No CI, teremos exportado como variável de ambiente a chave que consegue descriptografar o arquivo, bem como as outras chaves necessárias para assinar o APK.

A maneira mais simples de gerarmos uma chave é através do próprio Android Studio.

Com a chave gerada, utilizaremos o openssl para criptografar o arquivo. Podemos fazer isso utilizando o comando:

openssl aes-256-cbc -e -in release.keystore -out release.keystore-cipher -md sha256 -k $CIPHER_DECRYPT_KEY

Perceba que no comando temos uma variável chamada CIPHER_DECRYPT_KEY, que é a chave que irá descriptografar o nosso arquivo lá no CI.

Com o arquivo criptografado gerado, vamos criar uma pasta no projeto para guardar as chaves. Além do arquivo criptografado release.keystore-cipher, também estou colocando o arquivo que assina as releases de debug. Isso é particularmente útil quando existe mais de uma pessoa trabalhando no projeto ou quando eventualmente há a necessidade de se trocar ou formatar a máquina, já que essa chave é gerada sempre que uma nova instalação da SDK do Android é realizada.

Com os arquivos no projeto, vamos primeiramente dizer ao CI para descriptografar o arquivo antes de executar a build. Perceba que adicionamos um passo chamado Decrypt release key ao nosso arquivo config.yml. Também modifiquei o passo de build para chamar a task assemble em vez da build (para validar a parte de assinatura de ambos os build variants):

version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      - image: circleci/android:api-27-alpha</p>
<p>    working_directory: ~/social-app</p>
<p>    environment:<br />
      JVM_OPTS: -Xmx3200m<br />
      CIRCLE_JDK_VERSION: oraclejdk8</p>
<p>    steps:<br />
      - checkout</p>
<p>      - restore_cache:<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}</p>
<p>      - run:<br />
          name: Accept licenses<br />
          command: yes | sdkmanager --licenses || true</p>
<p>      - run:<br />
          name: Decrypt release key<br />
          command: openssl aes-256-cbc -d -in distribution/release.keystore-cipher -out distribution/release.keystore -md sha256 -k $CIPHER_DECRYPT_KEY</p>
<p>      - run:<br />
          name: Build<br />
          command: ./gradlew clean check assemble assembleAndroidTest</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}

Feito isso, vamos ao nosso build script configurar a assinatura e atribui-las para cada um dos build variants:

...</p>
<p>android {<br />
  ...<br />
  signingConfigs {<br />
    debug {<br />
      storeFile file(&quot;$rootDir/distribution/debug.keystore&quot;)<br />
      storePassword 'android'<br />
      keyAlias 'androiddebugkey'<br />
      keyPassword 'android'<br />
    }</p>
<p>    release {<br />
      storeFile file(&quot;$rootDir/distribution/release.keystore&quot;)<br />
      storePassword System.env.RELEASE_STORE_PASSWORD<br />
      keyAlias System.env.RELEASE_KEY_ALIAS<br />
      keyPassword System.env.RELEASE_KEY_PASSWORD<br />
    }<br />
  }</p>
<p>  buildTypes {<br />
    debug {<br />
      signingConfig signingConfigs.debug<br />
    }</p>
<p>    release {<br />
      signingConfig signingConfigs.release<br />
      minifyEnabled false<br />
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'<br />
    }<br />
  }<br />
}</p>
<p>...

Perceba que a parte da assinatura de release vai pegar também os dados através de variáveis de ambiente, que serão adicionadas ao CircleCI:

E é isso! O Pull Request foi aberto e aprovado para este caso, tendo passado no CI e integrado na branch develop do repositório.

No próximo post vamos fazer a configuração do Firebase no projeto 🙂

The Social Project: configurando o servidor de CI

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

No post anterior, criamos o repositório com um projeto vazio e configuramos o repositório com as branches persistentes que farão parte do nosso Git Flow.

Para essa tarefa, escolherei o CircleCI. Por quê? Primeiramente pelo seu suporte a Docker, o que facilita e muito na hora de fazer o setup do ambiente, caso a gente precise customizar o tooling. Além disso, ele possui uma cota gratuita generosa não só para projetos open-souce (que está sendo o caso deste daqui), mas também para projetos privados no Github ou Bitbucket.

Bom, primeiramente faremos o login no CircleCI com a nossa conta do Github e faremos o setup do nosso repositório.

Na tela seguinte, selecionamos uma máquina Linux e o Gradle como ferramenta de build. Nessa mesma tela, será oferecido um arquivo de configuração default, a ser colocado na pasta .circleci do repositório, com o nome de config.yml. O conteúdo inicial do arquivo é:

# Java Gradle CircleCI 2.0 configuration file<br />
#<br />
# Check https://circleci.com/docs/2.0/language-java/ for more details<br />
#<br />
version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      # specify the version you desire here<br />
      - image: circleci/openjdk:8-jdk</p>
<p>      # Specify service dependencies here if necessary<br />
      # CircleCI maintains a library of pre-built images<br />
      # documented at https://circleci.com/docs/2.0/circleci-images/<br />
      # - image: circleci/postgres:9.4</p>
<p>    working_directory: ~/repo</p>
<p>    environment:<br />
      # Customize the JVM maximum heap limit<br />
      JVM_OPTS: -Xmx3200m<br />
      TERM: dumb</p>
<p>    steps:<br />
      - checkout</p>
<p>      # Download and cache dependencies<br />
      - restore_cache:<br />
          keys:<br />
          - v1-dependencies-{{ checksum &quot;build.gradle&quot; }}<br />
          # fallback to using the latest cache if no exact match is found<br />
          - v1-dependencies-</p>
<p>      - run: gradle dependencies</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: v1-dependencies-{{ checksum &quot;build.gradle&quot; }}</p>
<p>      # run tests!<br />
      - run: gradle test</p>
<p>

A partir desse modelo, faremos algumas alterações. A primeira delas é mudar a imagem do docker para uma que já contenha todo o setup para que possamos buildar apps Android – temos uma lista das imagens mantidas pelo próprio CircleCI aqui. Além disso, configuramos a versão da JDK a ser utilizada e configuramos o cache de dependências (para que não precise ser baixadas novamente, caso não haja mudança nos arquivos do Gradle). Colocamos também uma task preventiva (Accept licenses) para evitar que o processo de build quebre por conta de licenças não aceitas.

Na task de build, configuramos a execução do Lint (através da task check), a compilação e execução de testes locais (através da task build) e, por enquanto, a compilação dos testes instrumentados (através da task assembleAndroidTest) – posteriormente faremos a delegação da execução desses testes para o Firebase Test Lab.

O arquivo final fica assim:

version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      - image: circleci/android:api-27-alpha</p>
<p>    working_directory: ~/social-app</p>
<p>    environment:<br />
      JVM_OPTS: -Xmx3200m<br />
      CIRCLE_JDK_VERSION: oraclejdk8</p>
<p>    steps:<br />
      - checkout</p>
<p>      - restore_cache:<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}</p>
<p>      - run:<br />
          name: Accept licenses<br />
          command: yes | sdkmanager --licenses || true</p>
<p>      - run:<br />
          name: Build<br />
          command: ./gradlew clean check build assembleAndroidTest</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}<br />

Feito isso, também colocarei um badge no README.md do repositório, indicando o atual status da nossa build. Tudo isso será então commitado em uma branch chamada rt/setup-ci e então um pull request será aberto.

Como sou administrador, eu poderia simplesmente ignorar o fluxo e eu mesmo mergear meu Pull Request. Porém, iria quebrar o fluxo que eu mesmo estou propondo! 🙂

Para fazer o review do meu PR, adicionei a Elessandra como revisora. No caso, a pessoa que revisa o PR pode fazer apontamentos e, caso esteja tudo certo, aprovar e mergear.

Como nem tudo são flores, eu commitei o arquivo com alguns erros de digitação na primeira tentativa. Como ainda não tínhamos o CI integrado para validar o arquivo de configuração, os olhos humanos deixaram passar. Com isso, fui obrigado a fazer outro PR revertendo a modificação. Pela própria interface do Github é possível solicitar desfazer o PR.

Corrigidos os erros (e, após algumas tentativas), finalmente consegui configurar o CI corretamente.

Como foram vários commits de tentativa e erro, o PR ficou com vários commits. Para esse caso, o ideal é que se faça o Squash (juntar todos os commits em um único), evitando assim que o histórico do Git seja poluído.

Por fim, após o merge é importante excluir a branch de origem, já que ela não é uma branch persistente. Assim, mantemos o repositório limpo e organizado 🙂

Por hoje é isso. O repositório já com as modificações está disponível aqui.