GroovyとGradleでJenkinsプラグインを作る

先日 [Yaml Axis Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Yaml+Axis+Plugin) を作った時にgradleとgroovyでJenkinsのプラグインを作ったのですが、その時のハマリポイントを中心にメモ

# tl;dr
* https://wiki.jenkins-ci.org/display/JENKINS/Hosting+Plugins
* https://wiki.jenkins-ci.org/display/JENKINS/Gradle+JPI+Plugin

# Gradle 
https://wiki.jenkins-ci.org/display/JENKINS/Gradle+JPI+Plugin にある `build.gradle` のサンプルをコピペして適宜編集して保存。

confluenceよりもGitHubの説明の方が詳しいです。
https://github.com/jenkinsci/gradle-jpi-plugin

Gradle JPI Plugin が gradle 2.3でしか動かないようなので

```groovy:build.gradle
task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}
```

のようにバージョンを合わせた方がいいです。

ちなみに2.10（現行最新）だと下記のような `StackOverflowError` が出ました。

```
$ ./gradlew build --stacktrace
feature.
:localizer UP-TO-DATE
:stapler UP-TO-DATE
:compileJava
:compileGroovy
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
:processResources UP-TO-DATE
:classes
:jpi FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':jpi'.
> java.lang.StackOverflowError (no error message)

* Try:
Run with --info or --debug option to get more log output.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':jpi'.
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
	at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
	at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:64)
	at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
	at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
	at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:203)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:185)
	at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:66)
	at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:50)
	at org.gradle.execution.taskgraph.ParallelTaskPlanExecutor.process(ParallelTaskPlanExecutor.java:47)
	at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:110)
	at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
	at org.gradle.execution.DefaultBuildExecuter.access$000(DefaultBuildExecuter.java:23)
	at org.gradle.execution.DefaultBuildExecuter$1.proceed(DefaultBuildExecuter.java:43)
	at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
	at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:30)
	at org.gradle.initialization.DefaultGradleLauncher$4.run(DefaultGradleLauncher.java:154)
	at org.gradle.internal.Factories$1.create(Factories.java:22)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:90)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:52)
	at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:151)
	at org.gradle.initialization.DefaultGradleLauncher.access$200(DefaultGradleLauncher.java:32)
	at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:99)
	at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:93)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:90)
	at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:62)
	at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:93)
	at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:82)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:94)
	at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)
	at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:43)
	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:28)
	at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:78)
	at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:48)
	at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:52)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:37)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)
	at org.gradle.util.Swapper.swap(Swapper.java:38)
	at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.health.DaemonHealthTracker.execute(DaemonHealthTracker.java:47)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:66)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:72)
	at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.health.HintGCAfterBuild.execute(HintGCAfterBuild.java:41)
	at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
	at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)
	at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:246)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
	at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
Caused by: java.lang.StackOverflowError
	at net.java.sezpoz.Index$LazyIndexIterator.peek(Index.java:144)
	at net.java.sezpoz.Index$LazyIndexIterator.hasNext(Index.java:185)
	at org.jenkinsci.gradle.plugins.jpi.JpiManifest.isSupportDynamicLoading(JpiManifest.groovy:129)
	at org.jenkinsci.gradle.plugins.jpi.JpiManifest.<init>(JpiManifest.groovy:92)
	at org.jenkinsci.gradle.plugins.jpi.Jpi.copy(Jpi.groovy:31)
	at org.jenkinsci.gradle.plugins.jpi.Jpi.copy(Jpi.groovy:32)
	at org.jenkinsci.gradle.plugins.jpi.Jpi.copy(Jpi.groovy:32)

（略）

BUILD FAILED

Total time: 1 mins 7.717 secs
```

