(gambar dari situs resmi )Buildbot, seperti namanya, adalah sistem integrasi berkelanjutan (ci). Sudah ada
beberapa artikel tentang dia di Habré, tetapi, dari sudut pandang saya, kelebihan alat ini tidak begitu jelas dari mereka. Selain itu, mereka hampir tidak memiliki contoh, yang membuatnya sulit untuk melihat kekuatan penuh dari program ini. Dalam artikel saya, saya akan mencoba untuk menebus kekurangan ini, berbicara tentang perangkat internal Buildbot'a dan memberikan contoh beberapa skrip non-standar.
Kata-kata umum
Saat ini, ada sejumlah besar sistem integrasi berkelanjutan, dan ketika datang ke salah satu dari mereka, pertanyaan yang cukup logis muncul dalam semangat "Mengapa ini diperlukan jika Anda sudah memiliki <program_name> dan semua orang menggunakannya?" Saya akan mencoba menjawab pertanyaan seperti itu tentang Buildbot. Beberapa informasi akan digandakan dengan artikel yang ada, beberapa dijelaskan dalam dokumentasi resmi, tetapi ini diperlukan untuk konsistensi narasi.
Perbedaan utama dari sistem integrasi berkelanjutan lainnya adalah bahwa Buildbot adalah kerangka kerja Python untuk menulis ci, dan bukan solusi di luar kotak. Ini berarti bahwa untuk menghubungkan proyek ke Buildbot, Anda harus terlebih dahulu menulis program python terpisah menggunakan kerangka kerja Buildbot yang mengimplementasikan fungsi integrasi berkelanjutan yang dibutuhkan proyek Anda. Pendekatan ini memberikan fleksibilitas luar biasa, memungkinkan Anda untuk menerapkan skenario pengujian rumit yang tidak mungkin untuk solusi out-of-box karena keterbatasan arsitektur.
Lebih jauh, Buildbot bukan layanan, dan karena itu Anda harus secara jujur menyebarkannya di infrastruktur Anda. Di sini saya perhatikan bahwa kerangka kerjanya sangat loyal terhadap sumber daya sistem. Ini tentu saja bukan C atau C ++, tetapi python menang melawan pesaing Java-nya. Di sini, misalnya, membandingkan konsumsi memori dengan GoCD (dan ya, terlepas dari namanya, ini adalah sistem Java):
Buildbot:

GoCD:

