import { repoDb } from "../scoped-db"; import { loadRepoById } from "../repos"; import { compileWorkflow } from "./compile"; import type { CompiledJob, CompileInput, NeedsEntry } from "./compile"; import { environmentIdFor, loadVariables } from "./config-values"; import { parseWorkflow } from "./spec"; import type { WorkflowJobRow, WorkflowRunRow } from "./jobs"; function aggregateResult(conclusions: Array): string { if (conclusions.some((value) => value === "failure")) return "failure"; if (conclusions.some((value) => value === "cancelled")) return "cancelled"; if (conclusions.length > 0 && conclusions.every((value) => value === "skipped")) return "skipped"; return "success"; } export function needsContext(dependencies: string[], siblings: WorkflowJobRow[]): Record { const needs: Record = {}; for (const dependency of dependencies) { const rows = siblings.filter((row) => { const spec: CompiledJob = JSON.parse(row.spec); return spec.baseKey === dependency; }); const outputs: Record = {}; for (const row of rows) Object.assign(outputs, row.outputs ? JSON.parse(row.outputs) : {}); needs[dependency] = { result: aggregateResult(rows.map((row) => row.conclusion)), outputs }; } return needs; } /** * Recompiles a job once its dependencies finished so `needs..outputs`, `needs..result` and * environment-scoped `vars` resolve, persisting the result as the job's spec. */ export async function recompileJob(runRow: WorkflowRunRow, job: WorkflowJobRow): Promise { const current: CompiledJob = JSON.parse(job.spec); if (!runRow.workflow_source || !runRow.context) return current; const parsed = parseWorkflow(runRow.workflow_source); if (!parsed.ok) throw new Error(parsed.error); const base: CompileInput = JSON.parse(runRow.context); const db = repoDb(runRow.repo_id); const siblings = await db.all("SELECT * FROM workflow_jobs WHERE run_id = ?", runRow.id); let vars = base.vars; if (job.environment && runRow.source_repo_id === runRow.repo_id && runRow.untrusted !== 1) { const repo = await loadRepoById(runRow.repo_id); if (repo) vars = await loadVariables(repo, await environmentIdFor(repo.id, job.environment)); } const compiled = compileWorkflow(parsed.spec, { ...base, vars, needs: needsContext(current.needs, siblings) }).find( (candidate) => candidate.key === job.job_key, ); if (!compiled) throw new Error(`job ${job.job_key} no longer exists in the workflow`); const spec: CompiledJob = { ...compiled, name: job.name, steps: compiled.steps.slice(0, current.steps.length) }; await db.run( "UPDATE workflow_jobs SET spec = ?, runs_on = ?, environment = ? WHERE id = ?", JSON.stringify(spec), JSON.stringify(spec.runsOn), spec.environment?.name ?? null, job.id, ); job.environment = spec.environment?.name ?? null; return spec; }