178 lines · 6.5 KB
import { env } from "cloudflare:workers";
import { repoDb, stmt } from "../scoped-db";
import { randomId } from "../crypto";
import { CAPS, numberVar } from "../limits";
import { loadRepoById } from "../repos";
import { isScratchRepo } from "../sites/config";
import { branchSha, listRefs } from "../git/refs";
import { minimumInterval, nextFireTime, parseCron } from "./cron";
import { readWorkflowFiles, scheduleWorkflows } from "./scheduler";
import { parseWorkflow } from "./spec";
import type { RepoRow } from "~/lib/types";

const DEFAULT_MIN_INTERVAL_MINUTES = 15;
const MINUTE_MS = 60_000;
const DUE_BATCH = 25;
/** Like GitHub, schedules pause on repositories without pushes for 60 days. */
const INACTIVE_MS = 60 * 24 * 60 * MINUTE_MS;

type ScheduleRow = { id: string; repo_id: string; workflow_path: string; cron: string; next_run_at: number };

function minIntervalMs(): number {
  return numberVar(env.MIN_SCHEDULE_INTERVAL_MINUTES, DEFAULT_MIN_INTERVAL_MINUTES) * MINUTE_MS;
}

/** Validates a cron entry against the frequency limit; returns its next fire time or an error. */
export function checkSchedule(cron: string, now: number): { next: number } | { error: string } {
  try {
    const schedule = parseCron(cron);
    const next = nextFireTime(schedule, now);
    if (next === null) return { error: "never fires" };
    if (minimumInterval(schedule, now) < minIntervalMs()) {
      return { error: `fires more often than every ${minIntervalMs() / MINUTE_MS} minutes` };
    }
    return { next };
  } catch (error) {
    return { error: error instanceof Error ? error.message : "invalid cron" };
  }
}

/**