- 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.