Continous Delivery mit GitLab am Beispiel einer Android-App: Signiertes Release-APK bauen, automatisches Git-Tagging und optionaler PlayStore-Upload.

Android: Automatisierung des App-Releases mit GitLab

  • Android + GitLab CI/CD:
  • Signiertes Release-APK bauen
  • Automatisches Git-Tagging
  • Optional: Automatischer PlayStore-Upload

Bei TheAppGuys benutzen wir die in GitLab integrierte CI/CD Funktionalität für unseren Entwicklungsworkflow.

So können wir sicherstellen, dass wir immer eine build-fähige Version unseres Projektcodes haben: Neue Features und Bugfixes werden schon während der Entwicklung ständig daraufhin geprüft, dass ihr Buildprozess mit allen Tests durchläuft. In den Haupt-Branch wird nur dann gemergt, wenn alle Haken grün sind.

Das an sich ist bereits eine große Hilfestellung im Arbeitsalltag.
Aber als Automatisierungsfans wollten wir wissen, was noch alles möglich ist.

Manuelle Prozesse sind fehleranfällig, weil man sich jedes Mal an jeden einzelnen Schritt in der richtigen Reihenfolge erinnern muss. Gleichzeitig sind sie oft repetitiv, was beim menschlichen Gehirn manchmal dafür sorgt, dass es sich gelangweilt in den Standby-Modus versetzt. Keine gute Kombination. Gerade wenn es um den App-Release geht, möchte man alle Fehlerquellen natürlich so gut es geht vermeiden.
Außerdem sollte das Wissen über den internen Release-Prozess, Signierschlüssel, etc. nicht nur bei wenigen Personen liegen: Die können bekanntermaßen erkranken, die Firma verlassen, oder einfach mal nicht da sein.

Unsere Build-Server haben kein Problem mit Langeweile oder Vergesslichkeit und führen netterweise sowohl komplexe als auch eintönige Prozesse für uns aus.

Automatisierung FTW! 🚀

  • Nervige, repetitive Prozesse von Maschinen übernehmen lassen
  • Sich auf die eigentliche Arbeit konzentrieren
  • Die eigene Vergesslichkeit austricksen
  • Formalisierte Prozessbeschreibungen zu verfassen, die außerhalb der Köpfe Weniger existieren

Dieser Artikel beschreibt am Beispiel eines Android-Projektes, wie wir unseren Release-Prozess automatisiert haben.

Signing

Für das manuelle Signing der Release-Versionen unserer Android-Apps nutzen wir normalerweise die in diesem Blogbeitrag beschriebene Vorgehensweise:

Die Teammitglieder, die Release-Builds ausführen, haben den Release-Key auf ihrem Rechner, im [USER_HOME]/.singing Ordner als project.keystore. Außerdem liegt da die Gradle-Siging-Config, mit Key-Passwort und -Alias:

