This commit is contained in:
Jake Bailey 2024-05-14 11:59:33 -07:00 коммит произвёл GitHub
Родитель e028274330
Коммит f03b148b82
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
53 изменённых файлов: 4276 добавлений и 4865 удалений

7
.github/workflows/ci.yml поставляемый
Просмотреть файл

@ -5,10 +5,11 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '10.x'
node-version: '20.x'
- run: npm install
- run: npm run build
- run: npm test
- run: npx knip

7
.github/workflows/upload.yml поставляемый
Просмотреть файл

@ -9,15 +9,16 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@v1
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '10.x'
node-version: '20.x'
- name: 'run npm'
run: |
npm install
npm run build --if-present
npm run test --if-present
npm prune --production
- uses: Azure/functions-action@v1
id: fa
with:

13
.knip.jsonc Normal file
Просмотреть файл

@ -0,0 +1,13 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"functions/*ts"
],
"project": [
"**/*.{js,ts,tsx}"
],
"ignore": ["dist/**", "src/util/createCLILogger.ts", "src/util/tests/**"],
"ignoreBinaries": ["func"],
"ignoreDependencies": ["@types/jest"],
"ignoreExportsUsedInFile": true
}

Просмотреть файл

@ -1,20 +0,0 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/TypeScriptRepoIssueWebhook/index.js"
}

Просмотреть файл

@ -1,3 +0,0 @@
{
"name": "Azure"
}

Просмотреть файл

@ -1,20 +0,0 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/TypeScriptRepoIssueWebhook/index.js"
}

Просмотреть файл

@ -1,3 +0,0 @@
{
"name": "Azure"
}

Просмотреть файл

@ -1,20 +0,0 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/TypeScriptRepoPullRequestWebhook/index.js"
}

Просмотреть файл

@ -1,3 +0,0 @@
{
"name": "Azure"
}

2
ambient.d.ts поставляемый
Просмотреть файл

@ -1,2 +0,0 @@
declare module '@octokit/webhooks/verify'
declare module '@octokit/webhooks/sign'

Просмотреть файл

@ -1,46 +1,46 @@
import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import { createGitHubClient } from "../src/util/createGitHubClient";
type NPMWebhook = {
event: string
name: string
type: string
version: string
change: {
"dist-tag": string
version: string
}
}
const crypto = require('crypto');
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const expectedSignature = crypto
.createHmac('sha256', process.env.NPM_HOOK_SECRET)
.update(req.body)
.digest('hex');
if (req.headers["x-npm-signature"] !== `sha256=${expectedSignature}`) {
throw new Error(`Bad signature received. Rejecting hook. (got ${expectedSignature} expected ${req.headers["x-npm-signature"]}`);
}
const webhook = req.body as NPMWebhook
const tag = webhook.change.version
const isProd = !tag.includes("-dev")
if (isProd) {
const gh = createGitHubClient()
const masterRef = await gh.repos.getBranch({ owner: "microsoft", repo: "Make-Monaco-Builds", branch: "master" })
await gh.git.createTag({ owner: "microsoft", repo: "Make-Monaco-Builds", tag: tag, message: "Auto-generated from TS webhooks", type: "commit", object: masterRef.data.commit.sha })
context.res = {
status: 200,
body: "Tagged"
}
} else {
context.res = {
status: 200,
body: "NOOP"
}
}
};
export default httpTrigger;
import { app, HttpHandler } from "@azure/functions"
import { createGitHubClient } from "../src/util/createGitHubClient";
type NPMWebhook = {
event: string
name: string
type: string
version: string
change: {
"dist-tag": string
version: string
}
}
const crypto = require('crypto');
const httpTrigger: HttpHandler = async function (req, context) {
const bodyText = await req.text();
const expectedSignature = crypto
.createHmac('sha256', process.env.NPM_HOOK_SECRET)
.update(bodyText)
.digest('hex');
if (req.headers.get("x-npm-signature") !== `sha256=${expectedSignature}`) {
throw new Error(`Bad signature received. Rejecting hook. (got ${expectedSignature} expected ${req.headers.get("x-npm-signature")}`);
}
const webhook = JSON.parse(bodyText) as NPMWebhook
const tag = webhook.change.version
const isProd = !tag.includes("-dev")
if (isProd) {
const gh = createGitHubClient()
const masterRef = await gh.repos.getBranch({ owner: "microsoft", repo: "Make-Monaco-Builds", branch: "master" })
await gh.git.createTag({ owner: "microsoft", repo: "Make-Monaco-Builds", tag: tag, message: "Auto-generated from TS webhooks", type: "commit", object: masterRef.data.commit.sha })
return {
status: 200,
body: "Tagged"
}
} else {
return {
status: 200,
body: "NOOP"
}
}
};
app.http("NPMNewTSReleaseWebhook", { handler: httpTrigger });

Просмотреть файл

@ -1,24 +1,28 @@
import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import verify = require('@octokit/webhooks/verify')
import sign = require('@octokit/webhooks/sign')
// The goal of these functions is to validate the call is real, then as quickly as possible get out of the azure
// context and into the `src` directory, where work can be done against tests instead requiring changes to happen
// against a real server.
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const isDev = process.env.AZURE_FUNCTIONS_ENVIRONMENT === "Development"
const secret = process.env.GITHUB_WEBHOOK_SECRET
// For process.env.GITHUB_WEBHOOK_SECRET see
// https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/JSTSTeam-Storage/providers/Microsoft.KeyVault/vaults/jststeam-passwords/secrets
if (!isDev && !verify(secret, req.body, sign(secret, req.body))) {
context.res = {
status: 500,
body: "This webhook did not come from GitHub"
};
return;
}
// NOOP
};
export default httpTrigger;
import { app, HttpHandler } from "@azure/functions"
import { verify } from "@octokit/webhooks-methods";
// The goal of these functions is to validate the call is real, then as quickly as possible get out of the azure
// context and into the `src` directory, where work can be done against tests instead requiring changes to happen
// against a real server.
const httpTrigger: HttpHandler = async function (request, context) {
const isDev = process.env.AZURE_FUNCTIONS_ENVIRONMENT === "Development"
// For process.env.GITHUB_WEBHOOK_SECRET see
// https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/JSTSTeam-Storage/providers/Microsoft.KeyVault/vaults/jststeam-passwords/secrets
const secret = process.env.GITHUB_WEBHOOK_SECRET
const body = await request.text();
const sig = request.headers.get("x-hub-signature-256");
if (!isDev && (!sig || !verify(secret!, body, `sha256=${sig}`))) {
context.log("Invalid signature");
return {
status: 500,
body: "This webhook did not come from GitHub"
};
}
// NOOP
return {};
};
app.http("TypeScriptRepoIssueWebhook", { handler: httpTrigger });

Просмотреть файл

