こんにちは。テクマトリックスの長久保です。
今回はJenkinsとRedmineを連携させる方法についていくつか紹介します。それぞれ単体でも便利なツールですが、例えば連携させることで以下のメリットを得ることができます。

  • 進捗の可視化
    Jenkinsの実行結果に応じたRedmineのステータス更新を自動化することで、プロジェクトやチケットの進捗を可視化し、進行状況の把握が容易になります。
  • 情報の一元管理
    ビルドのログやテスト結果など、プロジェクトに関する全ての情報をRedmine上で一元管理することができます。これにより、情報の拡散を防ぎ、必要な情報をすぐに取得できるようになります。
  • 時間の節約
    Jenkinsジョブの実行結果に応じて、Redmineのチケットを自動作成するようにしておけば、手動でのチケット作成にかかる時間を削減できるため、作業の効率化が図れます。

はじめに

前提

SCMは任意のSCMで問題ありませんが、コミット時にはチケット番号をコミットコメントに「refs #1234」といった形式で付与してコミットすることを前提とします。
Jenkinsから呼び出すRedmineのREST APIはPythonで記載します。PythonにはRequestsのインストールが必要です。

利用した環境

今回は以下の環境を利用しました。

Jenkins2.375.3
Redmine5.0.5.stable
Python3.10.11

下準備

今回Jenkins→Redmineの連携はREST APIを用いて実行します。
そのため、Redmine側でREST APIの利用を許可する必要があります。

  • APIアクセスを有効化
    管理者アカウントでログインし、上部のメニューから「管理」をクリックして管理画面にアクセスします。管理画面で「設定」をクリックし、表示される設定タブの中から「API」タブを選択します。ここで、「RESTによるWebサービスを有効にする」オプションにチェックを入れ、「保存」ボタンをクリックして設定を保存します。
  • APIキーの取得
    REST APIを使用するユーザーのアカウントでログインし、右上の「個人設定」をクリックしてユーザーメニューを開き、「APIアクセスキー」の表示をクリックするとAPIアクセスキーが生成されます。
  • REST APIの実行確認
    詳細は公式のWikiを参照ください。curlコマンドを実行してジョブ一覧が取得できるか確認します。
    • RedmineUrl:RedmineのURL
    • ProjectId:ジョブを取得するプロジェクトのID
    • ApiAccessKey:先ほど取得したAPIアクセスキー
curl -X GET "https://RedmineUrl/issues.json?project_id=ProjectId&limit=10" -H "X-Redmine-API-Key: ApiAccessKey"
プロジェクトに関連づいているチケットをAPIで取得できました。

JenkinsとRedmineの連携

チケットステータスの更新

Jenkinsの実行状況をRedmine上でも確認できるようになります。
例えば以下のような利用ケースが考えられます。

  • コミット時にSCMのWebhookやフックスクリプトでJenkinsが実行された際に、Redmineのチケットステータスを「Jenkins実行中」に更新する。
  • 問題なくJenkinsのジョブの実行が完了した場合は「Jenkins正常終了」にステータスを更新する。
  • コンパイルエラーなどでJenkinsのジョブが失敗したときは「Jenkins異常終了」にステータスを更新する。

このようなステータスを作成し、Jenkinsの実行状況に応じてステータスを更新することで、各チケットステータスの一元管理ができます。

またRedmineのステータスの更新がされるため、Redmineから通知をすることもできます。

更新対象のステータスの確認

チケットのステータスを更新するにはステータスIDが必要になります。
ステータスIDは以下の方法で確認できます。

  • Redmineにログインし、トップページから「管理」をクリックします。
  • 「チケットのステータス」をクリックし、IDを調べたい確認したいステータスを選択します。
  • URLを確認し、issue_statuses/の後ろに表示されている数字がIDとなります。

更新対象のチケットIDの確認

どのチケットを更新するかを指定するにはチケットIDが必要になります。今回はコミット時のコミットコメントからチケットIDを取得します。

前提に記載の通りコミットコメントは「refs #1234」といった形式でついてコミットコメントに記載されていることとします。
Jenkinsと連携しているSCMによってコミットコメントの取得方法が異なりますが、Gitプラグインを利用している場合、Gitの環境変数であるGIT_COMMITを活用し、パイプラインのScriptステップを利用しコミットコメントを取得できます。

steps {
  script {
    // コミットハッシュを取得
    def gitCommitHash = env.GIT_COMMIT
    // コミットハッシュからコミットコメントを取得
    env.GIT_COMMIT_MSG = bat(script: "git show -s --pretty=%%B ${commitHash}", returnStdout: true).trim()
  }
}

それ以外にも一度checkout scmを挟むことでcurrentBuild.changeSetsを設定し、コミットメッセージを取得する方法もあります。

