From 26ea56c5b9b12388ea049d766726ee79dffd81ea Mon Sep 17 00:00:00 2001 From: William Sawyer Date: Mon, 22 Jan 2024 00:22:33 +1000 Subject: [PATCH] Verify pull request contents --- src/index.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/utils.ts | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c3e8214..45ecc9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,13 @@ +import { components } from "@octokit/openapi-types"; import { EmitterWebhookEvent } from "@octokit/webhooks"; import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda"; +import { SpawnSyncReturns, execSync } from "child_process"; +import { writeFileSync } from "fs"; import { App } from "octokit"; -import { safeOctokitRequest } from "./utils"; +import { safeOctokitRequest, verifyFiles } from "./utils"; + +const requiredFiles = ["script.sh", "config.yaml"]; const app = new App({ appId: process.env.APP_ID!, @@ -76,6 +81,63 @@ async function verify({ payload }: EmitterWebhookEvent<"pull_request">) { repo: payload.repository.name, }); + const fileDiffs = await safeOctokitRequest(octokit.rest.pulls.listFiles, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + pull_number: payload.pull_request.number, + }); + + const errors = await verifyFiles( + octokit.rest.repos.getContent, + payload, + fileDiffs, + requiredFiles + ); + + if (errors.length) { + errors.unshift("Failed to verify pull request contents:"); + await safeOctokitRequest(octokit.rest.issues.updateComment, { + body: errors.join("\n"), + issue_number: payload.pull_request.number, + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: commentId, + }); + + return; + } + + fileDiffs.forEach(async (fileDiff) => { + const { name, content } = (await safeOctokitRequest(octokit.rest.repos.getContent, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + path: fileDiff.filename, + ref: payload.pull_request.head.sha, + })) as components["schemas"]["content-file"]; + + writeFileSync(`/tmp/${name}`, content, { + mode: name === "script.sh" ? 0o755 : 0o644, + }); + }); + + let result: Buffer; + try { + result = execSync("multi-gitter run /tmp/script.sh --config /tmp/config.yaml --dry-run"); + } catch (error) { + const stdout = (error as SpawnSyncReturns).stdout.toString(); + const stderr = (error as SpawnSyncReturns).stderr.toString(); + + await safeOctokitRequest(octokit.rest.issues.updateComment, { + body: "Failed to run `multi-gitter`.", + issue_number: payload.pull_request.number, + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: commentId, + }); + + return; + } + await safeOctokitRequest(octokit.rest.issues.updateComment, { body: "Done verifying.", issue_number: payload.pull_request.number, diff --git a/src/utils.ts b/src/utils.ts index 9107643..c4b5740 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { RestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types.js"; +import { EmitterWebhookEvent } from "@octokit/webhooks"; import { RequestError } from "octokit"; function camelCaseToSentenceCase(str: string) { @@ -42,3 +43,48 @@ export async function safeOctokitRequest< throw error; } } + +/** + * Verifies that the pull request contains the desired non-empty files. + * + * @param getContent the `octokit.rest.repos.getContent` method + * @param payload the pull request webhook payload + * @param fileDiffs the files in the pull request + * @param desiredFilenames the filenames to check for + * + * @returns markdown-formatted error messages for missing or empty files + */ +export async function verifyFiles( + getContent: RestEndpointMethods["repos"]["getContent"], + payload: EmitterWebhookEvent<"pull_request">["payload"], + fileDiffs: Awaited>["data"], + desiredFilenames: string[] +): Promise { + const errors: string[] = []; + + desiredFilenames.forEach(async (desiredFilename) => { + const file = fileDiffs.find((file) => file.filename === desiredFilename); + if (!file) { + return errors.push(` - \`${desiredFilename}\` does not exist`); + } + + const content = await safeOctokitRequest(getContent, { + owner: payload.repository.owner.login, + repo: payload.repository.name, + path: desiredFilename, + ref: payload.pull_request.head.ref, + }); + + if (content instanceof Array) { + return errors.push(` - \`${desiredFilename}\` is a directory`); + } + + if (content.type !== "file") { + return errors.push(` - \`script.sh\` is not a file - is a \`${content.type}\``); + } else if (!content.content) { + return errors.push(" - `script.sh` is empty"); + } + }); + + return errors; +}