82
102

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Declarative PipelineでJenkinsfileを書いてみた(Checkstyle,Findbugs,PMD,CPDとか)

Last updated at Posted at 2017-04-06

GradleのJavaアプリをJenkinsで1人CIするためのJenkinsfileを書いてみた。
Githubにプッシュ時に、AWS上のJenkinsのパイプラインジョブが動いて、テストが成功したら同じくAWS上のTomcatにデプロイするみたいな感じで使ってます。

Declarative Pipeline

いままではJenkinsfileを

node {
  ....
}

のように書いてましたが、Jenkinsの公式サイトを見ると
これはScripted Pipelinesの記法であり、
Pipeline Pluginのバージョン2.5移行からは

pipeline {
  ....  
}

のように書くDeclarative Pipelineという記法が導入されて、
そっちのほうがシンプルでわかりやすく書けるよ!ということだったので書き直してみました。
確かにすっきりしたし(特に最後のメールおくるとことか、デプロイするとことか)、
いざとなったら従来のScripted Pipelinesもミックスできるのでいい感じです。

追加したプラグイン

Jenkins初期設定時のSuggested Pluginに入っていないもの

  • Checkstyle Plugin(v3.47) - Checkstyeの結果収集用
  • FindBugs Plugin(v4.69) - Findbugsのレポート生成用
  • PMD Plugin(v3.46) - PMDのレポート生成用
  • DRY Plugin(v2.46) - CPD(重複コードチェック)のレポート生成用
  • Step Counter Plugin(v2.0.0) - ソースコードのステップ数を集計してくれる
  • Task Scanner Plugin(v4.50) - ソースコード中のTODOとかを一覧化してくれる
  • Javadoc Plugin(v1.4) - JavaDoc生成用
  • Warnings Plugin(v4.60) - ジョブ実行時の警告メッセージを収集してくれる
  • JaCoCo Plugin(v2.2.0) - テストカバレッジのレポート生成用

Jenkinsfile

GithubからWebhookでJenkinsのパイプラインジョブを実行する。
パイプラインジョブではGithubのJenkinsfileを使う。
ジョブの流れは下記。デプロイは静的コード解析とテストが成功したときだけ実行する。
image

