🧑‍💻

#175 DiscordのスラッシュコマンドでGitHub Actionsを起動する

に公開

概要

前回の記事では、discord botを無料でデプロイしてスラッシュコマンドを受け付ける方法を解説しました。
今回は、実際に有用なスラッシュコマンドを登録してみて、それを実行してみるまでを記載しようと思います。

登録するのは、以下のような形式で任意のGitHub actionを起動できるコマンドです。

text
/run repository:<リポジトリ> workflow_file:<ワークフローYAML> ref:<ブランチ(任意)>

Discord画面

事前準備

前回記事の内容の通りにテスト用のrunコマンドの登録まで行う。

手順

1. PAT払い出し & Vercel環境変数に登録

  • GitHubでアカウントのSettings > Developer settings > Personal access tokens からトークンを発行
    • 必要権限: Contents(Fine-grained tokens)/repo(Classic tokens)
  • Vercelのプロジェクトページ > Settings > Environment Variables にGITHUB_TOKENとして登録

2. ファイル配置(前回記事との差分のみ)

  • api/handlers/runCommand.ts
    • runコマンドの処理を定義しています。エラーの場合、入力の問題関連は実行者にだけ見える形で返信、サーバーの問題と思われる場合はチャンネルに通知します。
    • URLのownerは自身のGitHub環境に合わせて書き換えてください
api/handlers/runCommand.ts
import { Context } from 'hono';
import {
  InteractionResponseType,
  InteractionResponseFlags
} from 'discord-interactions';

export const runCommandHandler = {
  name: 'run',
  execute: async (c: Context, data: any) => {
    const repository = data.options?.find((opt: any) => opt.name === 'repository')?.value;
    const workflowFile = data.options?.find((opt: any) => opt.name === 'workflow_file')?.value;
    const ref = data.options?.find((opt: any) => opt.name === 'ref')?.value || 'main';

    if (!repository || !workflowFile) {
      return c.json({
        type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
        data: {
          content: '❌ リポジトリ名とワークフローファイル名は必須です。',
          flags: InteractionResponseFlags.EPHEMERAL
        }
      });
    }

    try {
      // GitHub Actions ワークフロー実行
      const response = await fetch(
        `https://api.github.com/repos/owner/${repository}/actions/workflows/${workflowFile}/dispatches`,
        {
          method: 'POST',
          headers: {
            'Authorization': `token ${process.env.GITHUB_TOKEN}`,
            'Accept': 'application/vnd.github.v3+json',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            ref: ref,
          }),
        }
      );

      if (response.ok) {
        return c.json({
          type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
          data: {
            content: `✅ GitHub Actions を起動しました!\nリポジトリ: ${repository}\nワークフロー: ${workflowFile}\nブランチ: ${ref}`,
          }
        });
      } else {
        const errorData = await response.json();

        // 404エラーの場合、利用可能なワークフローを取得
        if (response.status === 404) {
          const workflowsResponse = await fetch(
            `https://api.github.com/repos/owner/${repository}/contents/.github/workflows?ref=${ref}`,
            {
              headers: {
                'Authorization': `token ${process.env.GITHUB_TOKEN}`,
                'Accept': 'application/vnd.github.v3+json',
              },
            }
          );

          if (workflowsResponse.ok) {
            const workflows = await workflowsResponse.json() as any[];
            const availableWorkflows = workflows
              .filter((wf: any) => wf.type === 'file' && wf.name.endsWith('.yml'))
              .map((wf: any) => wf.name)
              .join(', ');

            return c.json({
              type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
              data: {
                content: `❌ ワークフロー "${workflowFile}" が見つかりません。\n利用可能なワークフロー: ${availableWorkflows}`,
                flags: InteractionResponseFlags.EPHEMERAL
              }
            });
          }
        }

        return c.json({
          type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
          data: {
            content: `❌ GitHub Actions の起動に失敗しました: ${JSON.stringify(errorData)}`,
          }
        });
      }
    } catch (error) {
      return c.json({
        type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
        data: {
          content: `❌ エラーが発生しました: ${error}`,
        }
      });
    }
  }
};
  • scripts/registerCommand.ts
    • コマンド登録用スクリプト
    • REPOSITORY_CHOICESは自身のGitHub環境に合わせて書き換えてください
