Сразу стоит отметить, что я не нашел в документации к Jenkins утверждения, что Jenkinsfile
пишется на Groovy.
В документации к Jenkins сказано:
Scripted Pipeline is a domain-specific language [3] based on Groovy, most Groovy syntax can be used in Scripted Pipeline without modification.
Но количество отсылок к Groovy столь велико, что у многих людей создаются ложные ожидания.
Я решил написать этот пост после многократного объяснения коллегам отличий скрипта Jenkinsfile
от Groovy.
Также важно отметить, что всё примеры проверялись на версии 2.414.3 (самый свежий LTS на момент написания статьи) и, возможно, ситуация изменится.
А что, собственно, не так?
Если оно выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка.
Несмотря на то, что код Jenkinsfile
компилируется при помощи Groovy-компилятора, в дальнейшем он подвергается дополнительной обработке и перестаёт вести себя как Groovy-код.
То есть он выглядит как Groovy, компилируется как Groovy, но не ведёт себя как Groovy.
И речь не о том, что он выполняется в песочнице и часть библиотек не доступна. Всё гораздо хуже: некоторые базовые конструкции тихо меняют своё поведение.
Отличие поведения циклов
Все мы знаем, что Linux великолепен… Он выполняет бесконечные циклы за 5 секунд.
Linus Torvalds
В Jenkins тело обычного цикла for
без условий (классический бесконечный цикл) не выполняется ни разу.
Пример проблемного Jenkinsfile
(можно также выполнить в https://groovyide.com/playground):
def messages = []
messages += "Begin"
for (; ;) {
messages += "Inside classic loop"
break
}
messages += "End"
// Show result with little hack to use same code in groovy and Jenkinsfile
def echo = "echo" in this ? echo : { it -> println(it) }
echo(messages.join('\\n'))
Вывод в https://groovyide.com/playground:
Begin
Inside classic loop
End
Вывод в Jenkins:
[Pipeline] Start of Pipeline
[Pipeline] echo
Begin
End
[Pipeline] End of Pipeline
Нарушенная спецификация:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14-110
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/for.html
Странное поведение вызова методов
В Jenkins по-другому работает вызов методов в стиле обращения к свойствам объекта.
Пример проблемного Jenkinsfile
(можно также выполнить в https://groovyide.com/playground):
def messages = []
messages += "Begin"
// List 1
if ([]) {
messages += "List 1 is not empty (WTF)"
} else {
messages += "List 1 is empty (OK)"
}
// List 2
if (["a"]) {
messages += "List 2 is not empty (OK)"
} else {
messages += "List 2 is empty (WTF)"
}
// List 3
if ([].isEmpty()) {
messages += "List 3 is empty (OK)"
} else {
messages += "List 3 is not empty (WTF)"
}
// List 4
if (["a"].isEmpty()) {
messages += "List 4 is empty (WTF)"
} else {
messages += "List 4 is not empty (OK)"
}
// List 5
if ([].empty) {
messages += "List 5 is empty (OK)"
} else {
messages += "List 5 is not empty (WTF)"
}
// List 6
if (["a"].empty) {
messages += "List 6 is empty (WTF)"
} else {
messages += "List 6 is not empty (OK)"
}
messages += "End"
// Show result with little hack to use same code in groovy and Jenkinsfile
def echo = "echo" in this ? echo : { it -> println(it) }
echo(messages.join('\\n'))
Вывод в https://groovyide.com/playground:
Begin
List 1 is empty (OK)
List 2 is not empty (OK)
List 3 is empty (OK)
List 4 is not empty (OK)
List 5 is empty (OK)
List 6 is not empty (OK)
End
Вывод в Jenkins:
[Pipeline] Start of Pipeline
[Pipeline] echo
Begin
List 1 is empty (OK)
List 2 is not empty (OK)
List 3 is empty (OK)
List 4 is not empty (OK)
List 5 is not empty (WTF)
List 6 is empty (WTF)
End
[Pipeline] End of Pipeline
Нарушенная спецификация:
Пара слов о данных примерах
После данных примеров особо хотел бы отметить:
Это далеко не исчерпывающий список проблем. В реальности всё гораздо хуже, просто эти два примера очень легко воспроизвести.
Код в случае подобных проблем меняет своё поведение тихо, а не выплёвывает ошибку. Это добавляет отдельной остроты в поиске проблем.
Очень тяжело писать код, когда ни в какой строке нельзя быть уверенным.
Причем это касается не только самого Jenkinsfile
, но и кода в Jenkins Shared Libraries. Хотя с Jenkins Shared Library немного проще – там можно писать код который не обладает подобными эффектами.
Как им удалось этого добиться?
Причина изменения поведения кода – Continuation Passing Style (CPS) преобразование.
Jenkins преобразовывает уже скомпилированный в байт-код скрипт к виду, когда может выполнять его по шагам сохраняя внутренне состояние отдельно. На этапе этого преобразования некоторые конструкции меняют своё поведение.
Как жить?
Лучше всего избегать сложной логики в Jenkinsfile
, но это не всегда возможно.
К счастью, есть проект JenkinsPipelineUnit, который позволяет из Unit-тестов на настоящем Groovy выполнять код после CPS-преобразования.
Этот проект позволяет писать тесты на код, выполняемый в скриптах Jenkins, но я так и не смог найти красивое решение по организации тестируемого кода в Jenkinfile
.
Общий механизм написания тестируемого кода у меня получился примерно следующий:
весь тестируемый код оформляется в Jenkins Shared Libraries
на тестируемый код пишутся тесты с помощью JenkinsPipelineUnit
при использования этого кода в
Jenkinsfile
, для подключения Jenkins Shared Library из того же репозитория использую метод library
Об использовании Jenkins Shared Libraries в том же репозитории много написано на StackOverflow: https://stackoverflow.com/questions/46213913/load-jenkins-pipeline-shared-library-from-same-repository
Суть проблемы в том, что через @Library
нельзя сослаться на тот же коммит того же репозитория.
В результате приходится загружать библиотеку динамически кодом вида:
def lib = library(identifier: "local@latest", retriever: legacySCM(scm)).com.mycorp.pipeline
lib.Utils.someStaticMethod()
Если собрать всё это вместе, то оно работает, но результат выглядит так себе:
в
Jenkinsfile
можно обращаться к статическим методам, но нельзя сослаться на типы;код Jenkins Shared Libraries лежит вперемешку с основным кодом репозитория.
Посмотреть репозиторий с запусков тестов для Jenkins-скриптов можно здесь: https://github.com/bozaro/jenkins-testing
В целом, каждый раз, когда речь заходит о Jenkins, я вспоминаю цитату из твита: «Jenkins is currently the CI gold standard and it’s a very low bar».
Комментарии (15)
dude_sam
15.11.2023 09:49Если использовать Declarative Pipeline, то Jenkinsfile может выглядеть вообще просто.
Screenshot
Bozaro Автор
15.11.2023 09:49Если сборка совсем типовая, то всю эту сложность можно попытаться спрятать, но что-то мне подсказывает, что под капотом
standardDeclarativePipelineTemplate
творится жесть.Ну и декларативный pipeline в Jenkins то еще поделие: это де-факто инструкция императивного pipeline, то есть всё равно нельзя сказать, что сделает pipeline не выполнив его.
Из-за этого, в частности:
вечно поломанная визуализация сборки;
объявления параметров и опций в pipeline влияет на следующую, а не на текущую сборку.
sshikov
15.11.2023 09:49+1Хм. Ну я вам так скажу - у нас ради безопасности запретили вызов стольких методов и стольких классов, что самый простой метод что-то сделать в Jenkins - это выкачать из репозитория проект, и собрать его. Это может быть maven проект (и там GMAvenPlus на том же груви), или gradle проект, или любой другой проект на чем вам удобно. И вот проектам - им уже практически все можно. Т.е. вся логика переносится в сборку. А в Jenkins остается минимум, упрощающий настройки на скажем окружение (разработка или пром).
И вообще - Jenkins конечно (или скорее всего) самый навороченный из похожих инструментов, но он был написан очень давно, его тогда звали Hudson, и он тащит за собой столько легаси кода, что скажем писать под него плагины - да ну его нафиг...
Evengard
15.11.2023 09:49Там конечно занимательные хитросплетения кода, но я бы не сказал что всё отвратительно. Тот же Гитлаб если была бы задача расширять, так же как это позволяет Женкинс, можно совсем закопаться и не вылезти, имхо.
sshikov
15.11.2023 09:49Не, ну разумеется плагины писать можно - их же написали множество (я давно не считал, может тысячи). Но по сравнению с условным эклипсом, где модель для плагинов OSGI (и где все тоже объективно сложно), писать плагины для дженкинса то еще сомнительное удовольствие.
Bozaro Автор
15.11.2023 09:49Проблему безопасности можно частично решить через вынос общих запчастей в отдельную "доверенную" Jenkins Shared Library. Тогда на её код в
src
не будут распространяться ограничения песочницы, хотя наvars
- всё ещё будут.На счет плагинов я с Вами согланен: в Jenkins безумное количество legacy и очень разное качество кода от плагина к плагину.
Ну и отдельная проблема в том, что там модульность доведена до абсурда: обычно функционал затрагивает сразу несколько плагинов и не всегда очевидно, какой именно за какую часть фичи отвечает.
rusik2293
15.11.2023 09:49Я больше удивлен что Джум ещё жив и даже есть на хабре
truthseeker
15.11.2023 09:49И не только жив, а ещё и популярен в некоторых странах бывшего СССР, где не очень налажена прямая работа с Китаем. К примеру, он популярен в странах Балтии, в Молдове, и некоторых соседних странах Восточной Европы.
Aquahawk
Имея опыт нескольких лет работы с Jenkins, TeamCity и меньше с GitLab CI, всё что я могу сказать, это то, что все эти CI системы крайне слабо развиты и Jenkins выглядит самым гибким, но да, глюков хватает. Если я буду делать новый CI на новом проекте, то я скорее опять выберу Jenkins, в котором будет такой груви пайплайн, в котором написан только список реп которые нужно вотчить и чекаутить и потом вызов моего скрипта который сделает всю работу.
slonpts
Тоже работал с разными CI, и писал общий Jenkins pipeline, который умел делать все со всеми десятками билдов, что были нужны.
И да, на Jenkins можно написать что угодно (с разными плагинами), и понятно, как решать проблемы - это же просто код на Groovy (ну, почти Groovy)
То с другими - вот у меня происходит проблема в каком-то странном месте. И я даже нашел CLI команду, которая бы делала, как мне надо. Но как ее вставить в нужное место этого стильного модного CI? Особенно если он пытается быть декларативным
Bozaro Автор
Когда речь заходит про CI, я всё время вспоминаю цитату из Симпсонов:
> Check out The Willie World News! I reviewed the new tractors! They're all shite!
Выбор CI выглядит как поиск наименее плохого варианта :(
lorc
Для мелких проектов мне прекрасно зашел buildbot. Там конфиг - это тупо скрипт на пайтоне. Можно делать все что угодно.
Bozaro Автор
Для совсем мелких проектов подходит почти всё, что угодно.
Проблемы начинаются когда увеличивается количество хотелок:
Хочется, чтобы в выводе сборки можно было узнать, какие тесты попадали и как?
Хочется иметь хоть какую-то статистику по тестам (тут с ностальгией вспоминаю TeamCity);
Хочется иметь разбивку по этам сборки, чтобы не нужно было искать ошибку в одном гигантском логе;
Хочется иметь нотификации авторам о падении тестов;
Хочется иметь интеграцию с системой контроля версий;
Хочется иметь возможность как-то управлять секретами;
Хочется иметь возможность делать цепочки сборок;
И т.д. и т.п.
При этом основные проблемы визуализации возникают в негативном сценарии: пока всё работает, оно никому не интересно. А вот возможность по выводу понять, что и где пошло не так, очень полезна.
lorc
Ну многое из этого билдбот поддерживает. Это ж все-таки CI, а не обертка над скриптом build.sh.
Вообще, у меня есть два протировечивых требования к CI: хочется иметь декларативное описание билдов и хочется иметь возможность создавать очень гибкие билды, ибо у нас тут embedded со своими приколами.