Jenkinsfile
pipeline {
    agent any
    // 定数や変数を定義する
    environment {
        reportDir = 'build/reports'
        javaDir = 'src/main/java'
        resourcesDir = 'src/main/resources'
        testReportDir = 'build/test-results/test'
        jacocoReportDir = 'build/jacoco' 
        javadocDir = 'build/docs/javadoc'
        libsDir = 'build/libs'
        appName = 'SampleApp'
        appVersion = '1.0.0'
    }
    
    // stagesブロック中に一つ以上のstageを定義する
    stages {
        stage('事前準備') {
            // 実際の処理はstepsブロック中に定義する
            steps {
                deleteDir()

                // このJobをトリガーしてきたGithubのプロジェクトをチェックアウト
                checkout scm

                // ジョブ失敗の原因調査用にJenkinsfileとbuild.gradleは最初に保存する
                archiveArtifacts "Jenkinsfile"
                archiveArtifacts "build.gradle"

                // scriptブロックを使うと従来のScripted Pipelinesの記法も使える
                script {
                    // Permission deniedで怒られないために実行権限を付与する
                    if(isUnix()) {
                        sh 'chmod +x gradlew'
                    }
                }
                gradlew 'clean'
            }
        }
        
        stage('コンパイル') {
            steps {
                gradlew 'classes testClasses'
            }
            
            // postブロックでstepsブロックの後に実行される処理が定義できる
            post {
                // alwaysブロックはstepsブロックの処理が失敗しても成功しても必ず実行される
                always {

                    // JavaDoc生成時に実行するとJavaDocの警告も含まれてしまうので
                    // Javaコンパイル時の警告はコンパイル直後に収集する
                    step([

                        // プラグインを実行するときのクラス指定は完全修飾名でなくてもOK
                        $class: 'WarningsPublisher',

                        // Job実行時のコンソールから警告を収集する場合はconsoleParsers、
                        // pmd.xmlなどのファイルから収集する場合はparserConfigurationsを指定する。
                        // なおparserConfigurationsの場合はparserNameのほかにpattern(集計対象ファイルのパス)も指定が必要
                        // パーサ名は下記プロパティファイルに定義されているものを使う
                        // https://github.com/jenkinsci/warnings-plugin/blob/master/src/main/resources/hudson/plugins/warnings/parser/Messages.properties
                        consoleParsers: [
                            [parserName: 'Java Compiler (javac)'],
                        ],
                        canComputeNew: false,
                        canResolveRelativesPaths: false,
                        usePreviousBuildAsReference: true
                    ])
                }
            }
        }
        
        stage('静的コード解析') {
            steps {
                // 並列処理の場合はparallelメソッドを使う
                parallel(
                    '静的コード解析' : {
                        gradlew 'check -x test'

                        // dirメソッドでカレントディレクトリを指定できる
                        dir(reportDir) {
                            step([
                                $class: 'CheckStylePublisher',
                                pattern: "checkstyle/*.xml"
                            ])
                            step([
                                $class: 'FindBugsPublisher',
                                pattern: "findbugs/*.xml"
                            ])
                            step([
                                $class: 'PmdPublisher',
                                pattern: "pmd/*.xml"
                            ])
                            step([
                                $class: 'DryPublisher',
                                pattern: "cpd/*.xml"
                            ])
                
                            archiveArtifacts "checkstyle/*.xml"
                            archiveArtifacts "findbugs/*.xml"
                            archiveArtifacts "pmd/*.xml"
                            archiveArtifacts "cpd/*.xml"
                        }
                    },
                    'ステップカウント': {
                        // レポート作成
                        // outputFileとoutputFormatを指定するとエクセルファイルも作成してくれる
                        stepcounter outputFile: 'stepcount.xls', outputFormat: 'excel', settings: [
                            [key:'Java', filePattern: "${javaDir}/**/*.java"],
                            [key:'SQL', filePattern: "${resourcesDir}/**/*.sql"],
                            [key:'HTML', filePattern: "${resourcesDir}/**/*.html"],
                            [key:'JS', filePattern: "${resourcesDir}/**/*.js"],
                            [key:'CSS', filePattern: "${resourcesDir}/**/*.css"]
                        ]
                        // 一応エクセルファイルも成果物として保存する
                        archiveArtifacts "stepcount.xls"
                    },
                    'タスクスキャン': {
                        step([
                            $class: 'TasksPublisher',
                            pattern: './**',
                            // 集計対象を検索するときに大文字小文字を区別するか
                            ignoreCase: true,
                            // 優先度別に集計対象の文字列を指定できる
                            // 複数指定する場合はカンマ区切りの文字列を指定する
                            high: 'System.out.System.err',
                            normal: 'TODO,FIXME,XXX',
                        ])
                    },
                    'JavaDoc': {
                        gradlew 'javadoc -x classes'
                        step([
                            $class: 'JavadocArchiver',
                            // Javadocのindex.htmlがあるフォルダのパスを指定する
                            javadocDir: "${javadocDir}",
                            keepAll: true
                        ])
                    }
                )
            }
            
            post {
                always {
                   // JavaDocの警告を収集
                    step([
                        $class: 'WarningsPublisher',
                        consoleParsers: [
                            [parserName: 'JavaDoc Tool']
                        ],
                        canComputeNew: false,
                        canResolveRelativesPaths: false,
                        usePreviousBuildAsReference: true
                    ])
                }
            }
        }
        

        stage('テスト') {
            steps {
                gradlew 'test jacocoTestReport -x classes -x testClasses'
                
                junit "${testReportDir}/*.xml"
                archiveArtifacts "${testReportDir}/*.xml"

                // カバレッジレポートを生成(テストクラスを除外)
                step([
                    $class: 'JacocoPublisher',
                    execPattern: "${jacocoReportDir}/*.exec",
                    exclusionPattern: '**/*Test.class'
                ])
            }
        }
        
        stage('デプロイ') {
            // whenブロックでstageを実行する条件を指定できる
            when {
                // 静的コード解析とテスト失敗時はデプロイしない
                expression {currentBuild.currentResult == 'SUCCESS'}
            }
            
            steps {
                gradlew 'jar'
                archiveArtifacts "${libsDir}/${appName}-${appVersion}.jar"
                gradlew 'war'
                archiveArtifacts "${libsDir}/${appName}-${appVersion}.war"
                deploy warDir: libsDir, appName: appName, appVersion: appVersion
            }
        }
    }
    
    // stagesブロックと同じレベルにpostブロックを定義すると
    // 全てのstage処理が終わった後の処理の定義が可能    
    post {
        always {
            // 最後にワークスペースの中身を削除
            deleteDir()
        }
        // 連続で成功しているとき以外は自分宛にメールを送信

        // 結果が前回と変わった時
        changed {
            sendMail("${currentBuild.previousBuild.result} => ${currentBuild.currentResult}")
        }
        // 失敗した時
        failure {
            sendMail(currentBuild.currentResult)
        }
        // 不安定な時(主にテスト失敗時)
        unstable {
            sendMail(currentBuild.currentResult)
        }
    }
}


// Gradlewコマンドを実行する
def gradlew(command) {
    if(isUnix()) {
        sh "./gradlew ${command} --stacktrace"
    } else {
        bat "./gradlew.bat ${command} --stacktrace"
    }
}

