Github, Travis CI ve Heroku Entegrasyonu

featured image

Hazır olun! Mahşerin üç çılgın atlısı geliyor. Hem de dıgıdık dıgıdık.

Yazdığımız projeleri deploy etmenin bir yığın yöntemi var. Eskiden değişiklik yaptığımız dosyaları ftp aracılığıyla manuel olarak sunucumuza upload ediyorduk ki bu acaip zorlu bir iş ve hiç de profesyonel değil. Sonraları sunuculara ssh ile erişip git üzerinden son değişiklikleri çekerek kullanmaya çalıştık. Bu da yemedi. Daha bir sürü saçma yöntem sayılabilir elbette. Ama artık bu işin çok klas bir çözümü var. Ayrıca bugün tek derdimiz deploy etmiş olmak da değil. Dinle.

Bugün sadece fonksiyonu yerine getirecek kodlar yazmıyoruz. Çeşitli test çeşitlerinde test kodları da yazıyoruz. Bu testler sistemin tamamının çalışır durumda olduğunu anlamamızı sağlıyor. Ancak kodumuzu canlıya veya staging ortamına göndermeden önce bu testleri çalıştırmayı unutma ihtimalimiz her zaman var var.

Vaziyet alın mevzu başlıyor.

gifpic-100

Öncelikle şu terimleri kısa kısa açıklayalım.

GitHub

Bunu bilmeyen yoktur sanırım ama yine de aramıza yeni katılan arkadaşları düşünerek bahsedelim. Takım arkadaşlarınız ile birlikte veya tek başınıza proje geliştirmeniz için size versiyon kontrol arayüzü sunan ve Git versiyon kontrol sistemi ile çalışan dünyanın en güzel icadı.

Travis CI

Ecnebiler buna "Continuous Integration" yani "Sürekli Entegrasyon" adını vermişler. Aslında Continuous Integration mevzusunu çoooook uzun bir makale altında başlı başına anlatmak istiyorum. Çünkü burada bir iki satıra sığdırılacak bir kavram değil. "Travis" bir Continuous Integration aracı. Özetle şu işi yapıyor. Sizin Travis konfigürasyon dosyasında verdiğiniz komutlara göre projenizi baştan aşağı build ediyor ve herhangi bir noktada hata ile karşılaşırsa bunu size bildiriyor. Hata olmadığı durumda da eğer isterseniz kodunuzu sizin yerinize deploy ediyor. Sonra da "bak ben bir hata ile karşılaşmadım, deploy'u da yaptım haberin olsun" şeklinde mail atıyor.

Heroku

Çoğu web dilini destekleyen ve bu diller ile yazılmış projeleri sunucuları üzerinde host eden kullanışlı bir bulut hizmeti.

Eslint

Çoğu zaman kodu farklı farklı şekillerde yazarak aynı sonuca ulaşabiliyoruz. Bunu algoritma anlamında söylemiyorum. Örneğin herhangi biri string ifadelerde çift tırnak kullanıyorken bir başkası tek tırnak kullanabiliyor. Kimisi aralıkları space ile kimisi tab ile veriyor.Ya da bir if ifadesi yazıldığında kimisi ilk süslü parantezi aynı satırda yazıyor, kimisi bir alt satırda. Hal böyle olunca ekip halinde geliştirilen projelerde standardizasyonu sağlamak bir hayli zor oluyor. Eslint burada devreye giriyor ve eslint konfigürasyon dosyasında belirtiğiniz kurallara göre sizi bu kurallara uymaya zorluyor. Büyük nimet efem. Detaylı bilgi için şöyle alalım sizi.

Eğlence Zamanı

Şöyle bir örnek proje üzerine örnek bir senaryo ile devam edelim. Çok basit bir NodeJS servisimiz olsun ve bu servise ait bazı unit testler yazalım. Ayrıca Eslint kullanarak kodun standardizasyonunu sağlayalım. Sonra Travis CI' yi projemize entegre edelim, unit testlerin ve eslint kurallarını kontrol etsin. Eğer her şey yolunda ise kodumuz Heroku üzerinde deploy edilsin.

Hemen işe koyulalım ve github üzerinden bir repo oluşturalım.

Github reposu

Repo hazır. İşte burada.

Çalışma dizinimizi oluşturalım.

cd ~/Desktop
mkdir blogpost && cd blogpost

GitHub repomuzu klonlayalım. Tabi siz burada kendi GitHub repo adresinizi yazacaksınız. Sondaki nokta, klonlamayı bulunduğunuz klasöre yapmanızı sağlar. Noktayı koymazsanız, repo bir klasör ile gelir.