@ -1,56 +1,60 @@
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import verify = require("@octokit/webhooks/verify");
import sign = require("@octokit/webhooks/sign");
import { handlePullRequestPayload } from "../src/anyRepoHandlePullRequest";
import { anyRepoHandleStatusUpdate } from "../src/anyRepoHandleStatusUpdate";
import { anyRepoHandleIssueCommentPayload } from "../src/anyRepoHandleIssueComment";
import { handleIssuePayload } from "../src/anyRepoHandleIssue";
// The goal of these functions is to validate the call is real, then as quickly as possible get out of the azure
// context and into the `src` directory, where work can be done against tests instead requiring changes to happen
// against a real server.
const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise<void> {
const isDev = process.env.AZURE_FUNCTIONS_ENVIRONMENT === "Development";
const secret = process.env.GITHUB_WEBHOOK_SECRET;
// For process.env.GITHUB_WEBHOOK_SECRET see
// https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/JSTSTeam-Storage/providers/Microsoft.KeyVault/vaults/jststeam-passwords/secrets
if (!isDev && !verify(secret, req.body, sign(secret, req.body))) {
context.res = {
status: 500,
body: "This webhook did not come from GitHub"
};
return;
}
// https://github.com/microsoft/TypeScript/settings/hooks/163309719
const event = req.headers["x-github-event"] as "pull_request" | "status" | "issue_comment" | "issues";
switch (event) {
case "pull_request":
await handlePullRequestPayload(req.body, context);
break;
case "status":
await anyRepoHandleStatusUpdate(req.body, context);
break;
case "issue_comment":
await anyRepoHandleIssueCommentPayload(req.body, context)
break;
case "issues":
await handleIssuePayload(req.body, context)
break;
default:
context.log.info("Skipped webhook, do not know how to handle the event: ", event)
}
};
export default httpTrigger;
import { app, HttpHandler } from "@azure/functions"
import { verify } from "@octokit/webhooks-methods";
import assert from "assert";
import { handlePullRequestPayload } from "../src/anyRepoHandlePullRequest";
import { anyRepoHandleStatusUpdate } from "../src/anyRepoHandleStatusUpdate";
import { anyRepoHandleIssueCommentPayload } from "../src/anyRepoHandleIssueComment";
import { handleIssuePayload } from "../src/anyRepoHandleIssue";
// The goal of these functions is to validate the call is real, then as quickly as possible get out of the azure
// context and into the `src` directory, where work can be done against tests instead requiring changes to happen
// against a real server.
const httpTrigger: HttpHandler = async function (request, context) {
const isDev = process.env.AZURE_FUNCTIONS_ENVIRONMENT === "Development"
// For process.env.GITHUB_WEBHOOK_SECRET see
// https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/JSTSTeam-Storage/providers/Microsoft.KeyVault/vaults/jststeam-passwords/secrets
const secret = process.env.GITHUB_WEBHOOK_SECRET
const bodyText = await request.text();
const sig = request.headers.get("x-hub-signature-256");
if (!isDev && (!sig || !verify(secret!, bodyText, `sha256=${sig}`))) {
context.log("Invalid signature");
return {
status: 500,
body: "This webhook did not come from GitHub"
};
}
const body = JSON.parse(bodyText);
// https://github.com/microsoft/TypeScript/settings/hooks/163309719
const event = request.headers.get("x-github-event") as "pull_request" | "status" | "issue_comment" | "issues";
switch (event) {
case "pull_request":
return handlePullRequestPayload(body, context);
case "status":
return anyRepoHandleStatusUpdate(body, context);
case "issue_comment":
return anyRepoHandleIssueCommentPayload(body, context)
case "issues":
return handleIssuePayload(body, context)
default:
context.info("Skipped webhook, do not know how to handle the event: ", event)
return {};
}
};
app.http("TypeScriptRepoPullRequestWebhook", { handler: httpTrigger });
export default httpTrigger;

Просмотреть файл

@ -1,3 +1,15 @@
{
"version": "2.0"
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.15.0, 4.0.0)"
}
}

8078
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,45 +2,41 @@
"name": "repos-automation",
"version": "1.0.0",
"description": "",
"private": true,
"engines": {
"node": ">=18"
},
"main": "dist/functions/*.js",
"scripts": {
"build": "tsc",
"postinstall": "node scripts/printSHA.js",
"watch": "tsc -w",
"prestart": "npm run build",
"start": "func start",
"deploy": "func azure functionapp publish TypeScriptReposAutomation",
"test": "jest"
},
"dependencies": {
"@azure/functions": "^1.0.2-beta2",
"@octokit/rest": "^16.35.0",
"@octokit/webhooks": "^6.3.2",
"@types/minimatch": "^3.0.3",
"minimatch": "^3.0.4",
"parse-diff": "^0.6.0",
"node-fetch": "^2.6.0"
"@azure/functions": "^4.4.0",
"@octokit/rest": "^20.1.1",
"@octokit/webhooks-methods": "^4.1.0",
"minimatch": "^9.0.4"
},
"devDependencies": {
"@tsconfig/node12": "^1.0.3",
"@types/jest": "^24.0.23",
"@types/node-fetch": "^2.5.7",
"codeowners": "^4.1.1",
"jest": "^24.9.0",
"prettier": "^2.0.5",
"ts-jest": "^24.2.0",
"ts-node": "^8.5.4",
"typescript": "4.0.0-beta"
},
"prettier": {
"printWidth": 120,
"semi": false,
"arrowParens": "avoid"
"@octokit/webhooks-types": "^7.5.1",
"@tsconfig/node18": "^18.2.4",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"knip": "^5.15.1",
"ts-jest": "^29.1.2",
"typescript": "^5.4.5"
},
"jest": {
"preset": "ts-jest",
"prettierPath": null,
"testPathIgnorePatterns": [
"/node_modules/",
"dist"
]
}
},
"packageManager": "npm@10.7.0+sha256.f443ed4364ea11ac5cf7cae7fb4731278c64dd6839093f8a46eabde0430e0fcd"
}

Просмотреть файл

@ -1,4 +0,0 @@
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {}
}

Просмотреть файл