Menyebarkan dan menulis sendiri program uji terpisah dapat membuat Anda sedih karena memikirkan pengaturan awal. Namun, skrip sangat disederhanakan oleh banyaknya kelas built-in. Kelas-kelas ini mencakup banyak operasi standar, apakah itu mendapatkan perubahan dari repositori github atau membangun proyek dengan CMake. Akibatnya, skrip standar untuk proyek kecil tidak akan lebih rumit daripada file YML untuk beberapa travis-ci. Saya tidak akan menulis tentang penyebaran, ini dibahas secara rinci dalam artikel yang ada dan tidak ada yang rumit di sana.
Fitur berikutnya dari Buildbot, saya perhatikan bahwa secara default logika pengujian diimplementasikan di sisi ci-server. Ini bertentangan dengan pendekatan "pipeline as a code" yang sekarang populer, di mana logika pengujian dijelaskan dalam file (seperti .travis.yml) yang terletak di repositori bersama dengan kode sumber proyek, dan server ci hanya membaca file ini dan mengeksekusi apa yang dikatakannya. Sekali lagi, ini hanya perilaku default. Kemampuan kerangka Buildbot memungkinkan Anda untuk menerapkan pendekatan yang dijelaskan dengan menyimpan skrip uji di repositori. Bahkan ada solusi siap pakai -
bb-travis , yang mencoba mengambil yang terbaik dari Buildbot dan travis-ci. Selain itu, nanti dalam artikel ini saya akan menjelaskan cara menerapkan sesuatu yang mirip dengan perilaku ini sendiri.
Buildbot secara default mengumpulkan setiap komit saat mendorong. Ini mungkin tampak seperti beberapa fitur kecil yang tidak perlu, tetapi bagi saya itu, sebaliknya, telah menjadi salah satu keunggulan utama. Banyak solusi populer di luar kotak (travis-ci, gitlab-ci) tidak memberikan kesempatan seperti itu sama sekali, hanya bekerja dengan komit terakhir di cabang. Bayangkan bahwa selama pengembangan Anda sering harus memilih komitmen. Akan tidak menyenangkan untuk mengambil komit yang tidak berfungsi, yang tidak diperiksa oleh sistem build karena fakta bahwa diluncurkan bersama dengan banyak commit dari atas. Tentu saja, di Buildbot Anda hanya dapat membangun komit terakhir, dan ini dilakukan dengan menetapkan hanya satu parameter.
Kerangka kerja ini memiliki dokumentasi yang cukup baik, yang menjelaskan semuanya secara terperinci, mulai dari arsitektur umum hingga pedoman untuk memperluas kelas bawaan. Namun, bahkan dengan dokumentasi semacam itu, Anda mungkin harus melihat beberapa hal dalam kode sumber. Ini sepenuhnya terbuka di bawah lisensi GPL v2 dan mudah dibaca. Dari minus - dokumentasi hanya tersedia dalam bahasa Inggris, dalam bahasa Rusia ada sangat sedikit informasi di jaringan. Alat itu tidak muncul kemarin, dengan bantuan
python ,
Wireshark ,
LLVM dan
banyak proyek terkenal lainnya dirakit. Pembaruan keluar, proyek ini didukung oleh banyak pengembang, sehingga kami dapat berbicara tentang keandalan dan stabilitas.
(Beranda Python Buildbot)Theormin
Bagian ini pada dasarnya adalah terjemahan bebas dari bab dokumentasi resmi tentang arsitektur kerangka kerja. Ini menunjukkan rangkaian tindakan lengkap dari menerima perubahan oleh sistem-ci hingga mengirim pemberitahuan hasil kepada pengguna. Jadi, Anda membuat perubahan pada kode sumber proyek dan mengirimkannya ke repositori jarak jauh. Apa yang terjadi selanjutnya secara skematis ditunjukkan pada gambar:
(gambar dari dokumentasi resmi )Pertama-tama, Buildbot entah bagaimana harus mengetahui bahwa ada perubahan dalam repositori. Ada dua cara utama - webhooks dan polling, meskipun tidak ada yang melarang datang dengan sesuatu yang lebih canggih. Dalam kasus pertama, di Buildbot, kelas-kelas turunan BaseHookHandler bertanggung jawab untuk ini. Ada banyak solusi siap pakai, misalnya,
GitHubHandler atau
GitoriusHandler . Metode kunci di kelas-kelas ini adalah
getChanges () . Logikanya sangat sederhana: harus mengubah permintaan HTTP menjadi daftar objek perubahan.
Untuk kasus kedua, Anda memerlukan
kelas turunan
PollingChangeSource . Sekali lagi, ada solusi siap pakai, seperti
GitPoller atau
HgPoller . Metode kuncinya adalah
polling () . Itu disebut dengan frekuensi tertentu dan entah bagaimana harus membuat daftar perubahan dalam repositori. Dalam kasus git, ini bisa menjadi panggilan untuk mengambil git dan perbandingan dengan keadaan tersimpan sebelumnya. Jika kemampuan bawaan tidak cukup, maka cukup buat kelas pewaris Anda sendiri dan kelebihan metode. Contoh menggunakan polling:
c['change_source'] = [changes.GitPoller( repourl = 'git@git.example.com:project', project = 'My Project', branches = True,
Webhook bahkan lebih mudah digunakan, yang utama adalah jangan lupa untuk mengkonfigurasinya di sisi server git. Ini hanya satu baris dalam file konfigurasi:
c['www']['change_hook_dialects'] = { 'github': {} }
Langkah selanjutnya, objek perubahan adalah input ke objek
scheduler (
penjadwal ). Contoh penjadwal
bawaan :
AnyBranchScheduler ,
NightlyScheduler ,
ForceScheduler , dll. Setiap penjadwal menerima semua objek perubahan sebagai input, tetapi hanya memilih yang lulus filter. Filter dilewatkan ke penjadwal di konstruktor melalui argumen
change_filter . Pada output, perencana membuat permintaan build. Penjadwal memilih pembangun berdasarkan argumen pembangun.
Beberapa perencana memiliki argumen rumit yang disebut
treeStableTimer . Ini berfungsi sebagai berikut: ketika perubahan diterima, scheduler tidak segera membuat permintaan build baru, tetapi memulai timer. Jika perubahan baru tiba dan timer belum kedaluwarsa, maka perubahan lama diganti dengan yang baru, dan timer diperbarui. Saat penghitung waktu berakhir, penjadwal hanya membuat satu permintaan build dari perubahan yang disimpan terakhir.
Dengan demikian, logika menyusun hanya komit terakhir ketika mendorong diterapkan. Contoh konfigurasi penjadwal:
c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'My Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'My Project'), builderNames = ['My Builder'] )]
Bangun permintaan, betapapun anehnya kedengarannya, pergi ke input pembangun. Tugas pengumpul adalah menjalankan perakitan pada “pekerja” yang dapat diakses. Worker adalah lingkungan build, seperti stretch64 atau ubuntu1804x64. Daftar pekerja dilewatkan melalui argumen
pekerja . Semua pekerja dalam daftar harus sama (mis. Nama-nama secara alami berbeda, tetapi lingkungan di dalamnya sama), karena pengumpul bebas memilih salah satu yang tersedia. Menetapkan banyak nilai di sini berfungsi untuk menyeimbangkan beban, dan bukan untuk membangun di lingkungan yang berbeda. Menggunakan argumen
faktor y, kolektor menerima urutan langkah-langkah untuk membangun proyek. Saya akan menulis tentang ini secara rinci di bawah ini.
Contoh mengkonfigurasi kolektor:
c['builders'] = [util.BuilderConfig( name = 'My Builder', workernames = ['stretch32'], factory = factory )]
Jadi, proyeknya sudah siap. Langkah terakhir Buildbot adalah memberi tahu build. Kelas reporter bertanggung jawab untuk ini. Contoh klasik adalah kelas
MailNotifier , yang mengirim email dengan hasil build.
Contoh Koneksi
MailNotifier :
c['services'] = [reporters.MailNotifier( fromaddr = 'buildbot@example.com', relayhost = 'mail.example.com', smtpPort = 25, extraRecipients = ['devel@example.com'], sendToInterestedUsers = False )]
Nah, sekarang saatnya untuk beralih ke contoh lengkap. Saya perhatikan bahwa Buildbot sendiri ditulis menggunakan kerangka Twisted, dan oleh karena itu keakraban dengannya akan sangat memudahkan penulisan dan pemahaman skrip Buildbot. Kami akan memiliki anak lelaki pencambuk untuk proyek yang disebut Proyek Pet. Biarkan ditulis dalam C ++, dirakit menggunakan CMake, dan kode sumbernya terletak di repositori git. Kami tidak terlalu malas dan menulis tes untuknya yang dijalankan oleh tim ctest. Baru-baru ini, kami membaca artikel ini dan menyadari bahwa kami ingin menerapkan pengetahuan yang baru diperoleh untuk proyek kami.
Contoh satu: agar bisa berfungsi
Sebenarnya, file konfigurasi:
100 baris kode python from buildbot.plugins import *
Dengan menulis baris-baris ini kita mendapatkan perakitan otomatis ketika mendorong ke repositori, wajah web yang indah, pemberitahuan email, dan atribut lain dari ci apa pun yang menghargai diri sendiri. Sebagian besar harus jelas: pengaturan penjadwal, pengumpul, dan objek lain dibuat mirip dengan contoh yang diberikan sebelumnya, nilai sebagian besar parameter bersifat intuitif. Secara terperinci, saya akan fokus hanya pada menciptakan pabrik, yang saya janjikan sebelumnya.
Pabrik terdiri dari
langkah -
langkah pembangunan yang harus diselesaikan Buildbot untuk proyek tersebut. Seperti halnya kelas-kelas lain, ada banyak solusi siap pakai. Pabrik kami terdiri dari lima langkah. Sebagai aturan, langkah pertama adalah mendapatkan kondisi repositori saat ini, dan di sini kita tidak akan membuat pengecualian. Untuk melakukan ini, kami menggunakan kelas standar
Git :
Langkah pertama factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True) )
Selanjutnya, kita perlu membuat direktori tempat proyek akan dirakit - kita akan membuat penuh dari membangun sumber. Sebelum ini, Anda harus ingat untuk menghapus direktori jika sudah ada. Jadi, kita perlu menjalankan dua perintah. Kelas
ShellSequence akan membantu kami dalam hal ini:
Langkah kedua factory.addStep(steps.ShellSequence( name = 'create builddir', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS, commands = [ util.ShellArg(command = ['rm', '-rf', 'build']), util.ShellArg(command = ['mkdir', 'build']) ]) )
Sekarang Anda harus memulai CMake. Untuk melakukan ini, logis untuk menggunakan salah satu dari dua kelas -
ShellCommand atau
CMake . Kami akan menggunakan yang terakhir, tetapi perbedaannya minimal: ini adalah
pembungkus sederhana di atas kelas pertama, membuatnya sedikit lebih nyaman untuk menyampaikan argumen yang spesifik ke CMake.
Langkah ketiga factory.addStep(steps.CMake( workdir = 'build', path = '../sources', haltOnFailure = True) )
Saatnya menyusun proyek. Seperti pada kasus sebelumnya, Anda dapat menggunakan
ShellCommand . Demikian pula, ada kelas
Kompilasi , yang merupakan pembungkus di atas
ShellCommand . Namun demikian, ini adalah pembungkus yang lebih rumit: kelas
Compile memonitor peringatan selama kompilasi dan secara akurat menampilkannya dalam log terpisah. Itu sebabnya kita akan menggunakan kelas
Compile :
Langkah keempat factory.addStep(steps.Compile( name = 'build project', workdir = 'build', haltOnFailure = True, warnOnWarnings = True, command = ['make']) )
Akhirnya, jalankan tes kami. Di sini kita akan menggunakan kelas
ShellCommand yang disebutkan sebelumnya:
Langkah kelima factory.addStep(steps.ShellCommand( name = 'run tests', workdir = 'build', haltOnFailure = True, command = ['ctest']) )
Contoh dua: pipeline sebagai kode
Di sini saya akan menunjukkan bagaimana menerapkan opsi anggaran untuk menyimpan logika pengujian bersama dengan kode sumber proyek, dan tidak dalam file konfigurasi ci-server. Untuk melakukan ini, letakkan file
.buildbot dalam repositori dengan kode, di mana setiap baris terdiri dari kata-kata, yang pertama ditafsirkan sebagai direktori untuk perintah yang akan dieksekusi, dan sisanya sebagai perintah dengan argumennya. Untuk Proyek Kesayangan kami, file
.buildbot akan terlihat seperti ini:
File .Buildbot dengan perintah. rm -rf build
. mkdir build
build cmake ../sources
build make
build ctest
Sekarang kita perlu memodifikasi file konfigurasi Buildbot. Untuk menganalisis file
.buildbot , kita harus menulis kelas langkah kita sendiri. Langkah ini akan membaca file
.buildbot , setelah itu untuk setiap baris tambahkan langkah
ShellCommand dengan argumen yang diperlukan. Untuk menambahkan langkah-langkah secara dinamis, kita akan menggunakan metode
build.addStepsAfterCurrentStep () . Sama sekali tidak menakutkan:
Analisis KelasLangkah class AnalyseStep(ShellMixin, BuildStep): def __init__(self, workdir, **kwargs): kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command', 'workdir', 'want_stdout']) BuildStep.__init__(self, **kwargs) self.workdir = workdir @defer.inlineCallbacks def run(self): self.stdio_log = yield self.addLog('stdio') cmd = RemoteShellCommand( command = ['cat', '.buildbot'], workdir = self.workdir, want_stdout = True, want_stderr = True, collectStdout = True ) cmd.useLog(self.stdio_log) yield self.runCommand(cmd) if cmd.didFail(): defer.returnValue(util.FAILURE) results = [] for row in cmd.stdout.splitlines(): lst = row.split() dirname = lst.pop(0) results.append(steps.ShellCommand( name = lst[0], command = lst, workdir = dirname ) ) self.build.addStepsAfterCurrentStep(results) defer.returnValue(util.SUCCESS)
Berkat pendekatan ini, pabrik untuk pengumpul menjadi lebih sederhana dan lebih fleksibel:
Pabrik untuk menganalisis file .buildbot factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True, mode = 'incremental') ) factory.addStep(AnalyseStep( name = 'Analyse .buildbot file', workdir = 'sources', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) )
Contoh tiga: pekerja sebagai kode
Sekarang bayangkan di sebelah kode proyek, kita perlu menentukan bukan urutan perintah, tetapi lingkungan untuk perakitan. Bahkan, kami mendefinisikan pekerja.
File .buildbot mungkin terlihat seperti ini:
File lingkungan .Buildbot{
"workers": ["stretch32", "wheezy32"]
}
File konfigurasi Buildbot dalam kasus ini akan menjadi lebih rumit, karena kami ingin rakitan pada lingkungan yang berbeda saling berhubungan (jika setidaknya satu lingkungan gagal, seluruh komit dianggap tidak beroperasi). Dua tingkat membantu kami memecahkan masalah. Kami akan memiliki pekerja lokal yang mem-parsing file
.buildbot dan menjalankan build berdasarkan pekerja yang diinginkan. Pertama, seperti pada contoh sebelumnya, kita akan menulis langkah kita untuk menganalisis file
.buildbot . Untuk memulai perakitan pada pekerja tertentu, satu bundel dari langkah
Trigger dan jenis khusus dari penjadwal
TriggerableScheduler digunakan . Langkah kami menjadi sedikit lebih rumit, tetapi cukup mudah dipahami:
Analisis KelasLangkah class AnalyseStep(ShellMixin, BuildStep): def __init__(self, workdir, **kwargs): kwargs = self.setupShellMixin(kwargs, prohibitArgs = ['command', 'workdir', 'want_stdout']) BuildStep.__init__(self, **kwargs) self.workdir = workdir @defer.inlineCallbacks def _getWorkerList(self): cmd = RemoteShellCommand( command = ['cat', '.buildbot'], workdir = self.workdir, want_stdout = True, want_stderr = True, collectStdout = True ) cmd.useLog(self.stdio_log) yield self.runCommand(cmd) if cmd.didFail(): defer.returnValue([])
Kami akan menggunakan langkah ini pada pekerja lokal. Harap perhatikan bahwa kami telah menyetel tag ke kolektor kami, "Pet Project Builder". Dengan itu, kita dapat memfilter
MailNotifier , dengan mengatakan bahwa surat harus dikirim hanya ke kolektor tertentu. Jika pemfilteran ini tidak dilakukan, maka saat membangun komit di dua lingkungan, kami akan menerima tiga huruf.
Kolektor umum factory = util.BuildFactory() factory.addStep(steps.Git( repourl = util.Property('repository'), workdir = 'sources', haltOnFailure = True, submodules = True, progress = True, mode = 'incremental') ) factory.addStep(AnalyseStep( name = 'Analyse .buildbot file', workdir = 'sources', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) ) c['builders'] = [util.BuilderConfig( name = 'Pet Project Builder', tags = ['generic_builder'], workernames = ['local'], factory = factory )]
Tetap bagi kami untuk menambah kolektor dan Penjadwal Terpicu yang sama untuk semua pekerja nyata kami:
Kolektor di lingkungan yang tepat for worker in allWorkers: c['schedulers'].append(schedulers.Triggerable( name = 'Pet Project ({}) Scheduler'.format(worker), builderNames = ['Pet Project ({}) Builder'.format(worker)]) ) c['builders'].append(util.BuilderConfig( name = 'Pet Project ({}) Builder'.format(worker), workernames = [worker], factory = specific_factory) )
(buat halaman proyek kami di dua lingkungan)Contoh Empat: satu huruf per beberapa komitmen
Jika Anda menggunakan salah satu contoh di atas, Anda dapat melihat satu fitur yang tidak menyenangkan. Karena satu huruf dibuat untuk setiap komit, ketika kami mendorong cabang dengan 20 komit baru, kami akan menerima 20 surat. Menghindari ini, seperti dalam contoh sebelumnya, kami akan membantu dua tingkat. Kita juga perlu memodifikasi kelas untuk mendapatkan perubahan. Alih-alih membuat banyak objek perubahan, kami hanya akan membuat satu objek seperti itu, di properti di mana daftar semua komit ditransmisikan. Tergesa-gesa, ini bisa dilakukan seperti ini:
Kelas MultiGitHubHandler class MultiGitHubHandler(GitHubHandler): def getChanges(self, request): new_changes = GitHubHandler.getChanges(self, request) if not new_changes: return ([], 'git') change = new_changes[-1] change['revision'] = '{}..{}'.format( new_changes[0]['revision'], new_changes[-1]['revision']) commits = [c['revision'] for c in new_changes] change['properties']['commits'] = commits return ([change], 'git') c['www']['change_hook_dialects'] = { 'base': { 'custom_class': MultiGitHubHandler } }
Untuk bekerja dengan objek perubahan yang tidak biasa, kita membutuhkan langkah khusus kita sendiri, yang secara dinamis membuat langkah-langkah yang mengumpulkan komit tertentu:
Kelas GenerateCommitSteps class GenerateCommitSteps(BuildStep): def run(self): commits = self.getProperty('commits') results = [] for commit in commits: results.append(steps.Trigger( name = 'Checking commit {}'.format(commit), schedulerNames = ['Pet Project Commits Scheduler'], waitForFinish = True, haltOnFailure = True, warnOnWarnings = True, sourceStamp = { 'branch': util.Property('branch'), 'revision': commit, 'codebase': util.Property('codebase'), 'repository': util.Property('repository'), 'project': util.Property('project') } ) ) self.build.addStepsAfterCurrentStep(results) return util.SUCCESS
Tambahkan kolektor umum kami, yang hanya terlibat dalam menjalankan rakitan komit individual. Itu harus ditandai untuk kemudian memfilter pengiriman surat dengan tag ini sendiri.
Pengambil Surat Umum c['schedulers'] = [schedulers.AnyBranchScheduler( name = 'Pet Project Branches Scheduler', treeStableTimer = None, change_filter = util.ChangeFilter(project = 'Pet Project'), builderNames = ['Pet Project Branches Builder'] )] branches_factory = util.BuildFactory() branches_factory.addStep(GenerateCommitSteps( name = 'Generate commit steps', haltOnFailure = True, hideStepIf = lambda results, s: results == util.SUCCESS) ) c['builders'] = [util.BuilderConfig( name = 'Pet Project Branches Builder', tags = ['branch_builder'], workernames = ['local'], factory = branches_factory )]
Tetap menambahkan hanya kolektor untuk komitmen individu. Kami hanya tidak memberi tag pada kolektor ini dengan tag, dan karenanya surat tidak akan dibuat untuk itu.
Pengambil Surat Umum c['schedulers'].append(schedulers.Triggerable( name = 'Pet Project Commits Scheduler', builderNames = ['Pet Project Commits Builder']) ) c['builders'].append(util.BuilderConfig( name = 'Pet Project Commits Builder', workernames = ['stretch32'], factory = specific_factory) )
Kata-kata terakhir
Artikel ini sama sekali tidak menggantikan membaca dokumentasi resmi, jadi jika Anda tertarik pada Buildbot, maka langkah Anda selanjutnya adalah membacanya. Versi lengkap dari file konfigurasi semua contoh tersedia di
github . Tautan terkait, dari mana sebagian besar bahan untuk artikel diambil:
- Dokumentasi resmi
- Kode sumber proyek