git clone https://github.com/meseven/blogpost.git .

NodeJS Projesi

Node ile basit bir servis yazacağız demiştik. Mimari ile çok fazla uğraşmamak adına express-generator ile hızlıca Node sunucumuzu ayağa kaldıralım.

Express generator modülünü kuralım.

sudo npm install express-generator -g

Express generator'u kullanarak proje yapısını oluşturalım.

express .

Bağımlılıkları yükleyelim

npm install

Şimdi http://localhost:3000 adresine gidelim ve node sunucumuzun ayağa kalktığını görelim.

node server

Buraya kadar olan kısmı git repomuza push edelim.

git add .
git commit -m "node server generated"
git push origin master

Node Servisi

Basit bir servis yazacaktık. Bu servisin görevi base url'de JSON formatında bir veri döndürmek olsun.

routes/index.js dosyasına gidelim ve aşağıdaki satırı,

res.render('index', { title: 'Express' });

Aşağıdaki satır ile değiştirelim.

res.json({ name: 'Mehmet', surname: 'Seven', age: 24 });

Projenin çalıştığı URL'e gidelim.

localhost screenshot

Her şey yolunda gibi görünüyor.

Değişiklikleri commitleyelim.

git add .
git commit -m "service"

Unit Testler

Yoksa sen hala unit test yazmaya başlamadın mı?

localhost screenshot

Şimdi servisimiz için iki adet basit unit test yazacağız. Testlerimizi mocha ile chai assertion'ını kullanarak yazacağız.

npm install chai chai-http mocha --save-dev

Kök dizinde test adında bir klasör oluşturalım. Bu klasör içerisinde index.js adında bir dosya oluşturalım.

index.js

let chai = require('chai');
let chaiHttp = require('chai-http');
let should = chai.should();
let server = require('../app');

chai.use(chaiHttp);

describe('Node Server', () => {
  it('(GET /) returns the homepage', (done) => {
    chai
      .request(server)
      .get('/')
      .end((err, res) => {
        res.should.have.status(200);
        done();
      });
  });
});

Bu test basitçe anasayfaya erişim olup olmadığını kontrol ediyor. Sunucunun döndüğü cevap 200 ise testten geçiyor. Hemen deneyelim!

mocha

mocha test

Görüldüğü üzere testten geçtik.

mocha test

Şimdi testimizi biraz daha detaylandıralım. Servisin bize döndüğü name,surname ve age alanlarının var olması gerektiğini, ayrıca name ve surname alanlarının string, age alanının integer olması gerektiğini belirtelim.

let chai = require('chai');
let chaiHttp = require('chai-http');
let should = chai.should();
let server = require('../app');

chai.use(chaiHttp);

describe('Node Server', () => {
  it('it should GET all the data', (done) => {
    chai
      .request(server)
      .get('/')
      .end((err, res) => {
        res.should.have.status(200);
        res.body.should.be.a('object');
        res.body.should.have.property('name').to.be.an('string');
        res.body.should.have.property('surname').to.be.an('string');
        res.body.should.have.property('age').to.be.an('number');
        done();
      });
  });
});

mocha'yı çalıştıralım.

mocha

mocha unit tests

Evet, test yine başarılı. Çünkü servisin teste aykırı olarak döndüğü herhangi bir veri yok.

Mesela servisin döndüğü name property'sini kaldıralım ve tekrar test edelim acaba testten geçebilecek mi?

routes/index.js dosyasına gidelim ve aşağıdaki satırı,

res.json({ name: 'Mehmet', surname: 'Seven', age: 24 });

Aşağıdaki satır ile değiştirelim.

res.json({ surname: 'Seven', age: 24 });

Tekrar test edelim.

mocha

test fail

Bu sefer testten geçemedik. Çünkü servisin name adında bir property dönmesi zorunlu.

Şimdi de age property'sinin döndüğü değeri String yapalım ve tekrar test edelim.

routes/index.js dosyasına gidelim ve aşağıdaki satırı,

res.json({ surname: 'Seven', age: 24 });

Aşağıdaki satır ile değiştirelim.

res.json({ name: 'Mehmet', surname: 'Seven', age: '24' });

Tekrar test edelim.

mocha

test fail

Evet yine beklediğimiz gibi test fail oldu. Çünkü age property'si Integer olmalı.

Her şey harika. Son olarak routes/index.js dosyamızı eski haline getirip commit'leyelim.

routes/index.js

res.json({ name: 'Mehmet', surname: 'Seven', age: 24 });

Commit

git add .
git commit -m "index test case"

Eslint Konfigürasyonu