android {
    signingConfigs {
        release {
            storeFile file(project.property("MyProject.signing") + ".keystore");
            storePassword "password";
            keyAlias "myproject";
            keyPassword "password";
        }
    };

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

In [USER_HOME]/.gradle/gradle.properties wird dann auf diesen Rechnern auf ein Gradle-Property gesetzt:

MyProject.signing=/home/username/.signing/myproject

In der Gradle-Config des eigentlichen Projektes steht dann nur folgendes:

if (project.hasProperty("MyProject.signing")
    && new File(project.property("MyProject.signing") + ".gradle").exists()) {
  apply from: project.property("MyProject.signing") + ".gradle";
}

Bei den Team-Mitgliedern, die das Property gesetzt haben, erstellt ./gradlew clean build automatisch einen signierten Release-Build. Bei allen anderen wird ein normaler Debug-Builds durchgeführt und keinerlei Fehlermeldung angezeigt.

Die Frage ist jetzt, wie wir das Setzen des Gradle-Properties auch für den CI-Runner durchführen können und wie dieser Zugriff auf Key und Config bekommt.
Idealerweise sollte dabei alles über das GitLab-Frontend definiert werden können, damit alle, die dort Admin-Rechte haben die Konfiguration einfach sehen und anpassen können.

Secret Variables

GitLab bietet die Möglichkeit an, dem CI-Runner für den Buildprozess „Secret Variables“ zur Verfügung zu stellen.
Inspiriert von diesem Blogartikel haben wir uns dazu entschlossen, den Einsatzzweck dieser geheimen Variablen etwas überzustrapazieren:
Normalerweise sind die nämlich nur für einzeilige Textstrings gedacht. Aber: Wenn wir den Signierschlüssel zu Base64 konvertieren, können wir ihn als einen solchen behandeln!

cat myproject.keystore | openssl base64 | tr -d '\n' | pbcopy

Dieser Befehl entfernt (auf Mac OS) die Zeilenumbrüche aus der Base64-Version unseres Keys und speichert das Ergebnis in der Zwischenablage.
So können wir den Key als geheime Variable hinzufügen:

In der CI-Config .gitlab-ci.yml kann der Key aus der Variable in eine Datei geschrieben werden:

before_script:
    echo $ANDROID_KEY_BASE64 | base64 -d > signing.keystore

Das Gleiche funktioniert auch mit der Signing-Config, wobei wir hier keine Base64-Konvertierung brauchen.

Wichtig: Weil GitLab die Zeilenumbrüche in der geheimen Variable ignoriert, müssen wir in der Gradle-Syntax Semikolons verwenden:

android {
    signingConfigs {
        release {
            storeFile file(project.property("MyProject.signing") + ".keystore");
            storePassword "password";
            keyAlias "myproject";
            keyPassword "password";
        }
    };

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

In .gitlab-ci.yml dann:

before_script:
    echo $ANDROID_SIGNING > signing.gradle

Jetzt müssen wir beim Aufruf des Build-Tasks noch das entsprechende Gradle Property setzen:

build:release:
    ...
    script: ./gradlew assembleRelease -PMyProject.signing=${CI_PROJECT_DIR}/signing

Folgende Zeilen bewirken, dass das Release-APK als Artefakt archiviert wird (wonach es dann herunter- und im Store hochgeladen werden kann.)

artifacts:
   paths:
     - app/build/outputs/apk/release/*.apk
   expire_in: 50 yrs

50 Jahre sind wahrscheinlich erstmal lang genug 😉

Tagging

Da wir jetzt durch das Merging in den Release-Branch automatisch einen signiertes Release-APK erstellen, wäre es super, wenn wir den entsprechenden Commit auch noch automatisch mit einem Git-Tag versehen könnten. Dann könnten wir ihn später noch mit der der Android-Versionsnummer in Verbindung bringen.

Git-Tag-Namen müssen eindeutig sein. Das heißt, entweder ein_e Entwickler_in muss vor jedem Release-Merge manuell den versionCode hochsetzen. Oder aber: Automation to the rescue!

Anstatt den versionCode manuell zu inkrementieren kann dieser auch automatisch bestimmt werden, z.B. aus der aktuellen Anzahl von Git-Commits.

(Dann sollten Releases allerdings immer vom gleichen Branch ausgeführt werden, auf dem die Commit-Anzahl immer ansteigt, also keine Commits gestasht werden o.Ä.)

Es gibt diverse Gradle-Plugins, die das Auslesen von Git-Informationen ermöglichen, aber für diesen simplen Fall sind eigentlich keine Plugins notwendig:

static def gitCommitCount() {
    def cmd = "git rev-list HEAD --count"
    return cmd.execute().text.trim().toInteger()
}
android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.example"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode gitCommitCount()
...

Danach legen wir einen Gradle-Task an, der ein Tag für den aktuellen Commit erstellt:

task tagReleaseBuild(type: Exec) {
    def versionName = android.defaultConfig.versionName
    def versionCode = android.defaultConfig.versionCode
    def tagName = "v$versionCode"
    commandLine "git", "tag", "-a", tagName, "-m", "\"$versionName ($versionCode)\""
    commandLine "echo", "Created tag $tagName"
}

Dieser Task kann jetzt aus einem Job im CI-Build-Prozess aufgerufen werden. Das einzige Problem: Das Tag wird zwar erstellt, kann aber nicht zurück zum Repository gepusht werden, denn:
Der CI-Runner hat ausschließlich lesenden Zugriff darauf.

Es gibt verschiedene Wege, um dem Runner diese Rechte zu geben, z.B. über einen SSH-Key auf dem entsprechenden Server bzw. Docker-Container.

Eine andere Lösung sind GitLabs sogenannte „Impersonation Tokens“. Deren Benutzung hat den Vorteil, dass wir einen Dummy-User anlegen können, der nicht mal ein Passwort gesetzt haben muss.
Alle GitLab-Admins können Impersonation Tokens direkt im GitLab-Frontend ansehen und anlegen. Es gibt also keine Notwendigkeit, ein weiteres Passwort oder Key über andere Kanäle herumreichen zu müssen.

Dem Dummy-Nutzer müssen jetzt natürlich noch Schreibrechte im entsprechenden Projekt gegeben werden. Dabei ist die Rolle „Developer“ ausreichend, weil nur das Tag gepusht werden muss, nicht der „protected branch“ selbst.

Das Impersonation Token muss dann wieder mal als geheime Variable hinzugefügt werden:

In .gitlab-ci.yml funktioniert das Tagging dann so (in einem separaten Deploy-Schritt):

tag_release:
 stage: deploy
...
 script:
   - git config user.email "gitlab@gitlab.mycompany.com"
   - git config user.name "Gitlab CI User"
   - gradle tagReleaseBuild
   - export LAST_TAG=$(git describe)
   - git push https://my_dummy_user:${IMPERSONATION_TOKEN}@gitlab.mycompany.com/myproject/myproject-android.git ${LAST_TAG}
 only:
   - release

Wichtig: Hier statt my_dummy_user den tatsächlichen Username verwenden.

Jetzt wird jedes Mal als Teil des Release Builds die aktuelle Android-Versionsnummer als Git-Tag gesetzt. 🎉

Ausblick: PlayStore Upload

Wenn man das Ganze noch einen Schritt weitertreiben möchte, könnte man jetzt auch noch den Upload in den PlayStore selbst automatisieren. Es gibt ein Gradle-Plugin namens gradle-play-publisher, das dies ermöglicht.

Die Vorgehensweise wäre jetzt ähnlich wie das Git-Tagging oben: Eine der von dem Plugin definierten Task müsste in einem Build-Schritt in .gitlab-ci.yml aufgerufen werden, und es gäbe einen weiteren Key, der als geheime Variable hinzugefügt wird.

Bei TheAppGuys machen wir diesen letzten Schritt gerade noch von Hand, da das gradle-play-publisher Plugin weitreichende globale Zugriffsrechte für den Play-Store-Account benötigt.
Wenn man das verkraften kann, ist es dann möglich, durch (Merge-)Knopfdruck den kompletten Release-Prozess bis zur fertig gebauten App im Store auszulösen.

Hier erfahren Sie mehr:

Oder rufen Sie uns an: 0221 / 291 993 72

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.