실패한 제공 자동으로 다시 제공 알아보기
이 문서에서는 GitHub App 웹후크에 대한 제공 실패를 찾아 다시 제공하는 스크립트를 작성하는 방법을 설명합니다. 제공 실패에 대한 자세한 내용은 "실패한 웹후크 제공 처리하기"을(를) 참조하세요.
이 예에서 보여 주는 항목은 다음과 같습니다.
- GitHub App 웹후크에 대한 제공 실패를 찾아서 다시 제공하는 스크립트
- 스크립트에 필요한 자격 증명 및 GitHub Actions 비밀로 자격 증명을 안전하게 저장하는 방법
- 자격 증명에 안전하게 액세스하고 주기적으로 스크립트를 실행할 수 있는 GitHub Actions 워크플로
이 예시에서는 GitHub Actions을(를) 사용하지만 웹후크 제공을 처리하는 서버에서 이 스크립트를 실행할 수도 있습니다. 자세한 내용은 "다른 메서드"을(를) 참조하세요.
스크립트에 대한 자격 증명 저장하기
실패한 웹후크를 찾아 다시 제공하는 엔드포인트에는 앱 ID 및 앱의 비공개 키에서 생성되는 JSON 웹 토큰이 필요합니다.
환경 변수의 값을 가져오고 업데이트하는 엔드포인트에는 personal access token, GitHub App 설치 액세스 토큰 또는 GitHub App 사용자 액세스 토큰이 필요합니다. 이 예시에서는 personal access token을(를) 사용합니다. 이 워크플로가 실행되는 리포지토리에 GitHub App이(가) 설치되어 있고 리포지토리 변수를 쓸 수 있는 권한이 있는 경우. GitHub Actions 워크플로 중에 personal access token을(를) 사용하는 대신 설치 액세스 토큰을 만들도록 이 예시를 수정할 수 있습니다. 자세한 내용은 "GitHub Actions 워크플로에서 GitHub 앱을 사용하여 인증된 API 요청 만들기"을(를) 참조하세요.
- GitHub App용 앱 ID를 찾습니다. 앱의 설정 페이지에서 앱 ID를 찾을 수 있습니다. 앱 ID는 클라이언트 ID와 다릅니다. GitHub App의 설정 페이지로 이동하는 방법에 대한 자세한 내용은 "GitHub 앱 등록 수정"을(를) 참조하세요.
- 이전 단계의 앱 ID를 워크플로를 실행할 리포지토리에 GitHub Actions 비밀로 저장합니다. 비밀 저장에 대한 자세한 내용은 "GitHub Actions에서 비밀 사용"을(를) 참조하세요.
- 앱에 대한 프라이빗 키를 생성합니다. 비공개 키 생성에 대한 자세한 내용은 "GitHub 앱에 대한 프라이빗 키 관리"을(를) 참조하세요.
- 이전 단계의
-----BEGIN RSA PRIVATE KEY-----
및-----END RSA PRIVATE KEY-----
을(를) 포함한 비공개 키를 워크플로를 실행할 리포지토리에 GitHub Actions 비밀로 저장합니다. - 다음 액세스 권한으로 personal access token을(를) 만듭니다. 자세한 내용은 "개인용 액세스 토큰 관리"을(를) 참조하세요.
- fine-grained personal access token의 경우 다음의 토큰 권한을 부여합니다.
- 리포지토리 변수 권한에 대한 쓰기 액세스
- 이 워크플로가 실행되는 리포지토리에 대한 액세스
- personal access token (classic)의 경우 토큰에
repo
범위 권한을 부여합니다.
- fine-grained personal access token의 경우 다음의 토큰 권한을 부여합니다.
- 이전 단계의 personal access token을(를) 워크플로를 실행할 리포지토리에 GitHub Actions 비밀로 저장합니다.
스크립트를 실행할 워크플로 추가하기
이 섹션에서는 GitHub Actions 워크플로를 사용하여 이전 섹션에 저장한 자격 증명에 안전하게 액세스하고, 환경 변수를 설정하고, 스크립트를 주기적으로 실행하여 실패한 제공을 찾아서 다시 제공하는 방법을 보여 줍니다.
이 GitHub Actions 워크플로를 해당 워크플로를 실행할 리포지토리의 .github/workflows
디렉터리에 있는 YAML 파일에 복사합니다. 아래 설명된 대로 Run script
단계의 자리 표시자를 바꿉니다.
# name: Redeliver failed webhook deliveries # This workflow runs every 6 hours or when manually triggered. on: schedule: - cron: '40 */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@v4 # 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_APP_ID_SECRET_NAME` with the name of the secret where you stored your app ID. # - Replace `YOUR_PRIVATE_KEY_SECRET_NAME` with the name of the secret where you stored your private key. # - Replace `YOUR_TOKEN_SECRET_NAME` with the name of the secret where you stored your personal access token. # - 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: APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }} PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }} TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }} LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME' WORKFLOW_REPO: ${{ github.event.repository.name }} WORKFLOW_REPO_OWNER: ${{ github.repository_owner }} run: | node .github/workflows/scripts/redeliver-failed-deliveries.js
name: Redeliver failed webhook deliveries
on:
schedule:
- cron: '40 */6 * * *'
workflow_dispatch:
This workflow runs every 6 hours or when manually triggered.
permissions:
contents: read
This workflow will use the built in GITHUB_TOKEN
to check out the repository contents. This grants GITHUB_TOKEN
permission to do that.
jobs:
redeliver-failed-deliveries:
name: Redeliver failed deliveries
runs-on: ubuntu-latest
steps:
- name: Check out repo content
uses: actions/checkout@v4
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: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
This step sets up Node.js. The script that this workflow will run uses Node.js.
- name: Install dependencies
run: npm install octokit
This step installs the octokit library. The script that this workflow will run uses the octokit library.
- name: Run script
env:
APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}
PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}
TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
WORKFLOW_REPO: ${{ github.event.repository.name }}
WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
run: |
node .github/workflows/scripts/redeliver-failed-deliveries.js
This step sets some environment variables, then runs a script to find and redeliver failed webhook deliveries.
- Replace
YOUR_APP_ID_SECRET_NAME
with the name of the secret where you stored your app ID. - Replace
YOUR_PRIVATE_KEY_SECRET_NAME
with the name of the secret where you stored your private key. - Replace
YOUR_TOKEN_SECRET_NAME
with the name of the secret where you stored your personal access token. - 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 withGITHUB_
or a number. For more information, see "변수에 정보 저장."
#
name: Redeliver failed webhook deliveries
# This workflow runs every 6 hours or when manually triggered.
on:
schedule:
- cron: '40 */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@v4
# 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_APP_ID_SECRET_NAME` with the name of the secret where you stored your app ID.
# - Replace `YOUR_PRIVATE_KEY_SECRET_NAME` with the name of the secret where you stored your private key.
# - Replace `YOUR_TOKEN_SECRET_NAME` with the name of the secret where you stored your personal access token.
# - 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:
APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}
PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}
TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
WORKFLOW_REPO: ${{ github.event.repository.name }}
WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
run: |
node .github/workflows/scripts/redeliver-failed-deliveries.js
스크립트 추가하기
이 섹션에서는 실패한 제공을 찾아서 다시 제공하는 스크립트를 작성하는 방법을 보여 줍니다.
위의 GitHub Actions 워크플로 파일을 저장한 것과 동일한 리포지토리의 .github/workflows/scripts/redeliver-failed-deliveries.js
파일에 이 스크립트를 복사합니다.
// 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 { App, Octokit } = require("octokit"); // async function checkAndRedeliverWebhooks() { // Get the values of environment variables that were set by the GitHub Actions workflow. const APP_ID = process.env.APP_ID; const PRIVATE_KEY = process.env.PRIVATE_KEY; const TOKEN = process.env.TOKEN; const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME; const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO; const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER; // Create an instance of the octokit `App` using the app ID and private key values that were set in the GitHub Actions workflow. // // This will be used to make API requests to the webhook-related endpoints. const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY, }); // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow. // // This will be used to update the configuration variable that stores the last time that this script ran. 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, app}); // 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, app}); } // 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, app}) { const iterator = app.octokit.paginate.iterator( "GET /app/hook/deliveries", { per_page: 100, headers: { "x-github-api-version": "2022-11-28", }, } ); 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, app}) { await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", { 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(); })();
const { App, Octokit } = require("octokit");
This script uses GitHub's Octokit SDK to make API requests. For more information, see "REST API 및 JavaScript를 사용하여 스크립팅."
async function checkAndRedeliverWebhooks() {
const APP_ID = process.env.APP_ID;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const TOKEN = process.env.TOKEN;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;
const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;
Get the values of environment variables that were set by the GitHub Actions workflow.
const app = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
});
Create an instance of the octokit App
using the app ID and private key values that were set in the GitHub Actions workflow.
This will be used to make API requests to the webhook-related endpoints.
const octokit = new Octokit({
auth: TOKEN,
});
try {
Create an instance of Octokit
using the token values that were set in the GitHub Actions workflow.
This will be used to update the configuration variable that stores the last time that this script ran.
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();
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 newWebhookRedeliveryTime = Date.now().toString();
Record the time that this script started redelivering webhooks.
const deliveries = await fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app});
Get the webhook deliveries that were delivered after lastWebhookRedeliveryTime
.
let deliveriesByGuid = {};
for (const delivery of deliveries) {
deliveriesByGuid[delivery.guid]
? deliveriesByGuid[delivery.guid].push(delivery)
: (deliveriesByGuid[delivery.guid] = [delivery]);
}
Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.
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);
}
}
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.
for (const deliveryId of failedDeliveryIDs) {
await redeliverWebhook({deliveryId, app});
}
Redeliver any failed deliveries.
await updateVariable({
variableName: LAST_REDELIVERY_VARIABLE_NAME,
value: newWebhookRedeliveryTime,
variableExists: Boolean(lastStoredRedeliveryTime),
repoOwner: WORKFLOW_REPO_OWNER,
repoName: WORKFLOW_REPO_NAME,
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.
console.log(
`Redelivered ${
failedDeliveryIDs.length
} failed webhook deliveries out of ${
deliveries.length
} total deliveries since ${Date(lastWebhookRedeliveryTime)}.`
);
} catch (error) {
Log the number of redeliveries.
if (error.response) {
console.error(
`Failed to check and redeliver webhooks: ${error.response.data.message}`
);
}
console.error(error);
throw(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.
async function fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app}) {
const iterator = app.octokit.paginate.iterator(
"GET /app/hook/deliveries",
{
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
}
);
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 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 "REST API 및 JavaScript를 사용하여 스크립팅."
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 redeliverWebhook({deliveryId, app}) {
await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
delivery_id: deliveryId,
});
}
This function will redeliver a failed webhook delivery.
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 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 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 function will update a configuration variable (or create the variable if it doesn't already exist). For more information, see "변수에 정보 저장."
(async () => {
await checkAndRedeliverWebhooks();
})();
This will execute the checkAndRedeliverWebhooks
function.
// 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 { App, Octokit } = require("octokit");
//
async function checkAndRedeliverWebhooks() {
// Get the values of environment variables that were set by the GitHub Actions workflow.
const APP_ID = process.env.APP_ID;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const TOKEN = process.env.TOKEN;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;
const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;
// Create an instance of the octokit `App` using the app ID and private key values that were set in the GitHub Actions workflow.
//
// This will be used to make API requests to the webhook-related endpoints.
const app = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
});
// Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.
//
// This will be used to update the configuration variable that stores the last time that this script ran.
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, app});
// 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, app});
}
// 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, app}) {
const iterator = app.octokit.paginate.iterator(
"GET /app/hook/deliveries",
{
per_page: 100,
headers: {
"x-github-api-version": "2022-11-28",
},
}
);
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, app}) {
await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
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();
})();
스크립트 테스트하기
워크플로를 수동으로 트리거하여 스크립트를 테스트할 수 있습니다. 자세한 내용은 "워크플로 수동 실행" 및 "워크플로 실행 로그 사용"을(를) 참조하세요.
대체 방법
이 예시에서는 GitHub Actions을(를) 사용하여 자격 증명을 안전하게 저장하고 일정에 따라 스크립트를 실행했습니다. 그러나 웹후크 제공을 처리하는 대신 서버에서 이 스크립트를 실행하려는 경우 다음을 수행할 수 있습니다.
- Azure Key Vault 등의 비밀 관리자와 같은 다른 보안 방식으로 자격 증명을 저장합니다. 또한 스크립트를 업데이트하여 새 위치에서 자격 증명에 액세스해야 합니다.
- 예를 들어 cron 작업 또는 작업 스케줄러를 사용하여 서버의 일정에 따라 스크립트를 실행합니다.
- 서버에서 액세스하고 업데이트할 수 있는 위치에 마지막 런타임을 저장하도록 스크립트를 업데이트합니다. 마지막 런타임을 GitHub Actions 비밀로 저장하지 않도록 선택한 경우, personal access token을(를) 사용할 필요가 없으며, 구성 변수에 액세스하고 업데이트하기 위해 API 호출을 제거할 수 있습니다.