gloriaJun / til

Lessoned Learned
3 stars 0 forks source link

Writing Jenkinsfiles #148

Open gloriaJun opened 2 years ago

gloriaJun commented 2 years ago

TL;DR;

Reference

gloriaJun commented 2 years ago

Parallel Stages

image

Declarative Pipeline

#!/usr/bin/env groovy

pipeline {
  agent any

  tools {
    nodejs "nodejs12.13.0"
    jdk "jdk11"
  }

  environment {
    HOME = "${env.WORKSPACE}"
    YARN_CACHE_FOLDER = "${env.HOME}/.yarn-cache"

    /**
      defined internal env
    */
    npmCommand = "yarn"
  }

  options {
      buildDiscarder(logRotator(
        artifactDaysToKeepStr: '1',
        artifactNumToKeepStr: '10',
        daysToKeepStr: '3',
        numToKeepStr: "10")
      )
      timestamps()
      timeout(time: 30, unit: 'MINUTES')
  }

  stages {
    stage('Checkout') {
      steps {
        echo "Branch: ${env.BRANCH_NAME}, PrBranch: ${env.CHANGE_BRANCH}"
        sh "which node; npm --version; node --version; yarn -version"
        sh 'which java; java -version; echo $JAVA_HOME'
        sh "printenv"
      }
    }

    stage('Install packages') {
      steps {
        sh "${npmCommand} install"
      }
    }

    stage('Lint') {
      steps {
        sh "${npmCommand} run lint -- -f json -o eslint.json"
      }
    }    

    stage('parallel test') {
      parallel {
        stage('parallel-1') {
          steps {
            script {
              sh "ls -al ${env.JAVA_HOME}/bin" 
              sh "which java; java -version; echo ${env.JAVA_HOME}
            }
          }
        }

        stage('parallel-2') {
          steps {
            script {
              sh "ls -al ${env.JAVA_HOME}/bin" 
              sh "which java; java -version; echo ${env.JAVA_HOME}
            }
          }
        }
      }
    }    
  }
}

Scripted Pipeline

node {
  /**
    set tools
  **/
  env.NODEJS_HOME = tool name: 'nodejs12.13.0'
  env.JAVA_HOME=tool name: 'jdk11'
  env.PATH="${env.JAVA_HOME}/bin:${env.NODEJS_HOME}/bin:${env.PATH}"

  /**
    set cache node_modules
  **/
  env.YARN_CACHE_FOLDER = "${env.WORKSPACE}/.yarn-cache"

  /**
    defined internal env vars
  **/
  npmCommand = "yarn"

  properties([
    // 오래된 빌드 삭제
    buildDiscarder(logRotator(
      artifactDaysToKeepStr: '1',
      artifactNumToKeepStr: '10',
      daysToKeepStr: '3',
      numToKeepStr: '10'
    )), 
  ])

  timestamps {
    timeout(time: 30, unit: 'MINUTES') {
      stage ('Env') {
        echo "Branch: ${env.BRANCH_NAME}, PrBranch: ${env.CHANGE_BRANCH}"
        sh "which node; npm --version; node --version; yarn -version"
        sh "which java; java -version; echo $JAVA_HOME"
        sh "printenv"
      }

      stage ('Checkout') {
        checkout scm
      }  

      stage ('Install Dependencies') {
        sh "${npmCommand} install"
      }      

      stage ('Lint') {
        sh "${npmCommand} run lint -- -f json -o eslint.json"
      }

      stage ('parallel test') {
        def stages = [:]

        stages['parallel-1'] = {
          sh "ls -al ${env.JAVA_HOME}/bin" 
          sh "which java; java -version; echo ${env.JAVA_HOME}" 
        }

        stages['parallel-2'] = {
          sh "ls -al ${env.JAVA_HOME}/bin" 
          sh "which java; java -version; echo ${env.JAVA_HOME}" 
        }

        parallel(stages)
      }         
    }
  }
}
gloriaJun commented 2 years ago

Declarative Pipeline Examples

