Redmine連携 » 履歴 » バージョン 1
廣瀬 僚一, 2026/06/01 17:20
| 1 | 1 | 廣瀬 僚一 | # MainWP 更新履歴 Redmine 自動連携 |
|---|---|---|---|
| 2 | |||
| 3 | ## 概要 |
||
| 4 | |||
| 5 | MainWP でプラグイン・テーマ・WordPress 本体を更新した際に、Redmine へ自動でチケットを作成する仕組み。 |
||
| 6 | |||
| 7 | - **対象サーバー**: MainWP ダッシュボードサーバー |
||
| 8 | - **方式**: MU-Plugins に WordPress フックを記述し、Redmine REST API へ直接 POST |
||
| 9 | - **WP Webhooks プラグインは不使用**(Basic 認証・IP 制限の影響を避けるため) |
||
| 10 | |||
| 11 | --- |
||
| 12 | |||
| 13 | ## ファイル構成 |
||
| 14 | |||
| 15 | ``` |
||
| 16 | /wp-content/mu-plugins/mainwp-webhook.php ← メインファイル |
||
| 17 | /wp-content/mainwp-redmine-debug.log ← デバッグログ(確認後削除可) |
||
| 18 | ``` |
||
| 19 | |||
| 20 | --- |
||
| 21 | |||
| 22 | ## 設置ファイル |
||
| 23 | |||
| 24 | ### `/wp-content/mu-plugins/mainwp-webhook.php` |
||
| 25 | |||
| 26 | ```php |
||
| 27 | <?php |
||
| 28 | /** |
||
| 29 | * MainWP更新後にRedmineへ直接通知 |
||
| 30 | * 設置場所: /wp-content/mu-plugins/mainwp-webhook.php |
||
| 31 | */ |
||
| 32 | |||
| 33 | define( 'REDMINE_URL', 'https://your-redmine.example.com' ); |
||
| 34 | define( 'REDMINE_API_KEY', 'YOUR_REDMINE_API_KEY' ); |
||
| 35 | define( 'REDMINE_PROJECT', 'wordpress-updates' ); |
||
| 36 | |||
| 37 | // ===== プラグイン・テーマ・翻訳の更新後 ===== |
||
| 38 | add_action( 'mainwp_install_update_actions', function( $website, $action, $data, $type ) { |
||
| 39 | if ( 'updated' !== $action ) return; |
||
| 40 | if ( ! in_array( $type, array( 'plugin', 'theme', 'trans' ) ) ) return; |
||
| 41 | |||
| 42 | $type_label = [ |
||
| 43 | 'plugin' => 'プラグイン', |
||
| 44 | 'theme' => 'テーマ', |
||
| 45 | 'trans' => '翻訳', |
||
| 46 | ][ $type ] ?? $type; |
||
| 47 | |||
| 48 | $site = $website->url ?? '不明'; |
||
| 49 | $datetime = ( new DateTime( 'now', new DateTimeZone( 'Asia/Tokyo' ) ) )->format( 'Y-m-d H:i:s' ); |
||
| 50 | |||
| 51 | // WPバージョン取得 |
||
| 52 | $site_info = json_decode( $website->site_info ?? '{}', true ); |
||
| 53 | $wp_version = $site_info['wpversion'] ?? '不明'; |
||
| 54 | |||
| 55 | // 更新内容を取得 |
||
| 56 | $updated_data = isset( $data['updated_data'] ) ? $data['updated_data'] : array(); |
||
| 57 | $details = ''; |
||
| 58 | if ( is_array( $updated_data ) && ! empty( $updated_data ) ) { |
||
| 59 | foreach ( $updated_data as $item ) { |
||
| 60 | $name = $item['name'] ?? $item['slug'] ?? '不明'; |
||
| 61 | $old = $item['old_version'] ?? '?'; |
||
| 62 | $new = $item['version'] ?? '?'; |
||
| 63 | $details .= "- {$name}: {$old} → {$new}\n"; |
||
| 64 | } |
||
| 65 | } |
||
| 66 | if ( empty( $details ) ) return; |
||
| 67 | |||
| 68 | $subject = "[WP更新] {$site} / {$type_label}"; |
||
| 69 | $body = "## 更新情報\n\n" |
||
| 70 | . "- **サイト:** {$site}\n" |
||
| 71 | . "- **種別:** {$type_label}\n" |
||
| 72 | . "- **WPバージョン:** {$wp_version}\n" |
||
| 73 | . "- **作業日時:** {$datetime}\n\n" |
||
| 74 | . "## 更新内容\n\n{$details}"; |
||
| 75 | |||
| 76 | mainwp_post_to_redmine( $subject, $body ); |
||
| 77 | }, 10, 4 ); |
||
| 78 | |||
| 79 | // ===== WordPress本体の更新後 ===== |
||
| 80 | add_action( 'mainwp_after_wp_update', function( $information, $website ) { |
||
| 81 | $site = $website->url ?? '不明'; |
||
| 82 | $datetime = ( new DateTime( 'now', new DateTimeZone( 'Asia/Tokyo' ) ) )->format( 'Y-m-d H:i:s' ); |
||
| 83 | |||
| 84 | // wp_upgradesから更新前後バージョンを取得 |
||
| 85 | $wp_upgrades = json_decode( $website->wp_upgrades ?? '{}', true ); |
||
| 86 | $old = $wp_upgrades['current'] ?? $information['old_version'] ?? '?'; |
||
| 87 | $new = $wp_upgrades['new'] ?? $information['new_version'] ?? '?'; |
||
| 88 | |||
| 89 | $subject = "[WP更新] {$site} / WordPress本体"; |
||
| 90 | $body = "## 更新情報\n\n" |
||
| 91 | . "- **サイト:** {$site}\n" |
||
| 92 | . "- **種別:** WordPress本体\n" |
||
| 93 | . "- **更新前:** {$old}\n" |
||
| 94 | . "- **更新後:** {$new}\n" |
||
| 95 | . "- **作業日時:** {$datetime}\n"; |
||
| 96 | |||
| 97 | mainwp_post_to_redmine( $subject, $body ); |
||
| 98 | }, 10, 2 ); |
||
| 99 | |||
| 100 | // ===== Redmine API へ直接POST ===== |
||
| 101 | function mainwp_post_to_redmine( $subject, $body ) { |
||
| 102 | $data = [ |
||
| 103 | 'issue' => [ |
||
| 104 | 'project_id' => REDMINE_PROJECT, |
||
| 105 | 'subject' => $subject, |
||
| 106 | 'description' => $body, |
||
| 107 | 'tracker_id' => 2, |
||
| 108 | 'status_id' => 5, // 終了 |
||
| 109 | 'done_ratio' => 100, |
||
| 110 | ] |
||
| 111 | ]; |
||
| 112 | |||
| 113 | $ch = curl_init( REDMINE_URL . '/issues.json' ); |
||
| 114 | curl_setopt_array( $ch, [ |
||
| 115 | CURLOPT_RETURNTRANSFER => true, |
||
| 116 | CURLOPT_POST => true, |
||
| 117 | CURLOPT_POSTFIELDS => json_encode( $data ), |
||
| 118 | CURLOPT_HTTPHEADER => [ |
||
| 119 | 'Content-Type: application/json', |
||
| 120 | 'X-Redmine-API-Key: ' . REDMINE_API_KEY, |
||
| 121 | ], |
||
| 122 | CURLOPT_TIMEOUT => 15, |
||
| 123 | ] ); |
||
| 124 | |||
| 125 | $response = curl_exec( $ch ); |
||
| 126 | $status = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); |
||
| 127 | curl_close( $ch ); |
||
| 128 | |||
| 129 | // デバッグログ(確認後削除してOK) |
||
| 130 | file_put_contents( |
||
| 131 | WP_CONTENT_DIR . '/mainwp-redmine-debug.log', |
||
| 132 | date('Y-m-d H:i:s') . " status={$status} response={$response}\n", |
||
| 133 | FILE_APPEND |
||
| 134 | ); |
||
| 135 | } |
||
| 136 | ``` |
||
| 137 | |||
| 138 | --- |
||
| 139 | |||
| 140 | ## 設定値 |
||
| 141 | |||
| 142 | | 定数 | 説明 | |
||
| 143 | |------|------| |
||
| 144 | | `REDMINE_URL` | Redmine の URL(末尾スラッシュなし) | |
||
| 145 | | `REDMINE_API_KEY` | Redmine の個人 API キー(マイアカウント画面で確認) | |
||
| 146 | | `REDMINE_PROJECT` | Redmine のプロジェクト識別子 | |
||
| 147 | |||
| 148 | ### Redmine チケット設定 |
||
| 149 | |||
| 150 | | 項目 | 値 | 説明 | |
||
| 151 | |------|----|------| |
||
| 152 | | `tracker_id` | `2` | トラッカー ID(環境に合わせて変更) | |
||
| 153 | | `status_id` | `5` | 終了ステータス | |
||
| 154 | | `done_ratio` | `100` | 進捗率 100% | |
||
| 155 | |||
| 156 | --- |
||
| 157 | |||
| 158 | ## 動作する WordPress フック |
||
| 159 | |||
| 160 | | フック名 | タイミング | 引数 | |
||
| 161 | |----------|-----------|------| |
||
| 162 | | `mainwp_install_update_actions` | プラグイン・テーマ・翻訳の更新後 | `$website, $action, $data, $type` | |
||
| 163 | | `mainwp_after_wp_update` | WordPress 本体の更新後 | `$information, $website` | |
||
| 164 | |||
| 165 | ### バージョン情報の取得元 |
||
| 166 | |||
| 167 | | 情報 | 取得元 | |
||
| 168 | |------|--------| |
||
| 169 | | プラグイン更新前バージョン | `$data['updated_data'][]['old_version']` | |
||
| 170 | | プラグイン更新後バージョン | `$data['updated_data'][]['version']` | |
||
| 171 | | WP バージョン | `$website->site_info` の `wpversion` | |
||
| 172 | | WP 本体更新前バージョン | `$website->wp_upgrades` の `current` | |
||
| 173 | | WP 本体更新後バージョン | `$website->wp_upgrades` の `new` | |
||
| 174 | |||
| 175 | --- |
||
| 176 | |||
| 177 | ## 調査過程でわかったこと |
||
| 178 | |||
| 179 | - `mainwp_after_plugin_theme_translation_update` フックは存在するが、**引数 `$information` が空で渡ってくる**ため使用不可 |
||
| 180 | - バージョン情報は `$information` ではなく `$data['updated_data']` に格納されている |
||
| 181 | - `mainwp_install_update_actions` フックが正しいバージョン情報を持つ唯一のフック |
||
| 182 | - WP Webhooks プラグインの無料版では `WordPress hook fired` が使えない(Pro 限定) |
||
| 183 | - Basic 認証・IP 制限がかかった環境では `wp_remote_post` による自サーバーへの HTTP リクエストが 401 で弾かれるため、中継 PHP ファイルを経由せず Redmine API へ直接 POST する構成が必要 |
||
| 184 | |||
| 185 | --- |
||
| 186 | |||
| 187 | ## デバッグログの確認 |
||
| 188 | |||
| 189 | ```bash |
||
| 190 | cat /var/www/html/wp-content/mainwp-redmine-debug.log |
||
| 191 | ``` |
||
| 192 | |||
| 193 | `status=201` であれば Redmine へのチケット作成成功。確認後はログ出力コードを削除してよい。 |