Update deps, functions (#44)
This commit is contained in:
Родитель
e028274330
Коммит
f03b148b82
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
14
host.json
14
host.json
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
42
package.json
42
package.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
|
||||
|
|
|
@ -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": ".",
|
||||
|
|
Загрузка…
Ссылка в новой задаче