Skip to content

Commit 030ae4e

Browse files
committed
feat: new lambda function to check and retry failed webhook deliveries
1 parent 7a4bd2f commit 030ae4e

19 files changed

+736
-44
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitattributes

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/deps.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/files.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/tasks.json

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projenrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
1919
'@octokit/auth-app',
2020
'@octokit/request-error',
2121
'@octokit/rest',
22+
'@octokit/types',
2223
'@aws-sdk/client-cloudformation',
2324
'@aws-sdk/client-codebuild',
2425
'@aws-sdk/client-ec2',

package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lambda-github.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createAppAuth } from '@octokit/auth-app';
22
import { Octokit } from '@octokit/rest';
3+
import { Endpoints } from '@octokit/types';
34
import { getSecretValue, getSecretJsonValue } from './lambda-helpers';
45

56
export function baseUrlFromDomain(domain: string): string {
@@ -132,3 +133,79 @@ export async function deleteRunner(octokit: Octokit, runnerLevel: RunnerLevel, o
132133
});
133134
}
134135
}
136+
137+
138+
export type WebhookDeliveries = Endpoints['GET /app/hook/deliveries']['response']['data'];
139+
export type WebhookDelivery = WebhookDeliveries[number];
140+
export type WebhookDeliveryDetail = Endpoints['GET /app/hook/deliveries/{delivery_id}']['response']['data'];
141+
export type WorkflowJob = Endpoints['GET /repos/{owner}/{repo}/actions/jobs/{job_id}']['response']['data'];
142+
143+
export async function getFailedDeliveries(
144+
octokit: Octokit,
145+
sinceDeliveryId: number,
146+
): Promise<{
147+
failedDeliveries: WebhookDeliveries;
148+
latestDeliveryId: number;
149+
}> {
150+
const failedDeliveries: WebhookDeliveries = [];
151+
if (sinceDeliveryId === 0) {
152+
// If no last delivery ID was set, just fetch the latest delivery to get the latest ID
153+
const deliveriesResponse = await octokit.rest.apps.listWebhookDeliveries({ per_page: 1 });
154+
if (deliveriesResponse.status !== 200) {
155+
throw new Error(`Failed to fetch webhook deliveries`);
156+
}
157+
return {
158+
failedDeliveries,
159+
latestDeliveryId: deliveriesResponse.data[0]?.id || 0,
160+
};
161+
}
162+
163+
let latestDeliveryId = 0;
164+
let deliveryCountSinceLastCheck = 0;
165+
for await (const response of octokit.paginate.iterator('GET /app/hook/deliveries')) {
166+
if (response.status !== 200) {
167+
throw new Error(`Failed to fetch webhook deliveries`);
168+
}
169+
latestDeliveryId = Math.max(latestDeliveryId, ...response.data.map((delivery) => delivery.id));
170+
171+
const deliveriesSinceLastCheck = response.data.filter((delivery) => delivery.id > sinceDeliveryId);
172+
deliveryCountSinceLastCheck += deliveriesSinceLastCheck.length;
173+
failedDeliveries.push(...deliveriesSinceLastCheck.filter((delivery) => delivery.status !== 'OK'));
174+
175+
if (deliveriesSinceLastCheck.length < response.data.length) {
176+
break;
177+
}
178+
}
179+
console.debug(
180+
`Searched through ${deliveryCountSinceLastCheck} deliveries since last check, found ${failedDeliveries.length} failed`,
181+
);
182+
183+
return {
184+
failedDeliveries,
185+
latestDeliveryId,
186+
};
187+
}
188+
189+
export async function getDeliveryDetail(
190+
octokit: Octokit,
191+
deliveryId: number,
192+
): Promise<WebhookDeliveryDetail> {
193+
const response = await octokit.rest.apps.getWebhookDelivery({
194+
delivery_id: deliveryId,
195+
});
196+
if (response.status !== 200) {
197+
throw new Error(`Failed to fetch webhook delivery with ID ${deliveryId}`);
198+
}
199+
return response.data;
200+
}
201+
202+
export async function redeliver(octokit: Octokit, deliveryId: number): Promise<void> {
203+
const response = await octokit.rest.apps.redeliverWebhookDelivery({
204+
delivery_id: deliveryId,
205+
});
206+
207+
if (response.status !== 202) {
208+
throw new Error(`Failed to redeliver webhook delivery with ID ${deliveryId}`);
209+
}
210+
console.log(`Successfully redelivered webhook delivery with ID ${deliveryId}`);
211+
}

src/lambda-helpers.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SecretsManagerClient, GetSecretValueCommand, UpdateSecretCommand } from '@aws-sdk/client-secrets-manager';
2+
import { GetParameterCommand, PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
23

34
export interface StepFunctionLambdaInput {
45
readonly owner: string;
@@ -13,6 +14,7 @@ export interface StepFunctionLambdaInput {
1314
}
1415

1516
const sm = new SecretsManagerClient();
17+
const ssm = new SSMClient();
1618

1719
export async function getSecretValue(arn: string | undefined) {
1820
if (!arn) {
@@ -79,3 +81,28 @@ export async function customResourceRespond(event: AWSLambda.CloudFormationCusto
7981
}
8082
});
8183
}
84+
85+
export async function getLastDeliveryId(): Promise<number> {
86+
if (!process.env.LAST_DELIVERY_ID_PARAM) {
87+
throw new Error('Missing LAST_DELIVERY_ID_PARAM environment variable');
88+
}
89+
90+
const paramName = process.env.LAST_DELIVERY_ID_PARAM;
91+
92+
const response = await ssm.send(new GetParameterCommand({ Name: paramName }));
93+
if (!response.Parameter?.Value) {
94+
throw new Error(`No Parameter.Value in ${paramName}`);
95+
}
96+
97+
return parseInt(response.Parameter.Value, 10);
98+
}
99+
100+
export async function setLastDeliveryId(id: number): Promise<void> {
101+
if (!process.env.LAST_DELIVERY_ID_PARAM) {
102+
throw new Error('Missing LAST_DELIVERY_ID_PARAM environment variable');
103+
}
104+
105+
const paramName = process.env.LAST_DELIVERY_ID_PARAM;
106+
107+
await ssm.send(new PutParameterCommand({ Name: paramName, Value: id.toString(), Overwrite: true }));
108+
}

0 commit comments

Comments
 (0)