Eslint konfigürasyonunu bazı genel kabul görmüş bazı standartlara göre (google,airbnb) veya kendi kurallarınıza göre belirleyebiliyorsunuz. Biz kendi kurallarımıza göre oluşturalım.

  • Indent tab ile yapılsın.
  • String ifadelerde tek tırnak kullanılsın.
  • Satır sonlarında noktalı virgül kullanmak zorunlu olsun.

Kurallarımız bunlar. Hemen mevzuya geçelim.

Kurulum için;

npm install -g eslint

Şimdi aşağıdaki komutu çalıştıralım.

eslint --init

Karşımıza "How would you like to configure ESLint?" şeklinde bir soru geliyor. Eslint'i nasıl konfigüre etmek istersin diye soruyor. Biz "Answer questions about your style" seçelim. Böylece eslint bize soru soracak ve vereceğimiz cevaplara göre bir config dosyası oluşturacak.

  • Are you using ECMAScript 6 features? (Y)
  • Are you using ES6 modules? (Y)
  • Where will your code run? (Node)
  • Do you use JSX? (N)
  • What style of indentation do you use? (Tabs)
  • What quotes do you use for strings? (Single)
  • What line endings do you use? (Unix)
  • Do you require semicolons? (Y)
  • What format do you want your config file to be in? (JSON)

Şimdi kök dizinimize baktığımızda .eslintrc.json adında abir dosya gelmiş olması gerekiyor. Eğer göremiyorsanız klasördeyken CTRL+H yaparak gizli dosyaları gösterebilirsiniz.

.eslintrc.json

{
  "env": {
    "es6": true,
    "node": true
  },
  "extends": "eslint:recommended",
  "parserOptions": {
    "sourceType": "module"
  },
  "rules": {
    "indent": ["error", "tab"],
    "linebreak-style": ["error", "unix"],
    "quotes": ["error", "single"],
    "semi": ["error", "always"]
  }
}

Evet tüm kurallarımızı belirledik. Hemen bakalım kurallarımıza uymayan kodlarımız var mı?

eslint .

eslint fail

Hobbbbbaaaaaa!

bomb

Aşağıdaki komut ile kurallarınıza uyacak şekilde problemleri çözüyor eslint. Fakat bazılarını size bırakıyor. Çözebileceği kadarını çözmesi için aşağıdaki komutu çalıştıralım.

eslint . --fix

eslint fix

Evet görüldüğü üzere çoğu problemi çözdü. Çözmediği sorunlar ise sistemin işleyişini bozma tehlikesi olabileceği düşüncesiyle bize bırakılmış. Bunları el ile temizlememiz gerekiyor.

Hatalardan bazıları şöyle,

  • Kök dizindeki app.js dosyanda favicon diye bir değişken var ama bunu hiçbir yerde kullanmadın diyor.

  • Next adında bir değişkenin var ama bunu kullanmadın diyor. (app.js, routes/index.js, routes/users.js)

Yukarıdaki sorunları halletmek için ilgili değişkenleri silmeniz yeterli.

test/index.js dosyasındaki hataları çözmek için ise .eslintrc.json dosyamızın "env" kısmını şöyle değiştirelim.

"env": {
  "es6": true,
  "node": true,
  "mocha": true
}

Şimdi geriye sadece test/index.js "3:5 error 'should' is assigned a value but never used" hatası kaldı. Burada should değişkenini kullanmadığımızı söylüyor ama aslında aşağıda kullanılıyor. Fakat farklı bir biçimde kullanıldığı için bunun farkına varamıyor. Biz ilgili satırda eslint kullanımını devre dışı bırakarak bu sorunu çözebiliriz.

test/index.js

let chai = require('chai');
let chaiHttp = require('chai-http');
/*eslint-disable */
let should = chai.should();
/*eslint-enable */
let server = require('../app');

Evet şimdi Eslint'i tekrar çalıştıralım ve bir sorun var mı bakalım.

eslint fixed

Evet! Eğer herhangi sorun yoksa, herhangi çıktı almıyorsunuz. Her şey yolunda.

gifpic-100

Değişiklikleri commit'leyelim.

git add .
git commit -m "eslint integration"

Heroku Entegrasyonu

Bir heroku hesabınız olduğunu varsayıyorum. Yeni bir uygulama oluşturalım. https://dashboard.heroku.com/new-app

heroku app

Şimdi Heroku komut satırı aracını kurmanız gerekiyor. İşletim sisteminize uygun kurulum talimatları şurada.

Heroku CLI kurulumunu yaptıktan sonra;

Komut satırı üzerinden herokuya login olalım.

heroku login