// デプロイする
// args.warDir warの格納ディレクトリ 
// args.appName アプリ名
// args.appVersion アプリのバージョン
def deploy(Map args) {
    // 秘密鍵のパス ※Tomcatサーバにファイル転送するので事前にJenkinsサーバのどこかに秘密鍵を格納しておく必要がある
    def keyDir = '/var/lib/jenkins/.ssh/xxx'
    // Tomcatサーバのアドレスとユーザ名
    def webServerAddress = 'ecX-XX-XXX-X-X.xx-xxxx-x.xxxxxxxx'
    def webServerUser = 'hoge-user'
    def webServer = "${webServerUser}@${webServerAddress}"
    
    def srcWar = "${args.appName}-${args.appVersion}.war"
    def destWar = "${args.appName}.war"
    
    // ファイル転送してTomcatのwebappsにwarを配置する
    sh "sudo -S scp -i ${keyDir} ./${args.warDir}/${srcWar} ${webServer}:/home/ec2-user"
    sh "sudo -S ssh -i ${keyDir} ${webServer} \"sudo cp /home/ec2-user/${srcWar} /usr/share/tomcat8/webapps/${destWar}\""
}

// メールをGmailに送信する
def sendMail(result) {
    mail to: "xxxxxxxx@gmail.com",
        subject: "${env.JOB_NAME} #${env.BUILD_NUMBER} [${result}]",
        body: "Build URL: ${env.BUILD_URL}.\n\n"
}

躓いたこと

  • 各プラグインともChangelogに「パイプライン対応したよ!」とは書いてあるが、
    具体的は書き方は明記していないことが多いので、
    各プラグインのGithubでソースコード(主に「なんちゃらPublisher」クラス)を見ながら、Jenkinsfileを書いた。
  • currentBuildオブジェクトの使い方がよくわからなかったが、Jenkinsのパイプラインジョブ > 設定 > Pipeline Syntax > Global Variables Reference に詳しく載っていた。
    image

image

image

  • カバレッジレポートはbuild.gradleのjacocoTestReportタスクでカバレッジレポート対象外にしていてもJenkinsのほうではうまく除外されなかったので、Jenkinsfileのほうでも対象外設定をした。
  • JenkinsからGmailにメールする場合、Jenkins > Jenkinsの管理 > システムの設定 > E-mail通知で下記のような設定が必要だった。

image

image

build.gradle

Jenkins自体は、Gradleのコマンドを実行して出力結果をもとにレポートを生成するだけなので、
GradleのJavaアプリのbuild.gradleで下記処理が実行できるようになっている必要があります。
またJenkinsでGradleをインストールしなくていいようにGradleラッパーを作成しておきます。

  • checkstyle
  • findbugs
  • pmd
  • cpd(重複コードチェック)
  • test
  • jacocoReport
  • jar
  • war

例えばこんな

build.gradle
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'jacoco'

ext {
    appVersion = '1.0.0'
    appName = 'SampleApp'
    javaVersion = 1.8
    defaultEncoding = 'UTF-8'
}

sourceCompatibility = javaVersion
targetCompatibility  = javaVersion
tasks.withType(AbstractCompile)*.options*.encoding = defaultEncoding
tasks.withType(GroovyCompile)*.groovyOptions*.encoding = defaultEncoding
mainClassName = 'jp.takumon.sapmleapp.App'

repositories {
    mavenCentral()
}

dependencies {
    // 依存ライブラリを記載 

    compile group: 'junit', name: 'junit', version: '4.12'
}

jar {
    baseName = appName
    version =  appVersion
}

war {
    baseName = appName
    version =  appVersion
}

checkstyle {
    // 失敗しても後続の処理を継続させる
    ignoreFailures = true
    sourceSets = [sourceSets.main]
    toolVersion = '7.6.1'
}

findbugs {
    // 失敗しても後続の処理を継続させる
    ignoreFailures = true
    sourceSets = [sourceSets.main]
    toolVersion = "3.0.1"
}

pmd {
    // 失敗しても後続の処理を継続させる
    ignoreFailures = true
    sourceSets = [sourceSets.main]
}

tasks.withType(Pmd) {
    reports {
      xml.enabled = true
    }
}

// CPD(重複コードチェック処理)をCheckタスクに追加
check.doLast {
    File outputDir = new File("$reportsDir/cpd/")
    outputDir.mkdirs()
  
    ant.taskdef(
        name: 'cpd', 
        classname: 'net.sourceforge.pmd.cpd.CPDTask',
        classpath: configurations.pmd.asPath)
  
    ant.cpd(
        minimumTokenCount: '100',
        format: 'xml',
        encoding: defaultEncoding,
        outputFile: new File(outputDir, 'cpd.xml')
    ) {
        fileset(dir: "src/main/java") {
            include(name: '**/*.java')
        }
    }
}

javadoc {
    failOnError = false
    // 好みのレベルで
    options.memberLevel = JavadocMemberLevel.PRIVATE
}

test {
    // 失敗しても後続の処理を継続させる
    ignoreFailures = true
    reports {
        junitXml.enabled = true
    }
}

jacoco {
    toolVersion = '0.7.5.201505241946'
}

jacocoTestReport {
    reports {
      xml.enabled = true
    }
    
    // カバレッジレポートからテストクラスを除外
    afterEvaluate { 
        classDirectories = files(classDirectories.files.collect {
            fileTree(dir: it, exclude: ['**/*Test.class']) 
        })
    }
}

task wrapper (type: Wrapper) {
    gradleVersion = '3.4.1'
}

以上。

82
102
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
102

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?