103 lines · 4.5 KB
import { WorkflowEntrypoint } from "cloudflare:workers";
import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers";
import { runDb } from "./scope";
import { loadRepoById } from "../repos";
import { emitWebhook, publicUrl } from "../events/emit";
import { cancelActiveJob, completeJob } from "./jobs";
import type { WorkflowRunRow } from "./jobs";
import { expireStaleJobs, loadStates, scheduleTick } from "./dispatch";
import type { JobState } from "./dispatch";
import { deploySitesForRun } from "../sites/triggers";
import { notifyRunCompleted } from "../notify/ci";
import type { Conclusion } from "~/lib/types";

export type CiRunParams = { runId: string };

const WAIT_TIMEOUT = "5 minutes";
const GATED_WAIT_TIMEOUT_SECONDS = 60 * 60;
const MAX_ITERATIONS = 1000;
const CAPACITY_RETRY = "1 minute";

function runConclusion(jobs: JobState[]): Conclusion {
  if (jobs.length === 0) return "skipped";
  if (jobs.some((job) => job.conclusion === "failure")) return "failure";
  if (jobs.some((job) => job.conclusion === "cancelled")) return "cancelled";
  if (jobs.every((job) => job.conclusion === "skipped")) return "skipped";
  return "success";
}

/** Gated jobs wake on review events; wait timers wake at their deadline. */
function gatedTimeout(wakeAt: number | null): `${number} seconds` {
  const seconds = wakeAt ? Math.ceil((wakeAt - Date.now()) / 1000) : GATED_WAIT_TIMEOUT_SECONDS;
  return `${Math.min(GATED_WAIT_TIMEOUT_SECONDS, Math.max(1, seconds))} seconds`;
}

/** Durable orchestration of one workflow run: dispatches jobs as their `needs` resolve and waits for completion events. */
export class CiRunWorkflow extends WorkflowEntrypoint<Env, CiRunParams> {
  override async run(event: Readonly<WorkflowEvent<CiRunParams>>, step: WorkflowStep): Promise<void> {
    const { runId } = event.payload;
    const started = await step.do("start", async () => {
      const db = await runDb(runId);