Slide 1

Slide 1 text

Automatisez vos workflows en Kotlin DevFest Paris - 2020 1

Slide 2

Slide 2 text

Automatisez vos workflows en Kotlin @martinbonnin 2

Slide 3

Slide 3 text

Dailymotion 3

Slide 4

Slide 4 text

Environnement 4 Github Jira Bitrise Play Store AppCenter Transifex Slack

Slide 5

Slide 5 text

Une journée (presque) classique 5 1. S’assigner un ticket jira 2. Ouvrir une branche github 3. Coder... 4. Créer une pull request 5. Déplacer le ticket 6. Merger la pull request 7. Déplacer le ticket 8. Créér une alpha ○ Incrémenter version ○ Tag ○ Compiler ○ Push ○ Archiver 9. Envoyer un message aux designers/product owners 10. Intégrer les retours 11. Revenir à l’étape 1. ● Envoyer les traductions ● Récupérer les traductions ● Mettre à jour le changelog ● Notifier la nouvelle version sur jira ● Envoyer en beta ● Mettre le changelog sur le play store. ● Démarrer le rollout ● Et aussi sur Amazon... ➡ Et tout nettoyer à la fin

Slide 6

Slide 6 text

Tout automatiser !! 6

Slide 7

Slide 7 text

Vraiment? 7 https://xkcd.com/1319/

Slide 8

Slide 8 text

Mais on gagne: ● En fiabilité ● Reproducibilité ● Auto-documentation ● Fun ● Et on peut gagner du temps (des fois) 8

Slide 9

Slide 9 text

Tout automatiser En Kotlin!! 9

Slide 10

Slide 10 text

Solutions existantes 10 Fastlane (Ruby) Transifex cli (Ruby) after_success.sh (Bash) generate_docs.sh (Bash) Github hub (Go) build.gradle (Groovy) Scripts ad-hoc Outils tiers Système de compilation Outils génériques

Slide 11

Slide 11 text

Pourquoi Kotlin? ● Comment faire un tableau en bash ? ● Est-ce qu’il faut échapper les ‘\’ ? ● Comment faire une requete HTTP en ruby ? ● Et parser un Json en go ? ● Avec Kotlin, pas de changement de Contexte 11

Slide 12

Slide 12 text

Mais aussi ● Language Moderne ● Un IDE au top (la plupart du temps!) ● Un écosystème riche 12

Slide 13

Slide 13 text

La boite à outils 13 ● Outils ○ Kscript ○ Clikt ● Bibliothèques ○ Process ○ HTTP ○ Json ○ ... ➡ Exemple d’application à Dailymotion

Slide 14

Slide 14 text

Kscript 14

Slide 15

Slide 15 text

Scripting 15 ● Pour des petits projets ● Un seul fichier ● Facile à mettre en place et à utiliser

Slide 16

Slide 16 text

Avec le compilateur Kotlin $ echo ‘println("Hello ${args[0]}!")’ > hello.kts $ kotlinc -script hello.kts “DevFest Paris” Hello DevFest Paris! 16

Slide 17

Slide 17 text

✨ Avec Kscript ✨ ● https://github.com/holgerbrandl/kscript ● Mise en cache des compilations ● Shebang ● Gestion des dépendances ● Distribution ● Intégration IntelliJ 17

Slide 18

Slide 18 text

Kscript - installation curl -s "https://get.sdkman.io" | bash # install sdkman source ~/.bash_profile # add sdkman to PATH sdk install kotlin # install Kotlin sdk install kscript # install Kscript touch hello.kts kscript --idea hello.kts # start the IDE 18

Slide 19

Slide 19 text

Formats d’entrée ● Fichier *.kts ● Url ● Stdin ● Ou juste des paramètres ○ kscript 'println(42.toString(16))' ● Et meme un REPL ○ kscript -i 19

Slide 20

Slide 20 text

