2
\$\begingroup\$

I am working on setting up automated build and deploy jobs in jenkins for a php based project. I would like some feedback on my Jenkinsfile. Specifically regarding the parallelization, the (in my opinion) huge usage of sh and the best practices regarding the site configuration (would a bunch of included files be better?).

#!groovy
//TODO: SSH-steps? https://jenkins.io/blog/2019/02/06/ssh-steps-for-jenkins-pipeline/
//TODO: move some stuff to shared library for ease of use https://jenkins.io/doc/book/pipeline/shared-libraries/
node{
     switch (env.BRANCH_NAME) {
        case "Production":
            echo "Production is not yet implemented"
            //DEPLOY_DIR          = ""

            //SITE_TITLE          = ""
            //SITE_NAME           = ""
            //SITE_FQDN           = ""
            //DEFAULT_FROM_EMAIL  = ""
            //SESSION_NAME        = ""

            //MYSQL_HOSTNAME      = ""
            //MYSQL_DB_NAME       = ""
            //MYSQL_USERNAME      = ""
            //MYSQL_PASSWORD      = ""

            //SSH_SERVER_NAME     = ""
            //SSH_USERNAME        = ""

            break
        case "Development":
        case "development":
            DEPLOY_DIR          = "/var/www/somesite_dev"

            SITE_TITLE          = "blablabla"
            SITE_NAME           = "blablabla.dev.mydomain.dk"
            SITE_FQDN           = "blablabla.dev.mydomain.dk"
            DEFAULT_FROM_EMAIL  = "dev@mydaomain.dk"
            SESSION_NAME        = "TestSession"

            MYSQL_HOSTNAME      = "localhost"
            MYSQL_DB_NAME       = "somedb"
            MYSQL_USERNAME      = "uname"
            MYSQL_PASSWORD      = "p@sswørd" //TODO Move to Jenkins credential management

            SSH_SERVER_NAME     = "ssh.mydomain.dk"
            SSH_USERNAME        = "jenkins"
            break
        case "Test":
            DEPLOY_DIR          = "/var/www/somesite_test"

            SITE_TITLE          = "blablabla"
            SITE_NAME           = "blablabla.test.mydomain.dk"
            SITE_FQDN           = "blablabla.test.mydomain.dk"
            DEFAULT_FROM_EMAIL  = "test@mydaomain.dk"
            SESSION_NAME        = "DevSession"

            MYSQL_HOSTNAME      = "localhost"
            MYSQL_DB_NAME       = "somedb"
            MYSQL_USERNAME      = "uname"
            MYSQL_PASSWORD      = "p@sswørd" //TODO Move to Jenkins credential management

            SSH_SERVER_NAME     = "ssh.mydomain.dk"
            SSH_USERNAME        = "jenkins"
            break
        default:
            echo "$BRANCH_NAME Does not yet have a configuration."
            break
    }   
}
pipeline {
    agent any
    environment {
        SOURCE_DIR="${WORKSPACE}/src"
        BACKUP_FNAME="/tmp/BACKUP-${SITE_NAME}-${(new java.text.SimpleDateFormat("yyyy-MM-dd-HHmm")).format((new Date()))}.tar.gz"
    }
    triggers {
        bitbucketPush()
        pollSCM('') // empty cron expression string
    }

    stages {
        stage ('Staging'){
            environment {
                TEMPLATE_FILE="globals.template.inc.php"
                CONFIG_FILE="globals.inc.php"
            }
            steps {
                echo "Cleanup build artifacts"
                //remove build folder
                sh 'rm -R -f ${WORKSPACE}/build'
                echo "Prepare for build"
                //re-create folders
                sh 'mkdir ${WORKSPACE}/build ${WORKSPACE}/build/api ${WORKSPACE}/build/coverage ${WORKSPACE}/build/logs ${WORKSPACE}/build/pdepend ${WORKSPACE}/build/phpdox'
                echo "Running composer"
                sh "composer install -o -d ${SOURCE_DIR}"
                echo "Building config file ${SOURCE_DIR}/${CONFIG_FILE}"
                script {
                    def inptext = readFile file: "${SOURCE_DIR}/${TEMPLATE_FILE}" 
                    //save deploydate
                    def deployDate = (new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm")).format((new Date()))                    
                    inptext = inptext.replaceAll(~/¤SITE_TITLE¤/, "${SITE_TITLE}")
                    inptext = inptext.replaceAll(~/¤SITE_NAME¤/, "${SITE_NAME}")
                    inptext = inptext.replaceAll(~/¤SITE_FQDN¤/, "${SITE_FQDN}")
                    inptext = inptext.replaceAll(~/¤DEFAULT_FROM_EMAIL¤/, "${DEFAULT_FROM_EMAIL}")
                    inptext = inptext.replaceAll(~/¤SESSION_NAME¤/, "${SESSION_NAME}")
                    inptext = inptext.replaceAll(~/¤MYSQL_HOSTNAME¤/, "${MYSQL_HOSTNAME}")
                    inptext = inptext.replaceAll(~/¤MYSQL_DB_NAME¤/, "${MYSQL_DB_NAME}")
                    inptext = inptext.replaceAll(~/¤MYSQL_USERNAME¤/, "${MYSQL_USERNAME}")
                    inptext = inptext.replaceAll(~/¤MYSQL_PASSWORD¤/, "${MYSQL_PASSWORD}")
                    inptext = inptext.replaceAll(~/¤DEPLOY_DATE¤/, "${deployDate}")
                    inptext = inptext.replaceAll(~/¤GIT_BRANCH¤/, "${env.GIT_BRANCH}")
                    inptext = inptext.replaceAll(~/¤GIT_COMMIT¤/, "${env.GIT_COMMIT}")
                    //inptext = inptext.replaceAll(~/¤GIT_TAG¤/, "${sh(returnStdout: true, script: "git -C . describe --tags").trim()}")
                    writeFile file: "${SOURCE_DIR}/${CONFIG_FILE}", text: inptext
                }
            }
        }
        stage ('Testing, Static Analysis & documenting'){
            parallel {
                stage ("Count LOC"){
                    steps {
                        //    echo "Running Lint"
                        //    sh "find . -path ./src/vendor -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 -P4 php -l -n | (! grep -v \"No syntax errors detected\" )"
                        echo "Running phploc"
                        sh "./src/vendor/phploc/phploc/phploc --exclude=./src/vendor --no-interaction --quiet --log-csv=./build/logs/loc.csv src tests"

                        echo "Running sloc"
                        sh "sloccount --duplicates --wide --details . > ./build/logs/sloccount.sc  2>/dev/null"
                    }
                }
                stage ("Copy-Paste Detection"){
                    steps{
                        echo "Running copy-paste detection"
                        sh "./src/vendor/sebastian/phpcpd/phpcpd --fuzzy . --exclude src/vendor --log-pmd ./build/logs/phpcpd.xml || true"
                    }
                }
                stage ("Mess Detection"){
                    steps{
                        echo "Running mess detection on code"
                        sh "./src/vendor/phpmd/phpmd/src/bin/phpmd src xml phpmd_ruleset.xml --reportfile ./build/logs/phpmd_code.xml --exclude vendor,build --ignore-violations-on-exit --suffixes php"
                        //echo "Running mess detection on tests"
                        //sh "./src/vendor/phpmd/phpmd/src/bin/phpmd tests xml codesize,cleancode,unusedcode,naming --reportfile ./build/logs/phpmd_tests.xml --suffixes php"
                    }
                }
                stage ("Testing"){
                    steps{
                        echo "Running PHPUnit w/o code coverage"
                        sh "./src/vendor/phpunit/phpunit/phpunit --configuration phpunit-quick.xml" 
                    }
                }
                //echo "Running PHP Codesniffer"
                //sh "phpcs --report=checkstyle --report-file=./build/logs/checkstyle.xml --standard=PSR2 --extensions=php --ignore=autoload.php ./src ./tests"

                //TODO: phpdox 

                // TODO: set up reporting
                // https://wiki.jenkins.io/display/JENKINS/Plot+Plugin
                // https://stackoverflow.com/a/48001251/1725871
            }
        }
        stage ("Deploy") {
            environment {
                SSH_TUNNEL_PORT=3309
            }
            parallel {
                stage ("Deploy code") {
                    agent any
                    steps {
                        echo "Deploying via SSH  on ${SSH_SERVER_NAME}:${DEPLOY_DIR}"
                        //TODO: rename backup file
                        sh "ssh ${SSH_USERNAME}@${SSH_SERVER_NAME} tar -cvpzf ${BACKUP_FNAME} ${DEPLOY_DIR}/* "
                        sh "ssh ${SSH_USERNAME}@${SSH_SERVER_NAME} rm -R -f ${DEPLOY_DIR}/*"
                        sh "scp -rpC ${SOURCE_DIR}/* ${SSH_USERNAME}@${SSH_SERVER_NAME}:${DEPLOY_DIR}"
                        //TODO: delete backup on success
                    }
                }
                stage ("Deploy DB changes") {
                    agent any //liquibase
                    steps {
                        echo "Create SSH tunnel"
                        sh "ssh -M -S deploy-control-socket -fnNT -L ${SSH_TUNNEL_PORT}:localhost:3306 ${SSH_USERNAME}@${SSH_SERVER_NAME}"

                        echo "Check tunnel"
                        sh "ssh -S deploy-control-socket -O check ${SSH_USERNAME}@${SSH_SERVER_NAME}"

                        echo "Sync liquibase"
                        sh "liquibase --driver=com.mysql.cj.jdbc.Driver --changeLogFile=${WORKSPACE}/resources/database/db.changelog-1.1.xml --url=\"jdbc:mysql://127.0.0.1:${SSH_TUNNEL_PORT}/${MYSQL_DB_NAME}?autoReconnect=true&useSSL=false&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC\" --username=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} update"

                        echo "Close SSH tunnel to ${SSH_SERVER_NAME}"
                        sh "ssh -S deploy-control-socket -O exit ${SSH_USERNAME}@${SSH_SERVER_NAME}"
                    }
                }
            }
        }
    }
    post {
        always {
            //archiveArtifacts artifacts: 'build/libs/**/*.jar', fingerprint: true
            junit "build/logs/junit.xml"

            sloccountPublish encoding: '', pattern: ''
            // warnings-ng https://github.com/jenkinsci/warnings-ng-plugin/blob/master/doc/Documentation.md
            recordIssues enabledForFailure: true, tool: cpd(pattern: 'build/logs/phpcpd.xml')
            recordIssues enabledForFailure: true, tool: pmdParser(pattern: 'build/logs/phpmd_code.xml')
        }
        //https://jenkins.io/doc/pipeline/tour/post/
        success {
            echo 'Successful build. Well done'
        }
        unstable {
            echo 'Unstable Build. Check test cases'
        }
        failure {
            echo 'I failed :('
        }
        changed {
            echo 'Things were different before...'
        }
    }
}  
\$\endgroup\$
  • 1
    \$\begingroup\$ I don't know enough about these technologies to help you, but I think people would receive your question better if you could be a little more specific, considering the amount of code there is to go through. \$\endgroup\$ – Confettimaker Sep 9 at 1:32
1
\$\begingroup\$

I have found an inherent flaw in the backup and copy part. Namely

sh "ssh ${SSH_USERNAME}@${SSH_SERVER_NAME} tar -cvpzf ${BACKUP_FNAME} ${DEPLOY_DIR}/*"
sh "ssh ${SSH_USERNAME}@${SSH_SERVER_NAME} rm -R -f ${DEPLOY_DIR}/*"

Which both exand the asterisk on the local mashine, and https://stackoverflow.com/questions/22222838/shell-script-calling-ssh-how-to-interpret-wildcard-on-remote-server

The solution for tar is simple:

sh "ssh ${SSH_USERNAME}@${SSH_SERVER_NAME} tar -cvpzf ${BACKUP_FNAME} ${DEPLOY_DIR}"

The solution for rm is more difficult. I have considered the ssh steps plugin, but I would like some more surety of whether it would work before I try it...

Right now I am looking into rsync with the --DELETE flag set.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.