Sbt #4 : Proje release ve CI

- Kalk yerine yat olm, boynun agrir


Oncelikle proje artifactlarini package etmemiz gerekiyor. Bunun icin sbt-native-packager pluginini kullanacagiz. Birkac adim izlemek gerekiyor:

1. project/plugins.sbt dosyasina plugini ekle
2. Packaging formatini tanimla
3. plugin tarafindan saglanan Setting'i calistir

Project klasoru altina plugins.sbt dosyasi olusturuyoruz. Icrigi su sekilde:

addSbtPlugin("com.typesafe.sbt" %% "sbt-native-packager" % "1.3.21")

Paketlemeye gecmeden son adim olarak da, build.sbt dosyasinda iki proje icin de enablePlugin metodu ile sbt-native-packager pluginini JavaAppPackaging formatini kullanacak sekilde konfigure ediyoruz. 

lazy val sansOyunlari = project
    .dependsOn(api)
    .enablePlugins(JavaAppPackaging)
    .settings(
        libraryDependencies ++= Dependencies.sansOyunlariDependecies
    )

Simdi stage komutu ile paketlemeye gecebiliriz. stage, sbt-native-packager tarafindan saglanan bir task. 

sbt:hello_sbt> ;clean ;stage

Goruyoruz ki sub-projeler bir bir derlendi ve kendi proje klasorlerinde target/scala-2.12 altinda konumlandilar. Bunun yaninda stage, artifactlarini calistirabilmek icin exectable'lari da olusturur. Calistiginiz platforma gore bat ya da bash script calistirip, class'imizi direk calistirabilirz. 

hello_sbt>sansOyunlari\target\universal\stage\bin\sayisal-tahmin.bat
23,23,15,18,20,23

hello_sbt>sansOyunlari\target\universal\stage\bin\at-yarisi-tahmin.bat sahbatur baturalp baturmort  
baturalp

evet ucuncu ayakta sahbaturu yine tek geciyoruz anlasilan. 

Buna ek olarak sbt native packager plugini docker image'leri de olusturabilir. Docker image'in ne kadar faydali bir deployment yontemi oldugunu tekrar anlatmaya gerek yok. Bunun icin build.sbt dosyasinda ufak degisiklikler yapiyoruz:

import com.typesafe.sbt.packager.docker.ExecCmd

name := "hello_sbt"
version := "0.1"
scalaVersion := "2.13.4"

lazy val sansOyunlari = project
  .dependsOn(api)
  .enablePlugins(JavaAppPackaging)
  .enablePlugins(DockerPlugin)
  .settings(
    libraryDependencies ++= Dependencies.sansOyunlariDependecies,
    dockerCommands ++= dockerCommands.value.filterNot {
      case ExecCmd("ENTRY_POINT", _) => true
      case _ => false
    },
    dockerCommands ++= Seq(ExecCmd("ENTRY_POINT", "/opt/docker/bin/sayisal-tahmin"))
  )

lazy val api = project
  .enablePlugins(JavaAppPackaging)
  .settings(
    libraryDependencies ++= Dependencies.apiDependencies
  )

Ilk degisiklik, enablePlugins(DockerPlugin) ile docker pluginini aktiflestiriyoruz. SOnra da settings icerisinde, kendi entry point degerimizi overwrite etmek icin birkac numara cekiyoruz. Daha sonra sbt shell'de docker:publishLocal komutu ile projenizi docker image seklinde paketleyebilir ve calistirabilirsiniz. 

Travis CI ile Continuous Integration 
Projemizi lokalde test edebiliyoruz, gayet guzel. Ama proje uzerinde birden fazla kisinin calistigini dusunelim. Herkes yaptigi degisiklikleri repository'e push ediyor. Peki bu yapilan degisikliklerin projeyi `brake` etmedigini nasil bilecegiz? Birisi tutup da dan diye problemi kodu push ederse nasil anlayacagiz? Gozumuz debugger degil ki.

Burada CI devreye giriyor ve her push edilen branch uzerinde testleri calistiriyor ve size bir feedback veriyor. Hatta fail olan test ve build'leri master'a merge ettirmeyerek codebase'i koruyor. Bunun icin cok cesitli araclar var. Ornek olarak TravisCI'i inceleyelim. Opensource projeler icin ucretsiz ve github ile iyi integre oluyor. Oncelikle proje root klasorune bir .travis.yml dosyasi ekleyip gerekli tanimlamalari yapmamiz gerekiyor.

