Drupal の管理画面から GitHub Actions をトリガーするカスタムモジュール「GitHub Webhook」を改善しました。
https://github.com/nakamura196/Drupal-module-github_webhook
元は複数リポジトリ対応の基本的なモジュールでしたが、UI のタブ分離、権限の細分化、ワークフローステータス表示、自動トリガーなどの機能を追加しています。
改善前のモジュール#
元のモジュールは、以下のような構成でした。
- ファイル数 : 5ファイル(
info.yml、routing.yml、links.menu.yml、permissions.yml、SettingsForm.php) - 対応バージョン : Drupal 10 のみ
- リポジトリ : 複数対応済み(AJAX で動的追加・削除)
- 画面 : 設定とトリガーが同一画面(アコーディオン2つ)
- 権限 :
access github webhook settings の1権限のみ(設定もトリガーも同じ権限) - トークン管理 : パスワードフィールドに
#default_value を設定(HTML ソースに平文で出力される) - HTTP クライアント :
new \GuzzleHttp\Client() を直接インスタンス化 - 例外クラス :
use 文なしで catch ブロックに記述(名前空間の解決が不正)
変更の全体像#
改善前後のファイル構成の比較です。* は変更、+ は新規追加を示します。
1. 単一画面からタブ分離へ#
Before#
設定とトリガーが1つの画面にアコーディオンで並んでいました。管理者もコンテンツ編集者も同じ画面を使うため、一般ユーザーにトークン入力欄が見えてしまう問題がありました。
After#
Drupal の Local Tasks (タブ)を使い、3つの画面に分離しました。一般ユーザーには「Trigger」タブのみが表示されます。
最も利用頻度の高い「Trigger」タブをデフォルトのルートに設定し、コンテンツ編集者が迷わない設計にしています。
2. 権限の分離#
Before#
カスタム権限 access github webhook settings が1つだけ定義されており、設定変更もトリガーも同じ権限でアクセスしていました。
After#
管理者向けと一般ユーザー向けの2つの権限に分離しました。
restrict access: true を付けることで、Drupal の権限画面で管理者権限に警告マークが表示されます。
この分離により、管理者が PAT(Personal Access Token)を登録しておけば、GitHub アカウントを持っていない一般ユーザーでもビルドをトリガーできます。成功メッセージの内容もロールに応じて切り替えています。
3. トークンセキュリティの改善#
Before#
パスワードフィールドに #default_value を設定していたため、HTML ソースにトークンが平文で出力されていました。
After#
#default_value を削除し、保存済みかどうかを説明文で示すようにしました。
0
保存時には、空欄の場合は既存のトークンを維持します。
1
Key モジュール連携(新機能)#
Key モジュールがインストールされている場合、トークンの保管方法を切り替えられるようにしました。Drupal の #states API でフォームフィールドを動的に切り替えます。
2
Key モジュールを使えば、トークンを環境変数や HashiCorp Vault に保管でき、Drupal データベースや drush config:export にトークンが含まれなくなります。
4. 設定スキーマの追加と設定構造の拡張#
Before#
複数リポジトリ対応は済んでいたものの、設定スキーマ(schema.yml)が存在せず、設定のバリデーションや型チェックが効いていませんでした。また、自動トリガー関連の設定もありませんでした。
After#
設定スキーマを新規に定義し、リポジトリ設定に token_source、token_key、workflow_file フィールドを追加。自動トリガー関連の設定も追加しました。
3
5. ビジネスロジックのサービス化#
Before#
Webhook のトリガー処理がフォームクラスの triggerWebhook() メソッドに直接実装されていました。
4
After#
WebhookTriggerService としてサービスに切り出しました。フォームからも Entity フック(自動トリガー)からも呼び出せます。
5
6
HTTP クライアントも \Drupal::httpClient() に変更し、Drupal のサービスコンテナ経由で取得するようにしました。
6. GitHub Actions ステータス表示(新機能)#

