/* * This is a generic jenkinsfile to build Asterisk and optionally * perform one or more of the following: * * Publish the API docs to the wiki * * Run the Unit tests * * Run Testsuite Tests * * This job can be triggered manually from Jenkins or be triggered * automatically on a schedule based on a cron string. * * To use this jenkinsfile, create a new "Multi-Branch Pipeline" job * in Jenkins. For easier configuration, the job name should contain * only letters, numbers, or the "-", "_" and "." special characters. * Use the "by Jenkinsfile" "Build Configuration" mode and specify * the path to this jenkinsfile. * * When you save this job definition, Jenkins will scan the git * repository and find any branches with this Jenkinsfile and then try * run the job. It's expected that the jobs will fail because you * haven't create the config file yet. * * The job is configured from a Jenkins managed config file named * "jobConfig". These files are created using the "Config Files" * option of the base job and are unique to a job so you can create * multiple jobs based on this Jenkinsfile without conflicts. * * Create the file as a "Json file" remembering to change the ID * from the auto-generated UUID to "jobConfig". * * Example contents: * { * cronString: 'H H(0-4) * * *', * jobTimeout: { * timeout: 2, * units: 'HOURS', * }, * jobCleanup: { * keepBuilds: 5, * artifactKeepBuilds: 2 * }, * throttleCategories: [ * 'default' * ], * docker: [ * images: [ * 'asterisk/jenkins-agent-centos7' * ] * ], * buildAsterisk: [ * build: true, * env: [ * REF_DEBUG: true * ] * ], * unitTests: [ * run: true, * testCommand: 'test execute all' * ] * } * * NOTE: The JSON file can actually reference variables from the * environment using string interpolation. For example, if you * need to substitute the current branch in a value for some reason, * you could use: * mybranch: "${BRANCH}" */ /* * All jobConfig parameters have defaults BUT if left that way, * only an Asterisk build will be done. * * NOTE: Groovy syntax uses brackets "[]" for both arrays and * maps/dictionaries where JSON uses brackets "[]" for arrays but * braces "{}" for maps/dictionaries. Your jobConfig file is JSON * but the defaults below are Groovy. */ def jobConfig = [ /* Must match a label assigned to agents. */ agentLabel: 'swdev-docker', /* * https://jenkins.io/doc/book/pipeline/syntax/#cron-syntax * If empty, job will not be scheduled and must be triggered manually. */ cronString: '', /* * An array of strings that name categories defined in Jenkins * Global Settings under "Throttle Concurrent Builds". If you * specify one or more categories, they MUST have been defined * or the job will fail. */ throttleCategories: [ ], jobTimeout: [ /* How long should the job be allowed to run? */ timeout: 120, /* Common valid units are "MINUTES", "HOURS", "DAYS". */ units: 'MINUTES' ], jobCleanup: [ /* The total number of past jobs to keep. */ keepBuilds: 14, /* But only this number will have their artifacts saved. */ artifactKeepBuilds: 7, /* Clean up the workspace on the agent when the job completes. */ cleanupWorkspace: true ], docker: [ /* The host and port of our Docker image registry. */ registry: 'swdev-docker0:5000', /* * An array of images that can be used for this job. * One will be chosen from the list at random. */ images: [ 'asterisk/jenkins-agent-centos7' ], ], buildAsterisk: [ /* Build Asterisk */ build: true, /* Additional envuronment variables to pass to buildAsterisk.sh */ env: [ ] ], unitTests: [ /* Run the Asterisk Unit Tests. */ run: false, /* The Asterisk CLI command to run the tests. */ testCommand: 'test execute all' ], wikiDocs: [ /* Build and publish the wiki documentation? */ publish: false, /* The URL to the "publish-docs" repository */ gitURL: "https://gerrit.asterisk.org/publish-docs", /* * Only for branches that match the regex. * I.E. Only the base branches excluding master. */ branchRegex: '^([0-9]+)$' ], testsuite: [ /* Run the Testsuite? */ run: false, /* The URL to the "testsuite" repository */ gitURL: "https://gerrit.asterisk.org/testsuite", /* * The name of the testsuite config file. * See the "Testsuite" stage below for more info. */ configFile: 'testsuiteConfig', ] ] /* * The easiest way to process the above defaults is to merge the * values from the jobConfig file over the defaults map. Groovy * provides a standard way to do this but it's not a deep operation * so we provide our own deep merge function. */ Map merge(Map onto, Map... overrides) { if (!overrides) return onto else if (overrides.length == 1) { overrides[0]?.each { k, v -> if (v instanceof Map && onto[k] instanceof Map) merge((Map) onto[k], (Map) v) else onto[k] = v } return onto } return overrides.inject(onto, { acc, override -> merge(acc, override ?: [:]) }) } /* * The job setup steps such as reading the config file and merging the * defaults can be done on the "built-in" node before we send the job off * to an agent. */ node('built-in') { def tempJobConfig configFileProvider([configFile(fileId: 'jobConfig', replaceTokens: true, variable: 'JOB_CONFIG_FILE')]) { echo "Retrieved jobConfig file from ${env.JOB_CONFIG_FILE}" tempJobConfig = readJSON file: env.JOB_CONFIG_FILE } script { merge(jobConfig, tempJobConfig) echo jobConfig.toString() causeClasses = currentBuild.getBuildCauses() causeClass = causeClasses[0] echo "Build Cause: ${causeClass.toString()}" } } pipeline { triggers { /* If jobConfig.cronString is empty (the default), the trigger will be ignored */ cron jobConfig.cronString } options { throttle(jobConfig.throttleCategories) timeout(time: jobConfig.jobTimeout.timeout, unit: jobConfig.jobTimeout.units) buildDiscarder( logRotator(numToKeepStr: "${jobConfig.jobCleanup.keepBuilds}", artifactNumToKeepStr: "${jobConfig.jobCleanup.artifactKeepBuilds}")) } agent { label jobConfig.agentLabel } stages { stage ("Setup") { when { /* * When you make changes to the base job or a new branch is discovered * Jenkins tries to run it the job. We probably don't want this to happen * so if "BranchIndexing" was teh cause, don't run any of the steps. */ not { triggeredBy 'BranchIndexingCause' } } steps { script { createSummary(icon: "/plugin/workflow-job/images/48x48/pipelinejob.png", text: "Docker Host: ${NODE_NAME}") sh "sudo chown -R jenkins:users ." sh "printenv -0 | sort -z | tr '\\0' '\\n'" sh "sudo tests/CI/setupJenkinsEnvironment.sh" /* Find a docker image, setup parameters and pull image */ def r = currentBuild.startTimeInMillis % jobConfig.docker.images.size() def ri = jobConfig.docker.images[(int)r] echo "Docker Image: ${ri}" def randomImage = jobConfig.docker.registry + "/" + ri echo "Docker Path: ${randomImage}" dockerOptions = "--privileged --ulimit core=0 --ulimit nofile=10240 " + " --tmpfs /tmp:exec,size=1G -v /srv/jenkins:/srv/jenkins:rw -v /srv/cache:/srv/cache:rw " + " --entrypoint=''" buildTag = env.BUILD_TAG.replaceAll(/[^a-zA-Z0-9_.-]/, '-') dockerImage = docker.image(randomImage) dockerImage.pull() }} } stage ("Build") { when { expression { jobConfig.buildAsterisk.build } not { triggeredBy 'BranchIndexingCause' } } steps { script { dockerImage.inside(dockerOptions + " --name ${buildTag}-build") { echo 'Building..' withEnv(jobConfig.buildAsterisk.env) { sh "./tests/CI/buildAsterisk.sh --branch-name=${BRANCH_NAME} --output-dir=tests/CI/output/Build --cache-dir=/srv/cache" } archiveArtifacts allowEmptyArchive: true, defaultExcludes: false, fingerprint: false, artifacts: "tests/CI/output/Build/*" } }} } stage ("WikiDocs") { when { expression { jobConfig.wikiDocs.publish } not { triggeredBy 'BranchIndexingCause' } } steps { script { dockerImage.inside(dockerOptions + " --name ${buildTag}-wikidocs") { sh "sudo ./tests/CI/installAsterisk.sh --branch-name=${BRANCH_NAME} --user-group=jenkins:users" checkout scm: [$class: 'GitSCM', branches: [[name: "master"]], extensions: [ [$class: 'RelativeTargetDirectory', relativeTargetDir: "tests/CI/output/publish-docs"], [$class: 'CloneOption', noTags: true, honorRefspec: true, shallow: false ], ], userRemoteConfigs: [[url: jobConfig.wikiDocs.gitURL]] ] sh "./tests/CI/publishAsteriskDocs.sh --user-group=jenkins:users --branch-name=${BRANCH_NAME} --wiki-doc-branch-regex=\"${jobConfig.wikiDocs.branchRegex}\"" } }} } stage ("UnitTests") { when { expression { jobConfig.unitTests.run } not { triggeredBy 'BranchIndexingCause' } } steps { script { dockerImage.inside(dockerOptions + " --name ${buildTag}-unittests") { def outputdir = "tests/CI/output/UnitTests" def outputfile = "${outputdir}/unittests-results.xml" sh "sudo ./tests/CI/installAsterisk.sh --uninstall-all --branch-name=${BRANCH_NAME} --user-group=jenkins:users" sh "tests/CI/runUnittests.sh --user-group=jenkins:users --output-dir='${outputdir}' --output-xml='${outputfile}' --unittest-command='${jobConfig.unitTests.testCommand}'" archiveArtifacts allowEmptyArchive: true, defaultExcludes: false, fingerprint: true, artifacts: "${outputdir}/**" junit testResults: outputfile, healthScaleFactor: 1.0, keepLongStdio: true } }} } /* Testsuite Tests * * When jobConfig.testsuite.run is true, load the JSON file specified by * jobConfig.testsuite.configFile (default "testsuiteConfig") and spin off a * separate docker container for each testGroup contained therein that also * has its "enabled" property set to true. * * If a testGroup has a customTests child, the specified custom tests repo * will be cloned into "/tests/custom" and can be referenced as * any other testsuite test. * * Example testsuiteConfig file: * * { * testGroups: [ * { * name: "ari1-mwi", * enabled: false, * dir: "tests/CI/output/ari1", * runTestsuiteOptions: "--test-timeout=180", * testcmd: "--test-regex=tests/rest_api --test-regex=tests/channels/pjsip/.*mwi" * }, * { * name: "custom1", * enabled: false, * dir: "tests/CI/output/custom1", * runTestsuiteOptions: "--test-timeout=180", * testcmd: "--test-regex=tests/custom/tests/stress", * customTests: { * branch: "master", * gitURL: "http://somehost/private-tests" * } * } * ] * } * */ stage("Testsuite") { when { expression { jobConfig.testsuite.run } } steps { script { testConfig = [ testGroups: [], ] def tempTestConfig configFileProvider([configFile(fileId: jobConfig.testsuite.configFile, variable: 'TESTSUITE_CONFIG_FILE')]) { echo "Retrieved test config file from ${env.TESTSUITE_CONFIG_FILE}" tempTestConfig = readJSON file: env.TESTSUITE_CONFIG_FILE } merge(testConfig, tempTestConfig) tasks = [ : ] testConfig.testGroups.each { def testGroup = it tasks[testGroup.name] = { dockerImage.inside("${dockerOptions} --name ${buildTag}-${testGroup.name}") { lock("${JOB_NAME}.${NODE_NAME}.installer") { sh "sudo ./tests/CI/installAsterisk.sh --uninstall-all --branch-name=${BRANCH_NAME} --user-group=jenkins:users" } sh "sudo rm -rf ${testGroup.dir} || : " checkout scm: [$class: 'GitSCM', branches: [[name: "${BRANCH_NAME}"]], extensions: [ [$class: 'RelativeTargetDirectory', relativeTargetDir: testGroup.dir], [$class: 'CloneOption', noTags: true, depth: 100, honorRefspec: true, shallow: true ], ], userRemoteConfigs: [[url: jobConfig.testsuite.gitURL]] ] echo "Test Custom Config: ${testGroup.customTests.toString()}" if (testGroup.customTests && testGroup.customTests?.branch && testGroup.customTests?.gitURL) { checkout scm: [$class: 'GitSCM', branches: [[name: testGroup.customTests.branch]], extensions: [ [$class: 'RelativeTargetDirectory', relativeTargetDir: "${testGroup.dir}/tests/custom"], [$class: 'CloneOption', noTags: true, depth: 100, honorRefspec: true, shallow: true ], ], userRemoteConfigs: [[url: testGroup.customTests.gitURL]] ] } sh "sudo tests/CI/runTestsuite.sh ${testGroup.runTestsuiteOptions} --testsuite-dir='${testGroup.dir}' --testsuite-command='${testGroup.testcmd}'" echo "Group result d: ${currentBuild.currentResult}" archiveArtifacts allowEmptyArchive: true, defaultExcludes: false, fingerprint: true, artifacts: "${testGroup.dir}/asterisk-test-suite-report.xml, ${testGroup.dir}/logs/**, ${testGroup.dir}/core*.txt" junit testResults: "${testGroup.dir}/asterisk-test-suite-report.xml", healthScaleFactor: 1.0, keepLongStdio: true } } } parallel tasks }} } } post { cleanup { script { if (jobConfig.jobCleanup.cleanupWorkspace) { cleanWs deleteDirs: true, notFailBuild: false } } } success { echo "Reporting ${currentBuild.currentResult} Passed" } failure { echo "Reporting ${currentBuild.currentResult}: Failed: Fatal Error" } unstable { echo "Reporting ${currentBuild.currentResult}: Failed: Tests Failed" } } }