language: scala
scala:
    - 2.13.4

Hepsi bu kadar. TravisCI, diger gerekli konfigurasyonlari build.sbt'den okuyacak kadar akilli. Projemizi github'da yeni bir repo olusturarak push ediyoruz. 



travis-ci.com adresinden, kendi github accoutumuz ile uye olabiliriz. Daha sonra travis-ci tum repolarinizi tarayacak. Biz de gidip son push ettigimiz sbt reposusunu bularak aktive edecegiz. Daha sonra sag ust koseden, More Options altinda, Trigger Build secenegi ile manuel olarak bir build trigger ediyoruz. Otomatikman proje build ediliyor ve hatta testler calistiriliyor. Ve build basarili:



Bunda sonra her yeni bir pull request olusturuldugunda travis-ci branci build edecek ve testleri calistiracak. Eger isterseniz github uzerinde tum checkler yesil degilse merge butonunu aktif etme seklinde ayarlayabilirsiniz. Ben deneme amacli olarak `braking change` iceren bir PR olsuturdum, ve travis-ci hemen calismaya basladi, build fail oldu, feedback geldi:


Artifact yayinciligi
Projemizi bitirdik, testleri calisiyor hatta CI'imiz bile kurulu. Simdi bur projeden diger insanlarin da faydalanabilmesi icin dunyaya publish edecegiz!

Bintray.com adresine giderek gene github accountumuz ile signup oluyoruz. 

Bintray'e direk publish edebilmek icin sbt-bintray pluginini kullanabiliriz. Gerekli tanimlamayi project/plugins.sbt dosyasina ekleyelim:

addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")

Ayrica, build.sbt dosyasinda da birkac degisiklik yapmamiz gerekiyor:

name := "hello_sbt"

ThisBuild / version := ""

scalaVersion := "2.13.4"

ThisBuild / licenses ++= Seq(("MIT", "http://opensource.org/licences/MIT"))

publish/skip := true

Ilk once proje versiyonunu degistirdik. Cunku bintray, snapshot versiyonlara izin vermiyor. Burada ThisBuild, build level settinglere refere ediyor. Sbt oncelikle proje level settignlere bakar. Eger bir deger bulursa onu kullanir. Bulunamaz ise, bu sefer ThisBuild altinda tanimlanan build-level setting'lere bakilir. Eger yine bulunamadiysa, global-level settinglere bakar ki biz hic global level setting tanimlamadik henuz projemizde. Eger ThisBuild/version seklinde tanimlama yapmaz ise, sbt shell'de version komutunu calistirdigimizda karsilacagimiz tablo bu sekilde olacak:

[info] sansOyunlari / Debian / version
[info]  0.1.0-SNAPSHOT
[info] api / Debian / version
[info]  0.1.0-SNAPSHOT
[info] version
[info]  0.1

Goruldugu gibi sadece root projesi 1.0 versiyonda ama sub-projeler SNAPSHOT versiyona sahip ki biz bunu istemiyoruz, cunku bintray istemiyor. Peki neden? Cunku sbt eger hic set edilmemis ise bir proje icin default versiyonu 0.1-SNAPSHOT olarak kabul ediyor. Burada sub-projeler icin ne proje seviyesinde ne build seviyesinde bir versiyon tanimlanmamis oluyor. Deneme amacli bir sub-project settings kisminda version set edebliriz:

.settings(
    version := "2.1")

gibi. Bu sefer versiyonu proje settings'ten aldigini gorecegiz.

[info] sansOyunlari / version
[info]  2.1
[info] api / Debian / version
[info]  0.1.0-SNAPSHOT
[info] version
[info]  1.0 

Neyse biz ThisBuild ile devam edelim. Ayrica build-level bir versiyon tanimalamasi ile tum sub-projeler ayni versiyona sahip olacak. 

publish/skip satirinda ise, root proje icin gecerli olmak uzere, publish task'ini skip ettiriyoruz. Cunku root projeyi publish etmek istemiyoruz. 

