{"meta":{"title":"Автоматическое повторение неудачных поставок для веб-перехватчика репозитория","intro":"Вы можете написать скрипт для обработки неудачных поставок веб-перехватчика репозитория.","product":"Веб-перехватчики","breadcrumbs":[{"href":"/ru/webhooks","title":"Веб-перехватчики"},{"href":"/ru/webhooks/using-webhooks","title":"Использование веб-перехватчиков"},{"href":"/ru/webhooks/using-webhooks/automatically-redelivering-failed-deliveries-for-a-repository-webhook","title":"Автоматическое повторное создание репозитория"}],"documentType":"article"},"body":"# Автоматическое повторение неудачных поставок для веб-перехватчика репозитория\n\nВы можете написать скрипт для обработки неудачных поставок веб-перехватчика репозитория.\n\n## О автоматическом повторном развертывании неудачных поставок\n\nВ этой статье описывается, как написать скрипт для поиска и повторного выполнения доставки для веб-перехватчика репозитория. Дополнительные сведения о неудачных поставках см. в разделе [Обработка неудачных поставок веб-перехватчика](/ru/webhooks/using-webhooks/handling-failed-webhook-deliveries).\n\nВ этом примере показано:\n\n* Скрипт, который будет находить и переэливерить неудачные поставки для веб-перехватчика репозитория\n* Какие учетные данные понадобятся вашему скрипту и как безопасно хранить эти учетные данные как GitHub Actions секреты\n* Рабочий GitHub Actions процесс, который может безопасно получать доступ к вашим учетным данным и периодически запускать скрипт\n\nВ этом примере используется GitHub Actions, но вы также можете запустить этот скрипт на вашем сервере, который обрабатывает доставку webhook. Дополнительные сведения см. в разделе [\"Альтернативные методы](#alternative-methods)\".\n\n## Хранение учетных данных для скрипта\n\nВстроенные `GITHUB_TOKEN` веб-перехватчики не имеют достаточных разрешений. Вместо использования `GITHUB_TOKEN`, в этом примере используется personal access token. В качестве альтернативы, вместо создания personal access token, вы можете создать GitHub App и использовать учетные данные приложения для создания токена доступа для установки во время рабочего GitHub Actions процесса. Дополнительные сведения см. в разделе [Создание аутентифицированных запросов 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. Создайте 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     * Разрешение на запись доступа к веб-перехватчикам репозитория\n     * Разрешение на запись в переменные репозитория\n   * Для personal access token (classic), присвоите жетону область `repo` действия.\n2. Храните personal access token вас в GitHub Actions виде секрета в репозитории, где хотите работать рабочий процесс. Дополнительные сведения см. в разделе [Использование секретов в GitHub Actions](/ru/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).\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: '20 */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: '18.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_SECRET_NAME` with the name of the secret where you stored your personal access token.\n      # - Replace `YOUR_REPO_OWNER` with the owner of the repository where the webhook was created.\n      # - Replace `YOUR_REPO_NAME` with the name of the repository where the webhook was created.\n      # - Replace `YOUR_HOOK_ID` with the ID of the webhook.\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          TOKEN: ${{ secrets.YOUR_SECRET_NAME }}\n          REPO_OWNER: 'YOUR_REPO_OWNER'\n          REPO_NAME: 'YOUR_REPO_NAME'\n          HOOK_ID: 'YOUR_HOOK_ID'\n          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'\n          \n          WORKFLOW_REPO_NAME: ${{ github.event.repository.name }}\n          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}\n        run: |\n          node .github/workflows/scripts/redeliver-failed-deliveries.js\n```\n\n## Добавление скрипта\n\nВ этом разделе показано, как создать скрипт для поиска и повторного создания неудачных поставок.\n\nСкопируйте этот скрипт в файл, вызываемый `.github/workflows/scripts/redeliver-failed-deliveries.js` в том же репозитории, где вы сохранили 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).\nconst { Octokit } = require(\"octokit\");\n\n//\nasync function checkAndRedeliverWebhooks() {\n  // Get the values of environment variables that were set by the GitHub Actions workflow.\n  const TOKEN = process.env.TOKEN;\n  const REPO_OWNER = process.env.REPO_OWNER;\n  const REPO_NAME = process.env.REPO_NAME;\n  const HOOK_ID = process.env.HOOK_ID;\n  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;\n  \n  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;\n  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;\n\n  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.\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({\n      lastWebhookRedeliveryTime,\n      repoOwner: REPO_OWNER,\n      repoName: REPO_NAME,\n      hookId: HOOK_ID,\n      octokit,\n    });\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({\n        deliveryId,\n        repoOwner: REPO_OWNER,\n        repoName: REPO_NAME,\n        hookId: HOOK_ID,\n        octokit,\n      });\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 ${new Date(Number(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({\n  lastWebhookRedeliveryTime,\n  repoOwner,\n  repoName,\n  hookId,\n  octokit,\n}) {\n  const iterator = octokit.paginate.iterator(\n    \"GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries\",\n    {\n      owner: repoOwner,\n      repo: repoName,\n      hook_id: hookId,\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({\n  deliveryId,\n  repoOwner,\n  repoName,\n  hookId,\n  octokit,\n}) {\n  await octokit.request(\n    \"POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts\",\n    {\n      owner: repoOwner,\n      repo: repoName,\n      hook_id: hookId,\n      delivery_id: deliveryId,\n    }\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 секрет, вы можете удалить API-вызовы для доступа и обновления переменной конфигурации."}