import { repoDb } from "../scoped-db"; import { loadPull } from "../pulls"; import type { RepoRow } from "~/lib/types"; import type { WorkflowRunRow } from "../ci/jobs"; import { notifyQuietly, quiet } from "./deliver"; import { repoWatchers } from "./recipients"; const FAILED = new Set(["failure", "timed_out", "startup_failure"]); /** * Failure notices go to the actor, the PR author for PR runs and (by the "ci_watching" pref) watchers; * a success right after a failure on the same workflow and ref sends a "fixed" notice to the same people. */ async function notifyRunCompletedUnsafe(repo: RepoRow, runRow: WorkflowRunRow, conclusion: string): Promise { const failed = FAILED.has(conclusion); if (!failed && conclusion !== "success") return; const db = repoDb(repo.id); const state = await db.first<{ conclusion: string; last_run_id: string | null; previous_conclusion: string | null }>( "SELECT conclusion, last_run_id, previous_conclusion FROM ci_notify_state WHERE repo_id = ? AND workflow_path = ? AND ref = ?", repo.id, runRow.workflow_path, runRow.ref, ); const previous = state?.last_run_id === runRow.id ? state.previous_conclusion : (state?.conclusion ?? null); if (state?.last_run_id !== runRow.id) { await db.run( `INSERT INTO ci_notify_state (repo_id, workflow_path, ref, conclusion, last_run_id, previous_conclusion, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (repo_id, workflow_path, ref) DO UPDATE SET conclusion = excluded.conclusion, last_run_id = excluded.last_run_id, previous_conclusion = excluded.previous_conclusion, updated_at = excluded.updated_at`, repo.id, runRow.workflow_path, runRow.ref, conclusion, runRow.id, previous, Date.now(), ); } const fixed = conclusion === "success" && previous !== null && FAILED.has(previous); if (!failed && !fixed) return; const pull = runRow.pr_number ? await loadPull(repo.id, runRow.pr_number) : null; const branch = runRow.ref.replace(/^refs\/(heads|tags)\//, ""); const where = pull ? `#${pull.number}` : branch; const title = failed ? `${runRow.workflow_name} failed on ${where} in ${repo.owner_name}/${repo.name}` : `${runRow.workflow_name} fixed on ${where} in ${repo.owner_name}/${repo.name}`; const details = [ `Run #${runRow.run_number} ${failed ? `concluded with ${conclusion.replace(/_/g, " ")}` : "succeeded after a failure"}.`, `Commit ${runRow.sha.slice(0, 7)} on ${branch}.`, ]; const url = `/${repo.owner_name}/${repo.name}/actions/runs/${runRow.run_number}`; const involved = [runRow.actor_id, pull?.author_id]; await notifyQuietly({ category: "ci", repoId: repo.id, accountId: null, access: "read", recipients: involved, dedupeKey: runRow.id, title, details, url, }); const watchers = (await repoWatchers(repo.id)).filter((id) => !involved.includes(id)); await notifyQuietly({ category: "ci_watching", repoId: repo.id, accountId: null, access: "read", recipients: watchers, dedupeKey: runRow.id, title, details, url, }); } export const notifyRunCompleted = quiet("notifyRunCompleted", notifyRunCompletedUnsafe);