Kscript 20 // weekend.kts #!/usr/bin/env kscript @file:DependsOn("com.squareup.okhttp3:okhttp :4.3.1") import okhttp3.OkHttpClient import okhttp3.Request val weekend = isItTheWeekEndApiCall() if (weekend) { println("It is the weekend!") } else { println("Not yet :-|") }

Slide 21

Slide 21 text

Kotlin - OkHttp 21 ● https://square.github.io/okhttp/ ● Développé par square ● 100% Kotlin depuis la version 4 ● API complète ● Bientôt multiplateforme ?

Slide 22

Slide 22 text

Kotlin - OkHttp 22 fun isItTheWeekEndApiCall(): Boolean { val request = Request.Builder() .get() .url("http://isitweekendyet.com/") .build() val response = OkHttpClient().newCall(request).execute() return response.body!!.string().toLowerCase().contains("yes") }

Slide 23

Slide 23 text

Kotlin - OkHttp 23 fun isItTheWeekEndApiCall(): Boolean { val request = Request.Builder() .get() .header("User-Agent", "DevFest - 2020") .url("http://isitweekendyet.com/") .build() val response = OkHttpClient().newCall(request).execute() return response.body!!.string().toLowerCase().contains("yes") }

Slide 24

Slide 24 text

Kotlin - OkHttp 24 fun isItTheWeekEndApiCall(): Boolean { val request = Request.Builder() .get() .header("User-Agent", "DevFest - 2020") .url("http://isitweekendyet.com/") .build() val response = OkHttpClient().newCall(request).execute() if (response.code() == 401) { // get credentials and retry } return response.body!!.string().toLowerCase().contains("yes") }

Slide 25

Slide 25 text

Kscript 25 // weekend.kts #!/usr/bin/env kscript @file:DependsOn("com.squareup.okhttp3:okhttp :4.3.1") import okhttp3.OkHttpClient import okhttp3.Request val weekend = Request.Builder().get().url("http://isitweek endyet.com/").build().let { OkHttpClient().newCall(it) }.execute().body!!.string().toLowerCase().co ntains("yes") if (weekend) { println("It is the weekend!") } else { println("Not yet :-|") } $ chmod +x weekend.kts $ ./weekend.kts Not yet :-|

Slide 26

Slide 26 text

Kscript pour remplacer sed ? 26

Slide 27

Slide 27 text

Kscript - IDE 27 $ kscript --idea weekend.kts

Slide 28

Slide 28 text

Kscript - Debugger 28

Slide 29

Slide 29 text

Kscript - Distribution 29 # Inclusion des dépendances $ kscript --package weekend.kts $ ./weekend argument another-argument ou # Installation de kscript $ kscript --add-bootstrap-header weekend.kts

Slide 30

Slide 30 text

● Standardisation ? KEEP #75 Kscript - alternative ? 30

Slide 31

Slide 31 text

● Rapide à mettre en place ○ Generation de documentation ○ Scripts d’installations ○ Migrations vers le Kotlin Gradle DSL ● JVM nécessaire ● Pas vraiment prévu pour des projets à plusieurs fichiers. Kscript - Conclusion 31

Slide 32

Slide 32 text

Clikt 32

Slide 33

Slide 33 text

Clikt 33 ● https://github.com/ajalt/clikt ● Parseur de ligne de commande ● Kotlin Multiplatform

Slide 34

Slide 34 text

Paramètres 34 ● Optionnels ou non. ● Entier, flottant ou chaine de caractere ou Booleen. ● Nommé ou positionnel. $ hello -d --count 1 --name “Paris”

Slide 35

Slide 35 text

En bash 35 POSITIONAL=() while [[ $# -gt 0 ]] do key="$1" case $key in -e|--extension) EXTENSION="$2" shift # past argument shift # past value ;; --default) DEFAULT=YES shift # past argument ;; *) # unknown option POSITIONAL+=("$1") # save it in an array for later shift # past argument ;; esac done set -- "${POSITIONAL[@]}" # restore positional parameters

Slide 36

Slide 36 text

En Kotlin 36 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 37

Slide 37 text

Hello World 37 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 38

Slide 38 text

Hello World 38 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 39

Slide 39 text

Hello World 39 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 40

Slide 40 text

Hello World 40 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { // No need to cast echo("Hello " + name.toString() + "!") } } }

Slide 41

Slide 41 text

Main 41 fun main(args: Array) = Hello().main(args) $ ./hello --count 3 --name Paris Hello Paris! Hello Paris! Hello Paris!

Slide 42

Slide 42 text

Validation 42 $ ./hello --count 3 Error: Missing option "--name". $ ./hello --count DevFest --name Paris Error: Invalid value for "--count": DevFest is not a valid integer $ ./hello --ooops Error: No such option "--ooops"

Slide 43

Slide 43 text

Aide 43 $./hello -h Options: --count INT --name TEXT Votre nom -h, --help Show this message and exit

Slide 44

Slide 44 text

Aide 44 class Hello : CliktCommand() { val count by option().int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 45

Slide 45 text

Aide 45 class Hello : CliktCommand() { val count by option(help = "Nombre de fois").int().default(1) val name by option(help = "Votre nom").required() override fun run() { for (i in 1..count) { echo("Hello $name!") } } }

Slide 46

Slide 46 text

Aide 46 $./hello -h Options: --count INT Nombre de fois --name TEXT Votre nom -h, --help Show this message and exit

Slide 47

Slide 47 text

Autocomplete! 47 $ _HELLO_COMPLETE=bash hello #!/usr/bin/env bash # Command completion for hello # Generated by Clikt [...] A lot of bash completion code [...] $ _HELLO_COMPLETE=bash hello > hello_complete $ source hello_complete

Slide 48

Slide 48 text

Autocomplete! 48

Slide 49

Slide 49 text

Clikt - Et après ? 49 ● Kotlinx.cli ? https://github.com/Kotlin/kotlinx.cli ● Kotlin-native & nodeJS

Slide 50

Slide 50 text

KScript vs Clikt 50 ● KScript ○ Rapide à mettre en place ○ Un seul fichier ● Clikt ○ Un peu de mise en place ○ Pour des projets plus larges ● Les deux sont complémentaires.

Slide 51

Slide 51 text

Clikt en pratique 51

Slide 52

Slide 52 text

En pratique: la commande buildTag 52 $ buildTag flavor=alpha build=debug versionCode=14500 ○ Génère un aab signé + universal apk ○ Exécute tous les test/lint ○ Génère le Changelog ○ Envoie sur appcenter ○ Envoie un message sur slack

Slide 53

Slide 53 text

Projet Gradle Application 53 plugins { application id("org.jetbrains.kotlin.jvm") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.3.61") implementation("com.github.ajalt:clikt:2.4.0") } application { mainClassName = "com.dailymotion.buildTag.MainKt" applicationName = "buildTag" }

Slide 54

Slide 54 text

Projet Gradle Application 54 plugins { application id("org.jetbrains.kotlin.jvm") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.3.61") implementation("com.github.ajalt:clikt:2.4.0") } application { mainClassName = "com.dailymotion.buildTag.MainKt" applicationName = "buildTag" }

Slide 55

Slide 55 text

La commande buildTag 55 // com.dailymotion.buildTag.Main.kt class BuildTag : CliktCommand(name = "buildTag", help = "Makes all tests and builds a release.") { [...] } fun main(args: Array) = BuildTag().main(args)

Slide 56

Slide 56 text

La commande buildTag 56 class BuildTag : CliktCommand(name = "buildTag", help = "Makes all tests and builds a release.") { val tag by argument(name = "tag", help = "The tag to build").long() val flavor by option(help = "The flavor to build. Will build all flavor if not specified.") .choice("alpha", "playStore") val build by option(help = "The buildType to build. Will build all buildTypes if not specified.") .choice("debug", "release") override fun run() { // Build the tag } }

Slide 57

Slide 57 text

La commande buildTag 57 class BuildTag : CliktCommand(name = "buildTag", help = "Makes all tests and builds a release.") { val tag by argument(name = "tag", help = "The tag to build").long() val flavor by option(help = "The flavor to build. Will build all flavor if not specified.") .choice("alpha", "playStore") val build by option(help = "The buildType to build. Will build all buildTypes if not specified.") .choice("debug", "release") override fun run() { // Build the tag } }

Slide 58

Slide 58 text

La commande buildTag 58 class BuildTag : CliktCommand(name = "buildTag", help = "Makes all tests and builds a release.") { val tag by argument(name = "tag", help = "The tag to build").long() val flavor by option(help = "The flavor to build. Will build all flavor if not specified.") .choice("alpha", "playStore") val build by option(help = "The buildType to build. Will build all buildTypes if not specified.") .choice("debug", "release") override fun run() { checkoutVersion(tag) executeGradle(flavor, build) computeChangelog() deploy(flavor, build, tag) sendSlack(tag) } }

Slide 59

Slide 59 text

Kotlin: executer Gradle 59 ProcessBuilder().command("gradle", "assembleProdDebug") .directory(File("projectDir")) .inheritIO() // same behaviour as bash .start() .waitFor()

Slide 60

Slide 60 text

Kotlin: executer Gradle 60 ProcessBuilder().command("gradle", "assembleProdDebug") .directory(File("projectDir")) .inheritIO() // same behaviour as bash .start() .waitFor()

Slide 61

Slide 61 text

Processus: lire la sortie 61 val process = ProcessBuilder().command("gradle", "assembleProdDebug") .directory(File("projectDir")) .start() val output = process.inputStream.reader().readText() val needle = output.lines().find { ... }

Slide 62

Slide 62 text

Processus: redirections 62 import java.io.File val processList = ProcessBuilder.startPipeline(listOf( ProcessBuilder().command("echo", """Hello |Paris""".trimMargin()), ProcessBuilder().command("grep", "Hello") )) val result = processList.get(1).inputStream.reader().readText()

Slide 63

Slide 63 text

Encore mieux: GradleRunner 63 val result = GradleRunner.create() .withProjectDir(projectDir) .withArguments("assembleProdDebug") .build() Assert.assertEquals( TaskOutcome.SUCCESS, result.task(":assembleProdDebug")?.outcome )

Slide 64

Slide 64 text

Reprenons 64 $ buildTag flavor=alpha build=debug versionCode=14500 ○ Génère un aab signé + universal apk ✅ ○ Exécute tous les test/lint ✅ ○ Génère le Changelog ○ Envoie sur appcenter et message Slack ⬦ OkHttp ✅ ⬦ Json ?

Slide 65

Slide 65 text

Kotlinx.serialization 65 ● Bibliothèque développée par Jetbrains ● Plugin compilateur qui génére des parsers sans reflexion. ● Compatible Json & protobuf ● 100% Multiplatforme

Slide 66

Slide 66 text

Kotlinx.serialization: mise en place 66 plugins { id("org.jetbrains.kotlin.jvm.plugin.serialization") } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serial ization-runtime:0.14.0") }

Slide 67

Slide 67 text

Kotlinx.serialization: Avec des classes 67 @Serializable class PostData(val release_notes: String, val destination: String) val json = Json.stringify( PostData("new features and new bugs", "everyone") ) val data = Json.parse(PostData.serializer(), json)

Slide 68

Slide 68 text

Kotlinx.serialization: Sans classes 68 // Serialization val json = JsonObject( mapOf( "destination_name" to JsonPrimitive("Everyone"), "release_notes" to JsonPrimitive("new features and new bugs") ) ).toString() // Deserialization val releaseNotes = Json.parse(json) .jsonObject .getPrimitive("release_notes") .content

Slide 69

Slide 69 text

Requête AppCenter 69 val json = Json.stringify( PostData("new features and new bugs", "everyone") ) val release_url = Request.Builder() .header("Content-Type", "application/json") .header("Accept", "application/json") .header("X-API-Token", token_) .patch(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json)) .url("https://api.appcenter.ms/${app}") .build() .let { OkHttpClient().newCall(it).execute().body()!!.string() }.let { Json.nonstrict.parse(ResultData.serializer(), it).release_url }

Slide 70

Slide 70 text

buildTag en Action 70 martin@n550~$ ./bin/buildTag --flavor=demo 14500 Compiling version 14500… Running lint… Running unit tests… Running connected tests… Generating universal apk… Uploading… Sending slack message… Done.

Slide 71

Slide 71 text

Kotlin: Ecosystème 71 ● Génération de code Swagger/Graphql ● Ktor pour embarquer un serveur web ● JGit pour s’intégrer avec git ● Votre bibliothèque préférée ● Et même Dagger si besoin !

Slide 72

Slide 72 text

Le couteau suisse Git ● startWork ● PR ● cleanLocal ● cleanRemote ● changelog ● hotfix ● branch Traductions ● txPull ● txPush ● txPR Travis/Bitrise ● trigger ● encrypt ● encryptFile Play Store metadatas ● uploadWhatsNew ● uploadListing ● uploadScreenshots ● generateScreenshots Play Store releases ● beta ● release Compilation ● nightly ● buildPR ● buildTag 72

Slide 73

Slide 73 text

Sur la machine de développement 73

Slide 74

Slide 74 text

Sur la machine de développement 4. Crééer une pull request 5. Mettre à jour le ticket startWork {TICKET_ID} 1. Assigner un ticket 2. Crééer une branche 7. Garder un repo propre 3. Code 6. Merger la pull request pr cleanLocal 74 ‍

Slide 75

Slide 75 text

Sur la machine d’intégration continue 75

Slide 76

Slide 76 text

Sur la machine d’intégration continue 76

Slide 77

Slide 77 text

Commandes réutilisables Git ● startWork ● PR ● cleanLocal ● cleanRemote ● changelog ● hotfix ● branch Traductions ● txPull ● txPush ● txPR Travis/Bitrise ● trigger ● encrypt ● encryptFile Play Store metadatas ● uploadWhatsNew ● uploadListing ● uploadScreenshots ● generateScreenshots Play Store releases ● beta ● release Compilation ● nightly ● buildPR ● buildTag 77

Slide 78

Slide 78 text

Commandes réutilisables Git ● startWork ● PR ● cleanLocal ● cleanRemote ● changelog ● hotfix ● branch Traductions ● txPull ● txPush ● txPR Travis/Bitrise ● trigger ● encrypt ● encryptFile Play Store metadatas ● uploadWhatsNew ● uploadListing ● uploadScreenshots ● generateScreenshots Play Store releases ● beta ● release Compilation ● nightly ● buildPR ● buildTag 78

Slide 79

Slide 79 text

Plusieurs niveaux 79 Commandes personnalisées Lib Commandes builtin

Slide 80

Slide 80 text

Plusieurs niveaux 80 Lib ● Méthodes unitaires ○ Github ○ Travis ○ Bitrise ○ Transifex ○ Google Play ○ Etc… ● Dépendance maven/gradle standard

Slide 81

Slide 81 text

Plusieurs niveaux 81 Commandes builtin ● playStore listing ● playStore beta ● playStore release ● playStore screenshots ● github pr ● gitlab pr ● transifex pull ● transifex push ● travis encrypt ● ...

Slide 82

Slide 82 text

Plusieurs niveaux 82 Commandes personnalisées ● startWork ● buildPR ● buildTag ● jira ● hotfix ● …

Slide 83

Slide 83 text

83 ● Open source ● Encore en gros développement ● https://github.com/dailymotion/kinta ● Ouvrez des ticket! Kinta

Slide 84

Slide 84 text

84 ● Clikt et Kscript sont complémentaires. ● HTTP, Json, Process, on peut tout faire en Kotlin ○ Aussi du backend et du web. Le mot de la fin 5 mars à Bastille

Slide 85

Slide 85 text

Merci! 85