トリガー後に GitHub にアクセスしなくても、ワークフローの実行状況を Drupal 上で確認できる機能を追加しました。
アーキテクチャ#
7
管理者が登録した PAT をサーバーサイドで使って GitHub API を呼び出すため、一般ユーザーは GitHub のアカウントがなくてもステータスを確認できます。
ステータスはカラードットで視覚的に表示されます。
| ステータス | 色 | 表示 |
|---|
| Queued | 黄色 | 静止 |
| In progress | 青 | パルスアニメーション |
| Success | 緑 | 静止 |
| Failed | 赤 | 静止 |
| Cancelled | グレー | 静止 |
管理者にはワークフロー実行の GitHub URL がリンクとして表示され、一般ユーザーにはテキストのみが表示されます。
サブディレクトリ配下での運用#
Drupal がサブディレクトリ(例: https://example.com/cms/)で運用されている場合、ステータス API のパスもサブディレクトリを含む必要があります。当初は /github-webhook/api/status とハードコードしていましたが、これではサブディレクトリ配下で動作しません。
Drupal の URL ジェネレーターを使い、ルートからベース URL を動的に生成するようにしました。
8
これにより、/cms/github-webhook/api/status のようなサブディレクトリ付きパスが正しく生成されます。
なお、ルートのパラメータ repo_index に \d+ の制約があるため、プレースホルダに文字列を渡すとエラーになります。数値 0 を渡してベース URL を構築し、末尾の /0 を削除する方法で対応しています。
注意点: トリガーしたランの特定#
repository_dispatch API は HTTP 204 (No Content) を返すため、トリガーされたワークフローの Run ID を直接取得できません。そのため、event=repository_dispatch でフィルタした最近の実行一覧を表示するアプローチを取っています。
7. コンテンツ保存時の自動トリガー(新機能)#
hook_entity_insert と hook_entity_update を実装し、ノードの保存時に自動的に Webhook をトリガーする機能を追加しました。
9
管理画面の「Auto Trigger」タブから、トリガー対象のコンテンツタイプとリポジトリを選択できます。ビジネスロジックをサービスに切り出したことで、フォームのサブミットハンドラと Entity フックの両方から同じ処理を呼び出せています。
8. Drupal 11 対応#
Before#
0
After#
1
コード面でも以下の改善を行いました。
| 改善前 | 改善後 | 理由 |
|---|
new \GuzzleHttp\Client() | \Drupal::httpClient() | Drupal のサービスコンテナを通すべき |
use 文なしで例外を catch | use GuzzleHttp\Exception\... を追加 | 名前空間の解決が不正だった |
\Drupal::messenger()->addMessage() | $this->messenger()->addMessage() | MessengerTrait を使うべき |
composer.json も新規作成し、Drupal.org の標準に準拠しました。
2
9. 多言語対応(新機能)#
すべての UI 文字列を $this->t() / Drupal.t() でラップし、日本語翻訳ファイルを同梱しました。
3
JavaScript 側の文字列も Drupal.t() を使用しているため、Drupal の翻訳システムで管理できます。
翻訳ファイルの自動インポート#
Drupal はカスタムモジュールの translations/ ディレクトリにある .po ファイルを自動的にはインポートしません。info.yml に interface translation server pattern を記述する方法もありますが、モジュールの配置パス(modules/custom/ や modules/contrib/ など)をハードコーディングする必要があり、環境によって動作しない問題があります。
そこで hook_locale_translation_projects_alter() を使い、モジュールのパスを動的に解決するようにしました。
4
これにより、モジュールがどのディレクトリに配置されていても、翻訳ファイルが自動的にインポートされます。Drupal の管理画面で日本語を追加するだけで UI が翻訳されます。
10. 開発環境(Docker)#
ローカルでの検証用に Docker 環境を追加しました。
5
モジュールのディレクトリをボリュームマウントし、ホスト側のファイル変更がコンテナに即座に反映されるようにしています。
付録: Fine-grained PAT の作成手順#
このモジュールでは、GitHub の Fine-grained Personal Access Token を使用します。Classic PAT よりも細かく権限を制御でき、対象リポジトリも限定できます。
作成手順#
- GitHub の Settings > Developer settings > Personal access tokens > Fine-grained tokens にアクセス
- Token name にわかりやすい名前を入力(例:
drupal-webhook) - Expiration で有効期限を設定
- Repository access で Only select repositories を選び、対象リポジトリを選択
- Repository permissions で以下を設定:
| 権限 | 値 | 用途 |
|---|
| Contents | Read and write | repository_dispatch イベントの送信(必須) |
| Actions | Read | ワークフロー実行ステータスの取得(任意) |
- Generate token をクリックし、生成されたトークン(
github_pat_ で始まる)をコピー
注意事項#
Actions: Read を付与しない場合、ステータス表示機能は動作しません(トリガー自体は可能)- Classic PAT の
repo スコープは権限が広すぎるため、Fine-grained PAT を推奨します - トークンの有効期限切れに注意してください。期限が近づいたら GitHub の設定画面から再生成が必要です
変更のまとめ#
| 項目 | Before | After |
|---|
| 対応バージョン | Drupal 10 のみ | Drupal 10 / 11 |
| 画面構成 | 設定とトリガーが同一画面 | 3タブに分離 |
| 権限 | 1権限(設定もトリガーも共通) | 管理者 / 一般ユーザーの2段階 |
| トークン保管 | #default_value に設定(危険) | パスワードフィールド + Key モジュール連携 |
| HTTP クライアント | new GuzzleHttp\Client() | \Drupal::httpClient() |
| ビジネスロジック | フォームクラスに直接記述 | サービスクラスに分離 |
| ステータス表示 | なし | GitHub API ポーリング + JS レンダリング |
| ステータス絞り込み | なし | ワークフローファイル名で絞り込み可能 |
| 自動トリガー | なし | Entity フックでコンテンツ保存時に自動実行 |
| 多言語対応 | なし | .po ファイル(日本語対応) |
| 設定スキーマ | なし | github_webhook.schema.yml |
| composer.json | なし | Drupal.org 準拠 |
| Docker 環境 | なし | Dockerfile + docker-compose.yml |
GitHub にアクセスできない一般ユーザーでも、Drupal の管理画面からビルドのトリガーとステータス確認ができるようになり、ヘッドレス CMS 構成でのワークフロー自動化に活用しやすくなりました。