41 lines · 2.2 KB
export type CheckState = "success" | "failure" | "error" | "pending" | "missing" | "untrusted" | "stale";

/** Polls of the fix commit's CI before giving up: 30 x 2 minutes covers an hour of CI. */
export const MAX_CI_POLLS = 30;
/** With no statuses at all after this many polls, the repo is assumed to have no CI for the branch. */
export const NO_CI_GRACE_POLLS = 3;
/** Attempts to put the pull back in the queue when enqueueing itself fails (for example the queue is full). */
export const MAX_ENQUEUE_ATTEMPTS = 3;

export const NO_CI_REASON = "No CI reported on the AI-resolved commit; a human must review and re-queue it";

export type Verdict = { kind: "green" } | { kind: "wait" } | { kind: "red"; reason: string };

/**
 * Decides whether the fix commit is green. Required checks (from base branch protection) must all succeed;
 * without required checks every reported status must succeed; with no statuses at all after a grace period
 * the machine-resolved commit is unverified, so it is handed back to a human instead of re-entering the queue.
 */
export function ciVerdict(input: {
  required: string[];
  requiredResults: CheckState[];
  statuses: Array<{ context: string; state: CheckState }>;
  poll: number;
}): Verdict {
  if (input.required.length > 0) {
    const bad = input.required.findIndex((_, index) =>
      ["failure", "error", "untrusted", "stale"].includes(input.requiredResults[index]),
    );
    if (bad >= 0) return { kind: "red", reason: `Required check "${input.required[bad]}" did not pass on the fix` };
    if (input.requiredResults.every((state) => state === "success")) return { kind: "green" };
  } else if (input.statuses.length > 0) {
    const bad = input.statuses.find((status) => ["failure", "error", "untrusted"].includes(status.state));
    if (bad) return { kind: "red", reason: `Check "${bad.context}" failed on the fix` };
    if (input.statuses.every((status) => status.state === "success")) return { kind: "green" };
  } else if (input.poll >= NO_CI_GRACE_POLLS) {
    return { kind: "red", reason: NO_CI_REASON };
  }
  if (input.poll >= MAX_CI_POLLS) return { kind: "red", reason: "Timed out waiting for CI on the fix" };
  return { kind: "wait" };
}