Git için remote bir sunucu ekleyelim. (blogpost-staging kısmına sizin app name'iniz ne ise onu yazmalısınız)

heroku git:remote -a blogpost-staging

Şimdi yaptığımız değişiklikleri önce github'a sonra herokuya push'layalım.

git add .
git commit -m "heroku integration"
git push origin master
git push heroku master

İşlem bittiğinde uygulamamızın heroku üzerinde yayınlandığı URL'e gidelim.

heroku deploy

Evet başarı ile deploy olmuş.

Travis Entegrasyonu

Travis'e github hesabımız ile kayıt olalım. Sonra login olalım ve profilimize gidelim.

Karşınıza şöyle bir zımbırtı gelecek. Aşağıda github üzerinde bulunan repolarınız listeleniyor. Travis ile kullanmak istediğimiz repoyu işaretleyelim. travis profile

Şimdi travis konfigürasyonunu yapmamız gerekiyor. Kök dizine .travis.yml adında bir dosya oluşturalım ve içeriğini şöyle yapalım.

.travis.yml

language: node_js
sudo: required
cache:
  directories:
    - node_modules
node_js:
  - 8
before_install:
  - npm install -g node-gyp
before_script:
  - export NODE_ENV=production
  - npm install
script:
  - npm run lint
  - npm run test

Bu konfigürasyonu okuyarak az çok anlayabilirsiniz. Mesela node v8'e göre build edilmesini, kurulumdan önce node-gyp modülünün kurulmasını, script çalıştırılmadan önce package.json dosyamda belirttiğim tüm npm paketlerinin kurulmasını. Son olarak da unit ve lint testlerini çalıştırmasını istedik. İsterseniz birden vazla node versiyonu için build etmesini isteyebilirsiniz. Yeni bir satır ekleyip node versiyonu belirtirseniz, o versiyona göre de build eder.

En alt satırdaki iki komut en son çalıştırılacak komutlar. Burada genelde geçmesimi beklediğimiz testleri başlatan komutlar olur. Bu komutları package.json dosyasından oluşturalım.

{
  "scripts": {
    "start": "node ./bin/www",
    "lint": "eslint .",
    "test": "mocha"
  }
}

Scripts kısmına iki satır ekledik. Biri unit testlerimizi yapan, diğeri ise lint denetimini yapan komutlar.

Şimdi son hali github üzerine push'layalım bakalım neler olacak. github push pending

Bakın github repomda commit listesini açtığımda en son commit'in sağ tarafında sarı bir yuvarlak icon belirdi. Bu icon commit'in travis üzerinde build edilmekte olduğunu belirtiyor. Eğer build başarısız olursa bu sarı iconun yerine kırmızı çarpı, başarılı olursa yeşil tik geliyor. Bu sarı icona tıklayarak travis üzerinde ki build durumunu görüntüleyebilirsiniz.

İlk build'imiz başarısız oldu.

build fail

Build detayına şuradan bakabilirsiniz. Şimdi github reponuzda commit listesine bakarsanız build'in başrısız olduğunu göreceksiniz.

github push fail

Hatanın sebebi şu. Biz eslint'i ve mocha'yı kendi bilgisayarımıza genel olarak kurmuştuk. Package.json dosyamızda dev dependency olarak da belirtmemiz gerekiyordu. Hemen ekleyip tekrar deneyelim.

npm install eslint mocha --save-dev

Şimdi tekrar push yapmamız gerekiyor. Artık anladık. Travis repoya gelen her push'dan sonra build etmeye başlıyor.

git add .
git commit -m "eslint added to package.json"
git push origin master

Yine fail olduk. Build Logs.

Bu sefer hatamız .travis.yml içerisinde before_script kısmında node env olarak production vermemizden kaynaklandı. Production ortamında dev dependency'ler yüklenmediği için mocha ve eslint de yüklenemedi haliyle.

.travis.yml dosyasından aşağıdaki satırı silmeniz gerekiyor.

export NODE_ENV=production

Şimdi yeniden push işlemi yapalım.

git add .
git commit -m "fix"
git push origin master

Bu sefer başarılıyız. Build Logs

github push success

Bitiyor az kaldı

Senaryomuzu hatırlayalım. Developer abidik gubidik bir şeyler geliştirecek, test kodlarını yazacak ve kodunu github üzerine push'layacak. Bu push işlemi bittikten hemen sonra travis build işlemi yapacak ve build başarılı olursa heroku üzerine otomatik deploy yapılacak. Şimdi son adım hariç tamamını yaptık. Onu da yapalım da ortalık şenlensin biraz.

Yapmamız gereken heroku üzerinden otomatik deploy işlemini aktifleştirmek.

Heroku profilinize gidin ve uygulamanızı seçin. Uygulama detayındaki "deploy" tab'ına geçin. Burada aşağıda gösterdiğim ayarları yapmanız gerekiyor.

heroku dashboard

Ben deploy'un master branch'im den yapılmasını istedim. Siz isterseniz farklı bir branch de seçebilirsiniz.

"Wait for CI to pass before deploy" seçeneğini işaretlemeniz önemli. Aksi halde build durumu başarısız olsa dahi deploy işlemi gerçekleştirilir. Bu seçenek işaretli olduğunda build işlemi başarısız olursa deploy yapılmaz.

Dans Zamanı

Servisimizde ufak bir değişiklik yapalım. Yeni bir property ekleyelim ve projenin gerçekten otomatik deploy olup olmadığını görelim.

routes/index.js Aşağıdaki satırı,

res.json({ name: 'Mehmet', surname: 'Seven', age: 24 });

Aşağıdaki satır ile değiştirelim.

res.json({ name: 'Mehmet', surname: 'Seven', age: 24, location: 'Istanbul' });

Şimdi,

git add .
git commit -m "new property"
git push origin master

Komutlarını çalıştırın ve kendinize bir bardak kahve almaya gidin. Döndüğünüzde tüm kodlarınız baştan aşağı build edilmiş, unit testleriniz yapılmış, lint kontrolünüz ve deploy işlemleriniz tamamlanmış olacak.

Build işlemi başarılı olmuş. Build Logs

new property

Yeni property gelmiş yani deploy gerçekleşmiş.

gifpic-50

Test Edelim

Bakalım herhangi lint hatası yaptığımızda veya unit testler patladığında deploy gerçekleşecek mi? Gerçekleşmemesi gerekiyor.

routes/index Aşağıdaki satırı,

res.json({ name: 'Mehmet', surname: 'Seven', age: 24, location: 'Istanbul' });

Aşağıdaki satır ile değiştirelim. Sondaki noktalı virgülü kaldırdık ve 'job' adında bir property ekledik.

res.json({
  name: 'Mehmet',
  surname: 'Seven',
  age: 24,
  location: 'Istanbul',
  job: 'jobless',
});

Eğer build işleminden sonra heroku üzerinde çalışan çalışmamızı yenilediğimizde 'job' alanını da döndürürse bir sıkıntı var demektir. Çünkü build başarısız olursa deploy etmemesi gerekiyor.

Hemen github'a push edelim ve build sonucunu bekleyelim.

git add .
git commit -m "test"
git push origin master

Evet, build'in başarısız olduğunu görüyoruz. Build Status

github failed

Ve görüyoruz ki deploy işlemi tam da istediğimiz gibi gerçekleşmemiş. Gerçekleşseydi job property'sini görüyor olacaktık.

github failed

routes/index.js üzerinde yaptığımız hatayı giderip tekrar push yaparak build'in başarılı olmasını ve deploy işleminin yapılmasını sağlayalım. Dosya üzerinde yaptığımız hatayı giderelim.

eslint . --fix

Tekrar push yapıyoruz.

git add .
git commit -m "fix lint problem"
git push origin master

Build başarılı oldu. Build status

Heroku'ya bakalım deploy gerçekleşmiş mi?

Yep!

Ne Kazandık

Unit ve lint testlerimizi manuel olarak yapabilirdik ve eğer hata varsa deploy yapmazdık. Buna neden ihtiyacımız var ?

Elbette bunu yapabilirsin. Fakat şöyle düşün. Ekibine yeni biri katıldı ve senin lint kurallarını altüst eder vaziyette bir kod yazdı. Bu sıkıntılı kodu düzeltmek için vakit bulabilecek misin? Ayrıca bu kodun canlı ortamında bulunmasını ister misin? Bu yapı sayesinde artık istese de gönderemeyecek. Sen unit ve lint testlerini her deploydan önce yapıyor olabilirsin ancak unutma sen de bir insansın, unutabilirsin. Bu unutma halinde unit testden geçememiş bir kodun canlıya gitmiş olması ve dolayısıyla hatalı bir satış sonrası bilmem kaç bin lira zarar etmiş olman umrunda değilse manuel yapabilirsin. Dahası birsürü angarya işten kurtuldun. Deploy işlemleri hiç olmadığı kadar kolaylaşmış oldu. Senin sürekli yapman gereken işlemleri bu araçlar senin yerine yapıyor ve arkana yaslanıp kahvenden bir yudum almak için zaman buluyorsun. Yine de sen bilirsin kardeş.

Kapanış

gifpic-50