steps {
  checkout scm // チェックアウトすることで currentBuild.changeSets が設定される
  script {
    for (def changeSet in currentBuild.changeSets) {
      for (def commit in changeSet.items) {
        env.GIT_COMMIT_MSG = commit.msg
      }
    }
  }
}

取得したコミットコメントから正規表現等でチケット番号を取得します。

スクリプトの準備

チケットステータスを更新するPythonのスクリプトを作成します。
第一引数にはチケット番号、第二引数に更新するステータスを渡せるようにします。

import requests
import sys

redmine_api_access_key = '★RedmineのApiAccessKey'
redmine_base_url = '★RedmineのURL(ホームをクリックした際に表示されるURL)'

ticket_id = sys.argv[1]
new_status_id = sys.argv[2]

url = f'{redmine_base_url}/issues/{ticket_id}.json'
headers = {
    'Content-Type': 'application/json',
    'X-Redmine-API-Key': redmine_api_access_key ,
}

data = {
    'issue': {
        'status_id': new_status_id,
    }
}

response = requests.put(url, headers=headers, json=data)

# ステータス更新のみだとレスポンスで返ってくる内容がなく204なので204も追加
if response.status_code == 200 or response.status_code == 204:
    print('チケットステータスが正常に更新されました')
else:
    print(f'更新に失敗しました: {response.status_code} - {response.text}')

★がついている箇所はそれぞれ利用環境に合わせた内容で置換してください。

パイプラインの記載

Jenkinsのジョブ実行結果に応じて、Redmineのステータスを更新します。
例えばチケットに関連するジョブが実行中の時は「Jenkins実行中」、ジョブが正常終了したら「Jenkins正常終了」、ジョブが異常終了したら「Jenkins異常終了」とします。
条件分岐にはpostセクションを利用します。

上記条件を満たすパイプラインを作成します。以下のようなイメージになります。

pipeline {
    agent any
    environment {
        JENKINS_RUNNING = '7' //Jenkins実行中
        JENKINS_SUCCESS = '8' //Jenkins正常終了
        JENKINS_FAILURE = '9' //Jenkins異常終了
    }
    stages {
        //コミットコメント取得
        stage('init') {
            steps {
                script {
                    String commitHash = env.GIT_COMMIT
                    env.GIT_COMMIT_MSG = bat(script: "git show -s --pretty=%%B ${commitHash}", returnStdout: true).trim()
                }
                //「Jenkins実行中」に更新
                bat(script: "python update_redmine_ticket_status.py ${env.GIT_COMMIT_MSG} ${env.JENKINS_RUNNING}")
            }
        }
        stage('build'){
            steps {
                echo 'ビルド'
            }
        }
    }
    post {
        success{
            //「Jenkins正常終了」に更新
            bat(script: "python update_redmine_ticket_status.py ${env.GIT_COMMIT_MSG} ${env.JENKINS_SUCCESS}")
        }
        failure{
            //「Jenkins異常終了」に更新
            bat(script: "python update_redmine_ticket_status.py ${env.GIT_COMMIT_MSG} ${env.JENKINS_FAILURE}")
        }
    }
}

Jenkinsのジョブが完了するとRedmineのステータスが更新されるようにできます。ジョブの実行状態について、Redmine上で確認するだけで把握できるようになります。

テスト結果をRedmine上で確認

Jenkinsで実行したテスト結果をRedmine上で確認することもできます。Redmineの仕様上、一度ファイルをアップロードし、アップロードしたファイルをチケットに添付するという形で実装します。
https://www.redmine.org/projects/redmine/wiki/Rest_api#Attaching-files

import requests
import json
import sys
import os

def upload_file_to_ticket(ticket_id, file_path):
    # RedmineのAPIキーとエンドポイントURL
    redmine_api_access_key = '★RedmineのApiAccessKey'
    redmine_base_url = '★RedmineのURL(ホームをクリックした際に表示されるURL)'

    # ファイルをバイナリモードで読み込む
    with open(file_path, 'rb') as f:
        file_content = f.read()

    # ファイルのアップロード
    headers = {'Content-Type': 'application/octet-stream', 'X-Redmine-API-Key': redmine_api_access_key}
    upload_response = requests.post(f'{redmine_base_url}/uploads.json', headers=headers, data=file_content)

    upload_data = json.loads(upload_response.content)

    # アップロードしたファイルをチケットに添付
    headers = {'Content-Type': 'application/json', 'X-Redmine-API-Key': redmine_api_access_key}
    data = {
        'issue': {
            'uploads': [{
                'token': upload_data['upload']['token'],
                'filename': os.path.basename(file_path),
                'content_type': 'application/octet-stream'
            }]
        }
    }
    issue_response = requests.put(f'{redmine_base_url}/issues/{ticket_id}.json', headers=headers, data=json.dumps(data))

    print('ファイル添付成功')

# コマンドライン引数からチケットIDとファイルパスを取得
ticket_id, file_path = sys.argv[1], sys.argv[2]

