{"id":31304,"date":"2019-10-31T21:40:34","date_gmt":"2019-10-31T18:40:34","guid":{"rendered":"https:\/\/prohoster.info\/blog\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\/"},"modified":"2019-10-31T21:40:34","modified_gmt":"2019-10-31T18:40:34","slug":"evolyutsiya-ci-v-komande-mobilnoj-razrabotki","status":"publish","type":"post","link":"https:\/\/prohoster.info\/hr\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","title":{"rendered":"Evolucija CI u timu za mobilni razvoj","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Danas ve\u0107ina softverskih proizvoda razvija se u timovima. Uslovi uspeha timskog razvoja mogu se predstaviti jednostavnom shemom.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/b283cfd7772d8f479be0754ebbe51b19.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nNakon \u0161to napi\u0161ete kod, morate biti sigurni da je:<\/p>\n<ol>\n<li>Funkcionalan.<\/li>\n<li>Ne razbija ni\u0161ta, uklju\u010duju\u0107i kod koji su napisali va\u0161i kolege.<\/li>\n<\/ol>\n<p>\nAko su oba uslova ispunjena, na putu ste ka uspehu. Da biste lako proveravali ove uslove i ostali na isplativom putu, uvedena je Kontinuirana Integracija.<\/p>\n<p>CI je radni proces u kojem \u0161to \u010de\u0161\u0107e integri\u0161ete svoj kod u zajedni\u010dki kod proizvoda. I ne samo da integri\u0161ete, ve\u0107 stalno proveravate da li sve funkcioni\u0161e. Kako se mnogo i \u010desto mora proveravati, vredi razmisliti o automatizaciji. Sve mo\u017eete proveravati ru\u010dno, ali ne bi trebalo, i evo za\u0161to.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<ul>\n<li><strong>Ljudi su skupi<\/strong>. Sat rada bilo kog programera skuplji je od sata rada bilo kog servera.<\/li>\n<li><strong>Ljudi gre\u0161e<\/strong>. Stoga mogu nastati situacije kada pokrenu testove na pogre\u0161noj grani ili sastave pogre\u0161an commit za testere.<\/li>\n<li><strong>Ljudi su lenji<\/strong>. Povremeno, kada zavr\u0161avam neku zada\u0107u, obi\u010dno pomislim: \"\u0160ta treba da proveravam? Napisao sam dve linije \u2014 sigurno sve funkcioni\u0161e!\" Mislim da su takve misli povremeno i vama na umu. No, uvek treba proveravati.<\/li>\n<\/ul>\n<p>\nKako je timu za mobilni razvoj Avita uvedena i razvijena Kontinuirana Integracija, kako je od 0 do\u0161lo do 450 buildova dnevno, i \u0161ta build-serveri sklapaju 200 sati dnevno, pri\u010da Nikolaj Nesterov (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/users\/nnesterov\/\" class=\"user_link\">nnesterov<\/a><\/noindex>) \u2014 u\u010desnik svih evolucijskih promena CI\/CD Android aplikacije.<\/p>\n<p>Pri\u010da je zasnovana na primeru Android tima, ali ve\u0107ina pristupa se mo\u017ee primeniti i na iOS.<\/p>\n<p><center><div class=\"youtube-placeholder\" data-id=\"lz8MNATTUCU\" onclick=\"loadVideo(this)\">\r\n        <img decoding=\"async\" src=\"https:\/\/img.youtube.com\/vi\/lz8MNATTUCU\/hqdefault.jpg\" alt=\"Pogledajte video\" loading=\"lazy\" width=\"480\" height=\"360\" style=\"width:100%;height:auto;\">\r\n        <div class=\"play-button\"><\/div>\r\n    <\/div><\/center><br \/>\nDavno, u Android timu Avita radio je samo jedan \u010dovek. Po definiciji, njemu nije trebala Kontinuirana Integracija: nije imao s kim da se integri\u0161e.<\/p>\n<p>Ali aplikacija je rasla, pojavljivalo se sve vi\u0161e i vi\u0161e novih zadataka, a samim tim je i tim rasla. U nekom trenutku do\u0161lo je vreme da se proces integracije koda formalizuje. Odlu\u010deno je da se koristi Git flow.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/f433effc5ab5a59de3d6bd20a7a87057.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nKoncept Git flow je poznat: u projektu postoji jedna zajedni\u010dka grana develop, a za svaku novu zna\u010dajku programeri otvaraju posebnu granu, izvr\u0161avaju commit na nju, pushuju, i kada \u017eele uvu\u0107i svoj kod u granu develop, otvaraju pull request. Za razmjenu znanja i raspravu o pristupima uveli smo code review, \u0161to zna\u010di da kolege trebaju pregledati i potvrditi kod jedni drugima.<\/p>\n<h2>Provjere<\/h2>\n<p>\nGledati kod o\u010dima \u2014 to je sjajno, ali nije dovoljno. Stoga se uvode automatske provjere.<\/p>\n<ul>\n<li>Prvo provjeravamo <strong>sastavljanje ARK-a<\/strong>.<\/li>\n<li>Puno <strong>Junit-testova<\/strong>.<\/li>\n<li><strong>Ra\u010dunamo code coverage<\/strong>, budu\u0107i da pokre\u0107emo testove.<\/li>\n<\/ul>\n<p>\nDa bismo shvatili kako bi trebali pokretati ove provjere, pogledajmo proces razvoja u Avitu.<\/p>\n<p>Shematski ga mo\u017eemo predstaviti ovako:<\/p>\n<ul>\n<li>Programer pi\u0161e kod na svom laptopu. Mo\u017ee pokrenuti provjere integracije upravo ovdje \u2014 ili putem commit hook-a, ili jednostavno pokretanjem provjera u pozadini.<\/li>\n<li>Nakon \u0161to je programer pushovao kod, otvara pull request. Da bi njegov kod u\u0161ao u granu develop, potrebno je pro\u0107i code review i prikupiti potrebni broj potvrda. Ovdje je mogu\u0107e uklju\u010diti provjere i build-ove: dok svi build-ovi nisu uspje\u0161ni, pull request ne mo\u017ee biti spojeno.<\/li>\n<li>Nakon \u0161to je pull request spojen i kod u\u0161ao u develop, mo\u017ee se odabrati pogodno vrijeme: na primjer, no\u0107u, kada su svi serveri slobodni, i pokrenuti provjere koliko god stane.<\/li>\n<\/ul>\n<p>\nPokretanje provjera na svom laptopu nikome se nije svidjelo. Kada programer zavr\u0161i zna\u010dajku, \u017eeli je \u0161to br\u017ee pushati i otvoriti pull request. Ako se u tom trenutku pokre\u0107u neke duge provjere, to nije samo neugodno, ve\u0107 tako\u0111er usporava razvoj: dok laptop ne\u0161to provjerava, na njemu nije mogu\u0107e normalno raditi.<\/p>\n<p>Pokretanje provjera no\u0107u se jako svidjelo, jer ima puno vremena i servera, mo\u017ee se u\u017eivati. No, na\u017ealost, kada kod zna\u010dajke u\u0111e u develop, programer ve\u0107 ima mnogo manje motivacije ispraviti gre\u0161ke koje je CI prona\u0161ao. Povremeno sam se hvatao kako sam pomislio, kada sam ujutro pogledao izvje\u0161taj sa svim prona\u0111enim gre\u0161kama, da \u0107u ih ispraviti nekad kasnije, jer trenutno u Jiri le\u017ei cool novi zadatak koji jednostavno \u017eelim po\u010deti raditi.<\/p>\n<p>Ako provjere blokiraju pull request, motivacije je dovoljno, jer dok build-ovi ne postanu zeleni, kod ne\u0107e u\u0107i u develop, a to zna\u010di da zadatak ne\u0107e biti zavr\u0161en.<\/p>\n<p>Na kraju smo odabrali strategiju: no\u0107u pokre\u0107emo maksimalan mogu\u0107i set provjera, a najkriti\u010dnije i, \u0161to je najva\u017enije, brze, pokre\u0107emo na pull requestu. No, tu ne stajemo \u2014 paralelno optimiziramo brzinu prolaska provjera kako bismo ih prenijeli iz no\u0107nog re\u017eima u provjere na pull requestu.<\/p>\n<p>U to vrijeme, sve na\u0161e izrade su prolazile dovoljno brzo, pa smo jednostavno uklju\u010dili blokiranje A\u0420K gra\u0111evine, Junit testove i izra\u010dun coveragea koda na pull requestu. Uklju\u010dili smo, razmislili \u2014 i odustali od coveragea koda, jer smo zaklju\u010dili da nam nije potreban.<\/p>\n<p><strong><em>Za cijelu postavku osnovnog CI-a su nam trebala dva dana (ovdje i dalje je vremenska procjena pribli\u017ena, potrebna za mjerilo). <\/em><\/strong><\/p>\n<p>Nakon toga smo po\u010deli razmi\u0161ljati dalje \u2014 provjeravamo li ispravno? Pokre\u0107emo li ispravno izrade na pull requestu?<\/p>\n<p>Pokretali smo izradu na posljednjem commit-u grane na kojoj je otvoren pull request. No provjere ovog commita mogu pokazati samo to da kod koji je napisao programer radi. No, ne dokazuju da ni\u0161ta nije pokvareno. U stvarnosti trebamo provjeriti stanje grane develop nakon \u0161to se u nju uklju\u010di zna\u010dajka.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/c1a179b68b02c04031177f9bf51a81ed.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZa to smo napisali jednostavan bash skript <strong>premerge.sh:<\/strong><\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\nset -e\n\ngit fetch origin develop\n\ngit merge origin\/develop<\/code><\/pre>\n<p>\nOvdje se jednostavno uvla\u010de sve najnovije promjene iz developa i prevode u trenutnu granu. Dodali smo skript premerge.sh kao prvi korak svih izrada i po\u010deli provjeravati upravo ono \u0161to \u017eelimo, to jest <strong>integraciju<\/strong>.<\/p>\n<p><strong><em>Za lokalizaciju problema, pronalazak rje\u0161enja i pisanje ovog skripta trebalo nam je tri dana.<\/em><\/strong><\/p>\n<p>Aplikacija se razvijala, zadataka je bilo sve vi\u0161e, ekipa je rasla, a premerge.sh nas je povremeno po\u010deo iznevjeravati. U develop su provaljivale konfliktne promjene koje su lomile izradu.<\/p>\n<p>Primjer kako to izgleda:<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/6762d9ce4e431549455c49f2317a8f20.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDva programera istovremeno zapo\u010dinju s radom na zna\u010dajkama A i B. Programer zna\u010dajke A otkriva u projektu neiskori\u0161tenu funkciju <code>answer()<\/code> i, kao dobar izvi\u0111a\u010d, bri\u0161e je. Pri tome programer zna\u010dajke B u svojoj grani dodaje novi poziv te funkcije.<\/p>\n<p>Programeri zavr\u0161avaju s radom i u isto vrijeme otvaraju pull request. Poke\u0107u se izrade, premerge.sh provjerava oba pull requesta s obzirom na svje\u017ee stanje developa \u2014 sve provjere su zelene. Nakon toga se mije\u0161a pull request zna\u010dajke A, mije\u0161a se pull request zna\u010dajke B\u2026 Bum! Develop se pokvari, jer u kodu developa postoji poziv nepostoje\u0107e funkcije.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/0261dcc9f7f2e5e018ab136081b4679b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nKada se develop ne gradi, to je <strong>lokalna katastrofa<\/strong>. Cijeli tim ne mo\u017ee ni\u0161ta prikupiti i predati na testiranje.<\/p>\n<p>Na\u0161ao sam se u situaciji da sam naj\u010de\u0161\u0107e bio zadu\u017een za infrastrukturne zadatke: analitika, mre\u017ea, baze podataka. Drugim rije\u010dima, ja sam pisao funkcije i klase koje koriste ostali programeri. Zbog toga sam se \u010desto na\u0161ao u sli\u010dnim situacijama. Imao sam \u010dak i sliku koja je neko vrijeme visjela.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/0eb1aadd05b33ce995027ab52d5c1e53.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nBudu\u0107i da to nije odgovaralo, po\u010deli smo razmatrati opcije kako to sprije\u010diti.<\/p>\n<h2>Kako ne slomiti develop<\/h2>\n<p>\nPrva opcija: <strong>ponovno sastaviti sve pull requestove prilikom a\u017euriranja developa. <\/strong>Ako u na\u0161em primjeru pull request s funkcionalno\u0161\u0107u A prvi u\u0111e u develop, pull request funkcionalnosti B \u0107e se ponovno sastaviti, a time \u0107e testovi propasti zbog gre\u0161ke u kompilaciji.<\/p>\n<p>Da bismo shvatili koliko \u0107e to vremena oduzeti, razmotrimo primjer s dva PR. Otvorimo dva PR: dva builda, dva pokretanja testova. Nakon \u0161to prvi PR u\u0111e u develop, drugi se mora ponovno sastaviti. Dakle, za dva PR potrebna su tri pokretanja testova: 2 + 1 = 3.<\/p>\n<p>U principu, to nije lo\u0161e. No, pogledali smo statistiku, i tipi\u010dna situacija u na\u0161em timu bila je 10 otvorenih PR, a tada je broj testova zbroj progresije: 10 + 9 +\u2026 + 1 = 55. Dakle, da bismo prihvatili 10 PR, morali bismo se ponovno sastaviti 55 puta. I to u idealnoj situaciji, kada svi testovi prolaze iz prve, kada nitko ne otvara dodatni pull request dok se obra\u0111uje ovih deset.<\/p>\n<p>Zamislite sebe kao programera kojem je potrebno br\u017ee pritisnuti gumb \"merge\", jer ako to u\u010dini kolega, morat \u0107e \u010dekati dok svi buildovi ponovo pro\u0111u\u2026 Ne, to ne ide, ozbiljno \u0107e usporiti razvoj.<\/p>\n<p>Druga mogu\u0107a opcija: <strong>sastavljati pull request nakon code review. <\/strong>To jest, otvorite pull request, sakupite potrebno odobrenje od kolega, ispravite \u0161to treba, a nakon toga pokrenite buildove. Ako su uspje\u0161ni, pull request se spaja s developom. U ovom slu\u010daju nema dodatnih ponovnih pokretanja, ali se povratna informacija zna\u010dajno usporava. Kao programer, kada otvorim pull request, odmah \u017eelim vidjeti ho\u0107e li se sastaviti. Na primjer, ako je neki test pao, potrebno ga je brzo popraviti. U slu\u010daju odgo\u0111enog sastavljanja usporava se povratna informacija, a time i cijeli razvoj. To nam tako\u0111er nije odgovaralo.<\/p>\n<p>Na kraju je ostala samo tre\u0107a opcija \u2014 <strong>voziti bicikl<\/strong>Sav na\u0161 kod, sve na\u0161e izvorne datoteke pohranjeni su u repozitoriju na Bitbucket poslu\u017eitelju. Stoga smo morali razviti dodatak za Bitbucket.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/5e4b1f770ea1a3449b616c3101640294.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nOvaj dodatak redefinira mehanizam spajanja pull requestova. Standardni postupak zapo\u010dinje otvaranjem PR-a, pokretanjem svih gradnji i prolaskom kroz pregled koda. Ali nakon \u0161to je pregled koda zavr\u0161en i developer odlu\u010di pritisnuti \u00abmerge\u00bb, dodatak provjerava u kojem je stanju develop bio kada su pokrenute provjere. Ako je nakon izrade develop bio a\u017euriran, dodatak ne\u0107e dopustiti spajanje takvog pull requesta u glavnu granu. Jednostavno \u0107e ponovo pokrenuti izrade na temelju svje\u017eeg developa.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/0edee880e1f2d286a0c64033103ac136.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nU na\u0161em primjeru s konfliktima takve gradnje ne\u0107e pro\u0107i zbog pogre\u0161ke pri kompilaciji. Stoga \u0107e programer zna\u010dajke B morati ispraviti kod, ponovo pokrenuti provjere, a tada \u0107e dodatak automatski primijeniti pull request.<\/p>\n<p>Prije implementacije ovog dodatka, u prosjeku smo imali 2,7 pokretanja provjere po pull requestu. S dodatkom, to je sada 3,6 pokretanja. To nam se svidjelo.<\/p>\n<p>Va\u017eno je napomenuti da ovaj dodatak ima nedostatak: ponovo pokre\u0107e gradnju samo jedanput. To zna\u010di da i dalje postoji malo vremensko okno kroz koje mogu pro\u0107i konfliktne promjene u develop. No vjerojatnost za to nije visoka, i zato smo se odlu\u010dili na ovaj kompromis izme\u0111u broja pokretanja i vjerojatnosti kvara. U proteklih dvije godine to se dogodilo samo jednom, pa vjerojatno nije bilo uzalud.<\/p>\n<p><strong><em>Za razvoj prve verzije dodatka za Bitbucket potrebna su nam bila dva tjedna. <\/em><\/strong><\/p>\n<h3>Nove provjere<\/h3>\n<p>\nU me\u0111uvremenu, na\u0161 tim je nastavio rasti. Uvode se nove provjere.<\/p>\n<p>Pomislili smo: za\u0161to ispraviti pogre\u0161ke kada ih mo\u017eemo sprije\u010diti? I zato smo uveli <strong>statisti\u010dku analizu koda<\/strong>. Po\u010deli smo s lintom, koji dolazi s Android SDK-om. No tada nije mogao raditi s Kotlin kodom, a ve\u0107 je 75% na\u0161e aplikacije bilo napisano na Kotlinu. Stoga smo lintu dodali ugra\u0111ene <strong>Android Studio provjere.<\/strong><\/p>\n<p>Za to je bilo potrebno puno izmisliti: uzeti Android Studio, pakirati je u Docker i pokrenuti na CI s virtualnim monitorom kako bi mislila da je pokrenuta na pravom laptopu. No to je radilo.<\/p>\n<p>Tako\u0111er, u to vrijeme smo po\u010deli pisati mnogo <strong>instrumentacijski testova<\/strong> i uveli <strong>testiranje zaslona<\/strong>. Ovo je kada se generira referentni screenshot za malu vizualizaciju, a test se sastoji od snimanja screenshot-a s vizualizacije i usporedbe s referencom piksel po piksel. Ako postoji razlika, to zna\u010di da je negdje do\u0161lo do problema s rasporedom ili u stilovima ne\u0161to nije u redu.<\/p>\n<p>No, instrumentation testove i screenshot testove treba pokretati na ure\u0111ajima: na emulatorima ili na pravim ure\u0111ajima. S obzirom na to da ima puno testova i \u010desto se pokre\u0107u, potrebna je cijela farma. Osnivanje vlastite farme je previ\u0161e naporno, pa smo na\u0161li gotovu opciju \u2014 Firebase Test Lab.<\/p>\n<h3>Firebase Test Lab<\/h3>\n<p>\nOdabran je jer je Firebase proizvod Google-a, \u0161to zna\u010di da bi trebao biti pouzdan i malo je vjerojatno da \u0107e ikada nestati. Cijene su pristupa\u010dne: 5 USD po satu rada stvarnog ure\u0111aja, 1 USD po satu rada emulatora.<\/p>\n<p><strong><em>Implementacija Firebase Test Lab-a u na\u0161 CI trajala je otprilike tri tjedna.<\/em><\/strong><\/p>\n<p>No, tim je nastavio rasti, i Firebase nas, na\u017ealost, po\u010deo je razo\u010daravati. U to vrijeme nije imao nikakvu SLA. Ponekad je Firebase zahtijevao da \u010dekamo dok se ne oslobodi potrebno broj ure\u0111aja za testove, a ne da ih odmah zapo\u010dne, kao \u0161to smo to \u017eeljeli. \u010cekanje u redu trajalo je do pola sata, a to je jako dugo. Instrumentation testovi su ra\u0111eni na svakom PR-u, a ka\u0161njenja su jako usporila razvoj, a zatim je do\u0161ao i ra\u010dun za mjesec s okruglom svotom. Ukratko, odlu\u010dili smo se odricati Firebase-a i raditi interno, budu\u0107i da je tim bio dovoljno velik.<\/p>\n<h3>Docker + Python + bash<\/h3>\n<p>\nUzmemo Docker, smjestimo u njega emulate i napi\u0161emo jednostavan program na Python-u koji u pravom trenutku pokre\u0107e potrebnu koli\u010dinu emulatora u pravoj verziji, a kada je potrebno, zaustavlja ih. I, naravno, nekoliko bash skripti \u2014 bez njih ne mo\u017ee.<\/p>\n<p><strong><em>Kreiranje vlastitog testnog okru\u017eenja trajalo je pet tjedana.<\/em><\/strong><\/p>\n<p>Kao rezultat, svaki pull request imao je op\u0161iran, blokiraju\u0107i popis provjera:<\/p>\n<ul>\n<li>Build ARK;<\/li>\n<li>Junit testovi;<\/li>\n<li>Lint;<\/li>\n<li>Android Studio provjere;<\/li>\n<li>Instrumentation testovi;<\/li>\n<li>Screenshot testovi.<\/li>\n<\/ul>\n<p>\nTo je sprje\u010davalo mnogo mogu\u0107ih kvarova. Tehni\u010dki je sve radilo, ali su se programeri \u017ealili da predugo \u010dekaju rezultate.<\/p>\n<p>Predugo \u2014 to koliko? Izvukli smo podatke iz Bitbucket-a i TeamCity-a u sustav analize i shvatili da <strong>prosje\u010dno vrijeme \u010dekanja iznosi 45 minuta<\/strong>. Tako da programer, kada otvori pull request, u prosjeku \u010deka rezultate builda 45 minuta. Po mom mi\u0161ljenju, to je jako puno, i nije u redu raditi tako.<\/p>\n<p>Naravno, odlu\u010dili smo ubrzati sve na\u0161e gradnje.<\/p>\n<h2>Ubrzavamo<\/h2>\n<p>\nKad smo vidjeli da \u010desto gradnje stoje u redu, prvo smo <strong>kupili dodatnu opremu<\/strong> \u2014 ekstenzivni razvoj je najjednostavniji. Gradnje vi\u0161e ne stoje u redu, ali vrijeme \u010dekanja se smanjilo samo malo, jer se neka provjere same po sebi dugo izvode.<\/p>\n<h3>Uklanjamo preduge provjere<\/h3>\n<p>\nNa\u0161 Continuous Integration mogao je uhvatiti takve tipove gre\u0161aka i problema.<\/p>\n<ul>\n<li><strong>Ne gradi se<\/strong>. CI mo\u017ee uhvatiti gre\u0161ku kompilacije kada zbog konfliktnih promjena ne\u0161to ne mo\u017ee biti sagra\u0111eno. Kao \u0161to sam ve\u0107 spomenuo, tada nitko ni\u0161ta ne mo\u017ee graditi, razvoj staje, a svi su nervozni.<\/li>\n<li><strong>Gre\u0161ka u pona\u0161anju<\/strong>. Na primjer, kada se aplikacija gradi, ali se ru\u0161i kada pritisnete dugme, ili dugme uop\u0161te ne reagira. To je lo\u0161e, jer takva gre\u0161ka mo\u017ee do\u0107i do korisnika.<\/li>\n<li><strong>Gre\u0161ka u dizajnu<\/strong>. Na primjer, dugme reagira, ali je pomjereno 10 piksela lijevo.<\/li>\n<li><strong>Pove\u0107anje tehni\u010dkog duga<\/strong>.<\/li>\n<\/ul>\n<p>\nPogledav\u0161i ovu listu, shvatili smo da su kriti\u010dne samo prve dvije stavke. Takve probleme \u017eelimo uhvatiti u prvom redu. Gre\u0161ke u dizajnu se otkrivaju u fazi revizije dizajna i tada se lako ispravljaju. Rad na tehni\u010dkom dugu zahtijeva poseban proces i planiranje, pa smo odlu\u010dili ne provjeravati ga na pull request-u.<\/p>\n<p>Na osnovu ove klasifikacije, preispitali smo cijeli popis provjera. <strong>Isklju\u010dili smo Lint<\/strong> i prebacili njegovo pokretanje na no\u0107: samo da bi isporu\u010dio izvje\u0161taj o broju problema u projektu. S tehni\u010dkim dugom smo se dogovorili raditi posebno, a <strong>od Android Studio provjera smo potpuno odustali<\/strong>. Android Studio u Docker-u za pokretanje inspekcija zvu\u010di zanimljivo, ali donosi mnogo neugodnosti u podr\u0161ci. Svaka nadogradnja verzije Android Studio je borba s nejasnim gre\u0161kama. Tako\u0111er je bilo te\u0161ko odr\u017eavati screenshot testove, jer je biblioteka radila nesigurno, bilo je la\u017enih alarma. <strong>Screenshot testove smo uklonili iz popisa provjera<\/strong>.<\/p>\n<p>Na kraju smo ostavili:<\/p>\n<ul>\n<li>Build ARK;<\/li>\n<li>Junit testovi;<\/li>\n<li>Instrumentation testove.<\/li>\n<\/ul>\n<h3>Gradle remote cache<\/h3>\n<p>\nBez te\u0161kih provjera sve je postalo bolje. Ali nema kraja savr\u0161enstvu!<\/p>\n<p>Na\u0161a aplikacija ve\u0107 je bila razbijena na otprilike 150 gradle modula. Obi\u010dno u takvim slu\u010dajevima dobro funkcionira Gradle remote cache, pa smo odlu\u010dili to isprobati.<\/p>\n<p>Gradle remote cache je usluga koja mo\u017ee ke\u0161irati artefakte gradnje za pojedina\u010dne zadatke u pojedinim modulima. Umjesto da stvarno kompajlira kod, Gradle putem HTTP-a provjerava remote cache i pita je li netko ve\u0107 izvr\u0161io taj zadatak. Ako jest, jednostavno preuzima rezultat.<\/p>\n<p><strong><em>Pokrenuti Gradle remote cache je lako, jer Gradle pru\u017ea Docker sliku. Uspjeli smo to napraviti za tri sata.<\/em><\/strong><\/p>\n<p>Sve \u0161to je trebalo bilo je pokrenuti Docker i dodati jednu liniju u projekt. No iako ga je lako pokrenuti, da bi sve dobro funkcioniralo, bit \u0107e potrebno dosta vremena.<\/p>\n<p>Ispod je grafikon proma\u0161aja ke\u0161a.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/458ef23b5506b04f3bd385be0c18607a.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nNa po\u010detku je postotak proma\u0161aja ke\u0161a bio oko 65. Tijekom tri tjedna uspjeli smo smanjiti tu vrijednost na 20%. Ispostavilo se da zadaci koje gradi Android aplikacija imaju \u010dudne tranzitivne ovisnosti, zbog kojih je Gradle proma\u0161ivao ke\u0161.<\/p>\n<p>Povezivanjem ke\u0161a zna\u010dajno smo ubrzali gradnju. No osim gradnje, izvr\u0161avaju se i instrumentation testovi, a oni se izvr\u0161avaju dugo. Mo\u017eda nije potrebno izvr\u0161avati sve testove za svaku pull request. Da bismo to utvrdili, koristimo analizu utjecaja.<\/p>\n<h3>Analiza utjecaja<\/h3>\n<p>\nNa pull requestu prikupljamo git diff i pronalazimo promijenjene Gradle module.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/471ca37206a0da4d5741c8ee1dbb7f03.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nIma smisla pokretati samo one instrumentation testove koji provjeravaju promijenjene module i sve module koji ovise o njima. Nema smisla pokretati testove za susjedne module: tamo se kod nije promijenio i ni\u0161ta ne mo\u017ee pokvariti.<\/p>\n<p>S instrumentation testovima nije sve tako jednostavno, jer moraju biti u najvi\u0161em modulu aplikacije. Primijenili smo heuristiku analize bytecode-a kako bismo shvatili kojem modulu svaki test pripada.<\/p>\n<p><strong><em>Modernizacija rada instrumentation testova, kako bi provjeravali samo uklju\u010dene module, trajala je oko osam tjedana.<\/em><\/strong><\/p>\n<p>Mjere za ubrzanje provjera uspje\u0161no su djelovale. S 45 minuta smanjili smo na otprilike 15. \u010cetvrt sata \u010dekati gradnju sada je ve\u0107 normalno.<\/p>\n<p>No sada su se programeri po\u010deli \u017ealiti da im nije jasno koje se gradnje pokre\u0107u, gdje pogledati log, za\u0161to je gradnja crvena, koji test je pao itd.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/aa76151174cd4e5a45ff281818e312f5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nProblemi s povratnom informacijom usporavaju razvoj, stoga smo se potrudili osigurati \u0161to jasnije i detaljnije informacije o svakoj PR i gradnji. Po\u010deli smo s komentarima u Bitbucketu na PR-u koji ozna\u010davaju koja je gradnja pala i za\u0161to, slali smo ciljane poruke u Slack. Na kraju smo napravili stranicu PR nadzorne plo\u010de sa popisom svih gradnji koje su trenutno pokrenute i njihovim stanjem: u redu, u tijeku, pala ili zavr\u0161ena. Mo\u017eete kliknuti na gradnju i do\u0107i do njezina loga.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/2d16b7a6b6d23e23903d291ef1526999.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n<strong><em>Na detaljnu povratnu informaciju potro\u0161eno je \u0161est tjedana.<\/em><\/strong><\/p>\n<h2>Planovi<\/h2>\n<p>\nPrelazimo na najnoviju povijest. Rje\u0161avaju\u0107i problem povratne informacije, pre\u0161li smo na novu razinu \u2014 odlu\u010dili smo izgraditi svoju farmu emulatora. Kada je testova i emulatora mnogo, te\u0161ko ih je upravljati. Na kraju su svi na\u0161i emulatori preba\u010deni u k8s klaster s fleksibilnim upravljanjem resursima.<\/p>\n<p>Osim toga, postoje i drugi planovi.<\/p>\n<ul>\n<li><strong>Vratiti Lint<\/strong> (i druge stati\u010dke analize). Ve\u0107 radimo na tom podru\u010dju.<\/li>\n<li>Pokretati sve na PR-u kao blokator <strong>end-to-end testove<\/strong> na svim verzijama SDK-a.<\/li>\n<\/ul>\n<p>\nDakle, pratili smo razvoj Continuous Integration-a u Avitu. Sada \u017eelim dati nekoliko savjeta iz perspektive iskusnog.<\/p>\n<h1>Savjeti<\/h1>\n<p>\nDa mogu dati samo jedan savjet, to bi bio ovaj:<\/p>\n<blockquote><p>Molim vas, budite oprezniji s shell skriptama!<\/p><\/blockquote>\n<p>\nBash je vrlo fleksibilan i sna\u017ean alat, vrlo je prakti\u010dno i brzo pisati skripte na njemu. No mo\u017ee vas upecati, i na\u017ealost, mi smo se upecali.<\/p>\n<p>Sve je po\u010delo s jednostavnim skriptama koje su se pokretale na na\u0161im build ma\u0161inama:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n.\/gradlew assembleDebug<\/code><\/pre>\n<p>\nNo, kako se ina\u010de zna, sve se s vremenom razvija i komplikuje \u2014 ajmo pokrenuti jednu skriptu iz druge, ajmo proslijediti neke parametre \u2014 na kraju je bilo potrebno napisati funkciju koja odre\u0111uje na kojoj razini ugnije\u017edenosti se trenutno nalazimo, kako bismo umetnuli potrebne navodnike da bi to sve proradilo.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/244a574f7b5d3b4fc77cfc4ccda2db69.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nMo\u017eete zamisliti radne napore u razvoju takvih skripti. Preporu\u010dujem da ne upadnete u ovu zamku.<\/p>\n<p>\u010cime to mo\u017ee zamijeniti?<\/p>\n<ul>\n<li>Bilo kojim skriptnim jezikom. Pisanje na <strong>Pythonu ili Kotlin Scriptu<\/strong> je prakti\u010dnije, jer je to programiranje, a ne skripte.<\/li>\n<li>Ili opisati svu logiku gradnji u obliku <strong>Custom gradle tasks<\/strong> za va\u0161 projekt.<\/li>\n<\/ul>\n<p>\nOdlu\u010dili smo se za drugu opciju i sada postupno bri\u0161emo sve bash skripte i pi\u0161emo mnogo prilago\u0111enih gradle taskova.<\/p>\n<p><strong>Savjet br. 2: pohraniti infrastrukturu u kodu.<\/strong><\/p>\n<p>Prakti\u010dno je kada se postavke Continuous Integration ne \u010duvaju u UI su\u010delju Jenkins-a ili TeamCity-a i sli\u010dno, ve\u0107 kao tekstualne datoteke direktno u repozitoriju projekta. To omogu\u0107uje verzioniranje. Ne\u0107e biti te\u0161ko vratiti se ili prikupiti kod na drugoj grani.<\/p>\n<p>Skripte se mogu \u010duvati u projektu. A \u0161to u\u010diniti s okru\u017eenjem?<\/p>\n<p><strong>Savjet br. 3: Docker mo\u017ee pomo\u0107i s okru\u017eenjem.<\/strong><\/p>\n<p>Android programerima sigurno \u0107e pomo\u0107i, dok iOS ne, na\u017ealost.<\/p>\n<p>Ovo je primjer jednostavnog docker fajla koji sadr\u017ei jdk i android-sdk:<\/p>\n<pre><code class=\"plaintext\">FROM openjdk:8\n\nENV SDK_URL=\"https:\/\/dl.google.com\/android\/repository\/sdk-tools-linux-3859397.zip\" \n    ANDROID_HOME=\"\/usr\/local\/android-sdk\" \n    ANDROID_VERSION=26 \n    ANDROID_BUILD_TOOLS_VERSION=26.0.2\n\n# Preuzmi Android SDK\nRUN mkdir \"$ANDROID_HOME\" .android \n    &amp;&amp; cd \"$ANDROID_HOME\" \n    &amp;&amp; curl -o sdk.zip $SDK_URL \n    &amp;&amp; unzip sdk.zip \n    &amp;&amp; rm sdk.zip \n    &amp;&amp; yes | $ANDROID_HOME\/tools\/bin\/sdkmanager --licenses\n\n# Instaliraj Android Build Tool i knji\u017enice\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager --update\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager \"build-tools;${ANDROID_BUILD_TOOLS_VERSION}\" \n    \"platforms;android-${ANDROID_VERSION}\" \n    \"platform-tools\"\n\nRUN mkdir \/application\nWORKDIR \/application\n<\/code><\/pre>\n<p>\nNapisao sam ovaj docker fajl (rekao bih u tajnosti, ne morate ga pisati, mo\u017eete preuzeti gotov s GitHub-a) i sastavljenjem slike dobijate virtuelnu ma\u0161inu na kojoj mo\u017eete sastaviti aplikaciju i pokrenuti Junit testove.<\/p>\n<p>Dva glavna argumenta za\u0161to to ima smisla: skalabilnost i ponovljivost. Pomo\u0107u dockera mo\u017eete brzo pokrenuti desetak build agenata koji \u0107e imati identi\u010dno okru\u017eenje kao i prethodni. To zna\u010dajno olak\u0161ava \u017eivot CI in\u017eenjerima. Uklju\u010divanje android-sdk u docker je prili\u010dno jednostavno, dok s emulatorima malo te\u017ee: potrebno je malo se potruditi (ili ponovo preuzeti gotovo s GitHub-a).<\/p>\n<p><strong>Savjet br. 4: ne zaboravite da se provjere ne rade samo radi provjera, ve\u0107 za ljude.<\/strong><\/p>\n<p>Razvojiteljima je jako va\u017ena brza i, \u0161to je najbitnije, jasna povratna informacija: \u0161to im se pokvarilo, koji je test pao, gdje pogledati build log.<\/p>\n<p><strong>Savjet br. 5: budite pragmati\u010dni pri razvoju Continuous Integration.<\/strong><\/p>\n<p>Jasno razumite koje tipove gre\u0161aka \u017eelite sprije\u010diti, koliko ste spremni potro\u0161iti resursa, vremena, ra\u010dunarskog vremena. Preduga provjeravanja mo\u017eete, na primjer, odgoditi za no\u0107. A od onih koja hvataju manje va\u017ene gre\u0161ke, potpuno se odricati.<\/p>\n<p><strong>Savjet br. 6: koristite gotove alate.<\/strong><\/p>\n<p>Trenutno postoji mnogo kompanija koje nude cloud CI.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/e57aabd49ec01d14e83b64331fd84ec5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZa male timove ovo je dobro rje\u0161enje. Ne morate ni\u0161ta odr\u017eavati, samo platite malo novca, okupite svoju aplikaciju i \u010dak pokrenite instrumentacijske teste.<\/p>\n<p><strong>Savjet br. 7: u velikom timu isplativije su in-house rje\u0161enja.<\/strong><\/p>\n<p>No, prije ili kasnije, s rastom tima isplativija \u0107e postati in-house rje\u0161enja. Postoji jedan aspekt ovih rje\u0161enja. U ekonomiji postoji zakon opadaju\u0107ih prihoda: u svakom projektu svako sljede\u0107e pobolj\u0161anje dolazi sve te\u017ee, zahtijeva sve vi\u0161e i vi\u0161e investicija.<\/p>\n<p>Ekonomija opisuje cijeli na\u0161 \u017eivot, uklju\u010duju\u0107i Continuous Integration. Izradio sam grafikon tro\u0161kova rada u svakoj fazi razvoja na\u0161eg Continuous Integration.<\/p>\n<p><img decoding=\"async\" alt=\"Evolucija CI u timu za mobilni razvoj\" src=\"\/wp-content\/uploads\/2019\/04\/97bf8bff64f3ac9ae587fae195251da2.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nMo\u017ee se vidjeti da svako pobolj\u0161anje postaje sve te\u017ee i te\u017ee. Gledaju\u0107i ovaj grafikon, mo\u017ee se razumjeti da se razvoj Continuous Integration treba uskladiti s rastom veli\u010dine tima. Za tim od dvoje ljudi utro\u0161iti 50 dana na razvoj interne farme emulatore \u2014 nije najbolja ideja. No, isto tako, za veliki tim potpuno ignorirati Continuous Integration \u2014 tako\u0111er je lo\u0161a ideja, jer \u0107e na probleme s integracijom, popravke komunikacije i sli\u010dno oti\u0107i jo\u0161 vi\u0161e vremena.<\/p>\n<p>Zapo\u010deli smo s idejom da je automatizacija potrebna, jer su ljudi skupi, grije\u0161e i \u010desto su lijeni. No automatiziraju tako\u0111er ljudi. Stoga se svi ti problemi odnose i na automatizaciju.<\/p>\n<ul>\n<li>Automatizacija je skupa. Sjetite se grafikona tro\u0161kova rada.<\/li>\n<li>Prilikom automatizacije ljudi grije\u0161e.<\/li>\n<li>Ponekad je vrlo nezgodno automatizirati, jer i ovako sve radi. Za\u0161to jo\u0161 pobolj\u0161avati, za\u0161to cijeli taj Continuous Integration?<\/li>\n<\/ul>\n<p>\nNo, imam statistiku: u 20% izrada dolaze gre\u0161ke. I to se ne doga\u0111a zato \u0161to na\u0161i programeri lo\u0161e pi\u0161u kod. To je zato \u0161to su programeri sigurni da, ako naprave neku gre\u0161ku, ona ne\u0107e u\u0107i u develop, uhvatit \u0107e je automatizirani testovi. Stoga programeri mogu tro\u0161iti vi\u0161e vremena na pisanje koda i zanimljive stvari, umjesto da lokalno ne\u0161to provode i provjeravaju.<\/p>\n<p><strong>Bavite se Continuous Integration. Ali umjereno.<\/strong><\/p>\n<blockquote><p>Usput, Nikolaj Nesterov ne samo da sam radi odli\u010dne prezentacije, ve\u0107 je i \u010dlan programskog odbora <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\">AppsConf<\/a><\/noindex> i poma\u017ee drugima da pripreme sadr\u017eajne nastupe za vas. Puninu i korisnost programa nadolaze\u0107e konferencije mo\u017eete procijeniti prema temama u <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\/schedule\">rasporedu<\/a><\/noindex>. A za vi\u0161e informacija do\u0111ite 22-23. travnja u InfoProstor.<\/p><\/blockquote>\n<p>Izvor: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/oleg-bunin\/blog\/447608\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445. \u0423\u0441\u043b\u043e\u0432\u0438\u044f \u0443\u0441\u043f\u0435\u0445\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0441\u0445\u0435\u043c\u044b. \u041d\u0430\u043f\u0438\u0441\u0430\u0432 \u043a\u043e\u0434, \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043e\u043d: \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043b\u043e\u043c\u0430\u0435\u0442, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0432\u0430\u0448\u0438 \u043a\u043e\u043b\u043b\u0435\u0433\u0438. \u0415\u0441\u043b\u0438 \u043e\u0431\u0430 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f, \u0442\u043e \u0432\u044b \u043d\u0430 \u043f\u0443\u0442\u0438 \u043a \u0443\u0441\u043f\u0435\u0445\u0443. \u0427\u0442\u043e\u0431\u044b \u043b\u0435\u0433\u043a\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u044d\u0442\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0438 \u043d\u0435 \u0441\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0441 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":23270,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-31304","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.4 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/hr\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.4\" \/>\n\t\t<meta property=\"og:locale\" content=\"hr_HR\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/hr\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Evolucija CI-a u timu za razvoj mobilnih aplikacija | ProHoster","description":"Danas se ve\u0107ina softverskih proizvoda razvija u timovima.","canonical_url":"https:\/\/prohoster.info\/hr\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"hr_HR","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster","og:description":"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.","og:url":"https:\/\/prohoster.info\/hr\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T18:40:34+00:00","article:modified_time":"2019-10-31T18:40:34+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"31304","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-21 05:30:21","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 03:19:49","updated":"2026-01-21 05:30:21","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/posts\/31304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/comments?post=31304"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/posts\/31304\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/media\/23270"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/media?parent=31304"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/categories?post=31304"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/hr\/wp-json\/wp\/v2\/tags?post=31304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}