# リポジトリの Webhook の失敗した配信を自動的に再配信する

リポジトリの Webhook の失敗した配信を処理するスクリプトを作成できます。

## 失敗した配信を自動的に再配信する方法について

この記事では、リポジトリ Webhook の失敗した配信を検出して再配信するスクリプトを記述する方法について説明します。 失敗した配信の詳細については、「[webhookの失敗した配信の処理](/ja/webhooks/using-webhooks/handling-failed-webhook-deliveries)」を参照してください。

この例では、次のことを示します。

* リポジトリの Webhook の失敗した配信を検出して再配信するスクリプト
* スクリプトに必要な資格情報と、資格情報を GitHub Actions シークレットとして安全に格納する方法
* 資格情報に安全にアクセスし、スクリプトを定期的に実行できる GitHub Actions ワークフロー

この例では GitHub Actionsを使用しますが、Webhook 配信を処理するサーバーでこのスクリプトを実行することもできます。 詳細については、「[その他の方法](#alternative-methods)」を参照してください。

## スクリプトの資格情報を格納する

ビルトインの `GITHUB_TOKEN` には、Webhook を再配信するための十分なアクセス許可がありません。 この例では、 `GITHUB_TOKEN`を使用する代わりに、 personal access tokenを使用します。 または、 personal access tokenを作成する代わりに、 GitHub App を作成し、アプリの資格情報を使用して、 GitHub Actions ワークフロー中にインストール アクセス トークンを作成することもできます。 詳しくは、「[GitHub Actions ワークフローでGitHub アプリを使用して認証済み API 要求を作成する](/ja/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)」をご覧ください。

1. 次のアクセス権を持つ personal access token を作成します。 詳しくは、「[個人用アクセス トークンを管理する](/ja/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)」をご覧ください。
   * fine-grained personal access tokenの場合は、トークンを付与します。
     * Webhook が作成されたリポジトリへのアクセス
     * このワークフローが実行されるリポジトリへのアクセス
     * リポジトリの Webhook 権限への書き込みアクセス
     * リポジトリの変数権限への書き込みアクセス
   * personal access token (classic)の場合は、トークンに`repo`スコープを付与します。
2. ワークフローを実行するリポジトリに personal access token を GitHub Actions シークレットとして格納します。 詳しくは、「[GitHub Actions でのシークレットの使用](/ja/actions/security-guides/encrypted-secrets)」をご覧ください。

## スクリプトを実行するワークフローを追加する

このセクションでは、 GitHub Actions ワークフローを使用して、前のセクションで保存した資格情報に安全にアクセスし、環境変数を設定し、失敗した配信を見つけて再配信するスクリプトを定期的に実行する方法について説明します。

この GitHub Actions ワークフローを、ワークフローを実行するリポジトリの `.github/workflows` ディレクトリ内の YAML ファイルにコピーします。 次に示すように、`Run script` 手順のプレースホルダーを置き換えます。

```yaml copy annotate
#
name: Redeliver failed webhook deliveries

# This workflow runs every 6 hours or when manually triggered.
on:
  schedule:
    - cron: '20 */6 * * *'
  workflow_dispatch:

# This workflow will use the built in `GITHUB_TOKEN` to check out the repository contents. This grants `GITHUB_TOKEN` permission to do that.
permissions:
  contents: read

#
jobs:
  redeliver-failed-deliveries:
    name: Redeliver failed deliveries
    runs-on: ubuntu-latest
    steps:
      # This workflow will run a script that is stored in the repository. This step checks out the repository contents so that the workflow can access the script.
      - name: Check out repo content
        uses: actions/checkout@v6

      # This step sets up Node.js. The script that this workflow will run uses Node.js.
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18.x'

      # This step installs the octokit library. The script that this workflow will run uses the octokit library.
      - name: Install dependencies
        run: npm install octokit

      # This step sets some environment variables, then runs a script to find and redeliver failed webhook deliveries.
      # - Replace `YOUR_SECRET_NAME` with the name of the secret where you stored your personal access token.
      # - Replace `YOUR_REPO_OWNER` with the owner of the repository where the webhook was created.
      # - Replace `YOUR_REPO_NAME` with the name of the repository where the webhook was created.
      # - Replace `YOUR_HOOK_ID` with the ID of the webhook.
      # - Replace `YOUR_LAST_REDELIVERY_VARIABLE_NAME` with the name that you want to use for a configuration variable that will be stored in the repository where this workflow is stored. The name can be any string that contains only alphanumeric characters and `_`, and does not start with `GITHUB_` or a number. For more information, see [AUTOTITLE](/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows).
      
      - name: Run script
        env:
          TOKEN: ${{ secrets.YOUR_SECRET_NAME }}
          REPO_OWNER: 'YOUR_REPO_OWNER'
          REPO_NAME: 'YOUR_REPO_NAME'
          HOOK_ID: 'YOUR_HOOK_ID'
          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
          
          WORKFLOW_REPO_NAME: ${{ github.event.repository.name }}
          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
        run: |
          node .github/workflows/scripts/redeliver-failed-deliveries.js
```

## スクリプトを追加する

このセクションでは、失敗した配信を検出して再配信するスクリプトを記述する方法について説明します。

このスクリプトを、上記の`.github/workflows/scripts/redeliver-failed-deliveries.js` ワークフロー ファイルを保存したのと同じリポジトリ内の GitHub Actions という名前のファイルにコピーします。

```javascript copy annotate
// This script uses GitHub's Octokit SDK to make API requests. For more information, see [AUTOTITLE](/rest/guides/scripting-with-the-rest-api-and-javascript).
const { Octokit } = require("octokit");

//
async function checkAndRedeliverWebhooks() {
  // Get the values of environment variables that were set by the GitHub Actions workflow.
  const TOKEN = process.env.TOKEN;
  const REPO_OWNER = process.env.REPO_OWNER;
  const REPO_NAME = process.env.REPO_NAME;
  const HOOK_ID = process.env.HOOK_ID;
  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
  
  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;
  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;

  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.
  const octokit = new Octokit({ 
    auth: TOKEN,
  });

  try {
    // Get the last time that this script ran from the configuration variable. If the variable is not defined, use the current time minus 24 hours.
    const lastStoredRedeliveryTime = await getVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
    });
    const lastWebhookRedeliveryTime = lastStoredRedeliveryTime || (Date.now() - (24 * 60 * 60 * 1000)).toString();

    // Record the time that this script started redelivering webhooks.
    const newWebhookRedeliveryTime = Date.now().toString();

    // Get the webhook deliveries that were delivered after `lastWebhookRedeliveryTime`.
    const deliveries = await fetchWebhookDeliveriesSince({
      lastWebhookRedeliveryTime,
      repoOwner: REPO_OWNER,
      repoName: REPO_NAME,
      hookId: HOOK_ID,
      octokit,
    });

    // Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.
    let deliveriesByGuid = {};
    for (const delivery of deliveries) {
      deliveriesByGuid[delivery.guid]
        ? deliveriesByGuid[delivery.guid].push(delivery)
        : (deliveriesByGuid[delivery.guid] = [delivery]);
    }

    // For each GUID value, if no deliveries for that GUID have been successfully delivered within the time frame, get the delivery ID of one of the deliveries with that GUID.
    //
    // This will prevent duplicate redeliveries if a delivery has failed multiple times.
    // This will also prevent redelivery of failed deliveries that have already been successfully redelivered.
    let failedDeliveryIDs = [];
    for (const guid in deliveriesByGuid) {
      const deliveries = deliveriesByGuid[guid];
      const anySucceeded = deliveries.some(
        (delivery) => delivery.status === "OK"
      );
      if (!anySucceeded) {
        failedDeliveryIDs.push(deliveries[0].id);
      }
    }

    // Redeliver any failed deliveries.
    for (const deliveryId of failedDeliveryIDs) {
      await redeliverWebhook({
        deliveryId,
        repoOwner: REPO_OWNER,
        repoName: REPO_NAME,
        hookId: HOOK_ID,
        octokit,
      });
    }

    // Update the configuration variable (or create the variable if it doesn't already exist) to store the time that this script started.
    // This value will be used next time this script runs.
    await updateVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      value: newWebhookRedeliveryTime,
      variableExists: Boolean(lastStoredRedeliveryTime),
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
    });

    // Log the number of redeliveries.
    console.log(
      `Redelivered ${
        failedDeliveryIDs.length
      } failed webhook deliveries out of ${
        deliveries.length
      } total deliveries since ${Date(lastWebhookRedeliveryTime)}.`
    );
  } catch (error) {
    // If there was an error, log the error so that it appears in the workflow run log, then throw the error so that the workflow run registers as a failure.
    if (error.response) {
      console.error(
        `Failed to check and redeliver webhooks: ${error.response.data.message}`
      );
    }
    console.error(error);
    throw(error);
  }
}

// This function will fetch all of the webhook deliveries that were delivered since `lastWebhookRedeliveryTime`.
// It uses the `octokit.paginate.iterator()` method to iterate through paginated results. For more information, see [AUTOTITLE](/rest/guides/scripting-with-the-rest-api-and-javascript#making-paginated-requests).
//
// If a page of results includes deliveries that occurred before `lastWebhookRedeliveryTime`,
// it will store only the deliveries that occurred after `lastWebhookRedeliveryTime` and then stop.
// Otherwise, it will store all of the deliveries from the page and request the next page.
async function fetchWebhookDeliveriesSince({
  lastWebhookRedeliveryTime,
  repoOwner,
  repoName,
  hookId,
  octokit,
}) {
  const iterator = octokit.paginate.iterator(
    "GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries",
    {
      owner: repoOwner,
      repo: repoName,
      hook_id: hookId,
      per_page: 100,
      headers: {
        "x-github-api-version": "2026-03-10",
      },
    }
  );

  const deliveries = [];

  for await (const { data } of iterator) {
    const oldestDeliveryTimestamp = new Date(
      data[data.length - 1].delivered_at
    ).getTime();

    if (oldestDeliveryTimestamp < lastWebhookRedeliveryTime) {
      for (const delivery of data) {
        if (
          new Date(delivery.delivered_at).getTime() > lastWebhookRedeliveryTime
        ) {
          deliveries.push(delivery);
        } else {
          break;
        }
      }
      break;
    } else {
      deliveries.push(...data);
    }
  }

  return deliveries;
}

// This function will redeliver a failed webhook delivery.
async function redeliverWebhook({
  deliveryId,
  repoOwner,
  repoName,
  hookId,
  octokit,
}) {
  await octokit.request(
    "POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts",
    {
      owner: repoOwner,
      repo: repoName,
      hook_id: hookId,
      delivery_id: deliveryId,
    }
  );
}

// This function gets the value of a configuration variable.
// If the variable does not exist, the endpoint returns a 404 response and this function returns `undefined`.
async function getVariable({ variableName, repoOwner, repoName, octokit }) {
  try {
    const {
      data: { value },
    } = await octokit.request(
      "GET /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
      }
    );
    return value;
  } catch (error) {
    if (error.status === 404) {
      return undefined;
    } else {
      throw error;
    }
  }
}

// This function will update a configuration variable (or create the variable if it doesn't already exist). For more information, see [AUTOTITLE](/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows).
async function updateVariable({
  variableName,
  value,
  variableExists,
  repoOwner,
  repoName,
  octokit,
}) {
  if (variableExists) {
    await octokit.request(
      "PATCH /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
        value: value,
      }
    );
  } else {
    await octokit.request("POST /repos/{owner}/{repo}/actions/variables", {
      owner: repoOwner,
      repo: repoName,
      name: variableName,
      value: value,
    });
  }
}

// This will execute the `checkAndRedeliverWebhooks` function.
(async () => {
  await checkAndRedeliverWebhooks();
})();

```

## スクリプトをテストする

ワークフローを手動でトリガーし、スクリプトをテストします。 詳細については、「[ワークフローの手動実行](/ja/actions/using-workflows/manually-running-a-workflow)」および「[ワークフロー実行ログの使用](/ja/actions/monitoring-and-troubleshooting-workflows/using-workflow-run-logs)」を参照してください。

## その他の方法

この例では、 GitHub Actions を使用して資格情報を安全に格納し、スケジュールに従ってスクリプトを実行しました。 ただし、Webhook 配信を処理するサーバーでこのスクリプトを実行する場合は、次のことができます。

* [Azure Key Vault](https://azure.microsoft.com/products/key-vault) などのシークレットマネージャーなど、別の安全な方法で資格情報を格納します。 新しい場所から資格情報にアクセスするために、スクリプトも更新する必要があります。
* たとえば、cron ジョブやタスク スケジューラを使用して、サーバー上のスケジュールに従ってスクリプトを実行します。
* スクリプトを更新して、サーバーがアクセスして更新できる場所に最終実行時間を保存します。 前回の実行時を GitHub Actions シークレットとして格納しない場合は、API 呼び出しを削除して構成変数にアクセスして更新できます。