tutorial
Pipline & jenkinsfile 手册:https://www.jenkins.io/doc/book/pipeline/getting-started/
提供的全局变量
env.:<Jenkins master的地址>/pipeline-syntax/globals#envpipline 实例:https://www.jenkins.io/doc/pipeline/examples/
尽可能应用Jenkins提供的命令,而不是一股脑儿使用shell脚本
比如,git clone,使用:
checkout scmGit(branches: [[name: params.BRANCH]],
extensions: [],
userRemoteConfigs: [[credentialsId: params.GITHUB_CREDENTIAL, url: 'https://github.com/xxx.git']])
jenkins是面向过程的,对于任务配置,多考虑使用表驱动
Pipline 中访问 env 变量
stage(){
println "WORKSPACE: ${env.WORKSPACE}"
echo env.WORKSPACE
echo WORKSPACE
}
环境变量可以通过 Groovy 代码访问,方式为 env.VARNAME 或者直接使用 VARNAME。你也可以修改这些属性,但只能通过使用 env. 前缀来写入。所以:
- jenkins job 中有保留的 env 变量,避免疑惑这些变量要加上
env.。 - env 变量即使在机器上设定了,也是可以修改的。
groovy中的数学计算
def number = 243 // 要计算的数
def fifthRoot = number ** (1.0/5) // 计算5次方根
两种pipline
在Jenkins中,有两种主要类型的Pipeline:Scripted Pipeline 和 Declarative Pipeline。
- Scripted Pipeline【我的工作中都是这种的脚本】:
- 使用Groovy语法编写,允许更灵活的流程控制和自定义逻辑。
- 通过node和stage等关键字来定义流水线的执行节点和阶段。
- 可以直接编写Groovy脚本来构建流水线。
- Declarative Pipeline:
- 使用更结构化的语法,更易于阅读和维护。
- 通过pipeline、agent、stages等关键字来定义流水线的结构和执行环境。
- 提供了更丰富的语法来定义构建、部署和测试等阶段。
jenkins 中如何在多个 NODE 并行执行任务
def parallel_tasks = [:]
if (params.GPU_TASK) {
parallel_tasks["GPU_TASK"] = {
node(params.GPU_NODE) {
stage(){}
...
}
}
}
if (params.CPU_TASK) {
parallel_tasks["CPU_TASK"] = {
node(params.CPU_NODE) {
stage(){}
...
}
}
}
parallel(parallel_tasks)
Elvis 操作符 ?:
这两句句话有什么不同:
Apple = Banana ?: ""
Apple = Banana ? Banana : ""
两者操作符不同,?: 是Elvis操作符,表示如果Banana不是null或空,则将其值赋给Apple,否则Apple将会是空字符串。两者效果相同。
关于proxy
jenkins服务中写死的 proxy 会影响 groovy 的函数,但是在sh中的执行 你依然可以使用自己的proxy。
使用Groovy语法创建并写入Python脚本
// Python脚本的内容
def pythonScriptContent = '''
print("Hello from Jenkins Pipeline!")
# Add more Python code here
'''
// 将内容写入Python脚本文件
writeFile file: 'script.py', text: pythonScriptContent
// 执行Python脚本
sh 'python script.py'
def createPythonScript() {
def scriptName = sh(script: 'echo \'print("Hello, world!")\' > example.py', returnStdout: true).trim()
return scriptName
}
node {
stage('Create Python Script') {
def scriptName = createPythonScript()
echo "Created Python script: ${scriptName}"
}
stage('Other Stage') {
def scriptName = createPythonScript()
// 在这里可以使用创建的Python脚本
}
}
jenkins 文件归档
如果在Jenkins上使用archiveArtifacts将生成的文件归档,即使在归档后删除了该文件,你仍然可以从Jenkins上访问该log文件,这是因为Jenkins将文件存档在特定的位置,而不是直接引用原始文件。当您使用 archiveArtifacts 步骤归档文件时,Jenkins将文件复制到其构建存档目录中,通常是Jenkins服务器上的特定位置。
通过页面的"Artifacts"或类似的部分,通常会提供一个链接或按钮,使您可以直接下载或查看归档的文件。注意,归档的文件会占用磁盘空间,因此建议在需要时删除归档的文件,以便释放磁盘空间。您可以在Jenkins的配置中调整构建存档的策略和保留期限,以满足您的需求。
csv += ... // 构建CSV文件内容
writeFile file: "result.csv", text: csv
archiveArtifacts 'result.csv'
writeFile file: 'run_ut.sh', text: '''
#!/bin/bash
set -ex
export CI_CACHE=/ci_cache
export SC_HOME=/AI
export CI_HOME=/AI/3rdparty/AI-ci
bash $CI_HOME/run_gcc_latest.sh
'''
withCredentials([gitUsernamePassword(credentialsId: params.GITHUB_CREDENTIAL, gitToolName: 'Default')]) {
sh "bash scripts/ci/checkout.sh"
}
...
sh """ sh ./run_ut.sh """ // 上文创建文件,这里执行
访问 已经归档的 文件,访问已有的 job 已有的 build
copyArtifacts filter: '*',
fingerprintArtifacts: true,
projectName: 'Triton_Release',
selector: specific(env.IPEX_BUILD_NUMBER)
import hudson.model.*
import jenkins.model.*
def jobName = "my-job" // 作业名称
def buildNumber = 42 // 构建号
def job = Jenkins.instance.getItem(jobName)
def build = job.getBuildByNumber(buildNumber)
if (build != null) {
def artifacts = build.artifacts
artifacts.each { artifact ->
def fileName = artifact.fileName
def relativePath = artifact.relativePath
println("File: $fileName, Path: $relativePath")
}
} else {
println("Build $buildNumber not found for job $jobName.")
}
groovy中使用 .each 循环 遍历一个 map,在遍历中进行 if 判断
data.each { model, modes ->
println(model + ":")
modes.each { mode, types ->
println("\t" + mode + ":")
types.each { type ->
if (type == 'int8') {
println("\t\t" + type + " (optimized)")
} else {
println("\t\t" + type)
}
}
}
}
publich HTML report
dir('result') {
copyArtifacts filter: '*_perf.log', fingerprintArtifacts: true,
projectName: 'Complex_fusion',
selector: specific(env.RESULT_NUMBER)
sh "touch conv_block.log mha.log mlp.log misc.log"
}
dir('baseline') {
copyArtifacts filter: '*_perf.log', fingerprintArtifacts: true,
projectName: 'Complex_fusion',
selector: specific(env.BASELINE_NUMBER)
sh "touch conv_block.log mha.log mlp.log misc.log"
}
// generate report for each model
for (workload in ["conv_block", "mha", "mlp", "misc"]) {
def accumulate_min = 1;
def count_min = 0;
def accumulate_avg = 1;
def count_avg = 0;
def summary = "<html><body>";
summary += "<link rel='stylesheet' href='perf_test.css'>";
summary += "<table border='1'>\n";
summary += "<tr>" +
"<td class='cases'>Case</td>" +
"<td class='column'>Result Min</td>" +
"<td class='column'>Result Avg</td>" +
"<td class='column'>Baseline Min</td>" +
"<td class='column'>Baseline Avg</td>" +
"<td class='column'>Ratio Min</td>" +
"<td class='column'>Ratio Avg</td>" +
"</tr>\n";
// make sure the log file exists
sh "touch result/${workload}.log baseline/${workload}.log"
def res = parse_result("result/${workload}.log");
def base = parse_result("baseline/${workload}.log");
// loop each entry in the result
// line [case,min,avg] => result {case: [min,avg]}
for (entry in res) {
def case_name = entry.key;
def perf_min = entry.value[0];
def perf_avg = entry.value[1];
def base_perf_min = 0.0;
def base_perf_avg = 0.0;
// find the base
if (base.containsKey(case_name)) {
base_perf_min = base[case_name][0];
base_perf_avg = base[case_name][1];
}
def ratio_min = "/";
def css_class_min = "na";
if (base_perf_min != 0) {
def res0 = compare(perf_min, base_perf_min, ratio_min);
css_class_min = res0[0]
ratio_min = res0[1]
}
if (ratio_min != "/" && ratio_min != "0.0") {
accumulate_min *= ratio_min.toDouble();
count_min += 1;
}
def ratio_avg = "/";
def css_class_avg = "na";
if (base_perf_avg != 0) {
def res1 = compare(perf_avg, base_perf_avg, ratio_avg);
css_class_avg = res1[0]
ratio_avg = res1[1]
}
if (ratio_avg != "/" && ratio_avg != "0.0") {
accumulate_avg *= ratio_avg.toDouble();
count_avg += 1;
}
summary += "<tr>" +
"<td>${case_name}</td>" +
"<td>${perf_min}</td>" +
"<td>${perf_avg}</td>" +
"<td class='value'>${base_perf_min}</td>" +
"<td class='value'>${base_perf_avg}</td>" +
"<td class='value ${css_class_min}'>${ratio_min}</td>" +
"<td class='value ${css_class_avg}'>${ratio_avg}</td>" +
"</tr>\n";
}
def geomean_min
def geomean_avg
if (count_min > 0) {geomean_min = accumulate_min ** (1.0/count_min);}
if (count_avg > 0) {geomean_avg = accumulate_avg ** (1.0/count_avg);}
def css_geo_min = up_or_down(geomean_min)
def css_geo_avg = up_or_down(geomean_avg)
summary += "<tr>" +
"<td class='geomean'>Geomean</td>" +
"<td class='column'></td>" +
"<td class='column'></td>" +
"<td class='column'></td>" +
"<td class='column'></td>" +
"<td class='column ${css_geo_min}'>${geomean_min}</td>" +
"<td class='column ${css_geo_avg}'>${geomean_avg}</td>" +
"</tr>\n";
summary += "</table>\n";
summary += "</body></html>\n";
dir('summary') {
// 生成两个文件,html(内容)和css(格式),
writeFile file: "perf_test.css", text: "" +
".upgrade {background: lightgreen;}\n" +
".downgrade {background: pink;}\n" +
".na {background: lightgrey;}\n" +
".value {text-align: right;}\n" +
".cases {width: 500px;}\n" +
".geomean {border: #333333; background: rgb(172, 206, 240);}" +
".column {width: 100px;}\n";
writeFile file: "${workload}.html", text: summary
}
}
publishHTML([allowMissing: true,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'summary',
reportFiles: 'conv_perf.html,mha.html,mlp.html,misc.html',
reportName: 'PERF Report',
reportTitles: '',
useWrapperFileDirectly: true])
Jenkins 脚本一般结构
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
node(params.NODE) {
def conda_env = pwd() + "/conda_llm";
withEnv([
'HTTP_PROXY=http://xxx.com:912',
'HTTPS_PROXY=http://xxx.com:912',
"CMAKE_PREFIX_PATH=${conda_env}",
"MALLOC_CONF=oversize_threshold:1,background_thread:true,metadata_thp:auto,dirty_decay_ms:9000000000,muzzy_decay_ms:9000000000",
"_CONSTANT_CACHE=1",
"_GRAPH_CONSTANT_TENSOR_CACHE_CAPACITY=cpu:-1",
"_DISABLE_COMPILER_BACKEND=${params.DISABLE_COMPILER}",
"VERBOSE=${param.VERBOSE}"
]) {
stage('setup & run') {
deleteDir();
dir('llm') {
checkout scmGit(branches: [[name: params.BRANCH]],
extensions: [],
userRemoteConfigs: [[credentialsId: params.GITHUB_CREDENTIAL, url: 'https://github.com/xxx.git']])
copyArtifacts filter: '*',
fingerprintArtifacts: true,
projectName: 'YYY_Release',
selector: specific(env.IPEX_BUILD_NUMBER)
retry(5) { sh "conda create --prefix ${conda_env} --file requirements.txt -c conda-forge python=3.10 -y" }
retry(5) { sh "conda install --prefix ${conda_env} jemalloc -y" }
dir('examples/cpu/inference/python/llm'){
sh """
bash ./tools/env_setup.sh 7
source ./tools/env_activate.sh
source activate ${conda_env}
python run.py
"""
}
}
sh """
source activate ${conda_env}
...
pip install ${params.PYTORCH_URL} --no-deps
pip install *.whl --no-deps
pip install -r requirements.txt
"""
}
withEnv([
"MALLOC_CONF=oversize_threshold:1,background_thread:true,metadata_thp:auto,dirty_decay_ms:9000000000,muzzy_decay_ms:9000000000",
"_CONSTANT_CACHE=1",
"GRAPH_CONSTANT_TENSOR_CACHE_CAPACITY=cpu:-1",
"_DISABLE_COMPILER_BACKEND=${params.DISABLE_COMPILER}",
"VERBOSE=${param.VERBOSE}"
]) {
stage('run llm') {
dir('vision') {
checkout scmGit(branches: [[name: 'master']],
extensions: [],
userRemoteConfigs: [[credentialsId: params.GITHUB_CREDENTIAL, url: 'https://github.com/xxx.git']])
sh """
source activate ${conda_env}
...
"""
for (dt in ["int8_ipex"]) {
sh """
source activate ${conda_env}
bash run_test.sh all ${dt} ${params.mode} int8_ipex
"""
}
sh """
cp logs/summary.log ./vision.log
"""
archiveArtifacts 'vision.log'
}
}
}
}
}
坑:shell的带特殊符号的环境变量
withEnv(["CACHE_CAPACITY=cpu:10240;gpu:20480",
"CACHE_CAPACITY_2='cpu:10240;gpu:20480'",
"CACHE_CAPACITY_3=\"cpu:10240;gpu:20480\""]){
echo env.CACHE_CAPACITY; // cpu:10240;gpu:20480 v
echo env.CACHE_CAPACITY_2; // 'cpu:10240;gpu:20480' x
echo env.CACHE_CAPACITY_3; // "cpu:10240;gpu:20480" x
sh"""
export CACHE_CAPACITY_4="cpu:10240;gpu:20480"
echo \${CACHE_CAPACITY} ## cpu:10240;gpu:20480 v
echo \${CACHE_CAPACITY_2} ## 'cpu:10240;gpu:20480' x
echo \${CACHE_CAPACITY_3} ## "cpu:10240;gpu:20480" x
echo \${CACHE_CAPACITY_4} ## cpu:10240;gpu:20480 v
"""
}
withEnv 中变量的值,不加双引号!
坑:jenkins 中的用户访问不到 GPU
在机器上给用户访问权限后,直接在机器上是可以访问,但是jenkins的pipline中相同的用户还是不能访问。
答:jenkins系统上需要重启机器,后就可以了 jenkins上的agent其实是一直运行 的,可以理解成jenkins系统中这个shell从来没有变过,重启才生效
坑:’’ vs ""
在Groovy中,’’ 和 "" 都表示空字符串,它们在功能上是等价的。因此,在my_list.add('')和my_list.add("")这两行代码中,它们的作用是相同的,都是向my_list中添加一个空字符串。 Groovy允许使用单引号或双引号来表示字符串,这两种表示方法在大多数情况下是可以互换使用的。
坑:withEnv()的参数是一个List,其内容可以是空但必须要给一个List 对象
jenkins 报错:hudson.remoting.ProxyException: java.lang.NullPointerException: Cannot invoke “java.util.List.iterator()” because “overrides” is null
坑:withEnv([])
jenkins 报错:process apparently never started in … 答:检查withEnv中的环境变量是否符合预期。注意要在sh中检查环境变量的值,最好将所有的 env 输出到文件并且archive起来,便于检查
坑:shell 的 pid
不同的 sh """ “”", 之间的 pid 不相同,不共用环境,包括conda环境。如果要公用,则需要通过 withEnv添加
坑:sh """ vd sh ’''
sh """ 中的变量要是 jenkins 脚本的变量, 先获取变量值,替换后,再执行shell sh ’’’ 中变量是 shell 脚本自身的变量, 直接执行shell
坑:案例分析
sh (script: """
source ~/miniconda3/etc/profile.d/conda.sh
if [ "\$(conda info --env | grep ${CONDA_ENV} | wc -l )" == "0" ];then
conda create -n ${CONDA_ENV} python=3.10 openpyxl csv -y
fi
conda activate ${CONDA_ENV}
bash ${WORKSPACE}/tools/bdnn/jenkins_utils/collect_result.sh ${cfg[2]}_perf.log perf.log
python ${WORKSPACE}/tools/bdnn/jenkins_utils/to_excel.py perf.log ${cfg[2]}_perf.xlsx
""", returnStdout: true)
if 条件中 "\$(conda info --env..." 的第一个$, 需要转义,告诉 jenkins 不要替换 $后的内容,其他jenkins变量替换之后,这个$作为shell脚本的内容执行。
坑:访问函数参数 sh""" vs sh’’’ 两者区别
def Select_Model(direction, dtype) {
println("======inside of Select()=====>" + direction)
sh'''
echo "------ > $direction"
'''
}
上面函数中变量direction 为什么在sh中访问不到,echo得到是空?
改为一下就可以:
def Select_Model(direction, dtype) {
println("======inside of Select()=====>" + direction)
sh"""
echo "------ > ${direction}" // allows for string interpolation, 先获取direction值,后执行shell
"""
}
因为 sh""" 里通过 $ 来获取groovy的变量,如上例,$direction 获取函数参数的值替换后,执行shell。而sh''' 里边不能获取groovy变量的值,只能通过 withEnv 的方式间接获取,所以如果要使用 sh''' 那么应改为:
def Select_Model(direction, dtype) {
withEnv(["Direction=${direction}"]) {
println("======inside of Select()=====>" + direction)
sh'''
echo "------ > ${Direction}" // 不会string interpolation, 原样输出,后执行shell,此时Direction已经是个环境变量了
'''
}
}
坑:dir ()
在Jenkins中,dir('${MY}')[错误] 和 dir("${MY}")[正确] 之间的主要区别在于引号的类型。单引号和双引号在Groovy中有不同的用途:
dir('${MY}') 使用了单引号,这意味着${MY}不会被解释为变量,而会被视为普通的字符串。这将导致Jenkins尝试切换到一个名为 ${MY} 的目录,而不是切换到环境变量 MY 的值所表示的目录。因此,这种写法通常不会达到预期的效果,除非您确实有一个名为${MY}的目录。
dir("${MY}") 使用了双引号,这意味着${MY}会被解释为变量,并用其值替换。如果环境变量 MY 的值是一个有效的目录路径,那么Jenkins会尝试切换到这个目录。
所以,一般情况下,如果您希望在dir命令中引用一个环境变量的值作为目录路径,应该使用双引号,如dir("${MY}")。这将使Jenkins正确地解释${MY}作为变量,并使用它的值作为目录路径
所以,Jenkins中没有特殊理由,都是用双引号 """ 。
坑:方法不可用
默认情况下,许多敏感方法是被禁止的,以确保Jenkins的安全性。当您在Jenkins中执行Groovy脚本时,可能会遇到安全限制,例如 “Scripts not permitted to use method” 的错误消息。这是由于Jenkins的脚本安全性设置所引起的。
Administer Jenkins 权限的用户可以配置 Jenkins 的脚本安全性设置,以允许或拒绝特定方法的使用。