Son olarak da artifactlarimi bintraya push edebilmek icin sbt shell'de bintrayChangeCredentials komutu ile kullanici adi ve api-key'imizi giriyoruz. Api key'i bin tray'de sag ust koseden edit profile / api key linki ile elde edebilirsiniz. Garip bir sekilde hem kullanici adinini hem de api key'i ucer kere soruyor.

Once reload ile credential degisikliklerinin aktif olmasini sagliyoruz. Daha sonra da clean ve publish ile bintray'e publish ediyoruz. 

Aslinda bende calismadi, birkac hata cikti. Ama cok da uzatmak istemiyorum. Genel gidisat bu sekilde olmasi gerekiyor. 

Akka.http ile Api Olusturma
Elimizdeki api projesi ile doviz kurlarini sorgulayabiliyoruz. Bunu bir api sekiline getirerk herokuya deploy etmek guzel bir egzersiz olacaktir. dependency'lerimize 3 yenisi ekleniyor, project/Dependencies.scala su hale geliyor:

import sbt._

object Dependencies {

  val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5"
  val requests = "com.lihaoyi" %% "requests" % "0.6.5"
  val scalaXml = "org.scala-lang.modules" %% "scala-xml" % "1.2.0"

  val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.8"
  val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.19"
  val json4s = "org.json4s" %% "json4s-native" % "3.6.5"

  val commonDependencies: Seq[ModuleID] = Seq(scalaTest % Test)
  val apiDependencies: Seq[ModuleID] = Seq(
    requests, scalaXml, akkaHttp, akkaStream, json4s
  ) ++ commonDependencies

  val sansOyunlariDependecies: Seq[ModuleID] = commonDependencies
}

Ve bu dependency;leri sadece api projesine ekledigimize dikkat edelim. Ve de build.sbt dosyasina dokunmamiza gerek olmuyor. Dependency'leri ayri bir sekilde manage edebilmeminz faydasini tekrardan gormus oluyoruz. 

api projesindeki DovizKur.scala dosyasina kucuk bir ek yaparak, doviz kurlarini json olarak alabilmeyi sagliyoruz:

import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._
import scala.xml.XML

object DovizKur extends App {

  def dovizurlariDownload(): Map[String, Double] = {
    val r = requests.get("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml")
    val xmlResponse = XML.loadString(r.text)
    val ulkeKodlari = (xmlResponse \\ "@currency").map(_.text)
    val euroCarpanlari = (xmlResponse \\ "@rate").map(_.text.toDouble)
    (ulkeKodlari zip euroCarpanlari).toMap
  }

  def getDovizKurAsJson: String = compact(render(dovizurlariDownload()))

  override def main(args: Array[String]): Unit = {
    println(getDovizKurAsJson)
  }
}

Tamam, bu noktada api/run ile test edeblir ve console'a json degerinin yazildigini gorebiliriz. 

Akabinde, http serverimiz icin api/main/scala altina WebServer.scala dosyasi olsuturu

import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.{HttpApp,Route}

object WebServer extends HttpApp {
  override def routes: Route =
    path("kurlar") {
      get {
        complete(HttpEntity(ContentTypes.`application/json`, DovizKur.getDovizKurAsJson))
      }
    }

  def main(args: Array[String]): Unit = {
    val port: Int = sys.env.getOrElse("PORT", "8080").toInt
    WebServer.startServer(host = "0.0.0.0", port)
  }
}

Burada HttpApp uzerinden basit bir webserver olusturup, kurlar isimli bir route tanimliyoruz ve bizim doviz kurlari metodundan gelen json'u buradan geri donduruyoruz. sbt shell'de api/runMain WebServer komutu ile webserverimizi calistiriyoruz. Browser'da localhost:8080/kurlar adresine gidersek, sonuc muhtesem:



Ozet
    - Artifact paketleme
    - Travis-CI ile Continous Integration
    - Harici bir repo'ya release etme (bintray)
    - HTTP Api olusturma  

Sbt burada bitmiyor, bundan sonra daha ileri konulara (versiyonlama ve release surecleri gibi) deginmek dilegiyle, dewamke. 


    

Yorumlar

Bu blogdaki popüler yayınlar

Python'da Multithreading ve Multiprocessing

Threat Modeling 1

Encoding / Decoding