# ファイルのアップロードとチケットへの添付を実行
upload_file_to_ticket(ticket_id, file_path)

このような添付するためのスクリプトを作成し、1.チケットステータスの更新で記載したようにパイプラインを作成すればRedmineにJenkinsの成果物を添付できます。
成果物を保存や、JUnitプラグインを利用することでJenkins上からも確認できますが、このようにRedmineに実行結果を添付することで情報の一元管理が可能になります。

Redmineの画面。

レビューする方はRedmineのステータスが「Jenkins正常終了」「Jenkins異常終了」のチケットを開くと、テスト結果が添付されているためレビューもしやすいかと思います。
また、ファイルを添付する際にコメントを追加することもできます。コメントにJenkinsのジョブのURLを入力しておくことで、何か問題があった場合はJenkinsのコンソールログを確認しに行くといったことも可能です。

Redmineの画面。test_result.pdfが作成されたJenkinsのジョブのURLをコメントに追加。

また、今回はテスト結果を直接添付する例を紹介しましたが、

  • 運用上コミットコメントに複数チケット番号が記載される
  • テスト結果のファイルサイズが大きい

といった場合、テスト結果を直接Redmineに添付するのではなく、JenkinsのジョブのURLのみを記載する方が良いケースもあります。

チケットの自動生成

静的解析で検出された警告がなかなか修正されずに溜まっていくといったこともあるかもしれません。そういった場合は「静的解析の実行結果で検出された件数が○○件以上であれば修正するチケットを作成する」といった形でチケット化してしまえば運用に組み込めそうです。
Redmineのチケット作成は以下のリファレンスを参照にしながらjsonファイルを作成します。今までPythonでコードを書いてきましたが、今回はcurlコマンドでjsonをRedmineに送ります。
https://www.redmine.org/projects/redmine/wiki/Rest_Issues#Creating-an-issue

{
    "issue": {
      "project_id": "★Redmineのプロジェクト名",
      "subject": "静的解析の実行結果修正",
      "description": "静的解析の実行結果で静的解析違反が1000個を超えました。修正してください。",
      "priority_id": 5
    }
  }
  
curl -X POST -H "Content-Type: application/json" -H "X-Redmine-API-Key: {★RedmineのApiAccessKey}" -d @{static_test_issues.jsonのパス} {★RedmineのURL(ホームをクリックした際に表示されるURL)/issues.json

Jenkinsのパイプラインは次のようになります。

pipeline {
    agent any
    stages {
        stage('Static Analysis') {
            steps {
                script {
                    // 静的解析ツールの実行
                    def result = sh(script: '静的解析の実行コマンド', returnStdout: true).trim()
                    // 結果から違反数を取得
                    //getViolationCountFromResultについては静的解析ツールに応じて実装する
                    def sa_data = new XmlSlurper().parse(new File("${env.WORKSPACE}/reports/static-analysis/report.xml"))
                    def sa_viol_count = sa_data.CodingStandards.TestedFilesDetails.Total['@total']
                    if (sa_viol_count.toInteger() >= 100) {
                        sh "curl -X POST -H "Content-Type: application/json" -H "X-Redmine-API-Key: {★RedmineのApiAccessKey}" -d @{static_test_issues.jsonのパス} {★RedmineのURL(ホームをクリックした際に表示されるURL)/issues.json"
                    }
                }
            }
        }
    }
}

例えば弊社取り扱い製品であるJtestの場合、report.xmlに静的解析の実行結果が出力されるため、上記のようにXmlSlurperでパースし、件数を取得できます。パースの方法については静的解析の出力方法によって異なります。

このようにJenkinsの実行結果を受け、自動でチケット化することもできます。
今回は静的解析の結果に応じてのチケット作成を例として紹介しましたが、それ以外の場合でも有効活用できる例はたくさんあるでしょう。

まとめ

今回はJenkinsとRedmineを連携し、以下の活用方法について紹介しました。

  • Jenkinsのジョブの実行結果に応じて、Redmineのステータスを更新
  • RedmineにJenkinsで実行されたテスト結果の添付やジョブのURLの記載
  • Jenkinsのジョブの実行結果に応じてRedmineのチケット作成

それぞれ単体でも便利なツールですが、ツールを連携することでより便利に活用できます。参考になれば幸いです。

弊社テクマトリックスのソフトウェア開発基盤構築ソリューションチームでは、ツール単体の活用だけでなく、複数ツールを組み合わせたソフトウェア開発ツールチェーン構築サービスを提供しています。現在の開発プロセスについて相談に乗りながら、開発者が開発に集中できる環境作成を支援し、生産性を最大化する開発環境の構築支援をいたしますので、ぜひご相談ください。

By nagakubo

主にCI環境構築をメインで担当しています。 Certified CloudBees Jenkins Engineer (CCJE)