Github, Travis CI ve Heroku Entegrasyonu
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.
Ö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.
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.
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.
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ı?
Ş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
Görüldüğü üzere testten geçtik.
Ş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
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
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
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 .
Hobbbbbaaaaaa!
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
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.
Evet! Eğer herhangi sorun yoksa, herhangi çıktı almıyorsunuz. Her şey yolunda.
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
Ş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.
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.
Ş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.
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 detayına şuradan bakabilirsiniz. Şimdi github reponuzda commit listesine bakarsanız build'in başrısız olduğunu göreceksiniz.
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
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.
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
Yeni property gelmiş yani deploy gerçekleşmiş.
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
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.
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ş.