Drupal の管理画面から GitHub Actions をトリガーするカスタムモジュール「GitHub Webhook」を改善しました。

https://github.com/nakamura196/Drupal-module-github_webhook

元は複数リポジトリ対応の基本的なモジュールでしたが、UI のタブ分離、権限の細分化、ワークフローステータス表示、自動トリガーなどの機能を追加しています。

改善前のモジュール

元のモジュールは、以下のような構成でした。

  • ファイル数 : 5ファイル(info.ymlrouting.ymllinks.menu.ymlpermissions.ymlSettingsForm.php
  • 対応バージョン : Drupal 10 のみ
  • リポジトリ : 複数対応済み(AJAX で動的追加・削除)
  • 画面 : 設定とトリガーが同一画面(アコーディオン2つ)
  • 権限 : access github webhook settings の1権限のみ(設定もトリガーも同じ権限)
  • トークン管理 : パスワードフィールドに #default_value を設定(HTML ソースに平文で出力される)
  • HTTP クライアント : new \GuzzleHttp\Client() を直接インスタンス化
  • 例外クラス : use 文なしで catch ブロックに記述(名前空間の解決が不正)
$]f;o'''r###mttd[yie:'ptfselae'eut'lt=ti>=_n>vg'a#sp$ld'atue]shef[si'a'wsugo-=lir>>ttdt_h'($vu,'cabGol_inuttfeoHikugeb-n>'Tg]oekt=e(n'['g)i,thub_token'),HTML
$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

変更の全体像

改善前後のファイル構成の比較です。* は変更、+ は新規追加を示します。

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

1. 単一画面からタブ分離へ

Before

設定とトリガーが1つの画面にアコーディオンで並んでいました。管理者もコンテンツ編集者も同じ画面を使うため、一般ユーザーにトークン入力欄が見えてしまう問題がありました。

/admi[[nST/erctiotOSgTniwugrfnnbeiigemrggsrig/]tWeg/eribtRhGheoiupotbokH_]uw/bebTWhoeokboehknoo/kEventType

After

Drupal の Local Tasks (タブ)を使い、3つの画面に分離しました。一般ユーザーには「Trigger」タブのみが表示されます。

/gith[[[uTRAbreu-iptwgooegsbeiThrtro]oiorgkig/eessre]]ttings
#gggiiigtrtbtrtbtrtbihoiahoiahoiatuutsuutsuutshbtlebtlebtleu_ee__ee__ee_bw_:rw_:rw_:r_enoenoenowba'uba'uba'uehmTthmRthmAtboereoeeeoeueho:i:o:p:o:t:okgkokoo.ggg.gsg.ggktieiriiiaiTi.rtrtetttutrtlih'hphohthihiguuouruougungbbsbib_bgbke__i_e_t_e_srwwtwswrwrw._eeoe'eie'ettbbrbbgbbaahhihhghhsbooeooeook:oosooroo.kk_kk_kky..t..t..mttartaatlrrberburii:pi:tiggogogggsg_geeieterrtrrroirgigeesr

最も利用頻度の高い「Trigger」タブをデフォルトのルートに設定し、コンテンツ編集者が迷わない設計にしています。

2. 権限の分離

Before

カスタム権限 access github webhook settings が1つだけ定義されており、設定変更もトリガーも同じ権限でアクセスしていました。

#acctdeiestsslcerg:iipt'thAiucobcne:wses'bAhGloilotokHwusbuestWeteribsnhgotsoo:kaScectetsisngGsi'tHubwebhookconfiguration.'

After

管理者向けと一般ユーザー向けの2つの権限に分離しました。

#atdrgmtdritdiiieegietntssgtshilctelcuserrrerbt:ii:i_epcgpwr'tti'teAitTibgdoahrohimncuinoti:cbg:ohnegkui'swe'.bsCserTpto:brewenhGirerftoigmbirotgihGgukHesoiue:ursotrbikHero:uWenbrepsebo.Wphsyeooimbsotlhikoot'rooykr_'ideiss,pattockhenwse,bhaonodksa.u'to-triggersettings.'

restrict access: true を付けることで、Drupal の権限画面で管理者権限に警告マークが表示されます。

この分離により、管理者が PAT(Personal Access Token)を登録しておけば、GitHub アカウントを持っていない一般ユーザーでもビルドをトリガーできます。成功メッセージの内容もロールに応じて切り替えています。

i}}f$)e$)t;lt;\h$sh$Diteitrshshu-i{-ip>s>sGam-m-ile>e>t:ststH:s(s(uce'e"bun.nGrg.giAre.etcerrHtn(<(uit)a)boU--ns>h>wsearaerdedb(dfdh)M=Mo-e"eo>s:skhsusaaratsglgrPe"eie((grtgmaeirrsgesedito=sn"u(_c'bcaledasmnsikfn"ui>lsVltiyeerwfogArictt@hiruoebnpsow<se/ibath>oo'ro,yk.'[")',):u[{r.l.'.]=)>$actions_url])

3. トークンセキュリティの改善

Before

パスワードフィールドに #default_value を設定していたため、HTML ソースにトークンが平文で出力されていました。

'#defa:ult_value'=>$config->get('github_token'),

After

#default_value を削除し、保存済みかどうかを説明文で示すようにしました。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

0

保存時には、空欄の場合は既存のトークンを維持します。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

1

Key モジュール連携(新機能)

Key モジュールがインストールされている場合、トークンの保管方法を切り替えられるようにしました。Drupal の #states API でフォームフィールドを動的に切り替えます。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

2

Key モジュールを使えば、トークンを環境変数や HashiCorp Vault に保管でき、Drupal データベースや drush config:export にトークンが含まれなくなります。

4. 設定スキーマの追加と設定構造の拡張

Before

複数リポジトリ対応は済んでいたものの、設定スキーマ(schema.yml)が存在せず、設定のバリデーションや型チェックが効いていませんでした。また、自動トリガー関連の設定もありませんでした。

After

設定スキーマを新規に定義し、リポジトリ設定に token_sourcetoken_keyworkflow_file フィールドを追加。自動トリガー関連の設定も追加しました。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

3

5. ビジネスロジックのサービス化

Before

Webhook のトリガー処理がフォームクラスの triggerWebhook() メソッドに直接実装されていました。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

4

After

WebhookTriggerService としてサービスに切り出しました。フォームからも Entity フック(自動トリガー)からも呼び出せます。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

5

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

6

HTTP クライアントも \Drupal::httpClient() に変更し、Drupal のサービスコンテナ経由で取得するようにしました。

6. GitHub Actions ステータス表示(新機能)

トリガー後に GitHub にアクセスしなくても、ワークフローの実行状況を Drupal 上で確認できる機能を追加しました。

アーキテクチャ

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

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 を動的に生成するようにしました。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

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_inserthook_entity_update を実装し、ノードの保存時に自動的に Webhook をトリガーする機能を追加しました。

$clien:tG=uznzelwe\GuzzleHttpn\eCwlient();

9

管理画面の「Auto Trigger」タブから、トリガー対象のコンテンツタイプとリポジトリを選択できます。ビジネスロジックをサービスに切り出したことで、フォームのサブミットハンドラと Entity フックの両方から同じ処理を呼び出せています。

8. Drupal 11 対応

Before

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

0

After

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

1

コード面でも以下の改善を行いました。

改善前改善後理由
new \GuzzleHttp\Client()\Drupal::httpClient()Drupal のサービスコンテナを通すべき
use 文なしで例外を catchuse GuzzleHttp\Exception\... を追加名前空間の解決が不正だった
\Drupal::messenger()->addMessage()$this->messenger()->addMessage()MessengerTrait を使うべき

composer.json も新規作成し、Drupal.org の標準に準拠しました。

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

2

9. 多言語対応(新機能)

すべての UI 文字列を $this->t() / Drupal.t() でラップし、日本語翻訳ファイルを同梱しました。

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

3

JavaScript 側の文字列も Drupal.t() を使用しているため、Drupal の翻訳システムで管理できます。

翻訳ファイルの自動インポート

Drupal はカスタムモジュールの translations/ ディレクトリにある .po ファイルを自動的にはインポートしません。info.ymlinterface translation server pattern を記述する方法もありますが、モジュールの配置パス(modules/custom/modules/contrib/ など)をハードコーディングする必要があり、環境によって動作しない問題があります。

そこで hook_locale_translation_projects_alter() を使い、モジュールのパスを動的に解決するようにしました。

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

4

これにより、モジュールがどのディレクトリに配置されていても、翻訳ファイルが自動的にインポートされます。Drupal の管理画面で日本語を追加するだけで UI が翻訳されます。

10. 開発環境(Docker)

ローカルでの検証用に Docker 環境を追加しました。

git+++++++++++++++huggggscggggssssjcctDdbiiiiroiiiirrrrssoroo_ttttcmttttccccsnaccwhhhh/phhhh///g/fnkkeuuuuF:ouuuuFFSCigiseebbbbbosbbbbooeotiglrrh____re____rrrnht/af-owwwwmrwwwwmmtuhstico:eeee/.eeee//irbucilokbbbbSjbbbbTAco-bhoem/hhhheshhhhruelw-enpooootoooooitlewmsoooootnoooogWebeaskkkkikkkkgTerhbje....n....erb/ohga.irlpglslmrihSooi.ynoiesieioFgotkotpmfunrFnrbdogoa-kholotkmokvrurekts-u.isirsialmrTutsbyn.sm.cre.Frsat_mgms.teipoiCtawl.eipasehrgouteynohs.spmgnsubmunpky..et.shl.s.myprrj.oy.ylmhSoscomymlpelsklmlrls.lvesircc.hepe.hm############pp##a##h.D11CJEpyroSnmum/tlppCi##aoStDls3SyW#o2eec14rbk1heorKoekyPHJPSONAPI

5

モジュールのディレクトリをボリュームマウントし、ホスト側のファイル変更がコンテナに即座に反映されるようにしています。

付録: Fine-grained PAT の作成手順

このモジュールでは、GitHub の Fine-grained Personal Access Token を使用します。Classic PAT よりも細かく権限を制御でき、対象リポジトリも限定できます。

作成手順

  1. GitHub の Settings > Developer settings > Personal access tokens > Fine-grained tokens にアクセス
  2. Token name にわかりやすい名前を入力(例: drupal-webhook
  3. Expiration で有効期限を設定
  4. Repository accessOnly select repositories を選び、対象リポジトリを選択
  5. Repository permissions で以下を設定:
権限用途
ContentsRead and writerepository_dispatch イベントの送信(必須)
ActionsReadワークフロー実行ステータスの取得(任意)
  1. Generate token をクリックし、生成されたトークン(github_pat_ で始まる)をコピー

注意事項

  • Actions: Read を付与しない場合、ステータス表示機能は動作しません(トリガー自体は可能)
  • Classic PAT の repo スコープは権限が広すぎるため、Fine-grained PAT を推奨します
  • トークンの有効期限切れに注意してください。期限が近づいたら GitHub の設定画面から再生成が必要です

変更のまとめ

項目BeforeAfter
対応バージョン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 構成でのワークフロー自動化に活用しやすくなりました。