-
Notifications
You must be signed in to change notification settings - Fork 115
Expand file tree
/
Copy pathwebhook.post.ts
More file actions
157 lines (135 loc) · 5.04 KB
/
webhook.post.ts
File metadata and controls
157 lines (135 loc) · 5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import type { PullRequestEvent } from "@octokit/webhooks-types";
import type { HandlerFunction } from "@octokit/webhooks/dist-types/types";
import type { PullRequestData, WorkflowData, WebhookDebugData } from "../types";
import { hash } from "ohash";
// mark a PR as a PR :)
const prMarkEvents: PullRequestEvent["action"][] = [
"opened",
"reopened",
"synchronize",
];
export default eventHandler(async (event) => {
const app = useOctokitApp(event);
const { test } = useRuntimeConfig(event);
const workflowsBucket = useWorkflowsBucket(event);
const pullRequestNumbersBucket = usePullRequestNumbersBucket(event);
const debugBucket = useDebugBucket(event);
const workflowHandler: HandlerFunction<"workflow_run", unknown> = async ({
payload,
}) => {
const [owner, repo] = payload.repository.full_name.split("/");
const metadata = {
owner,
repo,
run: payload.workflow_run.id,
attempt: payload.workflow_run.run_attempt,
actor: payload.sender.id,
};
const hashKey = hash(metadata);
if (payload.action === "completed") {
// Publishing is not available anymore
await workflowsBucket.removeItem(hashKey);
} else if (!(await workflowsBucket.hasItem(hashKey))) {
// "requested" or "in_progress"
// "requested" won't be received in re-running workflow jobs, but "in_progress" would be
const prData: PullRequestData = {
full_name: payload.workflow_run.head_repository.full_name,
ref: payload.workflow_run.head_branch,
};
// new: using the new key to avoid collision
const prKey = `${prData.full_name}:${prData.ref}`;
const isNewPullRequest = await pullRequestNumbersBucket.hasItem(prKey);
// old: the old of hashing the prData started to hit collision, so we need to use the new one (e.g. https://github.com/element-plus/element-plus/actions/runs/12351113750/job/34465376908)
const oldPrDataHash = hash(prData);
const isOldPullRequest =
await pullRequestNumbersBucket.hasItem(oldPrDataHash);
const isPullRequest = isNewPullRequest || isOldPullRequest;
const lookupKey = isNewPullRequest ? prKey : oldPrDataHash;
const prNumber = await pullRequestNumbersBucket.getItem(lookupKey);
const headBranch = payload.workflow_run.head_branch;
const isHeadOnUpstream =
payload.workflow_run.head_repository?.full_name ===
payload.repository.full_name;
const data: WorkflowData = {
owner,
repo,
sha: payload.workflow_run.head_sha,
ref: isPullRequest ? `${prNumber}` : headBranch!,
headBranch:
isPullRequest && isHeadOnUpstream && headBranch ? headBranch : null,
};
// Publishing is only available throughout the lifetime of a workflow_job
await workflowsBucket.setItem(hashKey, data);
const debugData: WebhookDebugData = {
action: payload.action,
head_branch: payload.workflow_run.head_branch,
head_repository_full_name:
payload.workflow_run.head_repository?.full_name || null,
full_name: payload.repository.full_name,
isPullRequest,
prNumber,
prNumberType: typeof prNumber,
isNewPullRequest,
isOldPullRequest,
prKey,
oldPrDataHash,
lookupKey,
data,
};
await debugBucket.setItem(hashKey, debugData);
}
};
const pullRequestHandler: HandlerFunction<"pull_request", unknown> = async ({
payload,
}) => {
const headRepo = payload.pull_request.head.repo;
if (!headRepo || !headRepo.full_name) {
throw new Error("Invalid payload: 'full_name' is undefined.");
}
const key: PullRequestData = {
full_name: headRepo.full_name,
ref: payload.pull_request.head.ref,
};
if (prMarkEvents.includes(payload.action)) {
await pullRequestNumbersBucket.setItem(
`${key.full_name}:${key.ref}`,
payload.number,
);
}
};
app.webhooks.on("workflow_run", workflowHandler);
app.webhooks.on("pull_request", pullRequestHandler);
type EmitterWebhookEvent = Parameters<
typeof app.webhooks.receive | typeof app.webhooks.verifyAndReceive
>[0];
const id: EmitterWebhookEvent["id"] = event.headers.get("x-github-delivery")!;
const name = event.headers.get(
"x-github-event",
) as EmitterWebhookEvent["name"];
const signature = event.headers.get("x-hub-signature-256") ?? "";
const payload = (await readRawBody(event))!;
try {
if (test) {
// TODO: fix typing with infer
await app.webhooks.receive({
id,
name,
payload: JSON.parse(payload),
} as any);
} else {
await app.webhooks.verifyAndReceive({ id, name, payload, signature });
}
return { ok: true };
} catch (error) {
if (error instanceof Error) {
app.log.error(error.message);
throw createError({
status: 500,
message: error?.message,
});
}
} finally {
app.webhooks.removeListener("workflow_run", workflowHandler);
app.webhooks.removeListener("pull_request", pullRequestHandler);
}
});