#!/usr/bin/env groovy
void setBuildStatus(String context, String message, String state, String url) {
  if (state == 'PENDING') {
    backref = "${env.RUN_DISPLAY_URL}"
  } else {
    backref = url
  }

  // step([
  //   $class: "GitHubCommitStatusSetter",
  //   reposSource: [$class: "ManuallyEnteredRepositorySource", url: "${env.GIT_URL}"],
  //   contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/${context}"],
  //   errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]],
  //   statusBackrefSource: [$class: "ManuallyEnteredBackrefSource", backref: backref],
  //   statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ]
  // ]);

  // To set publishChecks
  title = 'Build Check'

  if (state == 'PENDING') {
    publishChecks title: title,
      name: context,
      status: 'IN_PROGRESS',
      detailsURL: url
  } else if (state != 'SUCCESS') {
    publishChecks title: title,
      name: context,
      status: 'COMPLETED',
      conclusion: 'FAILURE',
      detailsURL: url
  } else {
    publishChecks title: title,
      name: context,
      detailsURL: url
  }
}

void notifySlack(String message, String color) {
  slackSend (channel: '#*******', color: color, message: message + ": Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}

pipeline {
  agent {
    dockerfile { filename './build/Dockerfile' }
  }

  environment {
    // npm_config_cache = "npm-cache"
    HOME = "${env.WORKSPACE}"
    // NPM_CONFIG_PREFIX = "${env.HOME}/.npm"
    YARN_CACHE_FOLDER = "${env.HOME}/.yarn-cache"

    /**
      defined internal env
    */
    npmCommand = "yarn"

    isDevelop = "${env.BRANCH_NAME ==~ /(develop)/}"
    isMainBranch = "${env.BRANCH_NAME ==~ /(master|develop|release.*)/}"
    isPrBranch= "${env.BRANCH_NAME ==~ /^PR-\d+$/}"
    isSonarCoverage= "${isMainBranch || env.isPrBranch && env.CHANGE_TARGET ==~ /(develop)/}"

    coverageReportDir = "tests/coverages/reports"
    junitReportFilename = 'test-junit-report.xml'
    sonarReportFilename = 'test-sonar-report.xml'

    buildUrl ="${env.BUILD_URL}"
    storybookUrl = "*******"
    // sonarQubeUrl = "http://*******/dashboard?id=*******&${isPrBranch == 'true' ? 'pullRequest=' + env.CHANGE_ID  : 'branch=' + env.BRANCH_NAME.replaceAll('/', '%2F')}"

    repository= "*******"
    githubEndpoint= "*******"
  }

  options {
      buildDiscarder(logRotator(artifactDaysToKeepStr: '1', artifactNumToKeepStr: '10', daysToKeepStr: '3',numToKeepStr: "10"))
      timestamps()
      timeout(time: 30, unit: 'MINUTES')
  }

  stages {
    stage('Checkout') {
      steps {
        echo "Branch: ${env.BRANCH_NAME}, PrBranch: ${env.CHANGE_BRANCH}"
        sh "which node; npm --version; node --version; yarn -version"
        sh 'which java; java -version'
        sh "printenv"
      }
    }

    stage('Install dependencies') {
      steps {
          echo 'Installing dependencies...'
          // sh "npm config set registry *******"
          // sh "npm install"
          // sh "npm ci --prefer-offline --no-audit"
          sh "yarn config set registry *******"
          sh "yarn install"
      }
    }

    stage('Lint') {
      environment {
        context="lint"
      }

      steps {
        script {
          try {
            sh "${npmCommand} run lint -- -f json -o eslint.json"
            sh "${npmCommand} run lint -- -f checkstyle -o checkstyle-eslint.xml"
          } catch (e) {
            sh "${npmCommand} run lint"
          }
        }
      }

      post {
        always {
          recordIssues enabledForFailure: true, aggregatingResults: true, tool: checkStyle(pattern: 'checkstyle-eslint.xml')
        }
      }
    }

    stage('Build') {
      parallel {
        stage('App') {
          environment {
            context="build"
          }

          steps {
            setBuildStatus("${context}", "${context} Progressing...", "PENDING", "${buildUrl}");
            sh "${npmCommand} run build"
          }

          post {
            always {
              script {
                if (currentBuild.currentResult != 'FAILURE') {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Success", "SUCCESS", "${buildUrl}");
                } else {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Failed", "FAILURE", "${buildUrl}");
                }
              }
            }
          }
        }    

        stage('Storybook') {
          when {
            expression { isMainBranch == 'true' }
          }

          environment {
            context="storybook/build"
          }

          steps {
            setBuildStatus("${context}", "${context} Progressing...", "PENDING", "${buildUrl}");

            script {
               withCredentials([string(credentialsId: 'ZEPLIN_TOKEN', variable: 'TOKEN')]) {
                sh "STORYBOOK_ZEPLIN_TOKEN=${TOKEN} ${npmCommand} run storybook:build"
               }
            }
          }

          post {
            always {
              script {
                if (currentBuild.currentResult != 'FAILURE') {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Success", "SUCCESS", "${buildUrl}");
                } else {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Failed", "FAILURE", "${buildUrl}");
                }
              }
            }
          }
        }    
      }
    }    

    // stage('Test') {
      // parallel {
        stage('Unit Test') {
          environment {
            context="unit"
          }

          steps {
            sh "${npmCommand} run test:unit:coverage --detectOpenHandles"
          }

          post {
            always {
              script {
                junit "${coverageReportDir}/${context}/${junitReportFilename}"
              }
            }
          }
        }

        stage('Storyshot') {
          environment {
            context="storybook/snapshot-test"
          }

          steps {
            // setBuildStatus(context, "${context} Progressing...", "PENDING", "${buildUrl}");
            catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
              script {
                try {
                  // sh "${npmCommand} run test:storybook:coverage"
                  sh "${npmCommand} run test:storybook"
                } catch (e) {
                  // setBuildStatus("${context}", "${context} Failed", "FAILURE", "${buildUrl}");
                  error e.message
                }
              }
            }
          }

          post {
            always {
              script {
                junit "${coverageReportDir}/storybook/${junitReportFilename}"
              }
            }
            // success {
            //   setBuildStatus("${context}", "${context} Pass", "SUCCESS", "${buildUrl}");
            // }
          }
        }
      // }
    // }    

    stage('Report') {
      parallel {
        stage('Coverage Report to CI') {
          steps {
            sh "${npmCommand} run combine-coverage"

            script {
              step([$class: 'CoberturaPublisher', coberturaReportFile: "${coverageReportDir}/combined/cobertura-coverage.xml"])
            }
          }
        }   

        stage('SonarQube Analysis') {
          when {
            expression { isSonarCoverage == 'true' }
          }

          environment {
            scannerHome = tool 'SonarQubeScanner'
          }

          steps {
            script{
              withCredentials([string(credentialsId: '*******', variable: 'TOKEN')]) {
                withSonarQubeEnv('SonarQubeServer') {
                  def args = ''

                  if (isPrBranch == 'true') {
                    args = args + " \
                      -Dsonar.pullrequest.key=${env.CHANGE_ID} \
                      -Dsonar.pullrequest.branch=${env.CHANGE_BRANCH} \
                      -Dsonar.pullrequest.base=${env.CHANGE_TARGET} \
                      -Dsonar.pullrequest.provider=github \
                      -Dsonar.pullrequest.github.repository=${repository} \
                      -Dsonar.pullrequest.github.endpoint=${githubEndpoint} \
                      "
                  } else {
                    args = args + " \
                      -Dsonar.branch.name=${env.BRANCH_NAME} \
                      "
                  } 

                    // -Dsonar.testExecutionReportPaths='${coverageReportDir}/unit/test-sonar-report.xml,${coverageReportDir}/storybook/test-sonar-report.xml' \
                  sh "BROWSERSLIST_IGNORE_OLD_DATA=true ${scannerHome}/bin/sonar-scanner \
                    -Dsonar.projectKey=******* \
                    -Dsonar.projectName=******* \
                    -Dsonar.sources=./src \
                    -Dsonar.exclusions='./build,./cypress,./tests,./src/assets,./src/xlt,./src/lib,./src/config,./src/languages,**/*.stories.js,**/*.stories.tsx,**/__snapshots__,**/*.snap.js' \
                    -Dsonar.tests=./src \
                    -Dsonar.test.inclusions='**/*.test.tsx,**/*.test.ts,**/*.test.js,**/*.spec.ts,**/*.spec.tsx' \
                    -Dsonar.testExecutionReportPaths='${coverageReportDir}/unit/${sonarReportFilename},${coverageReportDir}/storybook/${sonarReportFilename}' \
                    -Dsonar.javascript.lcov.reportPaths=${coverageReportDir}/combined/lcov.info \
                    -Dsonar.eslint.reportPaths=eslint.json \
                    -Dsonar.typescript.tsconfigPath=tsconfig.json \
                    ${args} \
                  "
                }
              }
            }
          }
        }
      }
    }

    stage('Deploy') {
      parallel {
        stage('Storybook image snapshot Test') {
         when {
           expression { isMainBranch == 'true' }
         }

         environment {
           context="storybook/snapshot-image-test"
         }

         steps {
           setBuildStatus(context, "${context} Progressing...", "PENDING", "${buildUrl}");

           catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
             script {
               try {
                 sh "${npmCommand} run test:image-snapshot"
               } catch (e) {
                 setBuildStatus("${context}", "${context} Failed", "FAILURE", "${buildUrl}");
                 error e.message
               }
             }
           }
         }

         post {
           success {
             setBuildStatus("${context}", "${context} Pass", "SUCCESS", "${buildUrl}");
           }
         }
       }

       stage('Deploy Storybook') {
          when {
            expression { isMainBranch == 'true' }
          }

          environment {
            context="storybook/deploy"
            host = "*******"
            outputDir = ".out"
            branchName = "${env.BRANCH_NAME.split("/")[0]}"
            deployTempDir = "/tmp/jenkins_tmp/*******/${env.GIT_BRANCH}"
            deployTargetDir = "~/deploy/storybook-*******/${env.branchName}"
          }

          steps {
            setBuildStatus(context, "${context} Progressing...", "PENDING", "${buildUrl}");

            script {
              def remote = [:]
              remote.name = "*******"
              remote.host = "*******"
              remote.allowAnyHosts = true

              withCredentials([sshUserPrivateKey(credentialsId: 'jenkins-private-key', keyFileVariable: 'identity')]) {
                  remote.user = '*******' 
                  remote.identityFile = identity
                  remote.logLevel = 'INFO' 

                  sshCommand remote: remote, command: "mkdir -p ${deployTempDir}"
                  sshPut remote: remote, from: "./${outputDir}", into: "${deployTempDir}" 
                  sshCommand remote: remote, command: "rsync -avzh ${deployTempDir}/${outputDir}/* ${deployTargetDir}/ --delete"
                  sshRemove remote: remote, path: "${deployTempDir}", failOnError: false
              }
            }
          }

          post {
            always {
              script {
                if (currentBuild.currentResult != 'FAILURE') {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Success", "SUCCESS", "${storybookUrl}/${branchName}");
                } else {
                  setBuildStatus("${context}", "${env.STAGE_NAME} Failed", "FAILURE", "${storybookUrl}/${branchName}");
                }
              }
            }
          }
        }
      }
    }

    stage('SonarQube Quality Gate') {
      when {
        expression { isSonarCoverage == 'true' }
      }

      // environment {
      //   context="SonarQube"
      // }

      steps {
        // setBuildStatus(context, "${context} Progressing...", "PENDING", "${buildUrl}");

        script{
          timeout(time: 10, unit: 'MINUTES') {
            waitForQualityGate abortPipeline: false
            // def qg = waitForQualityGate()

            // if (qg.status != 'OK') {
            //   echo "Pipeline aborted due to quality gate failure: ${qg.status}"
            //   // setBuildStatus("${context}", "${context} Failed", "UNSTABLE", "${sonarQubeUrl}");
            // }
          }
        }
      }

      // post {
      //   always {
      //     script {
      //       if (currentBuild.currentResult != 'FAILURE') {
      //         setBuildStatus("${context}", "${env.STAGE_NAME} Success", "SUCCESS", "${sonarQubeUrl}");
      //       } else {
      //         setBuildStatus("${context}", "${env.STAGE_NAME} Failed", "FAILURE", "${sonarQubeUrl}");
      //       }
      //     }
      //   }
      // }
    }    

   // stage('PR Comment') {
    //   when {
    //     expression { isPrBranch == 'true' }
    //   }

    //   steps {
    //     step([
    //       $class: 'ViolationsToGitHubRecorder', 
    //       config: [
    //         gitHubUrl: 'https://*******.git', 
    //         repositoryOwner: '*******', 
    //         repositoryName: '*******', 
    //         pullRequestId: "${env.CHANGE_ID}", 

    //         credentialsId: 'githubApp',

    //         createCommentWithAllSingleFileComments: true, 
    //         createSingleFileComments: true, 
    //         commentOnlyChangedContent: true, 
    //         commentOnlyChangedFiles: true,
    //         minSeverity: 'INFO',
    //         maxNumberOfViolations: 99999,
    //         keepOldComments: false,

    //         commentTemplate: """
    //           *Reporter**: {{violation.reporter}}{{#violation.rule}}

    //           **Rule**: {{violation.rule}}{{/violation.rule}}
    //           **Severity**: {{violation.severity}}
    //           **File**: {{violation.file}} L{{violation.startLine}}{{#violation.source}}

    //           **Source**: {{violation.source}}{{/violation.source}}

    //           {{violation.message}}
    //           """,

    //         violationConfigs: [
    //          [ pattern: '.*/checkstyle-*\\.xml$', parser: 'CHECKSTYLE', reporter: 'Checkstyle' ], 
    //         //  [ pattern: '.*/findbugsXml\\.xml$', parser: 'FINDBUGS', reporter: 'Findbugs' ], 
    //         //  [ pattern: '.*/pmd\\.xml$', parser: 'PMD', reporter: 'PMD' ], 
    //         ]
    //       ]
    //     ])
    //   }
    // }    
  }

  post {
    cleanup {
      cleanWs(
        deleteDirs: true,
        patterns: [
          [pattern: 'dist', type: 'INCLUDE'],
          [pattern: '.out', type: 'INCLUDE'],
        ]
      )
    }
    success {
      script {
        def previousResult = currentBuild.previousBuild?.result

        if (!previousResult || (previousResult && previousResult != currentBuild.result)) {
          notifySlack ('SUCCESS', '#00FF00')
        }
      }
    }
    unstable {
      notifySlack ('UNSTABLE', '#FFFF00')
    }
    failure {
      notifySlack ('FAILED', '#FF0000')
    }
  }
}
gloriaJun commented 2 years ago

