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"); } /** * The code that runs belongs to the pull request's author and the head repository's owner, not to whoever * triggered the event, so a maintainer reopening or editing a fork PR does not implicitly approve its code. */ export async function forkPullNeedsApproval(baseRepo: RepoRow, headRepo: RepoRow, authorId: string): Promise { if (baseRepo.id === headRepo.id) return false; if (await forkRunNeedsApproval(baseRepo, headRepo, authorId)) return true; return forkRunNeedsApproval(baseRepo, headRepo, headRepo.owner_id); } /** * Kicks off CI and mergeability checks for a pull request lifecycle event without blocking the request. * `expectedHeadSha` is the head the actor saw; if it moved before the background task ran, nothing is scheduled. */ export function afterPullChange( target: { repo_id: string; issue_id: string }, action: "opened" | "reopened" | "synchronize" | "ready_for_review", actor: Viewer, expectedHeadSha?: string, ): void { waitUntil( (async () => { const pull = await loadPullByIssueId(target.repo_id, target.issue_id); if (!pull) return; if (expectedHeadSha && pull.head_sha !== expectedHeadSha) 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, id: actor.id }, }); 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 forkPullNeedsApproval(baseRepo, headRepo, pull.author_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)); })(), ); }