275 lines · 11.4 KB
import { env } from "cloudflare:workers";
import { repoDb } from "../scoped-db";
import { registerLocator } from "../scoped-db/locators";
import { scopedId } from "../scoped-db/ids";
import { forbidden, badRequest } from "../errors";
import { currentBaseSha, headBranchConflicts, loadPullRepos, requireHeadPush, updateHeadBranch } from "../pull-merge";
import { hasPermission, repoPermission } from "../repos";
import { loadPullByIssueId, MergeError } from "../pulls";
import type { PullRow } from "../pulls";
import { actorViewer } from "../merge-automation";
import { clefModel, gatewayProvider } from "./gateway";
import { DEFAULT_LIMITS, ModelCallError, planMergeFix } from "./plan";
import { aiMeter } from "../metering/ai";
import { resolveAiEndpoint } from "../ai/endpoint";
import { assertWithinQuota, QuotaExceededError } from "../metering/quota";
import type { FixPlan } from "./plan";
import { loadRepoSettings, loadRun, ACTIVE_STATUSES, updateRun } from "./store";
import { resolveMode, shouldAutoApply } from "./proposal";
import type { MergeFixMode } from "./proposal";
import type { RunRow } from "./store";
import type { RepoRow, Viewer } from "~/lib/types";
import { captureServerEvent } from "../analytics";

/** Longer than the Workflow's worst case (plan 10 min + apply 5 min), so a live run is never treated as abandoned. */
const ACTIVE_RUN_MS = 30 * 60 * 1000;

export class MergeFixError extends Error {}

/** Private content only leaves the forge when every private repository involved has opted in. */
async function contentMayLeave(baseRepo: RepoRow, headRepo: RepoRow): Promise<boolean> {
  for (const repo of baseRepo.id === headRepo.id ? [baseRepo] : [baseRepo, headRepo]) {
    if (repo.visibility === "private" && !(await loadRepoSettings(repo.id)).allowPrivate) return false;
  }
  return true;
}

/** Starts a bounded fix run in a Workflow. Callers must already have checked the trigger's own permission. */
export async function startMergeFix(
  pull: PullRow,
  actor: Viewer,