[CHANGELOG](https://github.com/jenkinsci/gradle-jpi-plugin/blob/master/CHANGELOG.md#0130-2015-10-27) を見ると「fixed StackOverflowError when using Gradle 2.8」とあるので治ってそうなんですが、2.8でも同様のエラーが出たので2.3を使うのが確実みたいです。

## Gradle JPI Pluginでリリースする時にversionのタグをpushする

[Maven Jenkins Plugin](http://jenkinsci.github.io/maven-hpi-plugin/) だと `mvn release:prepare release:perform` で

* バージョンでtagを作成してpush
* リリース時のみバージョンの `-SNAPSHOT` を削り、リリース後にバージョンをインクリメントして-SNAPSHOTを付加

ということができるのですが、Gradle JPI Pluginだとできないので下記のようなタスクを作りました

```groovy:build.gradle
buildscript {
    dependencies {
        classpath 'org.ajoberstar:gradle-git:1.3.2'
    }
}

import org.ajoberstar.grgit.*

ext.repo = Grgit.open()

task tagRelease << {
    repo.tag.add {
        name = version
        message = "Release of ${version}"
    }

    repo.push(tags: true)
}

task checkVersion << {
    if(version.endsWith("-SNAPSHOT")){
        throw new GradleException("Remove '-SNAPSHOT' in version!")
    }
}

task release(dependsOn: ["checkVersion", "publish", "tagRelease"])
```

`./gradlew release` で

* `checkVersion` : `version` の末尾に `-SNAPSHOT` がついていたら中断
* `publish` [^1] : Update Centerにプラグインを公開
* `tagRelease` : `version` でtagを作成してpush

が実行されます

[^1]: `publish` はGradle JPI Pluginのタスク

## build.gradle
今回のbuild.gradleは https://github.com/jenkinsci/yaml-axis-plugin/blob/master/build.gradle にあります

# Groovy
普通に src/main/groovy とかにファイルを置くだけでいいですが、注意点としてはGroovyはJenkinsにバンドルされている1.8.9しか使えないこと。

最初2.4.5（現行最新）を使った時に、ビルドは通るのに `gradlew server` でローカルのJenkinsにプラグインを乗せた時に下記のようなエラーが出てハマりました。サブクラスからスーパークラスのコンストラクタを呼び出す時になぜかコンストラクタの数が一致しないというエラー。

```
javax.servlet.ServletException: java.lang.IncompatibleClassChangeError: the number of constructors during runtime and compile time for hudson.matrix.Axis do not match. Expected -1 but got 3
	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:796)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
	at org.kohsuke.stapler.MetaClass$6.doDispatch(MetaClass.java:249)
	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:876)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:649)
	at org.kohsuke.stapler.Stapler.service(Stapler.java:238)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:686)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1494)
	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:123)
	at hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:114)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
	at hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:48)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
	at hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:84)
	at hudson.security.ChainedServletFilter.doFilter(ChainedServletFilter.java:76)
	at hudson.security.HudsonFilter.doFilter(HudsonFilter.java:168)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
	at org.kohsuke.stapler.compression.CompressionFilter.doFilter(CompressionFilter.java:49)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
	at hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:81)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
	at org.kohsuke.stapler.DiagnosticThreadNameFilter.doFilter(DiagnosticThreadNameFilter.java:30)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1474)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:499)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:533)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1086)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:428)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1020)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116)
	at org.eclipse.jetty.server.Server.handle(Server.java:366)
	at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:489)
	at org.eclipse.jetty.server.AbstractHttpConnection.content(AbstractHttpConnection.java:960)
	at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.content(AbstractHttpConnection.java:1021)
	at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:865)
	at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:240)
	at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82)
	at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:668)
	at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52)
	at winstone.BoundedExecutorService$1.run(BoundedExecutorService.java:77)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
	at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.IncompatibleClassChangeError: the number of constructors during runtime and compile time for hudson.matrix.Axis do not match. Expected -1 but got 3
	at groovy.lang.MetaClassImpl.selectConstructorAndTransformArguments(MetaClassImpl.java:1413)
	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.selectConstructorAndTransformArguments(ScriptBytecodeAdapter.java:234)
	at org.jenkinsci.plugins.yamlaxis.YamlAxis.<init>(YamlAxis.groovy:25)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
	at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:77)
	at org.codehaus.groovy.runtime.callsite.ConstructorSite$ConstructorSiteNoUnwrapNoCoerce.callConstructor(ConstructorSite.java:102)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:54)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:182)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:198)
	at org.jenkinsci.plugins.yamlaxis.YamlAxis$DescriptorImpl.newInstance(YamlAxis.groovy:80)
	at org.jenkinsci.plugins.yamlaxis.YamlAxis$DescriptorImpl.newInstance(YamlAxis.groovy)
	at hudson.model.Descriptor.newInstancesFromHeteroList(Descriptor.java:919)
	at hudson.model.Descriptor.newInstancesFromHeteroList(Descriptor.java:900)
	at hudson.util.DescribableList.rebuildHetero(DescribableList.java:205)
	at hudson.matrix.MatrixProject.submit(MatrixProject.java:881)
	at hudson.model.Job.doConfigSubmit(Job.java:1203)
	at hudson.model.AbstractProject.doConfigSubmit(AbstractProject.java:785)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:601)
	at org.kohsuke.stapler.Function$InstanceFunction.invoke(Function.java:298)
	at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:161)
	at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:96)
	at org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:121)
	at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:746)
	... 48 more
```


build.gradle でGroovyのバージョンを固定するのが安全かもしれません。

```groovy:build.gradle
dependencies {
    runtime 'org.codehaus.groovy:groovy-all:1.8.9'
}
```

# Gradleで作ったプラグインを公開する
先述の `gradle publish` でupdate centerには公開されるのですが

* 自分が作ったプラグインをgithubの [jenkinsci organization](https://github.com/jenkinsci) からfork
* https://jenkins.ci.cloudbees.com/job/plugins/ でプラグインのビルドを設定

のためには別途依頼をする必要があります

[Jenkins Pluginを作成してみた。 - Qiita](http://qiita.com/ukiuni@github/items/0e164ac69b4f3c9ac56a) だとメーリングリストで申請をするようになってるのですが、今はJiraで依頼をするように変わっています

https://wiki.jenkins-ci.org/display/JENKINS/Hosting+Plugins#HostingPlugins-Requesthosting にもあるようにJiraにログイン後 https://issues.jenkins-ci.org/projects/HOSTING でissueを登録する必要があります

ちなみに自分が登録した時のはこれ

https://issues.jenkins-ci.org/browse/HOSTING-12 （Jiraのアカウントがないと見れません）

![Jira](https://qiita-image-store.s3.amazonaws.com/0/8059/091f6ba2-3360-f807-5e62-132a9bc29482.png)

cloudbeesのジョブを設定してもらった時にMavenのテンプレートが使われていたのでGradleのテンプレートでジョブを作りなおしてもらいました。

Jenkinsのプラグイン作る時はMavenが多数派なのでそれ以外でプラグインを作ってる場合はissueを立てる時に「このプロジェクトはGradleを使ってるのでGradleのテンプレートを使ってジョブを作って」と書くのが親切かもしれません。