import { waitUntil } from "cloudflare:workers"; import { hasPermission, loadRepoById, repoPermission } from "./repos"; import { loadUserById, toViewer } from "./auth"; import type { RepoRow } from "~/lib/types"; import { checkMergeability, loadPullByIssueId } from "./pulls"; import { scheduleWorkflows } from "./ci/scheduler"; import { requestCodeOwnerReviews } from "./review-requests"; import { emitWebhook, publicUrl } from "./events/emit"; import type { Viewer } from "~/lib/types"; /** * Pull requests from forks run untrusted code on our containers, so (like GitHub) runs from actors * without write access to the base repository wait for a maintainer to approve them. */ export async function forkRunNeedsApproval( baseRepo: RepoRow, headRepo: RepoRow, actorId: string | null, ): Promise { if (baseRepo.id === headRepo.id) return false; const actor = actorId ? await loadUserById(actorId) : null; return !hasPermission(await repoPermission(baseRepo, actor ? toViewer(actor) : null), "write"); } /** Kicks off CI and mergeability checks for a pull request lifecycle event without blocking the request. */ export function afterPullChange( issueId: string, action: "opened" | "reopened" | "synchronize" | "ready_for_review", actor: Viewer, ): void { waitUntil( (async () => { const pull = await loadPullByIssueId(issueId); if (!pull) return; const [baseRepo, headRepo] = await Promise.all([loadRepoById(pull.repo_id), loadRepoById(pull.head_repo_id)]); if (!baseRepo || !headRepo) return; await emitWebhook(baseRepo.id, "pull_request", { action, number: pull.number, pull_request: { number: pull.number, title: pull.title, body: pull.body, html_url: publicUrl(`/${baseRepo.owner_name}/${baseRepo.name}/pull/${pull.number}`), head: { ref: pull.head_ref, sha: pull.head_sha, repo: `${headRepo.owner_name}/${headRepo.name}` }, base: { ref: pull.base_ref, sha: pull.base_sha }, draft: pull.draft === 1, }, sender: { login: actor.name }, }); await scheduleWorkflows( baseRepo, { event: "pull_request", action, ref: `refs/pull/${pull.number}/head`, sha: pull.head_sha, actorId: actor.id, actorName: actor.name, title: pull.title, prNumber: pull.number, headRef: pull.head_ref, baseRef: pull.base_ref, changedPaths: null, inputs: {}, workflowPath: null, sourceRepo: headRepo, }, { requireApproval: await forkRunNeedsApproval(baseRepo, headRepo, actor.id) }, ); if (action !== "synchronize") { await requestCodeOwnerReviews(pull, baseRepo, headRepo, actor).catch((error) => console.error("Code owner review request failed", error), ); } await checkMergeability(pull).catch((error) => console.error("Mergeability check failed", error)); })(), ); }