Back to packages list

Vals using @octokit/core

Description from the NPM package:
Extendable client for GitHub's REST & GraphQL APIs

Used to post issues back to github from website's UI for non-technical users.

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
import process from "node:process";
export async function createIssue(issue_data) {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({
auth: process.env.gh_token,
});
const { summary, path, browser_type, browser_size, logs } = issue_data;
const body = `## Report
${summary}
## Session Info
${path}
${browser_type}
${browser_size}
\`\`\`
${JSON.stringify(logs, null, 2)}
\`\`\`
`;
const response = await octokit.request("POST /repos/the-log/svelte/issues", {
owner: "the-log",
repo: "svelte",
title: "Untriaged Issue",
body,
assignees: [
"andy-blum",
],
labels: [
"triage",
],
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
return response.data.html_url;
}
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 { response } from "https://esm.town/v/ferruccio/response";
import { thisValUrl } from "https://esm.town/v/neverstew/thisValUrl?v=1";
import { verifyGithubWebhookSignature } from "https://esm.town/v/vtdocs/verifyGithubWebhookSignature?v=2";
import process from "node:process";
export const githubWebhookApproveSmallPRs = async (req: Request) => {
// 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 = ["ferrucc-io", "vinayak-mehta"];
// the secret shared with the webhook
const webhookSecret = process.env.githubWebhookApproveSmallPRs;
// the token to make github requests (needs `repo` permissions)
const githubApiToken = process.env.githubApproveSmallPRsToken;
const valHttpUrl = thisValUrl();
const approvalMessage = `Nice one! Keep going 💪 Ping me if you want a more in-depth review`;
if (!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 payload: Payload = await req.json();
const verified = await verifyGithubWebhookSignature(
webhookSecret,
JSON.stringify(payload),
req.headers.get("X-Hub-Signature-256"),
);
if (!verified) {
return response(
401,
`Not verified, see ${valHttpUrl}`,
);
}
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.additions >= 60 || pr.deletions >= 100) {
return response(202, "Ignored (too many changes)");
}
const reviews = (await octokit.request(
`GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews`,
{
owner: organization.login,
repo: repository.name,
pull_number: pull_request.number,
},
)).data;
const alreadyApproved = reviews.some((review) => review.state === "APPROVED");
if (alreadyApproved) {
return response(202, "Already approved by bot");
}
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,
body: approvalMessage,
event: "APPROVE",
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
);
return response(201, "Approved");
};

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,

Imports Gists to Val Town

Import your TypeScript and JavaScript to Val Town

import-gists.png

Authentication

This function requires two keys: one from Github to get your gists, and one from Val Town to make the vals in your account:

  1. Github token: https://github.com/settings/tokens
  2. Val Town key: https://www.val.town/settings/api

Usage

You can use this function by calling it and passing your keys like so:

@maas.importGists({
  githubToken: @me.secrets.githubGists,
  valTownKey: @me.secrets.valtown,
});

Example usage: https://www.val.town/v/maas.untitled_harlequinCrawdad

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
import { createVal } from "https://esm.town/v/stevekrouse/createVal?v=2";
export const importGists = (async ({ githubToken, valTownKey }: {
githubToken: string;
valTownKey: string;
}) => {
const supportedLanguages = ["JavaScript", "TypeScript"];
let imported = [];
const headers = { "X-GitHub-Api-Version": "2022-11-28" };
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: githubToken });
let { data } = await octokit.request("GET /gists", { headers });
for (let i in data) {
let files = Object.values(data[i].files);
let { filename, language } = files[0];
if (!supportedLanguages.includes(language)) {
continue;
}
let gist = await octokit.request("GET /gists/{gist_id}", {
gist_id: data[i].id,
});
let { content } = Object.values(gist.data.files)[0];
createVal({ code: content, valTownKey: valTownKey });
imported.push(filename);
}
return `Imported ${imported.length} vals (${
imported.join(", ")
}) from GitHub Gists.`;
});

Imports Gists to Val Town

