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 ブロックに記述(名前空間の解決が不正)
// 改善前: トークンが #default_value に設定されていた
$form['settings']['github_token'] = [
  '#type' => 'password',
  '#title' => $this->t('GitHub Token'),
  '#default_value' => $config->get('github_token'),  // HTML に平文出力される
];
// 改善前: Guzzle クライアントを直接 new していた
$client = new \GuzzleHttp\Client();

変更の全体像

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

github_webhook/
  既存ファイル(変更):
  * github_webhook.info.yml           # Drupal 11 対応、PHP 要件追加
  * github_webhook.routing.yml        # 1ルート → 4ルートに拡張
  * github_webhook.links.menu.yml     # エントリポイント変更
  * github_webhook.permissions.yml    # 1権限 → 2権限に分離
  * src/Form/SettingsForm.php         # トリガー部分を分離、Key モジュール連携追加

  新規ファイル:
  + composer.json                     # Composer パッケージ定義
  + github_webhook.links.task.yml     # タブ定義(3タブ)
  + github_webhook.services.yml       # サービス定義
  + github_webhook.libraries.yml      # JS/CSS ライブラリ定義
  + github_webhook.module             # Entity フック(自動トリガー)
  + src/Form/TriggerForm.php          # 手動トリガー画面
  + src/Form/AutoTriggerForm.php      # 自動トリガー設定画面
  + src/Service/WebhookTriggerService.php  # Webhook 実行サービス
  + src/Controller/StatusController.php    # ワークフロー状態 JSON API
  + js/github-webhook-status.js       # ステータスポーリング
  + css/github-webhook-status.css     # ステータス表示スタイル
  + config/schema/github_webhook.schema.yml  # 設定スキーマ
  + translations/ja.po                # 日本語翻訳
  + Dockerfile                        # 検証用 Docker 環境
  + docker-compose.yml

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

Before

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

/admin/config/github_webhook
├── [Settings] アコーディオン  ← 管理者用
│   ├── Owner / Repo / Token / Event Type
│   └── Submit
└── [Trigger Webhook] アコーディオン  ← 一般ユーザー用
    └── Trigger GitHub Webhook ボタン

After

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

/github-webhook/settings
├── [Trigger] タブ         ← 一般ユーザー向け(デフォルト)
├── [Repositories] タブ    ← 管理者のみ
└── [Auto Trigger] タブ    ← 管理者のみ
# github_webhook.links.task.yml(新規)
github_webhook.trigger_tab:
  route_name: github_webhook.trigger
  title: 'Trigger'
  base_route: github_webhook.trigger

github_webhook.repositories_tab:
  route_name: github_webhook.repositories
  title: 'Repositories'
  base_route: github_webhook.trigger

github_webhook.auto_trigger_tab:
  route_name: github_webhook.auto_trigger
  title: 'Auto Trigger'
  base_route: github_webhook.trigger

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

2. 権限の分離

Before

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

# 改善前
access github webhook settings:
  title: 'Access GitHub Webhook Settings'
  description: 'Allow users to access GitHub webhook configuration.'

After

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

# github_webhook.permissions.yml
administer github webhook:
  title: 'Administer GitHub Webhook'
  description: 'Configure repositories, tokens, and auto-trigger settings.'
  restrict access: true

trigger github webhook:
  title: 'Trigger GitHub Webhook'
  description: 'Trigger repository_dispatch webhooks.'

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

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

// 管理者には GitHub Actions へのリンクを表示
if (\Drupal::currentUser()->hasPermission('administer github webhook')) {
  $this->messenger()->addMessage(
    $this->t('... View Actions', [':url' => $actions_url])
  );
} else {
  // 一般ユーザーにはテキストのみ
  $this->messenger()->addMessage(
    $this->t("GitHub webhook triggered successfully for @repository.", [...])
  );
}

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

Before

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

// 改善前: 危険
'#default_value' => $config->get('github_token'),

After

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

$has_manual_token = !empty($repos[$row_no]["github_token"]);
$token_field = [
  "#type" => "password",
  "#title" => $this->t("GitHub Token"),
  "#placeholder" => "github_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "#description" => $has_manual_token
    ? $this->t("A token is already saved. Leave blank to keep the current token.")
    : $this->t("Enter your GitHub personal access token. ..."),
];

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

if (!empty($github_token)) {
  $current_repo["github_token"] = $github_token;
} else {
  $existing_config = $this->config('github_webhook.settings')->get('repositories');
  $current_repo["github_token"] = $existing_config[$row_no]["github_token"] ?? "";
}

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

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