Scripted Pipeline Examples

gloriaJun commented 2 years ago

GitHubCommitStatusSetter & publishChecks

def setBuildStatus(String context, String message, String state, String url) {
  if (state == 'PENDING') {
    backref = "${env.RUN_DISPLAY_URL}"
  } else {
    backref = url
  }

  step([
    $class: "GitHubCommitStatusSetter",
    reposSource: [$class: "ManuallyEnteredRepositorySource", url: "${env.GIT_URL}"],
    contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "ci/jenkins/${context}"],
    errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]],
    statusBackrefSource: [$class: "ManuallyEnteredBackrefSource", backref: backref],
    statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ]
  ]);

  // To set publishChecks
  // title = 'Build Check'

  // if (state == 'PENDING') {
  //   publishChecks title: title,
  //     name: context,
  //     status: 'IN_PROGRESS',
  //     detailsURL: url
  // } else if (state != 'SUCCESS') {
  //   publishChecks title: title,
  //     name: context,
  //     status: 'COMPLETED',
  //     conclusion: 'FAILURE',
  //     detailsURL: url
  // } else {
  //   publishChecks title: title,
  //     name: context,
  //     detailsURL: url
  // }
}

def setBuildProcessingStatus(String context, String url) {
  setBuildStatus(context, "${context} Progressing...", "PENDING", url)
}

def setBuildResultStatus(String context, Boolean isSuccess, String url) {
  if (isSuccess) {
    setBuildStatus(context, "${context} Success", "SUCCESS", url)
  } else {
    setBuildStatus(context, "${context} Failed", "FAILURE", url)
  }
}
gloriaJun commented 2 years ago

Notification

//