$30 off During Our Annual Pro Sale. View Details »

Automatisez_vos_workflows_en_Kotlin.pdf

mbonnin
February 14, 2020

 Automatisez_vos_workflows_en_Kotlin.pdf

mbonnin

February 14, 2020
Tweet

More Decks by mbonnin

Other Decks in Programming

Transcript

  1. Automatisez vos workflows en Kotlin DevFest Paris - 2020 1

  2. Automatisez vos workflows en Kotlin @martinbonnin 2

  3. Dailymotion 3

  4. Environnement 4 Github Jira Bitrise Play Store AppCenter Transifex Slack

  5. 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
  6. Tout automatiser !! 6

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

  8. Mais on gagne: • En fiabilité • Reproducibilité • Auto-documentation

    • Fun • Et on peut gagner du temps (des fois) 8
  9. Tout automatiser En Kotlin!! 9

  10. 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
  11. 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
  12. Mais aussi • Language Moderne • Un IDE au top

    (la plupart du temps!) • Un écosystème riche 12
  13. La boite à outils 13 • Outils ◦ Kscript ◦

    Clikt • Bibliothèques ◦ Process ◦ HTTP ◦ Json ◦ ... ➡ Exemple d’application à Dailymotion
  14. Kscript 14

  15. Scripting 15 • Pour des petits projets • Un seul

    fichier • Facile à mettre en place et à utiliser
  16. Avec le compilateur Kotlin $ echo ‘println("Hello ${args[0]}!")’ > hello.kts

    $ kotlinc -script hello.kts “DevFest Paris” Hello DevFest Paris! 16
  17. ✨ Avec Kscript ✨ • https://github.com/holgerbrandl/kscript • Mise en cache

    des compilations • Shebang • Gestion des dépendances • Distribution • Intégration IntelliJ 17
  18. 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
  19. 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
  20. 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 :-|") }
  21. Kotlin - OkHttp 21 • https://square.github.io/okhttp/ • Développé par square

    • 100% Kotlin depuis la version 4 • API complète • Bientôt multiplateforme ?
  22. 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") }
  23. 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") }
  24. 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") }
  25. 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 :-|
  26. Kscript pour remplacer sed ? 26

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

  28. Kscript - Debugger 28

  29. 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
  30. • Standardisation ? KEEP #75 Kscript - alternative ? 30

  31. • 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
  32. Clikt 32

  33. Clikt 33 • https://github.com/ajalt/clikt • Parseur de ligne de commande

    • Kotlin Multiplatform
  34. Paramètres 34 • Optionnels ou non. • Entier, flottant ou

    chaine de caractere ou Booleen. • Nommé ou positionnel. $ hello -d --count 1 --name “Paris”
  35. 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
  36. 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!") } } }
  37. 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!") } } }
  38. 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!") } } }
  39. 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!") } } }
  40. 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() + "!") } } }
  41. Main 41 fun main(args: Array<String>) = Hello().main(args) $ ./hello --count

    3 --name Paris Hello Paris! Hello Paris! Hello Paris!
  42. 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"
  43. Aide 43 $./hello -h Options: --count INT --name TEXT Votre

    nom -h, --help Show this message and exit
  44. 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!") } } }
  45. 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!") } } }
  46. Aide 46 $./hello -h Options: --count INT Nombre de fois

    --name TEXT Votre nom -h, --help Show this message and exit
  47. 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
  48. Autocomplete! 48

  49. Clikt - Et après ? 49 • Kotlinx.cli ? https://github.com/Kotlin/kotlinx.cli

    • Kotlin-native & nodeJS
  50. 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.
  51. Clikt en pratique 51

  52. 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
  53. 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" }
  54. 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" }
  55. 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<String>) = BuildTag().main(args)
  56. 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 } }
  57. 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 } }
  58. 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) } }
  59. Kotlin: executer Gradle 59 ProcessBuilder().command("gradle", "assembleProdDebug") .directory(File("projectDir")) .inheritIO() // same

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

    behaviour as bash .start() .waitFor()
  61. 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 { ... }
  62. 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()
  63. Encore mieux: GradleRunner 63 val result = GradleRunner.create() .withProjectDir(projectDir) .withArguments("assembleProdDebug")

    .build() Assert.assertEquals( TaskOutcome.SUCCESS, result.task(":assembleProdDebug")?.outcome )
  64. 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 ?
  65. 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
  66. 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") }
  67. 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)
  68. 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
  69. 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 }
  70. 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.
  71. 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 !
  72. 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
  73. Sur la machine de développement 73

  74. 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 ‍
  75. Sur la machine d’intégration continue 75

  76. Sur la machine d’intégration continue 76

  77. 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
  78. 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
  79. Plusieurs niveaux 79 Commandes personnalisées Lib Commandes builtin

  80. Plusieurs niveaux 80 Lib • Méthodes unitaires ◦ Github ◦

    Travis ◦ Bitrise ◦ Transifex ◦ Google Play ◦ Etc… • Dépendance maven/gradle standard
  81. Plusieurs niveaux 81 Commandes builtin • playStore listing • playStore

    beta • playStore release • playStore screenshots • github pr • gitlab pr • transifex pull • transifex push • travis encrypt • ...
  82. Plusieurs niveaux 82 Commandes personnalisées • startWork • buildPR •

    buildTag • jira • hotfix • …
  83. 83 • Open source • Encore en gros développement •

    https://github.com/dailymotion/kinta • Ouvrez des ticket! Kinta
  84. 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
  85. Merci! 85