if ($key_module_available) {
  $form["repositories"]["repo" . $row_no][$row_no]["token_source"] = [
    "#type" => "select",
    "#options" => [
      "manual" => $this->t("Manual input"),
      "key" => $this->t("Key module"),
    ],
  ];
}

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

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

Before

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

After

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

# config/schema/github_webhook.schema.yml(新規)
github_webhook.settings:
  type: config_object
  mapping:
    repositories:
      type: sequence
      sequence:
        type: mapping
        mapping:
          owner:
            type: string
          repo:
            type: string
          event_type:
            type: string
          token_source:
            type: string
          github_token:
            type: string
          token_key:
            type: string
          workflow_file:
            type: string
    auto_trigger_enabled:
      type: boolean
    auto_trigger_content_types:
      type: sequence
      sequence:
        type: string
    auto_trigger_repositories:
      type: sequence
      sequence:
        type: integer

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

Before

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

// 改善前: フォームクラスにロジックが直接記述
public function triggerWebhook(array &$form, FormStateInterface $form_state) {
  $client = new \GuzzleHttp\Client();
  // ... API 呼び出し
}

After

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

class WebhookTriggerService {
  public function triggerRepository(array $repository): bool { ... }
  public function resolveToken(array $repository): ?string { ... }
  public function getWorkflowRuns(array $repository, int $perPage = 5): array { ... }
}
# github_webhook.services.yml(新規)
services:
  github_webhook.trigger:
    class: Drupal\github_webhook\Service\WebhookTriggerService

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

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

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

アーキテクチャ

[TriggerForm]
     drupalSettings で設定を渡す
[github-webhook-status.js]
     fetch()  5秒間隔ポーリング
[StatusController] /github-webhook/api/status/{repo_index}
     サービス経由で GitHub API を呼び出し
[WebhookTriggerService::getWorkflowRuns()]
     event=repository_dispatch でフィルタ
[GitHub API] /repos/{owner}/{repo}/actions/runs

管理者が登録した 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 を動的に生成するようにしました。

$status_url = \Drupal\Core\Url::fromRoute('github_webhook.status', ['repo_index' => 0])->toString();
$status_base_url = preg_replace('#/0$#', '', $status_url);

これにより、/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 をトリガーする機能を追加しました。

// github_webhook.module(新規)
function _github_webhook_auto_trigger(EntityInterface $entity) {
  if ($entity->getEntityTypeId() !== 'node') {
    return;
  }

  $config = \Drupal::config('github_webhook.settings');
  if (!$config->get('auto_trigger_enabled')) {
    return;
  }

  // コンテンツタイプのチェック
  $content_types = $config->get('auto_trigger_content_types') ?? [];
  if (!in_array($entity->bundle(), $content_types)) {
    return;
  }

  // 対象リポジトリに対してトリガー実行
  $trigger_service = \Drupal::service('github_webhook.trigger');
  foreach ($auto_trigger_repos as $repo_index) {
    $trigger_service->triggerRepository($repositories[$repo_index]);
  }
}

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

8. Drupal 11 対応

Before

core_version_requirement: ^10

After

core_version_requirement: ^10 || ^11
php: 8.3

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

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

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

{
  "name": "drupal/github_webhook",
  "type": "drupal-module",
  "require": {
    "drupal/core": "^10 || ^11",
    "php": ">=8.3"
  },
  "suggest": {
    "drupal/key": "For secure token storage via environment variables, files, or external services."
  }
}

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

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

# translations/ja.po
msgid "Trigger Webhook"
msgstr "Webhook をトリガー"

msgid "Loading workflow status..."
msgstr "ワークフローの状態を読み込み中..."

msgid "GitHub webhook triggered successfully for @repository."
msgstr "@repository の GitHub Webhook を正常にトリガーしました。"

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

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

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

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

// github_webhook.module
function github_webhook_locale_translation_projects_alter(&$projects) {
  $module_handler = \Drupal::service('module_handler');
  $module_path = $module_handler->getModule('github_webhook')->getPath();
  $projects['github_webhook'] = [
    'info' => [
      'interface translation project' => 'github_webhook',
      'interface translation server pattern' => $module_path . '/translations/%language.po',
    ],
  ];
}

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

10. 開発環境(Docker)

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

# Dockerfile(新規)
FROM drupal:11-apache
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
RUN composer require drush/drush --no-interaction --working-dir=/opt/drupal

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

付録: 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 構成でのワークフロー自動化に活用しやすくなりました。