@ -1,28 +1,29 @@
import { WebhookPayloadIssues } from "@octokit/webhooks"
import { Context, Logger } from "@azure/functions"
import { IssuesEvent } from "@octokit/webhooks-types"
import { InvocationContext, HttpResponseInit } from "@azure/functions"
import { Octokit } from "@octokit/rest"
import { sha } from "./sha"
import { addReprosLabelOnIssue } from "./checks/addReprosLabel"
import { createGitHubClient } from "./util/createGitHubClient"
import { Logger } from "./util/logger"
export const handleIssuePayload = async (payload: WebhookPayloadIssues, context: Context) => {
export const handleIssuePayload = async (payload: IssuesEvent, context: InvocationContext): Promise<HttpResponseInit> => {
const api = createGitHubClient()
const ran = [] as string[]
const run = (
name: string,
fn: (api: Octokit, payload: WebhookPayloadIssues, logger: Logger) => Promise<void>
fn: (api: Octokit, payload: IssuesEvent, logger: Logger) => Promise<void>
) => {
context.log.info(`\n\n## ${name}\n`)
context.info(`\n\n## ${name}\n`)
ran.push(name)
return fn(api, payload, context.log)
return fn(api, payload, context)
}
if (payload.repository.name === "TypeScript") {
run("Adding repro tags from issue bodies", addReprosLabelOnIssue)
}
context.res = {
return {
status: 200,
headers: { sha: sha },
body: ran.length ? `Issue success, ran: ${ran.join(", ")}`: "Success, NOOP",

Просмотреть файл

@ -1,22 +1,23 @@
import { WebhookPayloadIssueComment } from "@octokit/webhooks"
import { Context, Logger } from "@azure/functions"
import { IssueCommentEvent } from "@octokit/webhooks-types"
import { HttpResponseInit, InvocationContext } from "@azure/functions"
import { createGitHubClient } from "./util/createGitHubClient"
import { Octokit } from "@octokit/rest"
import { sha } from "./sha"
import { mergeThroughCodeOwners } from "./checks/mergeThroughCodeOwners"
import { addReprosLabelOnComments } from "./checks/addReprosLabel"
import { Logger } from "./util/logger"
export const anyRepoHandleIssueCommentPayload = async (payload: WebhookPayloadIssueComment, context: Context) => {
export const anyRepoHandleIssueCommentPayload = async (payload: IssueCommentEvent, context: InvocationContext): Promise<HttpResponseInit> => {
const api = createGitHubClient()
const ran = [] as string[]
const run = (
name: string,
fn: (api: Octokit, payload: WebhookPayloadIssueComment, logger: Logger) => Promise<void>
fn: (api: Octokit, payload: IssueCommentEvent, logger: Logger) => Promise<void>
) => {
context.log.info(`\n\n## ${name}\n`)
context.info(`\n\n## ${name}\n`)
ran.push(name)
return fn(api, payload, context.log)
return fn(api, payload, context)
}
// Making this one whitelisted to the website for now
@ -28,7 +29,7 @@ export const anyRepoHandleIssueCommentPayload = async (payload: WebhookPayloadIs
await run("Checking if we should add the repros label", addReprosLabelOnComments)
}
context.res = {
return {
status: 200,
headers: { sha: sha },
body: `Success, ran: ${ran.join(", ")}`,

Просмотреть файл

@ -1,32 +1,33 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { Context, Logger } from "@azure/functions"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { createGitHubClient } from "./util/createGitHubClient"
import { assignSelfToNewPullRequest } from "./checks/assignSelfToNewPullRequest"
import { addLabelForTeamMember } from "./checks/addLabelForTeamMember"
import { assignTeamMemberForRelatedPR } from "./checks/assignTeamMemberForRelatedPR"
import { addMilestoneLabelsToPRs } from "./checks/addMilestoneLabelsToPRs"
import { addCommentToUncommittedPRs } from "./checks/addCommentToUncommittedPRs"
import { Octokit } from "@octokit/rest"
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
import { sha } from "./sha"
import { isMemberOfTSTeam } from "./pr_meta/isMemberOfTSTeam"
import { getRelatedIssues } from "./pr_meta/getRelatedIssues"
import { HttpResponseInit, InvocationContext } from "@azure/functions"
import { Logger } from "./util/logger"
export const handlePullRequestPayload = async (payload: WebhookPayloadPullRequest, context: Context) => {
export const handlePullRequestPayload = async (payload: PullRequestEvent, context: InvocationContext): Promise<HttpResponseInit> => {
const api = createGitHubClient()
const ran = [] as string[]
const run = (
name: string,
fn: (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger, pr: PRInfo) => Promise<void>,
fn: (api: Octokit, payload: PullRequestEvent, logger: Logger, pr: PRInfo) => Promise<void>,
pr: PRInfo
) => {
context.log.info(`\n\n## ${name}\n`)
context.info(`\n\n## ${name}\n`)
ran.push(name)
return fn(api, payload, context.log, pr)
return fn(api, payload, context, pr)
}
if (payload.repository.name === "TypeScript") {
const pr = await generatePRInfo(api, payload, context.log)
const pr = await generatePRInfo(api, payload, context)
await run("Assigning Self to Core Team PRs", assignSelfToNewPullRequest, pr)
await run("Add a core team label to PRs", addLabelForTeamMember, pr)
@ -35,18 +36,17 @@ export const handlePullRequestPayload = async (payload: WebhookPayloadPullReques
await run("Adding comment on uncommitted PRs", addCommentToUncommittedPRs, pr)
}
context.res = {
return {
status: 200,
headers: { sha: sha },
body: ran.length ? `PR success, ran: ${ran.join(", ")}`: "Success, NOOP",
}
}
export type UnPromise<T> = T extends Promise<infer U> ? U : T
// The return type of generatePRInfo
export type PRInfo = UnPromise<ReturnType<typeof generatePRInfo>>
export type PRInfo = Awaited<ReturnType<typeof generatePRInfo>>
const generatePRInfo = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger) => {
const generatePRInfo = async (api: Octokit, payload: PullRequestEvent, logger: Logger) => {
const { repository: repo, pull_request } = payload
const thisIssue = {
@ -56,10 +56,10 @@ const generatePRInfo = async (api: Octokit, payload: WebhookPayloadPullRequest,
}
const options = api.issues.listComments.endpoint.merge(thisIssue)
const comments: Octokit.IssuesListCommentsResponse = await api.paginate(options)
const comments: RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"] = await api.paginate(options)
const authorIsMemberOfTSTeam = await isMemberOfTSTeam(payload.pull_request.user.login, api, logger)
const relatedIssues = await getRelatedIssues(pull_request.body, repo.owner.login, repo.name, api)
const relatedIssues = await getRelatedIssues(pull_request.body ?? "", repo.owner.login, repo.name, api)
return {
thisIssue,

Просмотреть файл

@ -1,18 +1,19 @@
import { WebhookPayloadStatus } from "@octokit/webhooks"
import { Context, Logger } from "@azure/functions"
import { StatusEvent } from "@octokit/webhooks-types"
import { createGitHubClient } from "./util/createGitHubClient"
import { Octokit } from "@octokit/rest"
import { sha } from "./sha"
import { mergeOnGreen } from "./checks/mergeOnGreen"
import { HttpResponseInit, InvocationContext } from "@azure/functions"
import { Logger } from "./util/logger"
export const anyRepoHandleStatusUpdate = async (payload: WebhookPayloadStatus, context: Context) => {
export const anyRepoHandleStatusUpdate = async (payload: StatusEvent, context: InvocationContext): Promise<HttpResponseInit> => {
const api = createGitHubClient()
const ran = [] as string[]
const run = (name: string, fn: (api: Octokit, payload: WebhookPayloadStatus, logger: Logger) => Promise<void>) => {
context.log.info(`\n\n## ${name}\n`)
const run = (name: string, fn: (api: Octokit, payload: StatusEvent, logger: Logger) => Promise<void>) => {
context.info(`\n\n## ${name}\n`)
ran.push(name)
return fn(api, payload, context.log)
return fn(api, payload, context)
}
// Run checks
@ -20,7 +21,7 @@ export const anyRepoHandleStatusUpdate = async (payload: WebhookPayloadStatus, c
await run("Checking For Merge on Green", mergeOnGreen)
}
context.res = {
return {
status: 200,
headers: { sha: sha },
body: `Success, ran: ${ran.join(", ")}`,

Просмотреть файл

@ -5,6 +5,7 @@ import { addCommentToUncommittedPRs } from "./addCommentToUncommittedPRs"
import { createMockGitHubClient, getPRFixture } from "../util/tests/createMockGitHubClient"
import { getFakeLogger } from "../util/tests/createMockContext"
import { createPRInfo } from "../util/tests/createPRInfo"
import { Label } from "@octokit/webhooks-types"
describe(addCommentToUncommittedPRs, () => {
it("Adds a comment to an uncommented, unlinked PR", async () => {
@ -85,7 +86,7 @@ describe(addCommentToUncommittedPRs, () => {
const pr = getPRFixture("opened")
pr.pull_request.body = `fixes #1123`
pr.pull_request.labels = [{ name: "For Backlog Bug" }]
pr.pull_request.labels = [{ name: "For Backlog Bug" } as Partial<Label> as Label]
const info = createPRInfo({
comments: [{ body: "The TypeScript team hasn't accepted the linked issue #1" }] as any,

Просмотреть файл

@ -1,20 +1,20 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import type { PRInfo } from "../anyRepoHandlePullRequest"
import { Logger } from "../util/logger"
/**
* Comment on new PRs that don't have linked issues, or link to uncommitted issues.
*/
export const addCommentToUncommittedPRs = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger, info: PRInfo) => {
export const addCommentToUncommittedPRs = async (api: Octokit, payload: PullRequestEvent, logger: Logger, info: PRInfo) => {
if (payload.pull_request.merged || payload.pull_request.draft || info.authorIsMemberOfTSTeam || info.authorIsTypescriptBot) {
return logger("Skipping")
return logger.trace("Skipping")
}
if (!info.relatedIssues || info.relatedIssues.length === 0) {
const message = "This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise."
const needsComment = !info.comments || !info.comments.find(c => c.body.startsWith(message.slice(0, 25)))
const needsComment = !info.comments || !info.comments.find(c => c.body?.startsWith(message.slice(0, 25)))
if (needsComment) {
await api.issues.createComment({
...info.thisIssue,
@ -23,12 +23,18 @@ export const addCommentToUncommittedPRs = async (api: Octokit, payload: WebhookP
}
}
else {
const isSuggestion = info.relatedIssues.some(issue => issue.labels?.find(l => l.name === "Suggestion"))
const isCommitted = info.relatedIssues.some(issue => issue.labels?.find(l => l.name === "Committed" || l.name === "Experience Enhancement" || l.name === "help wanted"))
const isSuggestion = info.relatedIssues.some(issue => issue.labels?.find(l => {
const name = typeof l === "string" ? l : l.name;
return name === "Suggestion"
}))
const isCommitted = info.relatedIssues.some(issue => issue.labels?.find(l => {
const name = typeof l === "string" ? l : l.name;
return name === "Committed" || name === "Experience Enhancement" || name === "help wanted"
}))
if (isSuggestion && !isCommitted) {
const message = `The TypeScript team hasn't accepted the linked issue #${info.relatedIssues[0].number}. If you can get it accepted, this PR will have a better chance of being reviewed.`
const needsComment = !info.comments || !info.comments.find(c => c.body.startsWith(message.slice(0, 25)))
const needsComment = !info.comments || !info.comments.find(c => c.body?.startsWith(message.slice(0, 25)))
if (needsComment) {
await api.issues.createComment({
...info.thisIssue,

Просмотреть файл

@ -1,12 +1,12 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import type { Logger } from "@azure/functions"
import type { PRInfo } from "../anyRepoHandlePullRequest"
import { Logger } from "../util/logger"
/**
* If the PR comes from a core contributor, add a label to indicate it came from a maintainer
*/
export const addLabelForTeamMember = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger, info: PRInfo) => {
export const addLabelForTeamMember = async (api: Octokit, payload: PullRequestEvent, logger: Logger, info: PRInfo) => {
const { repository: repo, pull_request } = payload
// Check the access level of the user

Просмотреть файл

@ -1,87 +0,0 @@
import {
WebhookPayloadPullRequest,
WebhookPayloadIssueComment,
WebhookPayloadPullRequestReview,
} from "@octokit/webhooks"
import { Octokit } from "@octokit/rest"
import { isMemberOfTSTeam } from "../pr_meta/isMemberOfTSTeam"
import type { Logger } from "@azure/functions"
import { mergeOrAddMergeLabel } from "../pr_meta/mergeOrAddMergeLabel"
/**
* If the PR comes from a core contributor, add a label to indicate it came from a maintainer
*/
export const addLabelForTeamMember = async (
api: Octokit,
payload: WebhookPayloadIssueComment | WebhookPayloadPullRequestReview,
logger: Logger
) => {
const org = payload.repository.owner.login
let issue: WebhookPayloadIssueComment["issue"] = null!
let text: string = null!
let userLogin: string = ""
let issueNumber: number = -1
if ("issue" in payload) {
issue = payload.issue
text = payload.comment.body
userLogin = payload.comment.user.login
issueNumber = issue.number
// Only look at PR issue comments, this isn't in the type system
if (!(issue as any).pull_request) {
return logger.info("Not a Pull Request")
}
}
if ("review" in payload) {
const repo = payload.repository
const response = await api.issues.get({
owner: repo.owner.login,
repo: repo.name,
number: payload.pull_request.number,
})
issue = response.data as any
text = payload.review.body as any
userLogin = payload.review.user.login
}
// Bail if there's no text from the review
if (!text) {
logger.info("Could not find text for the webhook to look for the merge on green message")
return
}
// Don't do any work unless we have to
const keywords = ["merge on green"]
const match = keywords.find(k => text.toLowerCase().includes(k))
if (!match) {
return logger.info(`Did not find any of the merging phrases in the comment beginning ${text.substring(0, 12)}.`)
}
// Check to see if the label has already been set
if (issue.labels.find(l => l.name.toLowerCase() === "merge on green")) {
return logger.info("Already has Merge on Green")
}
// Check for org access, so that some rando doesn't
// try to merge something without permission
const isTeamMember = await isMemberOfTSTeam(payload.sender.login, api, logger)
if (!isTeamMember) {
return logger.info(`Skipping because ${payload.sender.login} is not a member of the TS team`)
}
const repo = {
owner: org,
repo: payload.repository.name,
number: issue.number,
}
// Need to get the sha for auto-merging
const prResponse = await api.pulls.get(repo)
await mergeOrAddMergeLabel(api, repo, prResponse.data.head.sha, logger)
console.log("Updated the PR with a Merge on Green label")
}

Просмотреть файл

@ -5,6 +5,7 @@ import { createMockGitHubClient, getPRFixture } from "../util/tests/createMockGi
import { getFakeLogger } from "../util/tests/createMockContext"
import { getRelatedIssues } from "../pr_meta/getRelatedIssues"
import { Label } from "@octokit/webhooks-types"
const mockGetRelatedIssues = (getRelatedIssues as any) as jest.Mock
describe(addMilestoneLabelsToPRs, () => {
@ -13,7 +14,7 @@ describe(addMilestoneLabelsToPRs, () => {
mockGetRelatedIssues.mockResolvedValue([{ assignees: [] }])
const pr = getPRFixture("opened")
pr.pull_request.labels = [{ name: "Something" }, { name: "Other" }]
pr.pull_request.labels = [{ name: "Something" } as Partial<Label> as Label, { name: "Other" } as Partial<Label> as Label]
await addMilestoneLabelsToPRs(api, pr, getFakeLogger())
@ -49,7 +50,7 @@ describe(addMilestoneLabelsToPRs, () => {
const pr = getPRFixture("opened")
pr.pull_request.body = `fixes #1123`
pr.pull_request.labels = [{ name: "For Backlog Bug" }]
pr.pull_request.labels = [{ name: "For Backlog Bug" } as Partial<Label> as Label]
await addMilestoneLabelsToPRs(api, pr, getFakeLogger())
@ -82,7 +83,7 @@ describe(addMilestoneLabelsToPRs, () => {
const pr = getPRFixture("opened")
pr.pull_request.body = `fixes #1123`
pr.pull_request.labels = [{ name: "For Backlog Bug" }]
pr.pull_request.labels = [{ name: "For Backlog Bug" } as Partial<Label> as Label]
await addMilestoneLabelsToPRs(api, pr, getFakeLogger())

Просмотреть файл

@ -1,19 +1,19 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { getRelatedIssues } from "../pr_meta/getRelatedIssues"
import { Logger } from "../util/logger"
/**
* Keep track of the milestone related PRs which are based on linked issues in the PR body
*/
export const addMilestoneLabelsToPRs = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger) => {
export const addMilestoneLabelsToPRs = async (api: Octokit, payload: PullRequestEvent, logger: Logger) => {
const { repository: repo, pull_request } = payload
if (pull_request.state === "closed") {
return logger.info(`Skipping because the pull request is already closed.`)
}
const relatedIssues = await getRelatedIssues(pull_request.body, repo.owner.login, repo.name, api)
const relatedIssues = await getRelatedIssues(pull_request.body ?? "", repo.owner.login, repo.name, api)
const houseKeepingLabels = {
"For Milestone Bug": false,
@ -31,7 +31,7 @@ export const addMilestoneLabelsToPRs = async (api: Octokit, payload: WebhookPayl
for (const issue of relatedIssues) {
const milestone = issue.milestone
if (milestone) {
if (milestone.title !== "Backlog" || issue.assignees.length) {
if (milestone.title !== "Backlog" || issue.assignees?.length) {
houseKeepingLabels["For Milestone Bug"] = true
} else {
houseKeepingLabels["For Backlog Bug"] = true
@ -62,7 +62,10 @@ export const addMilestoneLabelsToPRs = async (api: Octokit, payload: WebhookPayl
if (houseKeepingLabels["For Milestone Bug"]) {
for (const issue of relatedIssues) {
if (!issue.labels.find(l => l.name === "Fix Available")) {
if (!issue.labels.find(l => {
const name = typeof l === "string" ? l : l.name;
return name === "Fix Available"
})) {
await api.issues.addLabels({ ...thisIssue, issue_number: issue.number, labels: ["Fix Available"] })
}
}

Просмотреть файл

@ -52,7 +52,7 @@ describe(addReprosLabelOnComments, () => {
it("NO-OPs when the action isn't opened or edited ", async () => {
const { mockAPI, api } = createMockGitHubClient()
const payload = getIssueCommentFixture("created")
payload.action = "closed"
payload.action = "deleted"
await addReprosLabelOnComments(api, payload, getFakeLogger())
@ -64,7 +64,7 @@ describe(addReprosLabelOnComments, () => {
const { mockAPI, api } = createMockGitHubClient()
const payload = getIssueCommentFixture("created")
payload.comment.body = "```ts repro"
payload.action = "closed"
payload.action = "deleted"
await addReprosLabelOnComments(api, payload, getFakeLogger())

Просмотреть файл

@ -1,30 +1,30 @@
import { WebhookPayloadIssueComment, WebhookPayloadIssues } from "@octokit/webhooks"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { pingDiscord, stripBody } from "./pingDiscordForReproRequests"
import { Logger } from "../util/logger"
import { IssueCommentEvent, IssuesEvent } from "@octokit/webhooks-types"
const checkForRepro = (toCheck: { body: string }) => {
const checkForRepro = (body: string) => {
const codeblocks = ["```ts repro", "```tsx repro", "```js repro", "```jsx repro"]
const hasRepro = codeblocks.find(c => toCheck.body.includes(c))
const hasRepro = codeblocks.find(c => body.includes(c))
return hasRepro
}
/**
* Adds the 'has repro' label to PRs with based on the issue body
*/
export const addReprosLabelOnIssue = async (api: Octokit, payload: WebhookPayloadIssues, logger: Logger) => {
export const addReprosLabelOnIssue = async (api: Octokit, payload: IssuesEvent, logger: Logger) => {
const actionable = ["opened", "edited"]
if (!actionable.includes(payload.action)) {
return logger.info("Skipping because this cannot change repro state")
}
if (payload.issue.labels.length === 0) {
if (!payload.issue.labels?.length) {
return logger.info("Skipping because we don't want to add the label until it's been triaged")
}
const { repository: repo, issue } = payload
const hasReproLabel = !!issue.labels.find(l => l.name === "Has Repro")
const hasReproLabel = !!issue.labels?.find(l => l.name === "Has Repro")
const thisIssue = {
repo: repo.name,
@ -32,7 +32,7 @@ export const addReprosLabelOnIssue = async (api: Octokit, payload: WebhookPayloa
issue_number: issue.number,
}
const hasReproInBody = checkForRepro(issue)
const hasReproInBody = checkForRepro(issue.body ?? "")
if (hasReproInBody && !hasReproLabel) {
await api.issues.addLabels({ ...thisIssue, labels: ["Has Repro"] })
@ -40,11 +40,11 @@ export const addReprosLabelOnIssue = async (api: Octokit, payload: WebhookPayloa
await pingDiscord(`Repro created by @${issue.user.login} on #${issue.number}`, {
number: issue.number,
title: issue.title,
body: stripBody(issue.body),
body: stripBody(issue.body ?? ""),
url: issue.html_url,
})
if (issue.labels.find(l => l.name === "Repro Requested")) {
if (issue.labels?.find(l => l.name === "Repro Requested")) {
await api.issues.removeLabel({ ...thisIssue, name: "Repro Requested" })
}
@ -58,7 +58,7 @@ export const addReprosLabelOnIssue = async (api: Octokit, payload: WebhookPayloa
*/
export const addReprosLabelOnComments = async (
api: Octokit,
payload: WebhookPayloadIssueComment,
payload: IssueCommentEvent,
logger: Logger
) => {
const actionable = ["created", "edited"]
@ -79,7 +79,7 @@ export const addReprosLabelOnComments = async (
issue_number: issue.number,
}
const hasReproInBody = checkForRepro(comment)
const hasReproInBody = checkForRepro(comment.body)
if (hasReproInBody && !hasReproLabel) {
await api.issues.addLabels({ ...thisIssue, labels: ["Has Repro"] })

Просмотреть файл

@ -5,13 +5,14 @@ import { createMockGitHubClient, convertToOctokitAPI, getPRFixture } from "../ut
import { getFakeLogger } from "../util/tests/createMockContext"
import { isMemberOfTSTeam } from "../pr_meta/isMemberOfTSTeam"
import { User } from "@octokit/webhooks-types"
const mockIsMember = (isMemberOfTSTeam as any) as jest.Mock
describe(assignSelfToNewPullRequest, () => {
it("NO-OPs when there's assignees already ", async () => {
const { mockAPI, api } = createMockGitHubClient()
const pr = getPRFixture("opened")
pr.pull_request.assignees = ["orta"]
pr.pull_request.assignees = [{ login: "orta" } as Partial<User> as User]
await assignSelfToNewPullRequest(api, pr, getFakeLogger())

Просмотреть файл

@ -1,13 +1,13 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { isMemberOfTSTeam } from "../pr_meta/isMemberOfTSTeam"
import { Logger } from "@azure/functions"
import { Logger } from "../util/logger"
/**
* If the PR comes from a core contributor, set themselves to be the assignee
* if one isn't set during the creation of the PR.
*/
export const assignSelfToNewPullRequest = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger) => {
export const assignSelfToNewPullRequest = async (api: Octokit, payload: PullRequestEvent, logger: Logger) => {
const { repository: repo, pull_request } = payload
if (pull_request.assignees.length > 0) {
return logger.info("Skipping because there are assignees already")

Просмотреть файл

@ -5,13 +5,14 @@ import { createMockGitHubClient, getPRFixture } from "../util/tests/createMockGi
import { getFakeLogger } from "../util/tests/createMockContext"
import { getRelatedIssues } from "../pr_meta/getRelatedIssues"
import { User } from "@octokit/webhooks-types"
const mockGetRelatedIssues = (getRelatedIssues as any) as jest.Mock
describe(assignTeamMemberForRelatedPR, () => {
it("NO-OPs when there's assignees already ", async () => {
const { mockAPI, api } = createMockGitHubClient()
const pr = getPRFixture("opened")
pr.pull_request.assignees = ["orta"]
pr.pull_request.assignees = [{ login: "orta" } as Partial<User> as User]
await assignTeamMemberForRelatedPR(api, pr, getFakeLogger())

Просмотреть файл

@ -1,25 +1,25 @@
import { WebhookPayloadPullRequest } from "@octokit/webhooks"
import { PullRequestEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { getRelatedIssues } from "../pr_meta/getRelatedIssues"
import { Logger } from "../util/logger"
/**
* If a community PR comes in with a 'fixes #43' and 43 is assigned to a team member, then assign that PR
*/
export const assignTeamMemberForRelatedPR = async (api: Octokit, payload: WebhookPayloadPullRequest, logger: Logger) => {
export const assignTeamMemberForRelatedPR = async (api: Octokit, payload: PullRequestEvent, logger: Logger) => {
const { repository: repo, pull_request } = payload
if (pull_request.assignees.length > 0) {
return logger.info("Skipping because there are assignees already")
}
const relatedIssues = await getRelatedIssues(pull_request.body, repo.owner.login, repo.name, api)
const relatedIssues = await getRelatedIssues(pull_request.body ?? "", repo.owner.login, repo.name, api)
if (!relatedIssues) {
return logger.info("Skipping because there are no related issues")
}
const assignees: string[] = []
for (const issue of relatedIssues) {
for (const issueAssignee of issue.assignees) {
for (const issueAssignee of issue.assignees ?? []) {
assignees.push(issueAssignee.login)
}
}

Просмотреть файл

@ -33,10 +33,10 @@ describe("for handling merging when green", () => {
const logger = getFakeLogger()
// Says al CI statuses are green
mockAPI.checks.listForRef.mockResolvedValueOnce({ data: { check_runs: [{ conclusion: "SUCCESS" }]}})
mockAPI.checks.listForRef.mockResolvedValueOnce({ data: { check_runs: [{ conclusion: "success" }]}})
// Gets a corresponding issue
mockAPI.search.issues.mockResolvedValueOnce({ data: { items: [{ number: 1 }] } })
mockAPI.search.issuesAndPullRequests.mockResolvedValueOnce({ data: { items: [{ number: 1 }] } })
// Returns an issue without the merge on green label
mockAPI.issues.get.mockResolvedValueOnce({ data: { labels: [{ name: "Dog Snoozer" }] } })
@ -57,10 +57,10 @@ describe("for handling merging when green", () => {
const logger = getFakeLogger()
// Says al CI statuses are green
mockAPI.checks.listForRef.mockResolvedValueOnce({ data: { check_runs: [{ conclusion: "SUCCESS" }]}})
mockAPI.checks.listForRef.mockResolvedValueOnce({ data: { check_runs: [{ conclusion: "success" }]}})
// Gets a corresponding issue
mockAPI.search.issues.mockResolvedValueOnce({ data: { items: [{ number: 1 }] } })
mockAPI.search.issuesAndPullRequests.mockResolvedValueOnce({ data: { items: [{ number: 1 }] } })
// Returns an issue without the merge on green label
mockAPI.issues.get.mockResolvedValueOnce({ data: { labels: [{ name: "Merge On Green" }] } })
@ -75,7 +75,7 @@ describe("for handling merging when green", () => {
expect(mockAPI.pulls.merge).toBeCalledWith({
commit_title: "Merge pull request #1 by microsoft/typescript-repos-automation",
number: 1,
pull_number: 1,
owner: "danger",
repo: "doggo",
})

Просмотреть файл

@ -1,12 +1,12 @@
import { WebhookPayloadStatus } from "@octokit/webhooks"
import { StatusEvent } from "@octokit/webhooks-types"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { Logger } from "../util/logger"
/**
* If the PR comes from a core contributor, set themselves to be the assignee
* if one isn't set during the creation of the PR.
*/
export const mergeOnGreen = async (api: Octokit, payload: WebhookPayloadStatus, logger: Logger) => {
export const mergeOnGreen = async (api: Octokit, payload: StatusEvent, logger: Logger) => {
if (payload.state !== "success") {
return logger.info(`Not a successful state - got ${payload.state}`)
}
@ -16,22 +16,25 @@ export const mergeOnGreen = async (api: Octokit, payload: WebhookPayloadStatus,
const repo = payload.repository.name
const status = await api.checks.listForRef({ owner, repo, ref: payload.commit.sha })
if (status.data.check_runs.every(c => c.conclusion !== "SUCCESS")) {
if (status.data.check_runs.every(c => c.conclusion !== "success")) {
return logger.info("Not all statuses are green")
}
// See https://github.com/maintainers/early-access-feedback/issues/114 for more context on getting a PR from a SHA
const repoString = payload.repository.full_name
const searchResponse = await api.search.issues({ q: `${payload.commit.sha} type:pr is:open repo:${repoString}` })
const searchResponse = await api.search.issuesAndPullRequests({ q: `${payload.commit.sha} type:pr is:open repo:${repoString}` })
// https://developer.github.com/v3/search/#search-issues
const prsWithCommit = searchResponse.data.items.map((i: any) => i.number) as number[]
for (const number of prsWithCommit) {
// Get the PR labels
const issue = await api.issues.get({ owner, repo, number })
const issue = await api.issues.get({ owner, repo, issue_number: number })
// Get the PR combined status
const mergeLabel = issue.data.labels.find(l => l.name.toLowerCase() === "merge on green")
const mergeLabel = issue.data.labels.find(l => {
const name = typeof l === "string" ? l : l.name;
return name?.toLowerCase() === "merge on green"
})
if (!mergeLabel) {
return logger.info(`PR ${number} does not have Merge on Green`)
} else {
@ -47,7 +50,7 @@ export const mergeOnGreen = async (api: Octokit, payload: WebhookPayloadStatus,
}
// Merge the PR
await api.pulls.merge({ owner, repo, number, commit_title: commitTitle })
await api.pulls.merge({ owner, repo, pull_number: number, commit_title: commitTitle })
logger.info(`Merged Pull Request ${number}`)
}
}

Просмотреть файл

@ -1,21 +1,21 @@
import { WebhookPayloadIssueComment } from "@octokit/webhooks"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { IssueCommentEvent } from "@octokit/webhooks-types"
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
import { hasAccessToMergePRs } from "../pr_meta/hasAccessToMergePR"
import { mergeOrAddMergeLabel } from "../pr_meta/mergeOrAddMergeLabel"
import { Logger } from "../util/logger"
export const mergePhrase = "ready to merge"
/**
* Allow someone to declare a PR should be merged if they have access rights via code owners
*/
export const mergeThroughCodeOwners = async (api: Octokit, payload: WebhookPayloadIssueComment, logger: Logger) => {
export const mergeThroughCodeOwners = async (api: Octokit, payload: IssueCommentEvent, logger: Logger) => {
if (!payload.comment.body.toLowerCase().includes(mergePhrase)) {
return logger.info(`Issue comment did not include '${mergePhrase}', skipping merge through code owners checks`)
}
// Grab the correlating PR
let pull: Octokit.Response<Octokit.PullsGetResponse>["data"]
let pull: RestEndpointMethodTypes["pulls"]["get"]["response"]["data"]
try {
const response = await api.pulls.get({
@ -36,7 +36,7 @@ export const mergeThroughCodeOwners = async (api: Octokit, payload: WebhookPaylo
logger.info("Looks good to merge")
await mergeOrAddMergeLabel(
api,
{ number: pull.number, repo: pull.base.repo.name, owner: pull.base.repo.owner.login },
{ pull_number: pull.number, repo: pull.base.repo.name, owner: pull.base.repo.owner.login },
pull.head.sha,
logger
)

Просмотреть файл

@ -1,7 +1,4 @@
import { WebhookPayloadIssues } from "@octokit/webhooks"
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import fetch from "node-fetch"
export const pingDiscord = async (msg: string, config: { number: number; title: string; body: string; url: string }) => {
if (!process.env.REPRO_REQUEST_DISCORD_WEBHOOK) throw new Error("No process var for REPRO_REQUEST_DISCORD_WEBHOOK")

Просмотреть файл

@ -1,41 +0,0 @@
import { Octokit } from "@octokit/rest"
/**
* A map of path: [owners]
*
* Note that CodeOwners has paths starting with a /
* whereas the GitHub API returns it without a slash prefix
*/
type CodeOwners = { [path: string]: string[] }
export const getCodeOwners = async (api: Octokit) => {
const allCodeOwnersResponse = await api.repos.getContents({
owner: "DefinitelyTyped",
repo: "DefinitelyTyped",
path: ".github/CODEOWNERS",
})
// @ts-ignore - types are mismatched
const base64Content = allCodeOwnersResponse.data.content
const raw = Buffer.from(base64Content, "base64").toString()
const codeOwners = {} as CodeOwners
// https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/.github/CODEOWNERS
for (const line of raw.split(/\r?\n/g)) {
if (line.trim().length === 0) continue
const match = /^(\S+)\s+(.*)$/.exec(line)
if (!match) throw new Error(`Expected the line from CODEOWNERS to match the regexp - ${line}`)
codeOwners[match[1]] = match[2].split(" ").map(removeLeadingAt)
}
function removeLeadingAt(s: string) {
if (s[0] === "@") return s.substr(1)
return s
}
return codeOwners
}
/** Thought this would be complex (but it wasn't) */
export const findMatchingOwners = (path: string, codeOwners: CodeOwners) => codeOwners[`/types/${path}/`] ?? []

Просмотреть файл

@ -1,83 +0,0 @@
import * as parseDiff from "parse-diff"
import { Octokit } from "@octokit/rest"
import { Context } from "@azure/functions"
import { getCodeOwners, findMatchingOwners } from "./getCodeOwners"
type PullRequest = import("@octokit/rest").Octokit.PullsGetResponse
/** The context around which you can make decisions */
export interface PRFilesContext {
diffString: string
diff: ReturnType<typeof parseDiff>
files: {
added: string[]
changed: string[]
deleted: string[]
}
pr: PullRequest
touchedModules: Array<{
name: string
codeOwners: string[]
files: string[]
}>
}
export const getPRFileContext = async (
api: Octokit,
prNumber: number,
context: Context
): Promise<PRFilesContext | undefined> => {
const thisPR = { owner: "DefinitelyTyped", repo: "DefinitelyTyped", pull_number: prNumber }
const diffHeaders = { accept: "application/vnd.github.v3.diff" }
// The diff API specifically is an untyped edge-case in the octokit API, so a few coercions are needed
const diffResponse = await api.pulls.get({ ...thisPR, headers: diffHeaders } as any)
const diffString = (diffResponse.data as unknown) as string
if (diffString === undefined) {
context.log.error(`Could not get a diff for PR ${prNumber}`)
return undefined
}
const diff = parseDiff(diffString)
// Filters to the raw diff Files
const added = diff.filter(diff => diff["new"])
const deleted = diff.filter(diff => diff["deleted"])
const changed = diff.filter(diff => !added.includes(diff) && !deleted.includes(diff))
// Converts to just the names
const files = {
// The weird ending is a work-around for danger/danger-js#807 - it's less likely to hit us on here, but better to be safe
added: added.map(d => d.to || (d.from && d.from.split(" b/")[0]) || "unknown"),
changed: changed.map(d => d.to || "unknown"),
deleted: deleted.map(d => d.from || "unknown"),
}
const filesInTypeModules = diff
.map(d => d.to || (d.from && d.from.split(" b/")[0]))
.filter(Boolean)
.filter(path => path?.startsWith("types")) as string[]
const projectNames = new Set(filesInTypeModules.map(p => p?.split("/")[1]))
const allCodeOwners = await getCodeOwners(api)
const touchedModules: PRFilesContext["touchedModules"] = Array.from(projectNames).map(name => {
return {
name: name,
files: filesInTypeModules.filter(p => p.startsWith(`types/${name}`)),
codeOwners: findMatchingOwners(name, allCodeOwners),
}
})
const prResponse = await api.pulls.get(thisPR)
const pr = prResponse.data
return {
diff,
diffString,
files,
pr,
touchedModules,
}
}

Просмотреть файл

@ -13,15 +13,15 @@ fixes https://github.com/microsoft/TypeScript/issues/6
const result = findIssuesInBody(body)
expect(result).toMatchInlineSnapshot(`
Array [
"#1",
"#2",
"microsoft/typescript#4",
"microsoft/typescript#3",
"#5",
"https://github.com/microsoft/typescript/issues/6",
]
`)
[
"#1",
"#2",
"microsoft/typescript#4",
"microsoft/typescript#3",
"#5",
"https://github.com/microsoft/typescript/issues/6",
]
`)
})
it("pulls out issues", () => {
@ -35,10 +35,10 @@ fixes https://github.com/microsoft/TypeScript/issues/6
const allResults = findIssuesInBody(body)
const constrainedResults = constrainIssuesToBaseRepo(allResults, "MiCrOSoFT/TypeScript")
expect(constrainedResults).toMatchInlineSnapshot(`
Array [
"3",
"6",
"1",
]
`)
[
"3",
"6",
"1",
]
`)
})

Просмотреть файл

@ -1,4 +1,4 @@
import { Octokit } from "@octokit/rest"
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
// https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords
const closePrefixes = ["close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"]
@ -6,7 +6,7 @@ const closePrefixes = ["close", "closes", "closed", "fix", "fixes", "fixed", "re
export const getRelatedIssues = async (body: string, owner: string, name: string, api: Octokit) => {
const allFixedIssues = findIssuesInBody(body)
const ourIssues = constrainIssuesToBaseRepo(allFixedIssues, `${owner}/${name}`)
const issues: import("@octokit/rest").Octokit.IssuesGetResponse[] = []
const issues: RestEndpointMethodTypes["issues"]["get"]["response"]["data"][] = []
for (const issueNumber of ourIssues) {
const response = await api.issues.get({ issue_number: Number(issueNumber), owner, repo: name })

Просмотреть файл

@ -1,9 +1,9 @@
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
import { getContents } from "../util/getContents"
import * as minimatch from "minimatch"
import { minimatch } from "minimatch"
import { Logger } from "../util/logger"
type PR = Octokit.Response<Octokit.PullsGetResponse>["data"]
type PR = RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
/**
* Checks if a user has access to merge via a comment
@ -67,7 +67,7 @@ async function getPRChangedFiles(octokit: Octokit, webhook: PR) {
pull_number: webhook.number,
})
const files = await octokit.paginate(options)
const files = await octokit.paginate<RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number]>(options)
const fileStrings = files.map(f => `/${f.filename}`)
return fileStrings
}

Просмотреть файл

@ -1,5 +1,5 @@
import { Octokit } from "@octokit/rest"
import type { Logger } from "@azure/functions"
import { Logger } from "../util/logger"
let cachedTSTeam: string[] = []
/** Checks if someone is a member of a team, and always bails with TS bot */
@ -10,7 +10,7 @@ export const isMemberOfTSTeam = async (username: string, api: Octokit, _log: Log
return cachedTSTeam.includes(username)
}
const contentResponse = await api.repos.getContents({
const contentResponse = await api.repos.getContent({
path: ".github/pr_owners.txt",
repo: "TypeScript",
owner: "Microsoft",

Просмотреть файл

@ -1,23 +1,23 @@
import { Octokit } from "@octokit/rest"
import { Logger } from "@azure/functions"
import { Logger } from "../util/logger"
type PullMeta = {
repo: string
owner: string
number: number
pull_number: number
}
export const mergeOrAddMergeLabel = async (api: Octokit, pullMeta: PullMeta, headCommitSHA: string, logger: Logger) => {
const allGreen = await api.repos.getCombinedStatusForRef({ ...pullMeta, ref: headCommitSHA })
if (allGreen.data.state === "success") {
logger("Merging")
logger.trace("Merging")
// Merge now
const commitTitle = "Merged automatically"
await api.pulls.merge({ ...pullMeta, commit_title: commitTitle })
} else {
logger("Adding Merge on Green")
logger.trace("Adding Merge on Green")
// Merge when green
await api.issues.addLabels({ ...pullMeta, labels: ["Merge on Green"] })
await api.issues.addLabels({ ...pullMeta, labels: ["Merge on Green"], issue_number: pullMeta.pull_number })
}
}

Просмотреть файл

@ -3,29 +3,30 @@ jest.mock("../anyRepoHandleStatusUpdate", () => ({ anyRepoHandleStatusUpdate: je
jest.mock("../anyRepoHandleIssueComment", () => ({ anyRepoHandleIssueCommentPayload: jest.fn() }))
jest.mock("../anyRepoHandleIssue", () => ({ handleIssuePayload: jest.fn() }))
import webhook from "../../TypeScriptRepoPullRequestWebhook/index"
import webhook from "../../functions/TypeScriptRepoPullRequestWebhook"
import { handlePullRequestPayload } from "../anyRepoHandlePullRequest"
import { anyRepoHandleStatusUpdate } from "../anyRepoHandleStatusUpdate"
import { anyRepoHandleIssueCommentPayload } from "../anyRepoHandleIssueComment"
import { handleIssuePayload } from "../anyRepoHandleIssue"
import { HttpRequest, InvocationContext } from "@azure/functions"
it("calls handle PR from the webhook main", () => {
it("calls handle PR from the webhook main", async () => {
process.env.AZURE_FUNCTIONS_ENVIRONMENT = "Development"
webhook({ log: { info: () => "" } } as any, { body: "{}", headers: { "x-github-event": "pull_request" } })
await webhook(new HttpRequest({ method: "POST", url: "https://example.org", body: { string: "{}" }, headers: { "x-github-event": "pull_request" } }), new InvocationContext({ logHandler: () => "" }))
expect(handlePullRequestPayload).toHaveBeenCalled()
})
it("calls handle status from the webhook main", () => {
it("calls handle status from the webhook main", async () => {
process.env.AZURE_FUNCTIONS_ENVIRONMENT = "Development"
webhook({ log: { info: () => "" } } as any, { body: "{}", headers: { "x-github-event": "status" } })
await webhook(new HttpRequest({ method: "POST", url: "https://example.org", body: { string: "{}" }, headers: { "x-github-event": "status" } }), new InvocationContext({ logHandler: () => "" }))
expect(anyRepoHandleStatusUpdate).toHaveBeenCalled()
})
it("calls handle comments from the webhook main", () => {
it("calls handle comments from the webhook main", async () => {
process.env.AZURE_FUNCTIONS_ENVIRONMENT = "Development"
webhook({ log: { info: () => "" } } as any, { body: "{}", headers: { "x-github-event": "issue_comment" } })
await webhook(new HttpRequest({ method: "POST", url: "https://example.org", body: { string: "{}" }, headers: { "x-github-event": "issue_comment" } }), new InvocationContext({ logHandler: () => "" }))
expect(anyRepoHandleIssueCommentPayload).toHaveBeenCalled()
})

Просмотреть файл

@ -1,12 +1,13 @@
import { Logger } from "@azure/functions"
import { Logger } from "./logger"
const cliLogger = (...args: any[]) => {
console.log(args)
const cliLogger: Logger = {
log: console.log,
trace: console.trace,
debug: console.debug,
info: console.info,
warn: console.warn,
error: console.error
}
cliLogger.error = console.error
cliLogger.warn = console.warn
cliLogger.info = console.info
cliLogger.verbose = console.info
/** Returns a logger which conforms to the Azure logger interface */
export const createCLILogger = (): Logger => cliLogger

Просмотреть файл

Просмотреть файл

@ -1,7 +1,7 @@
import { Octokit } from "@octokit/rest"
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest"
export const getContents = async (api: Octokit, opts: Octokit.ReposGetContentsParams) => {
const contentResponse = await api.repos.getContents(opts)
export const getContents = async (api: Octokit, opts: RestEndpointMethodTypes["repos"]["getContent"]["parameters"]) => {
const contentResponse = await api.repos.getContent(opts)
// @ts-ignore types are mismatched
const text = Buffer.from(contentResponse.data.content, "base64").toString()
return text

3
src/util/logger.ts Normal file
Просмотреть файл

@ -0,0 +1,3 @@
import { InvocationContext } from "@azure/functions";
export type Logger = Pick<InvocationContext, "log" | "trace" | "debug" | "info" | "warn" | "error">;

Просмотреть файл

@ -1,12 +1,15 @@
import { Logger, Context } from "@azure/functions"
import { InvocationContext } from "@azure/functions"
import { Logger } from "../logger"
/** Returns a logger which conforms to the Azure logger interface */
export const getFakeLogger = (): Logger => {
const cliLogger = jest.fn() as any
cliLogger.error = jest.fn()
cliLogger.warn = jest.fn()
const cliLogger = jest.fn() as any as Logger
cliLogger.log = jest.fn()
cliLogger.trace = jest.fn()
cliLogger.debug = jest.fn()
cliLogger.info = jest.fn()
cliLogger.verbose = jest.fn()
cliLogger.warn = jest.fn()
cliLogger.error = jest.fn()
return cliLogger
}
@ -14,7 +17,5 @@ export const getFakeLogger = (): Logger => {
* Create a mock context which eats all logs, only contains the logging subset
* for now, and should be extended if needed
*/
export const createMockContext = (): Context =>
({
log: getFakeLogger(),
} as any)
export const createMockContext = (): InvocationContext =>
(getFakeLogger() as Partial<InvocationContext> as InvocationContext)

Просмотреть файл

@ -1,80 +0,0 @@
import { PRFilesContext, getPRFileContext } from "../../pr_meta/getPRFileContext"
import { Context } from "@azure/functions"
import { createMockGitHubClient, getPRFixture, convertToOctokitAPI } from "./createMockGitHubClient"
import { createMockContext } from "./createMockContext"
/**
* Creates a version of the DT context object, with an API which lets you override any
* specific part of the object you want. It's quite wordy to set up, so you can use
* `createDefaultMockDTContext` if you just need a high level tool
*
* @example
*
* const context = createMockContext()
* const mockAPI = createMockGitHubClient();
*
* // A diff, see https://patch-diff.githubusercontent.com/raw/DefinitelyTyped/DefinitelyTyped/pull/40896.diff
* mockAPI.pulls.get.mockResolvedValueOnce({ data: '' })
*
* // A PR, see https://api.github.com/repos/definitelytyped/definitelytyped/pulls/40896
* const pr = getPRFixture("opened")
* mockAPI.pulls.get.mockResolvedValueOnce(pr)
*
* const api = convertToOctokitAPI(mockAPI);
* const dtContext = await createMockDTContext(api, context, {})
*/
export const createMockPRFileContext = async (
api: import("@octokit/rest").Octokit,
context: Context,
overrides: Partial<PRFilesContext>
) => {
const defaultCodeowners = `
/types/lambda-tester/ @ivank @HajoAhoMantila @msuntharesan
/types/langmap/ @grabofus
/types/lasso/ @darkwebdev
/types/later/ @jasond-s
/types/latinize/ @GiedriusGrabauskas
/types/latlon-geohash/ @rimig
/types/launchpad/ @rictic
/types/layzr.js/ @shermendev
/types/lazy-property/ @jank1310
/types/lazy.js/ @Bartvds @miso440 @gablorquet
/types/lazypipe/ @tomc974
`
let buff = Buffer.from(defaultCodeowners)
let base64data = buff.toString("base64")
// @ts-ignore
api.repos.getContents.mockResolvedValueOnce({ data: { content: base64data } })
const dtContext = await getPRFileContext(api, 9999, context)
if (!dtContext) throw new Error("Did not create a DT context")
return { ...dtContext, ...overrides }
}
/**
* A default set of contextual objects for writing tests against. You can use the
* partial object to override any results from the default setup
*/
export const createDefaultMockPRFileContext = async (
overrides: Partial<PRFilesContext>,
options?: { diff?: string; prFixture?: string }
) => {
const context = createMockContext()
const { mockAPI } = createMockGitHubClient()
// The diff
mockAPI.pulls.get.mockResolvedValueOnce({ data: options?.diff ?? "" })
// The PR
const pr = getPRFixture((options?.prFixture as any) ?? "opened")
mockAPI.pulls.get.mockResolvedValueOnce(pr)
const api = convertToOctokitAPI(mockAPI)
const dt = await createMockPRFileContext(api, context, overrides)
return {
api,
mockAPI,
dt,
context,
}
}

Просмотреть файл

@ -1,7 +1,7 @@
import { Octokit } from "@octokit/rest"
import { readFileSync } from "fs"
import { join } from "path"
import { WebhookPayloadPullRequest, WebhookPayloadIssues, WebhookPayloadIssueComment } from "@octokit/webhooks"
import { IssueCommentEvent, IssuesEvent, PullRequestEvent } from "@octokit/webhooks-types"
/**
* Creates a version of the GitHub API client where API calls
@ -23,7 +23,7 @@ export const createMockGitHubClient = () => {
const mockAPI = {
repos: {
checkCollaborator: jest.fn(),
getContents: jest.fn(),
getContent: jest.fn(),
getCombinedStatusForRef: jest.fn(),
createDispatchEvent: jest.fn()
},
@ -53,7 +53,7 @@ export const createMockGitHubClient = () => {
getMembership: jest.fn(),
},
search: {
issues: jest.fn(),
issuesAndPullRequests: jest.fn(),
},
paginate: jest.fn(),
}
@ -81,7 +81,7 @@ export const createFakeGitHubClient = () => {
const fake: PromisifiedInferredJestObj = {
repos: {
checkCollaborator: Promise.resolve({}),
getContents: Promise.resolve({}),
getContent: Promise.resolve({}),
getCombinedStatusForRef: Promise.resolve({}),
createDispatchEvent: Promise.resolve({}),
},
@ -107,7 +107,7 @@ export const createFakeGitHubClient = () => {
getMembership: Promise.resolve({ status: 200 }),
},
search: {
issues: Promise.resolve({}),
issuesAndPullRequests: Promise.resolve({}),
},
paginate: Promise.resolve({}) as any,
}
@ -126,13 +126,13 @@ export const convertToOctokitAPI = (mock: {}) => {
}
/** Grabs a known PR fixture */
export const getPRFixture = (fixture: "closed" | "opened" | "api-pr-closed"): WebhookPayloadPullRequest =>
export const getPRFixture = (fixture: "closed" | "opened" | "api-pr-closed"): PullRequestEvent =>
JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "fixtures", "pulls", fixture + ".json"), "utf8"))
/** Grabs a known issue fixture */
export const getIssueFixture = (fixture: "opened" | "labeled"): WebhookPayloadIssues =>
export const getIssueFixture = (fixture: "opened" | "labeled"): IssuesEvent =>
JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "fixtures", "issues", fixture + ".json"), "utf8"))
/** Grabs a known issue fixture */
export const getIssueCommentFixture = (fixture: "created"): WebhookPayloadIssueComment =>
export const getIssueCommentFixture = (fixture: "created"): IssueCommentEvent =>
JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "fixtures", "issue_comments", fixture + ".json"), "utf8"))

Просмотреть файл

@ -1,5 +1,5 @@
{
"extends": "@tsconfig/node12",
"extends": "@tsconfig/node18",
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",