{"meta":{"title":"Автоматическая повторная доставка неудачных доставок для вебхука приложения GitHub","intro":"Вы можете написать скрипт для обработки неудачных поставок GitHub App веб-перехватчика.","product":"Веб-перехватчики","breadcrumbs":[{"href":"/ru/webhooks","title":"Веб-перехватчики"},{"href":"/ru/webhooks/using-webhooks","title":"Использование веб-перехватчиков"},{"href":"/ru/webhooks/using-webhooks/automatically-redelivering-failed-deliveries-for-a-github-app-webhook","title":"Автоматическое повторное создание GitHub App"}],"documentType":"article"},"body":"# Автоматическая повторная доставка неудачных доставок для вебхука приложения GitHub\n\nВы можете написать скрипт для обработки неудачных поставок GitHub App веб-перехватчика.\n\n## О автоматическом повторном развертывании неудачных поставок\n\nВ этой статье описывается, как написать скрипт для поиска и повторного выполнения доставки для GitHub App веб-перехватчика. Дополнительные сведения о неудачных поставках см. в разделе [Обработка неудачных поставок веб-перехватчика](/ru/webhooks/using-webhooks/handling-failed-webhook-deliveries).\n\nВ этом примере показано:\n\n* Скрипт, который будет находить и повторно выполнить доставку GitHub App для веб-перехватчика\n* Какие учетные данные понадобятся вашему скрипту и как безопасно хранить эти учетные данные как GitHub Actions секреты\n* Рабочий GitHub Actions процесс, который может безопасно получать доступ к вашим учетным данным и периодически запускать скрипт\n\nВ этом примере используется GitHub Actions, но вы также можете запустить этот скрипт на вашем сервере, который обрабатывает доставку webhook. Дополнительные сведения см. в разделе [\"Альтернативные методы](#alternative-methods)\".\n\n## Хранение учетных данных для скрипта\n\nКонечные точки для поиска и повторного создания веб-перехватчиков требуют веб-маркера JSON, который создается из идентификатора приложения и закрытого ключа для приложения.\n\nКонечные точки для получения и обновления значения переменных среды требуют personal access tokenмаркера доступа, GitHub App маркера доступа установки или GitHub App маркера доступа пользователей. В этом примере используется personal access token. Если ваш GitHub App репозиторий установлен в репозитории, где этот рабочий процесс будет выполняться и имеет разрешение на запись переменных репозитория, можно изменить этот пример, чтобы создать маркер доступа к установке во время GitHub Actions рабочего процесса вместо использования personal access token. Дополнительные сведения см. в разделе [Создание аутентифицированных запросов API с помощью приложения GitHub в рабочем процессе GitHub Actions](/ru/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow).\n\n1. Найдите идентификатор приложения для вашего GitHub App. Идентификатор приложения можно найти на странице параметров приложения. Идентификатор приложения отличается от идентификатора клиента. Для получения дополнительной информации о переходе на страницу настроек GitHub Appдля вашего , [смотрите АВТОЗАГОЛОВОК](/ru/apps/maintaining-github-apps/modifying-a-github-app-registration#navigating-to-your-github-app-settings).\n2. Сохраните идентификатор приложения из предыдущего шага в качестве секрета GitHub Actions в репозитории, где требуется запустить рабочий процесс. Дополнительные сведения о хранении секретов см. в разделе [Использование секретов в GitHub Actions](/ru/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).\n3. Создайте закрытый ключ для приложения. Дополнительные сведения о создании закрытого ключа см. в разделе [Управление приватными ключами для приложений GitHub](/ru/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps).\n4. Сохраните закрытый ключ, в том числе `-----BEGIN RSA PRIVATE KEY-----` и `-----END RSA PRIVATE KEY-----`из предыдущего шага в качестве GitHub Actions секрета в репозитории, где требуется запустить рабочий процесс.\n5. Создайте personal access token с помощью следующего доступа. Дополнительные сведения см. в разделе [Управление личными маркерами доступа](/ru/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).\n   * Для , присвоите fine-grained personal access tokenжетон:\n     * Разрешение на запись в переменные репозитория\n     * Доступ к репозиторию, в котором будет выполняться этот рабочий процесс.\n   * Для personal access token (classic), присвоите жетону область `repo` действия.\n6. Сохраните свой personal access token из предыдущего шага в качестве секрета GitHub Actions в репозитории, где требуется запустить рабочий процесс.\n\n## Добавление рабочего процесса, который будет запускать скрипт\n\nВ этом разделе демонстрируется, как можно использовать GitHub Actions рабочий процесс для безопасного доступа к учетным данным, которые вы хранили в предыдущем разделе, задать переменные среды и периодически запускать скрипт для поиска и повторной доставки неудачных поставок.\n\nСкопируйте этот GitHub Actions рабочий процесс в YAML-файл в `.github/workflows` каталоге репозитория, где вы хотите запустить рабочий процесс. Замените заполнители на шаге `Run script` , как описано ниже.\n\n```yaml copy annotate\n#\nname: Redeliver failed webhook deliveries\n\n# This workflow runs every 6 hours or when manually triggered.\non:\n  schedule:\n    - cron: '40 */6 * * *'\n  workflow_dispatch:\n\n# This workflow will use the built in `GITHUB_TOKEN` to check out the repository contents. This grants `GITHUB_TOKEN` permission to do that.\npermissions:\n  contents: read\n\n#\njobs:\n  redeliver-failed-deliveries:\n    name: Redeliver failed deliveries\n    runs-on: ubuntu-latest\n    steps:\n      # 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.\n      - name: Check out repo content\n        uses: actions/checkout@v6\n\n      # This step sets up Node.js. The script that this workflow will run uses Node.js.\n      - name: Setup Node.js\n        uses: actions/setup-node@v7\n        with:\n          node-version: '20.x'\n\n      # This step installs the octokit library. The script that this workflow will run uses the octokit library.\n      - name: Install dependencies\n        run: npm install octokit\n\n      # This step sets some environment variables, then runs a script to find and redeliver failed webhook deliveries.\n      # - Replace `YOUR_APP_ID_SECRET_NAME` with the name of the secret where you stored your app ID.\n      # - Replace `YOUR_PRIVATE_KEY_SECRET_NAME` with the name of the secret where you stored your private key.\n      # - Replace `YOUR_TOKEN_SECRET_NAME` with the name of the secret where you stored your personal access token.\n      # - 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).\n      \n      - name: Run script\n        env:\n          APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}\n          PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}\n          TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}\n          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'\n          \n          WORKFLOW_REPO: ${{ github.event.repository.name }}\n          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}\n        run: |\n          node .github/workflows/scripts/redeliver-failed-deliveries.mjs\n```\n\n## Добавление скрипта\n\nВ этом разделе показано, как создать скрипт для поиска и повторного создания неудачных поставок.\n\nСкопируйте этот скрипт в файл, вызываемый `.github/workflows/scripts/redeliver-failed-deliveries.mjs` в том же репозитории, где вы сохранили GitHub Actions вышеуказанный файл рабочего процесса.\n\n```javascript copy annotate\n// 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).\nimport { App, Octokit } from \"octokit\";\n\n//\nasync function checkAndRedeliverWebhooks() {\n  // Get the values of environment variables that were set by the GitHub Actions workflow.\n  const APP_ID = process.env.APP_ID;\n  const PRIVATE_KEY = process.env.PRIVATE_KEY;\n  const TOKEN = process.env.TOKEN;\n  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;\n  \n  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;\n  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;\n\n  // Create an instance of the octokit `App` using the app ID and private key values that were set in the GitHub Actions workflow.\n  //\n  // This will be used to make API requests to the webhook-related endpoints.\n  const app = new App({\n    appId: APP_ID,\n    privateKey: PRIVATE_KEY,\n  });\n\n  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.\n  //\n  // This will be used to update the configuration variable that stores the last time that this script ran.\n  const octokit = new Octokit({ \n    auth: TOKEN,\n  });\n\n  try {\n    // 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.\n    const lastStoredRedeliveryTime = await getVariable({\n      variableName: LAST_REDELIVERY_VARIABLE_NAME,\n      repoOwner: WORKFLOW_REPO_OWNER,\n      repoName: WORKFLOW_REPO_NAME,\n      octokit,\n    });\n    const lastWebhookRedeliveryTime = lastStoredRedeliveryTime || (Date.now() - (24 * 60 * 60 * 1000)).toString();\n\n    // Record the time that this script started redelivering webhooks.\n    const newWebhookRedeliveryTime = Date.now().toString();\n\n    // Get the webhook deliveries that were delivered after `lastWebhookRedeliveryTime`.\n    const deliveries = await fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app});\n\n    // Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.\n    let deliveriesByGuid = {};\n    for (const delivery of deliveries) {\n      deliveriesByGuid[delivery.guid]\n        ? deliveriesByGuid[delivery.guid].push(delivery)\n        : (deliveriesByGuid[delivery.guid] = [delivery]);\n    }\n\n    // 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.\n    //\n    // This will prevent duplicate redeliveries if a delivery has failed multiple times.\n    // This will also prevent redelivery of failed deliveries that have already been successfully redelivered.\n    let failedDeliveryIDs = [];\n    for (const guid in deliveriesByGuid) {\n      const deliveries = deliveriesByGuid[guid];\n      const anySucceeded = deliveries.some(\n        (delivery) => delivery.status === \"OK\"\n      );\n      if (!anySucceeded) {\n        failedDeliveryIDs.push(deliveries[0].id);\n      }\n    }\n\n    // Redeliver any failed deliveries.\n    for (const deliveryId of failedDeliveryIDs) {\n      await redeliverWebhook({deliveryId, app});\n    }\n\n    // Update the configuration variable (or create the variable if it doesn't already exist) to store the time that this script started.\n    // This value will be used next time this script runs.\n    await updateVariable({\n      variableName: LAST_REDELIVERY_VARIABLE_NAME,\n      value: newWebhookRedeliveryTime,\n      variableExists: Boolean(lastStoredRedeliveryTime),\n      repoOwner: WORKFLOW_REPO_OWNER,\n      repoName: WORKFLOW_REPO_NAME,\n      octokit,\n      });\n\n    // Log the number of redeliveries.\n    console.log(\n      `Redelivered ${\n        failedDeliveryIDs.length\n      } failed webhook deliveries out of ${\n        deliveries.length\n      } total deliveries since ${Date(lastWebhookRedeliveryTime)}.`\n    );\n  } catch (error) {\n    // 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.\n    if (error.response) {\n      console.error(\n        `Failed to check and redeliver webhooks: ${error.response.data.message}`\n      );\n    }\n    console.error(error);\n    throw(error);\n  }\n}\n\n// This function will fetch all of the webhook deliveries that were delivered since `lastWebhookRedeliveryTime`.\n// 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).\n//\n// If a page of results includes deliveries that occurred before `lastWebhookRedeliveryTime`,\n// it will store only the deliveries that occurred after `lastWebhookRedeliveryTime` and then stop.\n// Otherwise, it will store all of the deliveries from the page and request the next page.\nasync function fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app}) {\n  const iterator = app.octokit.paginate.iterator(\n    \"GET /app/hook/deliveries\",\n    {\n      per_page: 100,\n      headers: {\n        \"x-github-api-version\": \"2026-03-10\",\n      },\n    }\n  );\n\n  const deliveries = [];\n\n  for await (const { data } of iterator) {\n    const oldestDeliveryTimestamp = new Date(\n      data[data.length - 1].delivered_at\n    ).getTime();\n\n    if (oldestDeliveryTimestamp < lastWebhookRedeliveryTime) {\n      for (const delivery of data) {\n        if (\n          new Date(delivery.delivered_at).getTime() > lastWebhookRedeliveryTime\n        ) {\n          deliveries.push(delivery);\n        } else {\n          break;\n        }\n      }\n      break;\n    } else {\n      deliveries.push(...data);\n    }\n  }\n\n  return deliveries;\n}\n\n// This function will redeliver a failed webhook delivery.\nasync function redeliverWebhook({deliveryId, app}) {\n  await app.octokit.request(\"POST /app/hook/deliveries/{delivery_id}/attempts\", {\n    delivery_id: deliveryId,\n  });\n}\n\n// This function gets the value of a configuration variable.\n// If the variable does not exist, the endpoint returns a 404 response and this function returns `undefined`.\nasync function getVariable({ variableName, repoOwner, repoName, octokit }) {\n  try {\n    const {\n      data: { value },\n    } = await octokit.request(\n      \"GET /repos/{owner}/{repo}/actions/variables/{name}\",\n      {\n        owner: repoOwner,\n        repo: repoName,\n        name: variableName,\n      }\n    );\n    return value;\n  } catch (error) {\n    if (error.status === 404) {\n      return undefined;\n    } else {\n      throw error;\n    }\n  }\n}\n\n// 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).\nasync function updateVariable({\n  variableName,\n  value,\n  variableExists,\n  repoOwner,\n  repoName,\n  octokit,\n}) {\n  if (variableExists) {\n    await octokit.request(\n      \"PATCH /repos/{owner}/{repo}/actions/variables/{name}\",\n      {\n        owner: repoOwner,\n        repo: repoName,\n        name: variableName,\n        value: value,\n      }\n    );\n  } else {\n    await octokit.request(\"POST /repos/{owner}/{repo}/actions/variables\", {\n      owner: repoOwner,\n      repo: repoName,\n      name: variableName,\n      value: value,\n    });\n  }\n}\n\n// This will execute the `checkAndRedeliverWebhooks` function.\n(async () => {\n  await checkAndRedeliverWebhooks();\n})();\n\n```\n\n## Тестирование скрипта\n\nВы можете вручную активировать рабочий процесс для тестирования скрипта. Дополнительные сведения см. в разделе \\[AUTOTITLE и [Запуск рабочего процесса вручную](/ru/actions/how-tos/manage-workflow-runs/manually-run-a-workflow)]\\(/actions/how-tos/monitor-workflows/use-workflow-run-logs).\n\n## Альтернативные методы\n\nЭтот пример использовался GitHub Actions для безопасного хранения учетных данных и запуска скрипта по расписанию. Однако если вы предпочитаете запускать этот скрипт на сервере, чем обрабатывает поставки веб-перехватчиков, вы можете:\n\n* Храните учетные данные в другом безопасном виде, например, в секретном менеджере, например [Azure хранилище ключей](https://azure.microsoft.com/products/key-vault). Вам также потребуется обновить скрипт, чтобы получить доступ к учетным данным из нового расположения.\n* Запустите скрипт по расписанию на сервере, например с помощью задания cron или планировщика задач.\n* Обновите скрипт, чтобы сохранить время последнего выполнения где-либо, к которому может обращаться и обновлять сервер. Если вы решили не хранить последнее время выполнения в качестве секрета GitHub Actions , вам не нужно использовать его personal access token, и вы можете удалить вызовы API для доступа к переменной конфигурации и обновить ее."}