Readme

A webhook to approve dependency PRs created by bots

The webhook can be configured on the repo or on the org level

  • it needs to have the Payload URL set to the "web endpoint" of the val ( ... -> Endpoints -> Copy web endpoint)
  • it needs to POST a json payload
  • it needs to receive the workflow_runs events
  • it needs to have the webhookSecret configured to the same value as in val town secrets (line 7)

(else response is 401: Not verified)

It will only approve if all of the following conditions are met:

  • the event action is completed, the workflow_run.conclusion has to be success, and the event is related to exactly one pull request
    (else response is 202: Ignored (event))
  • the PR is authored authored by one of the users listed in allowedAuthors (line 5)
    (else response is 202: Ignored (pr author))
  • the githubApiToken configured in line 9 needs to have repo access to the related repository
    (else response is 50x: some error message)
  • a branch protection has to be configured that requires at least one review approval and at least one status check
    (else response is 202: Ignored (branch protection))
  • the PR has auto-merge enabled
    (else response is 202: Ignored (pr status))
  • the PR has any failing status checks (even if not required)
    (else response is 202: Ignored (pr checks))
  • the current value for dryRun is false (line 3)
    (else response is 200: Would have been approved (dryRun))

If it approves the PR, it leaves a comment pointing to the website of this val.

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
import { SignatureCheck } from "https://esm.town/v/karfau/SignatureCheck";
import { response } from "https://esm.town/v/karfau/response";
import { isRequest } from "https://esm.town/v/karfau/isRequest";
import { thisValUrl } from "https://esm.town/v/neverstew/thisValUrl?v=1";
import process from "node:process";
export const githubWebhookApproveDependencyPRs = async (req?: Request | unknown) => {
// if true: do all the checks but don't approve PRs (status code 200 instead of 201)
const dryRun = false;
// only PRs created by these authors will be considered
const allowedAuthors = ["renovate[bot]"];
// the secret shared with the webhook
const webhookSecret = process.env.githubWebhookApproveDependencyPRs;
// the token to make github requests (needs `repo` permissions)
const githubApiToken = process.env.githubApproveDependencyPRsToken;
const valHttpUrl = thisValUrl();
const approvalMessage = `Automatically approved by ${valHttpUrl}`;
if (!isRequest(req)) {
return `Use the web endpoint for this val, see ${valHttpUrl}`;
}
else if (req.method === "GET") {
return new Response(
`Redirecting to <a href="${valHttpUrl}">${valHttpUrl}</a>`,
{ status: 303, headers: { Location: valHttpUrl } },
);
}
else if (req.method !== "POST") {
return response(
405,
`Method ${req.method} not allowed, see ${valHttpUrl}`,
);
}
const body = await req.text();
const signature = req.headers.get("X-Hub-Signature-256");
const { verify } = SignatureCheck();
const verified = await verify({ payload: body, signature }, webhookSecret);
if (!verified) {
return response(401, `Not verified, see ${valHttpUrl}`);
}
const payload: Payload = JSON.parse(body);
const { action, workflow_run, sender } = payload;
if (
action !== "completed" || workflow_run.conclusion !== "success" ||
workflow_run.pull_requests.length !== 1
) {
return response(202, "Ignored (event)");
}
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({
auth: githubApiToken,
});
const { repository, organization } = payload;
const [pull_request] = workflow_run.pull_requests;
const pr =
(await octokit.request(`GET /repos/{owner}/{repo}/pulls/{pull_number}`, {
owner: organization.login,
repo: repository.name,
pull_number: pull_request.number,
})).data;
if (!allowedAuthors.includes(pr.user.login)) {
return response(202, "Ignored (pr author)");
}
if (!pr.auto_merge) {
return response(202, "Ignored (pr status)");
}
const checks = (await octokit.request(pr.statuses_url, {
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
})).data;
if (checks.find((check) => check.state !== "success")) {
return response(202, "Ignored (pr checks)");
}
const { required_pull_request_reviews, required_status_checks } =
(await octokit.request(
"GET /repos/{owner}/{repo}/branches/{branch}/protection",
{
owner: organization.login,
repo: repository.name,
branch: pull_request.base.ref,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
)).data;
if (
required_pull_request_reviews?.required_approving_review_count !== 1 ||
required_status_checks?.contexts.length === 0
) {
return response(202, "Ignored (branch protection)");
}
if (dryRun) {
return response(200, "Would have been approved (dryRun)");
}
await octokit.request(
"POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews",
{
owner: organization.login,
repo: repository.name,
pull_number: pull_request.number,
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.