Jenkinsで最新のビルドだけ実行したい

# TL;DR

Jenkinsで最新ビルドが欲しいのに古いジョブがキューイングされてしまっていてビルドできない！なんてことはありませんか？
実は最新ビルド以外を自動でキャンセルさせるいい方法があるんです！

それはJenkinsで最新のビルドだけ実行したい場合は`disableConcurrentBuilds(abortPrevious: true)`を使うことです！
出典 : https://www.jenkins.io/doc/book/pipeline/syntax/#:~:text=options%20%7B%20checkoutToSubdirectory(%27foo%27)%20%7D-,disableConcurrentBuilds,-Disallow%20concurrent%20executions

・・・なんて思っていた時期もありました。
これには欠点があり、実行してみればわかるのですが`${JOB_NAME}@N`という末尾に数字の付いたworkspaceが新規で作られてしまうのです！
つまりはジョブがほぼ同時に多数実行されてしまうと無制限(とは流石にいかないでしょうが)にディレクトリが増えていくことに・・・！
しかもそのジョブがキャンセルされなければcheckoutされてしまうディレクトリも増えるということで・・・つまり無制限にストレージ容量も食ってしまう！！！

ということで試行錯誤した結果`milestone`と`lock`と`stages`のネストを駆使すればなんとかいけるというところまで漕ぎつけたのでまとめてみます。

### 未調査項目

JenkinsPipelineを使わない方法については**未調査**です。
また、master-slave構成ではない場合についても**未調査**です。
悪しからず。

# 要素説明

調べたり試したりした順番で記載します。

## milestone

https://www.jenkins.io/doc/pipeline/steps/pipeline-milestone-step/

milestoneコマンドを呼び出すと、それ以前に同じ数字で呼び出した前のJobをキャンセルしてくれるらしい。
と思って試してみたものの、うまくいかず・・・。~~例にもれず癖のある挙動をするようです。~~
どうも後続のJobがより大きな数字で呼び出した=処理を追い越したと判断する模様？

おとなしくstackoverflowに載っていた↓のコードを使いましょう。

```groovy
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
```
出典 : https://stackoverflow.com/a/55818301

## agent none

milestoneでキャンセルされたよやったね！と喜んだのもつかの間、また`@N`ディレクトリが使用されてしまうことが発覚。
pipelineの開始時点で既にworkspaceは決定されてしまった後であるらしく、`abortPrevious`と同じ問題を抱えることに。

じゃあworkspaceが決定される前にmilestone処理を実行すればいいのでは？ということで、stage内でagentを指定することに。

```groovy
pipeline {
  agent none

  stages {
    stage('milestone') {
      agent { label 'master' } // 管理してるだけのmasterマシン
      steps {
        script {
          def buildNumber = env.BUILD_NUMBER as int
          if (buildNumber > 1) milestone(buildNumber - 1)
          milestone(buildNumber)
        }
      }
    }
    stage('build') {
      agent { label 'slave' } // 実際にビルドが走るslaveマシン
      steps {
        ...
      }
    }
  }
```
`agent none`についてはこちら : https://www.jenkins.io/doc/book/pipeline/syntax/#agent:~:text=example%3A%20agent%20any-,none,-When%20applied%20at

が、失敗。変わらず`@N`が付いてしまう結果に…。

> **TIPS**
> ちなみにpostがある場合、以下のように書かないとエラーになります。
> ```groovy
>   post {
>     success { node(label: 'slave') { ... } }
>     failure { node(label: 'slave') { ... } }
>   }
> ```
> 
> まとめて管理するためにenvironmentで管理すべきかもしれませんね。
> 以下のように書くことが可能です。
> 
> ```groovy
>   environment {
>       BUILD_MACHINE = "slave"
>   }
> ...
>     stage('build') {
>       agent { label env.BUILD_MACHINE } // 実際にビルドが走るslaveマシン
>       steps {
>         ...
>       }
>     }
> ...
>   post {
>     success { node(label: env.BUILD_MACHINE) { ... } }
>     failure { node(label: env.BUILD_MACHINE) { ... } }
>   }
> ```
> 
> environment内で`sh`を呼び出している場合などにおいても同じような問題があります。
> こちらについてはどうしようもなさそうなのでgroovyの構文で解決できないか検討すべきでしょう。
> 未確認ですが、`agent`を指定済みのstage内に`environment`を置くことで回避できるかもしれません。

## lock

https://www.jenkins.io/doc/pipeline/steps/lockable-resources/

問題はmilestoneを呼び出してキャンセルしてからstage(もしくはagentかsteps)に到達するまでに前のジョブが終わっていないことだと**推測**。
(本当にここについては私の推測にすぎません。根拠はないです。)

