41 lines · 1.6 KB
import { run } from "../db";
import { randomId } from "../crypto";

type UsageKind = "self_hosted_runner_ms" | "cache_bytes_stored" | "cache_bytes_freed";

/**
 * Single seam for usage metering of self-hosted runner time and Actions cache storage. Events land in `runner_usage_events` until the
 * shared metering API exists; swapping this function body is the only change needed to forward them there.
 */
async function recordRunnerUsage(repoId: string, kind: UsageKind, quantity: number, subjectId: string | null) {
  if (!Number.isFinite(quantity) || quantity <= 0) return;
  try {
    await run(
      `INSERT INTO runner_usage_events (id, owner_id, repo_id, kind, quantity, subject_id, created_at)
       SELECT ?, owner_id, id, ?, ?, ?, ? FROM repos WHERE id = ?`,
      randomId(),
      kind,
      Math.round(quantity),
      subjectId,
      Date.now(),
      repoId,
    );
  } catch (error) {
    console.error("Failed to record usage", kind, repoId, error);
  }
}

/** Self-hosted runner time is free, but recorded per job (wall-clock milliseconds). */
export async function meterSelfHostedJob(repoId: string, jobId: string, startedAt: number | null, endedAt: number) {
  if (startedAt === null) return;
  await recordRunnerUsage(repoId, "self_hosted_runner_ms", endedAt - startedAt, jobId);
}

export async function meterCacheStorage(repoId: string, bytes: number): Promise<void> {
  await recordRunnerUsage(repoId, "cache_bytes_stored", bytes, null);
}

export async function meterCacheFreed(repoId: string, bytes: number): Promise<void> {
  await recordRunnerUsage(repoId, "cache_bytes_freed", bytes, null);
}