Import your TypeScript and JavaScript to Val Town

Authentication

This function requires two keys: one from Github to get your gists, and one from Val Town to make the vals in your account:

  1. Github token: https://github.com/settings/tokens
  2. Val Town key: https://www.val.town/settings/api

Usage

You can use this function by calling it and passing your keys like so:

@stevekrouse.importGists({
  githubToken: @me.secrets.githubGists,
  valTownKey: @me.secrets.valtown,
});

Example usage: https://www.val.town/v/stevekrouse.untitled_redAardvark

Credits

This val was almost entirely made by @maas. I just forked it and fixed a couple bugs.

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
import { createVal } from "https://esm.town/v/stevekrouse/createVal";
export const importGists = (async ({ githubToken, valTownKey }: {
githubToken: string;
valTownKey: string;
}) => {
const supportedLanguages = ["JavaScript", "TypeScript"];
let imported = [];
const headers = { "X-GitHub-Api-Version": "2022-11-28" };
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: githubToken });
let { data } = await octokit.request("GET /gists", { headers });
for (let i in data) {
let files = Object.values(data[i].files);
let { filename, language } = files[0];
if (!supportedLanguages.includes(language)) {
continue;
}
let gist = await octokit.request("GET /gists/{gist_id}", {
gist_id: data[i].id,
});
let { content } = Object.values(gist.data.files)[0];
createVal({ code: content, valTownKey: valTownKey });
imported.push(filename);
}
return `Imported ${imported.length} vals (${
imported.join(", ")
}) from GitHub Gists.`;
});
// Forked from @maas.importGists
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
export async function ghAddAssigneesToAssignable(
assignableId: string,
assigneeIds: string[],
token: string,
) {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: token });
const graphqlVariables = {
assignableId,
assigneeIds,
};
const mutation = `
mutation($assigneeIds:[ID!]!, $assignableId:ID!){
addAssigneesToAssignable(input: {assigneeIds: $assigneeIds, assignableId: $assignableId}) {
assignable {
... on Issue {
id,
number,
title
}
}
}
}
`;
return octokit.graphql(mutation, graphqlVariables);
}
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
export async function ghIssueFromNodeId(nodeId: string, token: string): Promise<{
id: string;
number: number;
repository: string;
owner: string;
}> {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: token });
const graphqlVariables = {
nodeId,
};
const query = `
query($nodeId: ID!) {
node(id: $nodeId) {
... on Issue {
id,
number,
repository {
id,
name,
owner {
id
login
}
}
}
}
}`;
const { node } = await octokit.graphql(query, graphqlVariables);
return {
id: node.id,
number: node.number,
repository: node.repository.name,
owner: node.repository.owner.login,
};
}
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
import { ghIssueFromNodeId } from "https://esm.town/v/augustohp/ghIssueFromNodeId";
export async function ghProjectColumnOfIssue(
nodeId: string,
token: string,
): Promise<string> {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: token });
const issue = await ghIssueFromNodeId(nodeId, token);
const graphqlVariables = {
owner: issue.owner,
repository: issue.repository,
issueNumber: issue.number,
};
const query = `
query($owner:String!, $repository:String!, $issueNumber:Int!, $first:Int=20) {
organization(login: $owner) {
repository(name: $repository) {
issue(number: $issueNumber) {
url,
title,
state,
projectItems(first:$first) {
nodes {
id,
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
}
}
}
}`;
const result = await octokit.graphql(query, graphqlVariables);
const statusColumn = result.organization?.repository?.issue?.projectItems
?.nodes
?.shift();
return statusColumn.fieldValueByName?.name ?? "";
}
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
/**
* Given an issue (GraphQL's Node ID), lists all available
* "fields" (from Projects V2) attached to it - without their
* values.
*
* @example ghProjectFieldsFromIssue("I_kwDOJYe5Es5qSNa2", 2, "github_pat_yadayada")
*/
export async function ghProjectFieldsFromIssue(
nodeId: string,
projectNumber: number,
token: string,
): Promise<{
id: string;
name: string;
}[]> {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({ auth: token });
const graphqlVariables = {
nodeId,
projectNumber,
};
const query = `
query fieldsFromIssue($nodeId: ID!, $projectNumber: Int!, $first: Int = 15) {
node(id: $nodeId) {
... on Issue {
id,
projectV2(number: $projectNumber) {
fields(first: $first) {
nodes {
... on ProjectV2Field {
name,
id
},
... on ProjectV2FieldCommon {
name,
id
},
... on ProjectV2IterationField {
name,
id
},
... on ProjectV2SingleSelectField {
name,
id
}
}
}
}
}
}
}`;
let results = [];
const { node } = await octokit.graphql(query, graphqlVariables);
results.push(...node.projectV2?.fields?.nodes);
return results;
}
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
import { isDateInLastNumberOfDays } from "https://esm.town/v/onemanwenttomow/isDateInLastNumberOfDays";
import process from "node:process";
export const getGithubTeamCommitHistories = async (members) => {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({
auth: process.env.githubToken,
});
const memberEvents = await Promise.all(
members.map((member) => octokit.request(`GET /users/${member}/events`)),
);
const memberCommits = memberEvents.map(({ url, data }) => {
const commits = data.filter((event) => event.type === "PushEvent")
.map((event) => ({
repo: event.repo.name,
created_at: event.created_at,
}));
return {
url,
username: url
.replace("https://api.github.com/users/", "")
.replace("/events", ""),
commitsLast7Days: commits.filter((commit) =>
isDateInLastNumberOfDays(
7,
new Date(commit.created_at),
)
).length,
commitsLast3Days: commits.filter((commit) =>
isDateInLastNumberOfDays(
3,
new Date(commit.created_at),
)
).length,
};
});
return memberCommits;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { coaches } from "https://esm.town/v/onemanwenttomow/coaches";
import process from "node:process";
export const getGithubUsersByTeam = async (team_slug) => {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit({
auth: process.env.githubToken,
});
const { data } = await octokit.request(
`GET /orgs/neuefische/teams/${team_slug}/members`,
);
const members = data.map((user) => user.login).filter((user) =>
!coaches.includes(user)
);
return members;
};
1
2
3
4
5
6
7
8
9
10
11
export const getGithubUserViaOctokit = async (username: string) => {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit();
const user = await octokit.request("GET /users/{username}", {
username,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
return user;
};
1
2
3
4
5
6
7
8
9
10
11
export const octokitExample = (async () => {
const { Octokit } = await import("npm:@octokit/core");
const octokit = new Octokit();
const user = await octokit.request("GET /users/{username}", {
username: "stevekrouse",
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
console.log(user);
})();
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
export async function ghOrgRepos(
owner: string,
token: string,
type: "all" | "public" | "private" | "forks" | "sources" | "member" = "all",
sort: "created" | "updated" | "pushed" | "full_name" = "created",
direction: "asc" | "desc" = "desc",
): Promise<{
name: string;
full_name: string;
description: string;
html_url: string;
private: boolean;
fork: boolean;
archived: boolean;
}[]> {
const { Octokit } = await import("npm:@octokit/core");
const { paginateRest } = await import("npm:@octokit/plugin-paginate-rest");
const PaginatedOctokit = Octokit.plugin(paginateRest);
const gh = new PaginatedOctokit({ auth: token });
const parameters = {
type,
sort,
direction,
per_page: 100,
};
let repositories = [];
try {
const repos = await gh.paginate(`GET /orgs/${owner}/repos`, parameters);
console.log(`${repos.length} repositories found for ${owner}.`);
for (const r of repos) {
repositories.push({
name: r.name,
full_name: r.full_name,
description: r.description,
html_url: r.html_url,
private: r.private,
fork: r.fork,
archived: r.archived,
});
}
return repositories;
}
catch ({ name, message }) {
if (name == "HttpError") {
console.log(`Error: ${owner} is not an organization.`);
}
else {
console.log(`${name}: ${message}`);
}
return [];
}
}
1
Next