何かJob同士の協調ができるようなものはないかと探した結果・・・ありました`lock`が！

> ちなみにJenkinsのメニュー内にある`Lockable Resources`でリソースを管理するのかと思いきや、
> `If the resource does not exist in Global Settings it will be automatically created on build execution. Either a resource or a label need to be specified.`
> ということらしく、`${JOB_BASE_NAME}`あたりを突っ込んでおけば良さそうでした。

ただ、[github](https://github.com/jenkinsci/lockable-resources-plugin)を見ても使えそうで使えないという感じなんですよね・・・。
なんせ複数のstageに跨ってlockをかけられない・・・！
色々なところに挿入してみたものの構文エラーが多発し断念・・・。

## stagesのネスト

方向性は合っているはずとあきらめきれずググってみたところ↓のようなブログを発見。
https://www.prkz.de/blog/20-jenkins-using-lockable-resource-across-multiple-stages

`stages`は入れ子にできるのか・・・！！！

ということで↓のような形に。

```groovy
pipeline {
  agent none

  stages {
    stage('milestone') {
      agent { label 'master' }
      steps {
        script {
          def buildNumber = env.BUILD_NUMBER as int
          if (buildNumber > 1) milestone(buildNumber - 1)
          milestone(buildNumber)
        }
      }
    }
    stage ('lock') {
      options { lock("${JOB_BASE_NAME}") }
      stages {
        stage ('build') {
          agent { label 'slave' }
          steps {
            ...
          }
        }
      }
    }
  }
}
```

ちなみに`options`と同じレイヤに`agent`を指定すると`@N`が付くことがあったためstagesの中に入れています。


これで`@N`が付かなくなった！workspaceは1つでよくなったぞ！目的達成！
・・・の前にもう1課題。

## stagesの入れ子2

やりたいことは達成できたものの、`stage`すべてに`agent`を指定しないといけない状態でした。
と、ここで思い出します。`stages`は入れ子にできた・・・ということは？

```groovy
pipeline {
  agent none

  stages {
    stage('milestone') {
      agent { label 'master' }
      steps {
        script {
          def buildNumber = env.BUILD_NUMBER as int
          if (buildNumber > 1) milestone(buildNumber - 1)
          milestone(buildNumber)
        }
      }
    }
    stage ('lock') {
      options { lock("${JOB_BASE_NAME}") }
      stages {
        stage ('agent') {
          agent { label 'slave' }
          stages
  { // インデント調整用
    stage('build-step1') {
      steps {
        ...
      }
    }
    stage('build-step2') {
      steps {
        ...
      }
    }
  }}}}
  }
}
```

これは酷い。
酷いんだけど必要要件はこれで揃いました。
ここからもう少し楽にできないか見てみたのですがうまくいかず・・・。
私はこの辺りで妥協しました。

求）更にリファクタしたコード

# 完成

というわけで(妥協の結果とはいえ)古いジョブはキャンセルしつつ、最新のジョブのみを実行することができました。
が、まだ問題点も残っているようで・・・(Jenkins側の問題ではないかと疑っている)

## TIPS

### `post`は`stages`に対応する

つまり、以下のように書くことでlockを外す前にpost処理を行うことができます。

```groovy
pipeline {
  agent none

  stages {
    stage('milestone') { ... }
    stage ('lock') {
      options { lock("${JOB_BASE_NAME}") }
      stages {
        stage ('agent') {
          agent { label 'slave' }
  stages   { // インデント調整
    ...
  }
  post
  {
    success { ... }
    failure { ... }
  }
  }}}}
  }
}
```

## 問題点

### 問題点1 : キャンセルされないジョブがある

タイミングによっては古いジョブがキャンセルしきれず終わらないことがありました。
その場合は単純に待つか急ぎであればコンソールログ上に表示される`Click here to forcibly terminate running steps`というリンクを踏めば良さそうです。

### 問題点2 : `@N`が付く状態が固定化されることがある

今回の作業中試行錯誤をしたせいなのか、短い時間で連打したせいなのか`@N`が固定化されてしまい、単独でトリガーしても`@N`が付いたままになってしまうことがありました。
こちらについては以下のページにある通り再起動すると直ったのでJenkinsのバグだと思います。
https://stackoverflow.com/a/71828212

# 最後に

いかがでしたでしょうか？
最新ビルドだけを実行することができればビルドマシンを効率よく利用できますね！
~~かなり~~若干見づらいコードとなってしまったのは残念ですが、もう少し効率のいい書き方を発見できたらこの記事をアップデートしようと思います！
ではまた別の記事でお会いしましょう！