UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

205 lines (204 loc) • 8.82 kB
import { m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { n as gitHubPublicApi } from "./github-public-api-COOMiXSo.mjs"; //#region src/gateway/control-ui-session-prs-checks.ts const FAILING_CHECK_CONCLUSIONS = /* @__PURE__ */ new Set([ "failure", "timed_out", "cancelled", "action_required", "startup_failure" ]); const PAGE_SIZE = 100; const MAX_PAGES = 10; const PAGE_BYTES = 1048576; const MAX_ACTIONS_SUITES = 20; const MAX_STEPS = 200; function sessionPullRequestCheckState(status, conclusion) { if (typeof conclusion === "string" && FAILING_CHECK_CONCLUSIONS.has(conclusion)) return "failed"; if (status !== "completed" || conclusion === "stale") return "running"; return conclusion === "skipped" ? "skipped" : "passed"; } function sessionPullRequestRepositoryApiUrl(target) { return `${target.apiBaseUrl}/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}`; } function incomplete() { return new gitHubPublicApi.ControlUiGitHubError(502, "GitHub CI details were incomplete or changed while loading"); } /** One bounded pagination owner serves both the compact rollup and on-demand detail. */ async function fetchSessionPullRequestCheckRuns(target, request) { return fetchPages(`${sessionPullRequestRepositoryApiUrl(target)}/commits/${target.headSha}/check-runs?filter=latest`, "check_runs", request); } async function fetchPages(url, field, request) { const rows = []; let expected; for (let page = 1; page <= MAX_PAGES; page += 1) { const value = await request(`${url}&per_page=${PAGE_SIZE}&page=${page}`, PAGE_BYTES); if (!isRecord(value)) throw incomplete(); const items = value[field]; const total = value.total_count; if (!Array.isArray(items) || items.length > PAGE_SIZE || typeof total !== "number" || !Number.isSafeInteger(total) || total < 0 || total > 1e3 || expected !== void 0 && expected !== total) throw incomplete(); expected = total; for (const item of items) { if (!isRecord(item)) throw incomplete(); rows.push(item); } if (rows.length === total) return rows; if (rows.length > total || items.length < PAGE_SIZE) throw incomplete(); } throw incomplete(); } async function fetchSessionPullRequestCheckRollup(item, request) { if (!item.headSha || !/^[0-9a-f]{40}$/i.test(item.headSha)) return; const runs = await fetchSessionPullRequestCheckRuns({ ...item, headSha: item.headSha }, request); if (runs.length === 0) return; const counts = { passed: 0, failed: 0, skipped: 0, running: 0 }; for (const run of runs) counts[sessionPullRequestCheckState(run.status, run.conclusion)] += 1; return { state: counts.failed > 0 ? "failing" : counts.running > 0 ? "pending" : "passing", ...counts }; } function positiveId(value) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw incomplete(); return value; } function text(value, max = 512) { const result = readNonBlankString(value); if (!result || result.length > max) throw incomplete(); return result; } function timing(value) { const timestamp = (raw) => { if (raw === null || raw === void 0) return; const date = text(raw, 40); if (!Number.isFinite(Date.parse(date))) throw incomplete(); return date; }; return { startedAt: timestamp(value.started_at), completedAt: timestamp(value.completed_at) }; } function readConclusion(value) { return value === null || value === void 0 ? void 0 : text(value, 64); } function safeCheckLink(value) { if (typeof value !== "string" || value.length > 2048) return; const url = URL.parse(value); return url?.protocol === "https:" && !url.username && !url.password ? url.href : void 0; } function parseChecks(rows, target) { const ids = /* @__PURE__ */ new Set(); return rows.map((row) => { const id = positiveId(row.id); if (ids.has(id) || row.head_sha !== target.headSha) throw incomplete(); ids.add(id); const source = isRecord(row.app) && row.app.slug === "github-actions" ? "actions" : "check"; const status = text(row.status, 64); const result = readConclusion(row.conclusion); return { check: { id, name: text(row.name), status, conclusion: result, state: sessionPullRequestCheckState(status, result), source, detailsUrl: safeCheckLink(row.details_url ?? row.html_url), ...timing(row) }, ...source === "actions" && isRecord(row.check_suite) ? { suiteId: positiveId(row.check_suite.id) } : {} }; }); } function parseSteps(value) { if (!Array.isArray(value) || value.length > MAX_STEPS) throw incomplete(); const numbers = /* @__PURE__ */ new Set(); return value.map((step) => { if (!isRecord(step)) throw incomplete(); const number = positiveId(step.number); if (numbers.has(number)) throw incomplete(); numbers.add(number); return { number, name: text(step.name), status: text(step.status, 64), conclusion: readConclusion(step.conclusion), ...timing(step) }; }).toSorted((left, right) => left.number - right.number); } async function loadSuiteJobs(target, suiteId, checks, request) { const base = sessionPullRequestRepositoryApiUrl(target); const runs = await fetchPages(`${base}/actions/runs?head_sha=${target.headSha}&check_suite_id=${suiteId}`, "workflow_runs", request); if (runs.length !== 1 || runs[0]?.check_suite_id !== suiteId || runs[0]?.head_sha !== target.headSha) throw incomplete(); const runId = positiveId(runs[0].id); const attempt = runs[0].run_attempt === void 0 ? void 0 : positiveId(runs[0].run_attempt); const jobs = await fetchPages(`${base}/actions/runs/${runId}/jobs?filter=all`, "jobs", request); const wanted = new Map(checks.filter((row) => row.suiteId === suiteId).map(({ check }) => [`${base}/check-runs/${check.id}`.toLowerCase(), check])); const result = /* @__PURE__ */ new Map(); for (const job of jobs) { if (job.run_id !== runId || job.head_sha !== target.headSha || job.run_attempt !== void 0 && attempt !== void 0 && positiveId(job.run_attempt) > attempt) throw incomplete(); const check = typeof job.check_run_url === "string" ? wanted.get(job.check_run_url.toLowerCase()) : void 0; if (!check) continue; if (result.has(check.id)) throw incomplete(); const jobId = positiveId(job.id); const status = text(job.status, 64); const verdict = readConclusion(job.conclusion); result.set(check.id, { ...check, status, conclusion: verdict, state: sessionPullRequestCheckState(status, verdict), ...timing(job), detailsUrl: `https://${target.host}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/actions/runs/${runId}/job/${jobId}`, steps: parseSteps(job.steps ?? []) }); } if ([...wanted.values()].some((check) => check.state !== "skipped" && !result.has(check.id))) throw incomplete(); return result; } /** Checks remain useful when Actions permissions, history, or the bounded detail budget fail. */ async function fetchSessionPullRequestCheckDetails(target, request) { const rows = parseChecks(await fetchSessionPullRequestCheckRuns(target, request), target); const priority = { failed: 0, running: 1, passed: 2, skipped: 3 }; const compareChecks = (a, b) => priority[a.state] - priority[b.state] || a.name.localeCompare(b.name) || a.id - b.id; const orderedRows = rows.toSorted((a, b) => compareChecks(a.check, b.check)); const suites = [...new Set(orderedRows.filter(({ check }) => check.source === "actions" && check.state !== "skipped").map(({ suiteId }) => suiteId))]; let error; const details = /* @__PURE__ */ new Map(); for (const suite of suites.slice(0, MAX_ACTIONS_SUITES)) try { if (suite === void 0) throw incomplete(); const jobs = await loadSuiteJobs(target, suite, rows, request); for (const [id, check] of jobs) details.set(id, check); } catch (failure) { error = failure; if (failure instanceof gitHubPublicApi.ControlUiGitHubError && failure.statusCode === 429) break; } if (suites.length > MAX_ACTIONS_SUITES && !error) error = new gitHubPublicApi.ControlUiGitHubError(502, "Some Actions steps exceeded the CI detail limit; open the job on GitHub"); if (suites.length > 0 && !error) { const latest = parseChecks(await fetchSessionPullRequestCheckRuns(target, request), target); const identity = (values) => values.map(({ check, suiteId }) => `${check.id}:${suiteId ?? ""}`).toSorted().join(","); if (identity(latest) !== identity(rows)) throw new gitHubPublicApi.ControlUiGitHubError(409, "CI jobs were rerun while loading; reopen CI details"); } return { checks: orderedRows.map(({ check }) => details.get(check.id) ?? check).toSorted(compareChecks), error }; } //#endregion export { fetchSessionPullRequestCheckRollup as n, sessionPullRequestRepositoryApiUrl as r, fetchSessionPullRequestCheckDetails as t };