import { repoDb } from "../scoped-db"; import { withStore } from "../git/store"; 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 INVALID_PREFIX = "Invalid workflow: "; /** A run that failed to compile stored its error as its title; a re-run gets the commit's subject back. */ async function rerunTitle(repo: RepoRow, runRow: WorkflowRunRow): Promise { if (!runRow.title.startsWith(INVALID_PREFIX)) return runRow.title; const commit = await withStore(repo.artifacts_name, (store) => store.commit(runRow.sha)).catch(() => null); return commit?.subject ?? `${runRow.workflow_path} re-run`; } 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 repoDb(repo.id).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 fromFork = runRow.source_repo_id !== repo.id; // A fork run's commit is still reachable through refs/pull/N/head after the fork is deleted; falling back to the // base would read the fork's workflow at that commit as trusted code with every secret. const sourceRepo = fromFork ? await loadRepoById(runRow.source_repo_id) : repo; if (!sourceRepo) throw badRequest("The fork this run came from no longer exists, so it cannot be re-run"); 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: await rerunTitle(repo, runRow), 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 || fromFork, workflowSource, cron: typeof payload.schedule === "string" ? payload.schedule : undefined, }; }