This repository has been archived by the owner on Jul 1, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a general shape for discussions support (#412)
* Adds a general shape for discussions support * WIP * Get green * Feedback WIP * Adds a general shape for discussions support * WIP * Get green * Feedback WIP * Some fixes: * `discussions-trigger`: added some uses of `txt` to make it more readable and other minor reformatting for the gql queries * Call to `verifyIsFromGitHub` should be negated * `verifyIsFromGitHub` doesn't need to be exported * Pings owners etc * Reuse repo ID from query * Feedback Co-authored-by: Eli Barzilay <eli@barzilay.org>
- Loading branch information
1 parent
fac6976
commit 9ed27cd
Showing
11 changed files
with
341 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"bindings": [ | ||
{ | ||
"authLevel": "function", | ||
"type": "httpTrigger", | ||
"direction": "in", | ||
"name": "req", | ||
"methods": [ | ||
"get", | ||
"post" | ||
] | ||
}, | ||
{ | ||
"type": "http", | ||
"direction": "out", | ||
"name": "res" | ||
} | ||
], | ||
"scriptFile": "../bin/discussions-trigger.js" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/// <reference types="jest" /> | ||
import {canHandleRequest, extractNPMReference} from "../discussions-trigger"; | ||
|
||
describe(canHandleRequest, () => { | ||
const eventActions = [ | ||
["discussion", "created", true], | ||
["discussion", "edited", true], | ||
["discussion", "updated", false], | ||
["pull_request", "created", false] | ||
] as const; | ||
|
||
test.concurrent.each(eventActions)("(%s, %s) is %s", async (event, action, expected) => { | ||
expect(canHandleRequest(event, action)).toEqual(expected); | ||
}); | ||
}); | ||
|
||
describe(extractNPMReference, () => { | ||
const eventActions = [ | ||
["[node] my thingy", "node"], | ||
["OK [react]", "react"], | ||
["I think [@typescript/twoslash] need improving ", "@typescript/twoslash"], | ||
["[@types/node] needs X", "node"], | ||
] as const; | ||
|
||
test.concurrent.each(eventActions)("(%s, %s) is %s", async (title, result) => { | ||
expect(extractNPMReference({ title })).toEqual(result); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { HttpRequest, Context } from "@azure/functions"; | ||
import fetch from "node-fetch"; | ||
import { gql } from "@apollo/client/core"; | ||
import { Discussion, DiscussionWebhook } from "./types/discussions"; | ||
import { createMutation, client } from "./graphql-client"; | ||
import { reply } from "./util/reply"; | ||
import { httpLog, shouldRunRequest } from "./util/verify"; | ||
import { txt } from "./util/util"; | ||
import { getOwnersOfPackage } from "./pr-info"; | ||
import { fetchFile } from "./util/fetchFile"; | ||
|
||
export async function run(context: Context, req: HttpRequest) { | ||
httpLog(context, req); | ||
|
||
if (!(await shouldRunRequest(req, canHandleRequest))) { | ||
reply(context, 204, "Can't handle this request"); | ||
} | ||
|
||
const { body, headers } = req; | ||
return handleTrigger({ event: headers["x-github-event"]!, action: body.action, body }, context); | ||
} | ||
|
||
export const canHandleRequest = (event: string, action: string) => { | ||
const name = "discussion"; | ||
const actions = ["created", "edited"]; | ||
return event == name && actions.includes(action); | ||
}; | ||
|
||
const handleTrigger = (info: { event: string; action: string; body: DiscussionWebhook }, context: Context) => { | ||
const categoryID = info.body.discussion.category.slug; | ||
if (categoryID === "issues-with-a-types-package") { | ||
return pingAuthorsAndSetUpDiscussion(info.body.discussion); | ||
} else if (categoryID === "request-a-new-types-package" && info.action === "created") { | ||
return updateDiscordWithRequest(info.body.discussion); | ||
} | ||
return reply(context, 204, "Can't handle this specific request"); | ||
}; | ||
|
||
export function extractNPMReference(discussion: { title: string }) { | ||
const title = discussion.title; | ||
if (title.includes("[") && title.includes("]")) { | ||
const full = title.split("[")[1]!.split("]")[0]; | ||
return full!.replace("@types/", ""); | ||
} | ||
return undefined; | ||
} | ||
|
||
const couldNotFindMessage = txt` | ||
|Hi, we could not find a reference to the types you are talking about in this discussion. | ||
|Please edit the title to include the name on npm inside square brackets. | ||
| | ||
|E.g. | ||
|- \`"[@typescript/vfs] Does not x, y"\` | ||
|- \`"Missing x inside [node]"\` | ||
|- \`"[express] Broken support for template types"\` | ||
`; | ||
|
||
const errorsGettingOwners = (str: string) => txt` | ||
|Hi, we could not find [${str}] in DefinitelyTyped, is there possibly a typo? | ||
`; | ||
|
||
const couldNotFindOwners = (str: string) => txt` | ||
|Hi, we had an issue getting the owners for [${str}] - please raise an issue on DefinitelyTyped/dt-mergebot if this | ||
`; | ||
|
||
|
||
const gotAReferenceMessage = (module: string, owners: string[]) => txt` | ||
|Thanks for the discussion about "${module}", some useful links for everyone: | ||
| | ||
| - [npm](https://www.npmjs.com/package/${module}) | ||
| - [DT](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/${module}) | ||
| - [Related discussions](https://github.com/DefinitelyTyped/DefinitelyTyped/issues?q=is%3Aopen+is%3Aissue+label%3A%22Pkg%3A+${module}%22/) | ||
| | ||
|Pinging the DT module owners: ${owners.join(", ")}. | ||
`; | ||
|
||
|
||
async function pingAuthorsAndSetUpDiscussion(discussion: Discussion) { | ||
const aboutNPMRef = extractNPMReference(discussion); | ||
if (!aboutNPMRef) { | ||
// Could not find a types reference | ||
await updateOrCreateMainComment(discussion, couldNotFindMessage); | ||
} else { | ||
const owners = await getOwnersOfPackage(aboutNPMRef, "master", fetchFile); | ||
if (owners instanceof Error) { | ||
await updateOrCreateMainComment(discussion, errorsGettingOwners(aboutNPMRef)); | ||
} else if (!owners) { | ||
await updateOrCreateMainComment(discussion, couldNotFindOwners(aboutNPMRef)); | ||
} else { | ||
const message = gotAReferenceMessage(aboutNPMRef, owners); | ||
await updateOrCreateMainComment(discussion, message); | ||
await addLabel(discussion, "Pkg: " + aboutNPMRef, `Discussions related to ${aboutNPMRef}`); | ||
} | ||
} | ||
} | ||
|
||
async function updateDiscordWithRequest(discussion: Discussion) { | ||
const discordWebhookAddress = process.env.DT_MODULE_REQ_DISCORD_WEBHOOK; | ||
if (!discordWebhookAddress) throw new Error("DT_MODULE_REQ_DISCORD_WEBHOOK not set in ENV"); | ||
|
||
// https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html | ||
const webhook = { content: `New DT Module requested:`, embeds: [ { title: discussion.title, url: discussion.html_url } ] }; | ||
await fetch(discordWebhookAddress, { method: "POST", body: JSON.stringify(webhook), headers: { "content-type": "application/json" } }); | ||
} | ||
|
||
|
||
async function updateOrCreateMainComment(discussion: Discussion, message: string) { | ||
const discussionComments = await getCommentsForDiscussionNumber(discussion.number); | ||
const previousComment = discussionComments.find(c => c.author.login === "typescript-bot"); | ||
if (previousComment) { | ||
await client.mutate(createMutation<any>("updateDiscussionComment" as any, { body: message, commentId: previousComment.id })); | ||
} else { | ||
await client.mutate(createMutation<any>("addDiscussionComment" as any, { body: message, discussionId: discussion.node_id })); | ||
} | ||
} | ||
|
||
async function addLabel(discussion: Discussion, labelName: string, description?: string) { | ||
const existingLabel = await getLabelByName(labelName); | ||
let labelID = null; | ||
if (existingLabel.label && existingLabel.label.name === labelName) { | ||
labelID = existingLabel.label.id; | ||
} else { | ||
const color = "eeeeee"; | ||
const newLabel = await client.mutate(createMutation("createLabel" as any, { name: labelName, repositoryId: existingLabel.repoID, color, description })) as any; | ||
labelID = newLabel.data.label.id; | ||
} | ||
await client.mutate(createMutation<any>("addLabelsToLabelable" as any, { labelableId: discussion.node_id, labelIds: [labelID] })); | ||
} | ||
|
||
async function getLabelByName(name: string) { | ||
const info = await client.query({ | ||
query: gql` | ||
query GetLabel($name: String!) { | ||
repository(name: "DefinitelyTyped", owner: "DefinitelyTyped") { | ||
id | ||
name | ||
labels(query: $name, first: 1) { | ||
nodes { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
}`, | ||
variables: { name }, | ||
fetchPolicy: "no-cache", | ||
}); | ||
|
||
const label: { id: string, name: string } | undefined = info.data.repository.labels.nodes[0]; | ||
return { repoID: info.data.repository.id, label }; | ||
} | ||
|
||
async function getCommentsForDiscussionNumber(number: number) { | ||
const info = await client.query({ | ||
query: gql` | ||
query GetDiscussionComments($discussionNumber: Int!) { | ||
repository(name: "DefinitelyTyped", owner: "DefinitelyTyped") { | ||
name | ||
discussion(number: $discussionNumber) { | ||
comments(first: 100, orderBy: { field: UPDATED_AT, direction: DESC }) { | ||
nodes { | ||
author { | ||
login | ||
} | ||
id | ||
body | ||
} | ||
} | ||
} | ||
} | ||
}`, | ||
variables: { discussionNumber: number }, | ||
fetchPolicy: "no-cache", | ||
}); | ||
|
||
return info.data.repository.discussion.comments.nodes as Array<{ author: { login: string}, body: string, id: string }>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Generated from the JSON response because it's not in the upstream tooling | ||
|
||
export interface DiscussionWebhook { | ||
action: string; | ||
discussion: Discussion; | ||
repository: any; | ||
sender: any; | ||
} | ||
|
||
export interface Discussion { | ||
repository_url: string; | ||
category: Category; | ||
answer_html_url: null; | ||
answer_chosen_at: null; | ||
answer_chosen_by: null; | ||
html_url: string; | ||
id: number; | ||
node_id: string; | ||
number: number; | ||
title: string; | ||
user: Sender; | ||
state: string; | ||
locked: boolean; | ||
comments: number; | ||
created_at: Date; | ||
updated_at: Date; | ||
author_association: string; | ||
active_lock_reason: null; | ||
body: string; | ||
} | ||
|
||
export interface Category { | ||
id: number; | ||
repository_id: number; | ||
emoji: string; | ||
name: string; | ||
description: string; | ||
created_at: Date; | ||
updated_at: Date; | ||
slug: string; | ||
is_answerable: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Context } from "@azure/functions"; | ||
|
||
export const reply = (context: Context, status: number, body: string) => { | ||
context.res = { status, body }; | ||
context.log.info(`${body} [${status}]`); | ||
}; |
Oops, something went wrong.