-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(Jenkinsfile): Fix jenkins pipline for CI
It appears that the current stash and unstash has some issues with asynchronous behaviour. For example an unstash occurs on a node in a working directory that is different than what is expected when running a test. It also appears that some directories are not being cleaned. The following commit makes a number of changes to fix that: - makes jenkinsfile declarative as this is better supported - run all tests on node before releasing to fix any shared workspace problems - Cleanup function names and steps to make it more readable - Add timeouts to overall process and timeout per node after it starts - Handle errors if unstash fails only stop the node - Allow a timeout/stop to exit the whole set of tests
- Loading branch information
1 parent
a60786e
commit a72ab97
Showing
1 changed file
with
113 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,64 @@ | ||
def nodes = nodesByLabel('HIL') | ||
def boards = [] | ||
def tests = [] | ||
def nodeMap = [:] | ||
|
||
def triggers = [] | ||
|
||
if ("${env.BRANCH_NAME}" == 'nightly') { | ||
// build master with latest RIOT daily at 1:00 AM | ||
triggers << parameterizedCron('0 1 * * * % HIL_RIOT_VERSION=master') | ||
pipeline { | ||
agent { label 'master' } | ||
options { | ||
// If the whole process takes more than x hours then exit | ||
// This must be longer since multiple jobs can be started but waiting on nodes to complete | ||
timeout(time: 3, unit: 'HOURS') | ||
// Failing fast allows the nodes to be interrupted as some steps can take a while | ||
parallelsAlwaysFailFast() | ||
} | ||
parameters { | ||
choice(name: 'HIL_RIOT_VERSION', choices: ['submodule', 'master', 'pull'], description: 'The RIOT branch or PR to test.') | ||
string(name: 'HIL_RIOT_PULL', defaultValue: '0', description: 'RIOT pull request number') | ||
} | ||
triggers { | ||
parameterizedCron('0 1 * * * % HIL_RIOT_VERSION=master') | ||
} | ||
stages { | ||
stage('setup') { | ||
steps { | ||
stepClone() | ||
stash name: 'sources' | ||
} | ||
} | ||
stage('node test') { | ||
steps { | ||
runParallel items: nodes.collect { "${it}" } | ||
} | ||
} | ||
stage('Notify') { | ||
steps { | ||
emailext ( | ||
body: '''${SCRIPT, template="groovy-html.template"}''', | ||
mimeType: 'text/html', | ||
subject: "${currentBuild.fullDisplayName}", | ||
from: '[email protected]', | ||
to: '${DEFAULT_RECIPIENTS}', | ||
replyTo: '${DEFAULT_RECIPIENTS}' | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
properties([ | ||
parameters([ | ||
choice(name: 'HIL_RIOT_VERSION', choices: ['submodule', 'master', 'pull'], description: 'The RIOT branch or PR to test.'), | ||
string(name: 'HIL_RIOT_PULL', defaultValue: '0', description: 'RIOT pull request number') | ||
]), | ||
pipelineTriggers(triggers) | ||
]) | ||
def runParallel(args) { | ||
parallel args.items.collectEntries { name -> [ "${name}": { | ||
|
||
node (name) { | ||
stage("${name}") { | ||
// We want to timeout a node if it doesn't respond | ||
// The timeout should only start once it is acquired | ||
timeout(time: 60, unit: 'MINUTES') { | ||
script { | ||
stepRunNodeTests() | ||
} | ||
} | ||
} | ||
} | ||
}]} | ||
} | ||
|
||
def stepClone() | ||
{ | ||
|
@@ -56,118 +98,83 @@ def stepClone() | |
} | ||
} | ||
|
||
def stepPrintEnv(board, test) | ||
def stepRunNodeTests() | ||
{ | ||
sh 'dist/tools/ci/print_environment.sh' | ||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { | ||
def tests = [] | ||
stage( "${env.BOARD} setup"){ | ||
stepPrepareNodeWorkingDir() | ||
tests = stepDiscoverTests() | ||
} | ||
for (int i=0; i < tests.size(); i++) { | ||
stage("${tests[i]}") { | ||
def timeout_stop_exc = null | ||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE', catchInterruptions: false) { | ||
stepPrintEnv() | ||
stepReset(tests[i]) | ||
stepMake(tests[i]) | ||
stepFlash(tests[i]) | ||
stepTest(tests[i]) | ||
stepArchiveTestResults(tests[i]) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
def stepPrepareWorkingDir() | ||
def stepPrepareNodeWorkingDir() | ||
{ | ||
deleteDir() | ||
unstash name: 'sources' | ||
sh 'pwd' | ||
sh 'ls -alh' | ||
} | ||
|
||
def stepReset(board, test) | ||
{ | ||
sh "python3 -m bph_pal --philip_reset" | ||
sh "make -C ${test} reset" | ||
def stepDiscoverTests() { | ||
return sh(returnStdout: true, | ||
script: """ | ||
for dir in \$(find tests -maxdepth 1 -mindepth 1 -type d); do | ||
[ -d \$dir/tests ] && { echo \$dir ; } || true | ||
done | ||
""").tokenize() | ||
} | ||
|
||
def stepFlash(board, test) | ||
def stepPrintEnv() | ||
{ | ||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { | ||
sh "make -C ${test} flash" | ||
} | ||
sh 'dist/tools/ci/print_environment.sh' | ||
} | ||
|
||
def stepTests(board, test) | ||
def stepReset(test) | ||
{ | ||
def test_name = test.replaceAll('/', '_') | ||
sh "make -C ${test} robot-clean || true" | ||
catchError(buildResult: 'UNSTABLE', stageResult: 'SUCCESS') { | ||
sh "make -C ${test} robot-test" | ||
} | ||
sh "make -C ${test} robot-html || true" | ||
|
||
archiveArtifacts artifacts: "build/robot/${board}/${test_name}/*.xml" | ||
archiveArtifacts artifacts: "build/robot/${board}/${test_name}/*.html" | ||
junit "build/robot/${board}/${test_name}/xunit.xml" | ||
} | ||
|
||
// function to return steps per board | ||
def parallelSteps (board, test) { | ||
return { | ||
node (board) { | ||
catchError() { | ||
stepPrintEnv(board, test) | ||
stepReset(board, test) | ||
stepFlash(board, test) | ||
stepTests(board, test) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// detect connected boards and available tests | ||
stage ("setup") { | ||
node ("master") { | ||
stepClone() | ||
stash name: 'sources' | ||
// discover test applications | ||
tests = sh(returnStdout: true, | ||
script: """ | ||
for dir in \$(find tests -maxdepth 1 -mindepth 1 -type d); do | ||
[ -d \$dir/tests ] && { echo \$dir ; } || true | ||
done | ||
""").tokenize() | ||
echo "run TESTS: " + tests.join(",") | ||
// discover available boards | ||
for (int i=0; i<nodes.size(); ++i) { | ||
def nodeName = nodes[i]; | ||
node (nodeName) { | ||
boards.push(env.BOARD) | ||
} | ||
} | ||
boards.unique() | ||
echo "use BOARDS: " + boards.join(",") | ||
} | ||
sh "python3 -m bph_pal --philip_reset" | ||
sh "make -C ${test} reset" | ||
} | ||
|
||
for (int i=0; i<nodes.size(); ++i) { | ||
def nodeName = nodes[i]; | ||
nodeMap[nodeName] = { | ||
node { | ||
stepPrepareWorkingDir() | ||
} | ||
} | ||
def stepMake(test) | ||
{ | ||
sh "make -C ${test}" | ||
} | ||
|
||
stage ("worker setup") { | ||
parallel (nodeMap) | ||
def stepFlash(test) | ||
{ | ||
sh "make -C ${test} flash-only" | ||
} | ||
|
||
// create a stage per test with one step per board | ||
for(int i=0; i < tests.size(); i++) { | ||
test = tests[i].trim() | ||
stage(test) { | ||
parallel ( | ||
boards.collectEntries { | ||
["${it}" : parallelSteps(it, test)] | ||
} | ||
) | ||
def stepTest(test) | ||
{ | ||
def test_name = test.replaceAll('/', '_') | ||
sh "make -C ${test} robot-clean || true" | ||
// We don't want to stop running other tests since the robot-test is allowed to fail | ||
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE', catchInterruptions: false) { | ||
sh "make -C ${test} robot-test" | ||
} | ||
} | ||
|
||
stage('Notify') { | ||
node("master") { | ||
def jobName = currentBuild.fullDisplayName | ||
emailext ( | ||
body: '''${SCRIPT, template="groovy-html.template"}''', | ||
mimeType: 'text/html', | ||
subject: "${jobName}", | ||
from: '[email protected]', | ||
to: '${DEFAULT_RECIPIENTS}', | ||
replyTo: '${DEFAULT_RECIPIENTS}' | ||
) | ||
} | ||
def stepArchiveTestResults(test) | ||
{ | ||
def test_name = test.replaceAll('/', '_') | ||
sh "make -C ${test} robot-html || true" | ||
archiveArtifacts artifacts: "build/robot/${env.BOARD}/${test_name}/*.xml" | ||
archiveArtifacts artifacts: "build/robot/${env.BOARD}/${test_name}/*.html" | ||
junit "build/robot/${env.BOARD}/${test_name}/xunit.xml" | ||
} |