scripts/registerCommand.ts
const fetch = require('node-fetch');
require('dotenv').config();

// Discord API定数(discord-interactionsにないので手動定義)
const ApplicationCommandOptionType = {
  STRING: 3,
} as const;

const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
const DISCORD_GUILD_ID = process.env.DISCORD_GUILD_ID;

const REPOSITORY_CHOICES = [
  { name: 'api', value: 'api' },
  { name: 'front', value: 'front' },
];

const command = {
  name: 'run',
  description: 'GitHub Actions を実行します',
  options: [
    {
      name: 'repository',
      description: 'リポジトリ名',
      type: ApplicationCommandOptionType.STRING,
      required: true,
      choices: REPOSITORY_CHOICES,
    },
    {
      name: 'workflow_file',
      description: 'ワークフローファイル名 (例: ci.yml)',
      type: ApplicationCommandOptionType.STRING,
      required: true,
    },
    {
      name: 'ref',
      description: 'ブランチ名 (デフォルト: main)',
      type: ApplicationCommandOptionType.STRING,
      required: false,
    },
  ],
};

async function registerCommand() {

  const res = await fetch(`https://discord.com/api/v10/applications/${DISCORD_CLIENT_ID}/guilds/${DISCORD_GUILD_ID}/commands`, {
    method: 'POST',
    headers: {
      Authorization: `Bot ${DISCORD_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(command),
  });

  const responseText = await res.text();

  if (!res.ok) {
    console.error('❌ コマンド登録に失敗しました');
    console.error(`エラー: ${res.status} - ${responseText}`);
    process.exit(1);
  }

  const responseData = JSON.parse(responseText);
  console.log('コマンド登録が成功しました!');
  console.log(`コマンドID: ${responseData.id}`);
}

registerCommand().catch(console.error);

3. 再デプロイ

vercel --prod

4. コマンド登録しなおし

ts-node scripts/registerCommand.ts

5. 対象リポジトリでYAMLファイル準備

  • 対象リポジトリの.github/workflows/<任意>.ymlにアクションファイルを作成
    • workflow_dispatch:があることを確認

最小例:

name: Discord Bot Test

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Show message
      run: echo "Hello from Discord bot!"

使い方

  1. Discordの対象サーバーで/runを入力
  2. repositoryworkflow_file を指定
  3. 必要なら ref を指定(未指定の場合はmain
  4. botから実行結果に応じた返信が来ます

Discord画面

おまけ:アクションの開始/終了時にdiscordに通知する

GitHubマーケットプレイスで配布されている、Action Status Discordを利用することで比較的簡単にdiscordへの通知を行えます。

手順

  1. discordのサーバー設定 > 連携サービス > ウェブフック > 新しいウェブフック からウェブフックURLを発行
  2. GitHubのリポジトリ > Settings > Secrets and variables > Actions にDISCORD_WEBHOOKという名前で発行したウェブフックURLを登録
  3. .github/workflowsのymlファイルに通知アクションを追加

アクションファイル例:

name: Discord Bot Test

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Notify Start
      uses: sarisia/actions-status-discord@v1
      if: always()
      with:
        webhook: ${{ secrets.DISCORD_WEBHOOK }}
        noprefix: true
        color: 0x5865F2
    - name: Simulate long task
      run: |
        echo "Starting long task..."
        for i in {1..5}; do
          echo "Working... step $i/5"
          sleep 10
        done
        echo "Long task finished."
    - name: Notify End
      uses: sarisia/actions-status-discord@v1
      if: always()
      with:
        webhook: ${{ secrets.DISCORD_WEBHOOK }}

Discord画面

まとめ

今回はDiscord Botを活用したCI/CDについて紹介しました。
Github Actionsに限らず色々活用できると思いますので、Discordを使った開発をする際は有効に活用していきたいですね。
最後まで読んでいただきありがとうございます。

参考

Discussion