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を使う。
ジョブの流れは下記。デプロイは静的コード解析とテストが成功したときだけ実行する。
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 に詳しく載っていた。
- カバレッジレポートはbuild.gradleのjacocoTestReportタスクでカバレッジレポート対象外にしていてもJenkinsのほうではうまく除外されなかったので、Jenkinsfileのほうでも対象外設定をした。
- JenkinsからGmailにメールする場合、Jenkins > Jenkinsの管理 > システムの設定 > E-mail通知で下記のような設定が必要だった。
- JenkinsからGmailにメールする場合、安全性の低いアプリがアカウントにアクセスするのを許可するの手順に従って許可を有効にする必要があった。
build.gradle
Jenkins自体は、Gradleのコマンドを実行して出力結果をもとにレポートを生成するだけなので、
GradleのJavaアプリのbuild.gradleで下記処理が実行できるようになっている必要があります。
またJenkinsでGradleをインストールしなくていいようにGradleラッパーを作成しておきます。
- checkstyle
- findbugs
- pmd
- cpd(重複コードチェック)
- test
- jacocoReport
- jar
- war
例えばこんな
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'
}
以上。