import { first } from "../db"; import { badRequest } from "../errors"; import { loadRepoById } from "../repos"; import { loadPull } from "../pulls"; import { toExprValue } from "./event-triggers"; import type { CompileInput } from "./compile"; import type { ExprValue } from "./expressions"; import type { WorkflowRunRow } from "./jobs"; import type { Trigger, TriggerEvent } from "./triggers"; import type { RepoRow, Viewer } from "~/lib/types"; const EVENTS: TriggerEvent[] = [ "push", "pull_request", "release", "workflow_dispatch", "schedule", "issues", "issue_comment", "pull_request_review", "workflow_run", "create", "delete", ]; function isTriggerEvent(value: string): value is TriggerEvent { return (EVENTS as string[]).includes(value); } function storedEvent(runRow: WorkflowRunRow): Record { if (!runRow.context) return {}; const context: CompileInput = JSON.parse(runRow.context); const event = toExprValue(context.event.event); return event && typeof event === "object" && !Array.isArray(event) ? event : {}; } const MERGE_QUEUE_REF_PREFIX = "refs/heads/gitfor-merge-queue/"; /** Merge queue runs must keep reading workflow definitions from the base commit, never the merge group. */ async function queueWorkflowSource(repo: RepoRow, runRow: WorkflowRunRow): Promise { if (!runRow.ref.startsWith(MERGE_QUEUE_REF_PREFIX)) return undefined; const entry = await first<{ base_sha: string | null }>( "SELECT base_sha FROM merge_queue_entries WHERE repo_id = ? AND speculative_sha = ? AND state = 'testing'", repo.id, runRow.sha, ); if (!entry?.base_sha) throw badRequest("This merge queue run is no longer active and cannot be re-run"); return { repo, sha: entry.base_sha }; } /** Rebuilds a run's trigger (event, ref, payload and inputs) so a re-run sees the same `github` context. */ export async function rerunTrigger(repo: RepoRow, runRow: WorkflowRunRow, viewer: Viewer): Promise { const sourceRepo = (await loadRepoById(runRow.source_repo_id)) ?? repo; const workflowSource = await queueWorkflowSource(repo, runRow); const pull = runRow.pr_number ? await loadPull(repo.id, runRow.pr_number) : null; const event = isTriggerEvent(runRow.event) ? runRow.event : "push"; const payload = storedEvent(runRow); const action = typeof payload.action === "string" && payload.action ? payload.action : null; const inputs = payload.inputs && typeof payload.inputs === "object" && !Array.isArray(payload.inputs) ? payload.inputs : {}; return { event, action: action ?? (event === "pull_request" ? "synchronize" : event === "release" ? "published" : null), ref: runRow.ref, sha: runRow.sha, actorId: viewer.id, actorName: viewer.name, title: runRow.title, prNumber: runRow.pr_number, headRef: pull?.head_ref ?? null, baseRef: pull?.base_ref ?? null, changedPaths: null, inputs, workflowPath: runRow.workflow_path, sourceRepo, payload, depth: runRow.trigger_depth, untrusted: runRow.untrusted === 1, workflowSource, cron: typeof payload.schedule === "string" ? payload.schedule : undefined, }; }