import { cloudRunnerSize } from "../ci/runner-sizes"; import type { RunnerBinding } from "../ci/runner-sizes"; import { repoDb } from "../scoped-db"; import type { MeterId } from "~/lib/meters"; import { recordUsage } from "./record"; /** Docker-capable twins bill under their base size's meter: same instance type, so no extra charge. */ const METER_FOR_BINDING: Record = { RUNNER: "compute.runner", RUNNER_LARGE: "compute.runner_large", RUNNER_XLARGE: "compute.runner_xlarge", RUNNER_DIND: "compute.runner", RUNNER_LARGE_DIND: "compute.runner_large", RUNNER_XLARGE_DIND: "compute.runner_xlarge", }; export function runnerMeter(runsOn: string[]): MeterId { const size = cloudRunnerSize(runsOn); return size ? METER_FOR_BINDING[size.binding] : "compute.runner_self_hosted"; } /** Exact runner seconds, never rounded up; a job that never started costs nothing. */ export function jobSeconds(startedAt: number | null, completedAt: number): number { if (startedAt === null || completedAt <= startedAt) return 0; return (completedAt - startedAt) / 1000; } /** * Bills a completed job's runner time to the account owning the repository the run executes in. Fork pull * request runs execute in the base repository, so the base owner pays. Keyed by job id: rerun attempts are * new job rows, while a duplicate completion of the same job is ignored. */ export async function meterCompletedJob(input: { jobId: string; runId: string; accountId: string; repoId: string; runsOn: string[]; startedAt: number | null; completedAt: number; sourceRepoId: string; }): Promise { const seconds = jobSeconds(input.startedAt, input.completedAt); const steps = await repoDb(input.repoId).all<{ count: number }>( "SELECT COUNT(*) AS count FROM job_steps WHERE job_id = ? AND conclusion != 'skipped'", input.jobId, ); const attributes = { run: input.runId, runs_on: input.runsOn.join(","), fork: input.sourceRepoId !== input.repoId }; await recordUsage({ accountId: input.accountId, repoId: input.repoId, meter: runnerMeter(input.runsOn), quantity: seconds, unit: "seconds", attributes, idempotencyKey: `ci:${input.jobId}`, at: input.completedAt, }); await recordUsage({ accountId: input.accountId, repoId: input.repoId, meter: "compute.workflow_steps", quantity: steps[0]?.count ?? 0, attributes, idempotencyKey: `ci:${input.jobId}:steps`, at: input.completedAt, }); }